diff --git a/.github/renovate.json b/.github/renovate.json index 5f21ee01db..8bafa45fd1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -6,7 +6,7 @@ "dependencyDashboard": true, "rangeStrategy": "update-lockfile", "rebaseWhen": "conflicted", - "baseBranches": ["1.11.x", "1.10.x"], + "baseBranches": ["1.11.x"], "packageRules": [ { "matchPackagePatterns": ["*"], @@ -15,7 +15,7 @@ { "matchPaths": ["+(composer.json)"], "enabled": true, - "matchBaseBranches": ["1.10.x"] + "matchBaseBranches": ["1.11.x"] }, { "matchPaths": ["build-cs/**"], diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index a6ebd4d40b..36b9730690 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: branches: - - "1.11.x" + - "1.12.x" paths: - 'src/**' - 'composer.lock' @@ -89,7 +89,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} - - uses: peter-evans/repository-dispatch@v2 + - uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} repository: "phpstan/phpstan" diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml index 77f484f73a..4769c56165 100644 --- a/.github/workflows/build-issue-bot.yml +++ b/.github/workflows/build-issue-bot.yml @@ -43,7 +43,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" diff --git a/.github/workflows/checksum-phar.yml b/.github/workflows/checksum-phar.yml index 43aa672104..1558436ad4 100644 --- a/.github/workflows/checksum-phar.yml +++ b/.github/workflows/checksum-phar.yml @@ -33,7 +33,7 @@ jobs: steps: - name: "Checkout phpstan-dist" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: phpstan/phpstan path: phpstan-dist @@ -50,12 +50,12 @@ jobs: run: "rm -r phpstan-dist" - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ steps.info.outputs.commit }} - name: "Checkout latest PHAR compiler" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: phpstan-src ref: ${{ github.sha }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 290183c285..aeb7efb6fc 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -134,6 +134,20 @@ jobs: mv src/Bar.php.orig src/Bar.php echo -n > phpstan-baseline.neon ../../bin/phpstan -vvv + - script: | + cd e2e/bug10449 + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug10449b + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv - script: | cd e2e/bug-9622 echo -n > phpstan-baseline.neon @@ -159,6 +173,84 @@ jobs: export PHPSTAN_SCOPE_CLASS=MyTestScope ACTUAL=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9 | jq --raw-output '.scopeClass') [[ "$ACTUAL" == "MyTestScope" ]]; + - 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" + - script: | + cd e2e/env-int-key + env 1=1 ../../bin/phpstan analyse test.php + - script: | + cd e2e/trait-caching + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + - script: | + cd e2e/trait-caching + ../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ + patch -b data/TraitOne.php < TraitOne.patch + OUTPUT=$(../../bin/phpstan analyze --no-progress --level 8 --error-format raw data/ || true) + echo "$OUTPUT" + ../bashunit -a line_count 1 "$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) + echo "$OUTPUT" + ../bashunit -a line_count 1 "$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) + echo "$OUTPUT" + ../bashunit -a line_count 2 "$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) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in ignoreErrors' "$OUTPUT" + ../bashunit -a contains 'tests is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c phpneon.php 2>&1 || true) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in ignoreErrors' "$OUTPUT" + ../bashunit -a contains 'src/test.php is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c excludePaths.neon 2>&1 || true) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in excludePaths' "$OUTPUT" + ../bashunit -a contains 'tests is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c phpneon2.php 2>&1 || true) + echo "$OUTPUT" + ../bashunit -a contains 'Invalid entry in excludePaths' "$OUTPUT" + ../bashunit -a contains 'src/test.php is neither a directory, nor a file path, nor a fnmatch pattern.' "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c ignoreNonexistentExcludePath.neon) + echo "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + cp -r tmp-node-modules node_modules + OUTPUT=$(../../bin/phpstan analyse -c ignoreNonexistentExcludePath.neon) + echo "$OUTPUT" + - script: | + cd e2e/bad-exclude-paths + OUTPUT=$(../../bin/phpstan analyse -c ignoreReportUnmatchedFalse.neon) + echo "$OUTPUT" steps: - name: "Checkout" @@ -178,6 +270,9 @@ jobs: - name: "Patch PHPStan" run: "patch src/Analyser/Error.php < e2e/PHPStanErrorPatch.patch" + - name: "Install bashunit" + run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.13.0" + - name: "Test" run: "${{ matrix.script }}" @@ -212,6 +307,10 @@ jobs: cd e2e/baseline-uninit-prop-trait ../../bin/phpstan analyse --configuration test-no-baseline.neon --generate-baseline test-baseline.neon ../../bin/phpstan analyse --configuration test.neon + - script: | + cd e2e/discussion-11362 + composer install + ../../bin/phpstan steps: - name: "Checkout" diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index 0b3389a196..df03ddeb72 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -11,14 +11,14 @@ on: - 'changelog-generator/**' push: branches: - - "1.11.x" + - "1.12.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' env: - COMPOSER_ROOT_VERSION: "1.11.x-dev" + 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 @@ -41,14 +41,14 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.3" - name: "Install Issue Bot dependencies" working-directory: "issue-bot" run: "composer install --no-interaction --no-progress" - name: "Cache downloads" - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./issue-bot/tmp key: "issue-bot-download-v6-${{ github.run_id }}" @@ -91,7 +91,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 --no-dev" @@ -100,7 +100,7 @@ jobs: working-directory: "issue-bot" run: "composer install --no-interaction --no-progress" - - uses: Wandalen/wretry.action@v1.3.0 + - uses: Wandalen/wretry.action@v3.5.0 with: action: actions/download-artifact@v4 with: | @@ -133,7 +133,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.3" - name: "Install Issue Bot dependencies" working-directory: "issue-bot" @@ -163,13 +163,13 @@ jobs: if: github.event_name == 'pull_request' env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - run: echo "$(./console.php evaluate)" >> $GITHUB_STEP_SUMMARY + run: ./console.php evaluate >> $GITHUB_STEP_SUMMARY - name: "Evaluate results - push" working-directory: "issue-bot" - if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/1.11.x'" + if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/1.12.x'" env: GITHUB_PAT: ${{ secrets.PHPSTAN_BOT_TOKEN }} PHPSTAN_SRC_COMMIT_BEFORE: ${{ github.event.before }} PHPSTAN_SRC_COMMIT_AFTER: ${{ github.event.after }} - run: echo "$(./console.php evaluate --post-comments)" >> $GITHUB_STEP_SUMMARY + run: ./console.php evaluate --post-comments >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 11ff5ac8b5..14fbfbc500 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -103,8 +103,8 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Composer Require Checker" - run: "make composer-require-checker" + - name: "Composer Dependency Analyser" + run: "make composer-dependency-analyser" name-collision: name: "Name Collision Detector" diff --git a/.github/workflows/merge-maintained-branch.yml b/.github/workflows/merge-maintained-branch.yml index 7c7cb8f853..4b609e26e2 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.10.x" + - "1.11.x" jobs: merge: @@ -20,5 +20,5 @@ jobs: with: github_token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" source_ref: ${{ github.ref }} - target_branch: '1.11.x' + target_branch: '1.12.x' commit_message_template: 'Merge branch {source_ref} into {target_branch}' diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 3818f36159..af601aa93b 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -216,7 +216,7 @@ jobs: run: "gpg --verify phpstan.phar.asc" - name: "Install lucky_commit" - uses: baptiste0928/cargo-install@v2 + uses: baptiste0928/cargo-install@v3 with: crate: lucky_commit args: --no-default-features diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml index 30791921a3..84015d215a 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.11.x' + - '1.12.x' jobs: @@ -17,8 +17,8 @@ jobs: steps: - name: Comment PR - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: - body: "You've opened the pull request against the latest branch 1.11.x. If your code is relevant on 1.10.x and you want it to be released sooner, please rebase your pull request and change its target to 1.10.x." + 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." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-marked-as-ready.yml b/.github/workflows/pr-marked-as-ready.yml index 7bdbc585c9..b9785a2a3c 100644 --- a/.github/workflows/pr-marked-as-ready.yml +++ b/.github/workflows/pr-marked-as-ready.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Comment PR - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: body: "This pull request has been marked as ready for review." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index e93039baec..b3db1c394c 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -73,7 +73,7 @@ jobs: - "8.3" steps: - - uses: Wandalen/wretry.action@v1.3.0 + - uses: Wandalen/wretry.action@v3.5.0 with: action: actions/download-artifact@v4 with: | diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml new file mode 100644 index 0000000000..8fd37ce761 --- /dev/null +++ b/.github/workflows/spelling.yml @@ -0,0 +1,23 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Spelling" + +on: + pull_request: + push: + branches: + - "1.11.x" + +jobs: + typos: + name: "Check for typos" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Check for typos" + uses: "crate-ci/typos@v1.23.6" + with: + files: "README.md src/" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 280f26b45b..390bfa4b7e 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -67,7 +67,7 @@ jobs: - name: "Downgrade PHPUnit" if: matrix.php-version == '7.2' - run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" + run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 composer/semver:^1.2 --update-with-dependencies --ignore-platform-reqs" - name: "PHPStan" run: "make phpstan" @@ -102,12 +102,12 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Cache Result cache" - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./tmp - key: "result-cache-v11-${{ matrix.php-version }}-${{ github.run_id }}" + key: "result-cache-v14-${{ matrix.php-version }}-${{ github.run_id }}" restore-keys: | - result-cache-v11-${{ matrix.php-version }}- + result-cache-v14-${{ matrix.php-version }}- - name: "PHPStan with result cache" run: | diff --git a/.github/workflows/tests-levels-matrix.php b/.github/workflows/tests-levels-matrix.php new file mode 100644 index 0000000000..1344e8e81e --- /dev/null +++ b/.github/workflows/tests-levels-matrix.php @@ -0,0 +1,40 @@ +testCaseClass as $testCaseClass) { + foreach($testCaseClass->testCaseMethod as $testCaseMethod) { + if ((string) $testCaseMethod['groups'] !== 'levels') { + continue; + } + + $testCaseName = (string) $testCaseMethod['id']; + + [$className, $testName] = explode('::', $testCaseName, 2); + $fileName = 'tests/'. str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; + + $filter = str_replace('\\', '\\\\', $testCaseName); + + $testFilters[] = sprintf("%s --filter %s", escapeshellarg($fileName), escapeshellarg($filter)); + } +} + +if ($testFilters === []) { + throw new RuntimeException('No tests found'); +} + +$chunkSize = (int) ceil(count($testFilters) / 10); +$chunks = array_chunk($testFilters, $chunkSize); + +$commands = []; +foreach ($chunks as $chunk) { + $commands[] = implode("\n", array_map(fn (string $ch) => sprintf('php vendor/bin/phpunit %s --group levels', $ch), $chunk)); +} + +echo json_encode($commands); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d620b90b0..f1a49d48f3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -98,11 +98,46 @@ jobs: - name: "Tests" run: "make tests-integration" + tests-levels-matrix: + name: "Determine levels tests matrix" + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=1G + + - name: "Install PHPUnit 10.x" + run: "composer remove --dev brianium/paratest && composer require --dev --with-all-dependencies phpunit/phpunit:^10" + + - id: set-matrix + run: echo "matrix=$(php .github/workflows/tests-levels-matrix.php)" >> $GITHUB_OUTPUT + + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + tests-levels: + needs: tests-levels-matrix + name: "Levels tests" runs-on: ubuntu-latest timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + script: "${{fromJson(needs.tests-levels-matrix.outputs.matrix)}}" + steps: - name: "Checkout" uses: actions/checkout@v4 @@ -111,7 +146,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.3" tools: pecl extensions: ds,mbstring ini-file: development @@ -121,7 +156,7 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Tests" - run: "make tests-levels" + run: "${{ matrix.script }}" tests-old-phpunit: name: "Tests with old PHPUnit" @@ -161,7 +196,7 @@ jobs: shell: bash - name: "Downgrade PHPUnit" - run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" + run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 composer/semver:^1.2 --update-with-dependencies --ignore-platform-reqs" - name: "Tests" run: "make tests-coverage" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index e2803f561e..f0fc56a9b1 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -16,7 +16,7 @@ jobs: - name: "Checkout" uses: actions/checkout@v4 with: - ref: 1.10.x + ref: 1.11.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP" @@ -39,7 +39,7 @@ jobs: run: "./bin/generate-function-metadata.php" - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} branch-suffix: random diff --git a/.gitignore b/.gitignore index f138e3cb50..47f19ba656 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /tests/.phpunit.result.cache /tests/PHPStan/Reflection/data/golden/ tmp/.memory_limit +e2e/bashunit diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000000..2687f239ed --- /dev/null +++ b/.typos.toml @@ -0,0 +1,10 @@ +[files] +extend-exclude = [ + ".git/", +] +ignore-hidden = false + +[default.extend-identifiers] +# Known typos +NonRemoveableTypeTrait = "NonRemoveableTypeTrait" +supportsLessOverridenParametersWithVariadic = "supportsLessOverridenParametersWithVariadic" diff --git a/Makefile b/Makefile index a1e21f23dd..6670f1cea2 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ tests-golden-reflection: lint: php vendor/bin/parallel-lint --colors \ --exclude tests/PHPStan/Analyser/data \ + --exclude tests/PHPStan/Analyser/nsrt \ --exclude tests/PHPStan/Rules/Methods/data \ --exclude tests/PHPStan/Rules/Functions/data \ --exclude tests/PHPStan/Rules/Names/data \ @@ -72,6 +73,8 @@ lint: --exclude tests/PHPStan/Rules/Keywords/data/declare-strict-nonsense.php \ --exclude tests/PHPStan/Rules/Keywords/data/declare-strict-nonsense-bool.php \ --exclude tests/PHPStan/Rules/Keywords/data/declare-inline-html.php \ + --exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \ + --exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \ src tests cs: @@ -95,8 +98,8 @@ phpstan-generate-baseline-php: phpstan-pro: php -d memory_limit=448M bin/phpstan --pro -composer-require-checker: - php build/composer-require-checker.phar check --config-file $(CURDIR)/build/composer-require-checker.json - name-collision: php vendor/bin/detect-collisions --configuration build/collision-detector.json + +composer-dependency-analyser: + php vendor/bin/composer-dependency-analyser --config build/composer-dependency-analyser.php diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index a63280eacd..838d2ecd5c 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -72,12 +72,14 @@ 'count' => ['hasSideEffects' => false], 'connection_aborted' => ['hasSideEffects' => true], 'connection_status' => ['hasSideEffects' => true], + 'error_log' => ['hasSideEffects' => true], 'fclose' => ['hasSideEffects' => true], 'fflush' => ['hasSideEffects' => true], 'fgetc' => ['hasSideEffects' => true], '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], @@ -153,6 +155,19 @@ 'DateTimeImmutable::getTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::getTimezone' => ['hasSideEffects' => false], + 'SplFileObject::fflush' => ['hasSideEffects' => true], + 'SplFileObject::fgetc' => ['hasSideEffects' => true], + 'SplFileObject::fgetcsv' => ['hasSideEffects' => true], + 'SplFileObject::fgets' => ['hasSideEffects' => true], + 'SplFileObject::fgetss' => ['hasSideEffects' => true], + 'SplFileObject::fpassthru' => ['hasSideEffects' => true], + 'SplFileObject::fputcsv' => ['hasSideEffects' => true], + 'SplFileObject::fread' => ['hasSideEffects' => true], + 'SplFileObject::fscanf' => ['hasSideEffects' => true], + 'SplFileObject::fseek' => ['hasSideEffects' => true], + 'SplFileObject::ftruncate' => ['hasSideEffects' => true], + 'SplFileObject::fwrite' => ['hasSideEffects' => true], + 'XmlReader::next' => ['hasSideEffects' => true], 'XmlReader::read' => ['hasSideEffects' => true], ]; diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 404e742aa6..f6f5131a53 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -85,6 +85,7 @@ public function enterNode(Node $node) 'random_int', 'connection_aborted', 'connection_status', + 'file_get_contents', ], true)) { continue; } diff --git a/bin/phpstan b/bin/phpstan index 537f3e123d..bb97758ff6 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -3,6 +3,7 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\ClearResultCacheCommand; +use PHPStan\Command\DiagnoseCommand; use PHPStan\Command\DumpParametersCommand; use PHPStan\Command\FixerWorkerCommand; use PHPStan\Command\WorkerCommand; @@ -24,6 +25,8 @@ use Symfony\Component\Console\Helper\ProgressBar; define('__PHPSTAN_RUNNING__', true); + $analysisStartTime = microtime(true); + $devOrPharLoader = require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../preload.php'; $composer = ComposerHelper::getComposerConfig(getcwd()); @@ -91,34 +94,30 @@ use Symfony\Component\Console\Helper\ProgressBar; require_once $autoloaderInWorkingDirectory; } - $autoloadProjectAutoloaderFile = function (string $file) use (&$composerAutoloaderProjectPaths): void { - $path = dirname(__DIR__) . $file; - if (!extension_loaded('phar')) { + $path = dirname(__DIR__, 3) . '/autoload.php'; + if (!extension_loaded('phar')) { + if (@is_file($path)) { + $composerAutoloaderProjectPaths[] = dirname($path, 2); + + require_once $path; + } + } else { + $pharPath = \Phar::running(false); + if ($pharPath === '') { if (@is_file($path)) { $composerAutoloaderProjectPaths[] = dirname($path, 2); require_once $path; } } else { - $pharPath = \Phar::running(false); - if ($pharPath === '') { - if (@is_file($path)) { - $composerAutoloaderProjectPaths[] = dirname($path, 2); - - require_once $path; - } - } else { - $path = dirname($pharPath) . $file; - if (@is_file($path)) { - $composerAutoloaderProjectPaths[] = dirname($path, 2); + $path = dirname($pharPath, 3) . '/autoload.php'; + if (@is_file($path)) { + $composerAutoloaderProjectPaths[] = dirname($path, 2); - require_once $path; - } + require_once $path; } } - }; - - $autoloadProjectAutoloaderFile('/../../autoload.php'); + } /** @var array|false $autoloadFunctionsAfter */ $autoloadFunctionsAfter = spl_autoload_functions(); @@ -160,11 +159,12 @@ use Symfony\Component\Console\Helper\ProgressBar; $application->setDefaultCommand('analyse'); ProgressBar::setFormatDefinition('file_download', ' [%bar%] %percent:3s%% %fileSize%'); - $reversedComposerAutoloaderProjectPaths = array_reverse($composerAutoloaderProjectPaths); - $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths)); + $reversedComposerAutoloaderProjectPaths = array_values(array_unique(array_reverse($composerAutoloaderProjectPaths))); + $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths, $analysisStartTime)); $application->add(new WorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new ClearResultCacheCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new FixerWorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new DumpParametersCommand($reversedComposerAutoloaderProjectPaths)); + $application->add(new DiagnoseCommand($reversedComposerAutoloaderProjectPaths)); $application->run(); })(); diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php new file mode 100644 index 0000000000..de637bfc79 --- /dev/null +++ b/build/composer-dependency-analyser.php @@ -0,0 +1,39 @@ +addPathToScan(__DIR__ . '/../bin', true) + ->ignoreErrorsOnPackages( + [ + ...$pinnedToSupportPhp72, // those are unused, but we need to pin them to support PHP 7.2 + ...$polyfills, // not detected by composer-dependency-analyser + ], + [ErrorType::UNUSED_DEPENDENCY], + ) + ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling + ->ignoreErrorsOnPackage('jetbrains/phpstorm-stubs', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) // there is no direct usage, but we need newer version then required by ondrejmirtes/BetterReflection + ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION]) // to be able to test invalid symbols + ->ignoreUnknownClasses([ + 'JetBrains\PhpStorm\Pure', // not present on composer's classmap + 'PHPStan\ExtensionInstaller\GeneratedConfig', // generated + ]); diff --git a/build/composer-require-checker.json b/build/composer-require-checker.json deleted file mode 100644 index 67fb8c0dff..0000000000 --- a/build/composer-require-checker.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "symbol-whitelist" : [ - "null", "true", "false", - "static", "self", "parent", - "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed", - "PHPUnit\\Framework\\TestCase", "PHPUnit\\Framework\\AssertionFailedError", "Composer\\Autoload\\ClassLoader", "PHPUnit\\Framework\\ExpectationFailedException", - "JSON_THROW_ON_ERROR", "JSON_INVALID_UTF8_IGNORE", "JsonSerializable", "SimpleXMLElement", "PHPStan\\ExtensionInstaller\\GeneratedConfig", "Nette\\DI\\InvalidConfigurationException", - "CURLOPT_SSL_VERIFYHOST", "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_ENCODED", "FILTER_SANITIZE_MAGIC_QUOTES", "FILTER_SANITIZE_NUMBER_FLOAT", - "FILTER_SANITIZE_NUMBER_INT", "FILTER_SANITIZE_SPECIAL_CHARS", "FILTER_SANITIZE_STRING", "FILTER_SANITIZE_URL", "FILTER_VALIDATE_BOOLEAN", - "FILTER_VALIDATE_EMAIL", "FILTER_VALIDATE_FLOAT", "FILTER_VALIDATE_INT", "FILTER_VALIDATE_IP", "FILTER_VALIDATE_MAC", "FILTER_VALIDATE_REGEXP", - "FILTER_VALIDATE_URL", "FILTER_NULL_ON_FAILURE", "FILTER_FORCE_ARRAY", "FILTER_SANITIZE_ADD_SLASHES", "FILTER_DEFAULT", "FILTER_UNSAFE_RAW", "opcache_invalidate", "ValueError", "ReflectionUnionType", "ReflectionIntersectionType", "Attribute", "ReflectionEnum", "ReflectionEnumBackedCase", "enum_exists", - "React\\Async\\await", "Hoa\\File\\Read" - ], - "php-core-extensions" : [ - "json", - "Core", - "date", - "pcre", - "Phar", - "Reflection", - "SPL", - "standard", - "mbstring", - "hash", - "tokenizer", - "dom" - ] -} diff --git a/build/composer-require-checker.phar b/build/composer-require-checker.phar deleted file mode 100644 index 12b80f423a..0000000000 Binary files a/build/composer-require-checker.phar and /dev/null differ diff --git a/build/datetime-php-83.neon b/build/datetime-php-83.neon new file mode 100644 index 0000000000..953379bf4f --- /dev/null +++ b/build/datetime-php-83.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^If condition is always false\\.$#" + count: 1 + path: ../src/Type/Php/DateTimeModifyReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between DateTime and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/DateTimeModifyReturnTypeExtension.php diff --git a/build/downgrade.php b/build/downgrade.php index b9b3f182f5..a440588e85 100644 --- a/build/downgrade.php +++ b/build/downgrade.php @@ -10,6 +10,7 @@ 'tests/*/data/*', 'tests/*/Fixture/*', 'tests/PHPStan/Analyser/traits/*', + 'tests/PHPStan/Analyser/nsrt/*', 'tests/PHPStan/Generics/functions.php', 'tests/e2e/resultCache_1.php', 'tests/e2e/resultCache_2.php', diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index cd049179a5..62de0070e5 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -34,8 +34,14 @@ $includes[] = __DIR__ . '/spl-autoload-functions-php-8.neon'; } +if (PHP_VERSION_ID >= 80300) { + $includes[] = __DIR__ . '/datetime-php-83.neon'; +} + $config = []; $config['includes'] = $includes; + +// overrides config.platform.php in composer.json $config['parameters']['phpVersion'] = PHP_VERSION_ID; return $config; diff --git a/build/phpstan.neon b/build/phpstan.neon index 511d5735c7..bd0a5c6422 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -22,16 +22,13 @@ parameters: checkUninitializedProperties: true checkMissingCallableSignature: true excludePaths: - - ../src/Reflection/SignatureMap/functionMap.php - - ../src/Reflection/SignatureMap/functionMetadata.php - ../tests/*/data/* - ../tests/tmp/* + - ../tests/PHPStan/Analyser/nsrt/* - ../tests/PHPStan/Analyser/traits/* - ../tests/notAutoloaded/* - - ../tests/PHPStan/Generics/functions.php - ../tests/PHPStan/Reflection/UnionTypesTest.php - ../tests/PHPStan/Reflection/MixedTypeTest.php - - ../tests/PHPStan/Reflection/StaticTypeTest.php - ../tests/e2e/magic-setter/* - ../tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php - ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -87,6 +84,10 @@ parameters: message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" count: 1 path: ../src/Command/CommandHelper.php + - + message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" + count: 1 + path: ../src/Diagnose/PHPStanDiagnoseExtension.php - '#^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 @@ -95,6 +96,8 @@ parameters: - stubs/ReactChildProcess.stub - stubs/ReactStreams.stub - stubs/NetteDIContainer.stub + - stubs/PhpParserName.stub + services: - class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension diff --git a/build/spl-autoload-functions-pre-php-7.neon b/build/spl-autoload-functions-pre-php-7.neon index a34657f54b..abafcfbf08 100644 --- a/build/spl-autoload-functions-pre-php-7.neon +++ b/build/spl-autoload-functions-pre-php-7.neon @@ -4,3 +4,7 @@ parameters: 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/PhpParserName.stub b/build/stubs/PhpParserName.stub new file mode 100644 index 0000000000..a044fbf684 --- /dev/null +++ b/build/stubs/PhpParserName.stub @@ -0,0 +1,25 @@ +|self $name Name as string, part array or Name instance (copy ctor) + * @param array $attributes Additional attributes + */ + public function __construct($name, array $attributes = []) { + } + + /** @return non-empty-string */ + public function toString() : string { + } + + /** @return non-empty-string */ + public function toCodeString() : string { + } +} diff --git a/compiler/src/Console/PrepareCommand.php b/compiler/src/Console/PrepareCommand.php index 42cb964062..f7f2b6cdf6 100644 --- a/compiler/src/Console/PrepareCommand.php +++ b/compiler/src/Console/PrepareCommand.php @@ -64,7 +64,7 @@ private function fixComposerJson(string $buildDir): void unset($json['replace']); $json['name'] = 'phpstan/phpstan'; - $json['require']['php'] = '^7.2'; + $json['require']['php'] = '^7.2|^8.0'; // simplify autoload (remove not packed build directory] $json['autoload']['psr-4']['PHPStan\\'] = 'src/'; diff --git a/composer.json b/composer.json index 2c33c85cd4..58a4c837ce 100644 --- a/composer.json +++ b/composer.json @@ -9,29 +9,30 @@ "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", "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", - "hoa/regex": "1.17.01.13", - "jetbrains/phpstorm-stubs": "dev-master#50172be1abfe13a1b9b8fbc46b115d531250d80f", + "hoa/file": "1.17.07.11", + "jetbrains/phpstorm-stubs": "dev-master#bdc8acd7c04c0c87197849c7cdd27e44b67b56c7", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", - "nette/finder": "^2.5", "nette/neon": "^3.3.1", "nette/schema": "^1.2.2", "nette/utils": "^3.2.5", "nikic/php-parser": "^4.17.1", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.21.0", - "phpstan/php-8-stubs": "0.3.82", - "phpstan/phpdoc-parser": "1.25.0", + "ondrejmirtes/better-reflection": "6.25.0.13", + "phpstan/php-8-stubs": "0.3.95", + "phpstan/phpdoc-parser": "1.29.1", + "psr/http-message": "^1.1", "react/async": "^3", "react/child-process": "^0.6.4", "react/dns": "^1.10", "react/event-loop": "^1.2", "react/http": "^1.1", - "react/promise": "^2.8", + "react/promise": "^3.2", "react/socket": "^1.3", "react/stream": "^1.1", "symfony/console": "^5.4.3", @@ -53,6 +54,7 @@ "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", @@ -60,6 +62,7 @@ "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "^9.5.4", + "shipmonk/composer-dependency-analyser": "^1.5", "shipmonk/name-collision-detector": "^2.0" }, "config": { @@ -75,12 +78,17 @@ "extra": { "composer-exit-on-patch-failure": true, "patches": { + "composer/ca-bundle": [ + "patches/cloudflare-ca.patch" + ], "hoa/iterator": [ "patches/Buffer.patch", "patches/Lookahead.patch" ], "hoa/compiler": [ - "patches/Rule.patch" + "patches/HoaException.patch", + "patches/Rule.patch", + "patches/Lexer.patch" ], "hoa/consistency": [ "patches/Consistency.patch" @@ -95,7 +103,12 @@ "jetbrains/phpstorm-stubs": [ "patches/PDO.patch", "patches/ReflectionProperty.patch", - "patches/SessionHandler.patch" + "patches/SessionHandler.patch", + "patches/xmlreader.patch", + "patches/dom_c.patch" + ], + "nette/di": [ + "patches/DependencyChecker.patch" ] } }, @@ -118,6 +131,83 @@ "tests/PHPStan" ] }, + "repositories": [ + { + "type": "package", + "package": { + "name": "nette/di", + "version": "v3.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2 || ~4.0.0", + "nette/schema": "^1.2", + "nette/utils": "^3.2.5 || ~4.0.0", + "php": "7.2 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com/" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.", + "homepage": "https://nette.org/", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "support": { + "issues": "https://github.com/nette/di/issues", + "source": "https://github.com/nette/di/tree/v3.1.5" + }, + "time": "2023-10-02T19:58:38+00:00" + } + } + ], "minimum-stability": "dev", "prefer-stable": true, "bin": [ diff --git a/composer.lock b/composer.lock index 84562ce52d..cbed841d93 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": "df6c2c42501959b3ad03f0e6279de391", + "content-hash": "db22a3caebfecfcc88bd82e014d25b19", "packages": [ { "name": "clue/ndjson-react", @@ -72,28 +72,28 @@ }, { "name": "composer/ca-bundle", - "version": "1.3.7", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "76e46335014860eec1aa5a724799a00a2e47cc85" + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/76e46335014860eec1aa5a724799a00a2e47cc85", - "reference": "76e46335014860eec1aa5a724799a00a2e47cc85", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan": "^1.10", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -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.3.7" + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" }, "funding": [ { @@ -144,20 +144,20 @@ "type": "tidelift" } ], - "time": "2023-08-30T09:31:38+00:00" + "time": "2024-03-15T14:00:32+00:00" }, { "name": "composer/pcre", - "version": "3.1.0", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", - "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", "shasum": "" }, "require": { @@ -199,7 +199,88 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.0" + "source": "https://github.com/composer/pcre/tree/3.1.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-03-19T10:26:25+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "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.0" }, "funding": [ { @@ -215,20 +296,20 @@ "type": "tidelift" } ], - "time": "2022-11-17T09:50:14+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { "name": "composer/xdebug-handler", - "version": "3.0.3", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -239,7 +320,7 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -263,9 +344,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -281,32 +362,32 @@ "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -326,9 +407,9 @@ ], "support": { "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "time": "2017-07-23T21:35:13+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { "name": "fidry/cpu-core-counter", @@ -1353,20 +1434,19 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "50172be1abfe13a1b9b8fbc46b115d531250d80f" + "reference": "bdc8acd7c04c0c87197849c7cdd27e44b67b56c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/50172be1abfe13a1b9b8fbc46b115d531250d80f", - "reference": "50172be1abfe13a1b9b8fbc46b115d531250d80f", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/bdc8acd7c04c0c87197849c7cdd27e44b67b56c7", + "reference": "bdc8acd7c04c0c87197849c7cdd27e44b67b56c7", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "@stable", - "nikic/php-parser": "@stable", - "php": "^8.3", - "phpdocumentor/reflection-docblock": "@stable", - "phpunit/phpunit": "@stable" + "friendsofphp/php-cs-fixer": "v3.46.0", + "nikic/php-parser": "v5.0.0", + "phpdocumentor/reflection-docblock": "5.3.0", + "phpunit/phpunit": "10.5.5" }, "default-branch": true, "type": "library", @@ -1394,7 +1474,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-01-02T11:04:54+00:00" + "time": "2024-07-05T11:52:49+00:00" }, { "name": "nette/bootstrap", @@ -1968,21 +2048,21 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v4.19.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.1" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", @@ -2018,9 +2098,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "ondram/ci-detector", @@ -2096,21 +2176,21 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.21.0", + "version": "6.25.0.13", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "79f87b0a5eaf8c0e074c09baec9470d5370fd476" + "reference": "ee473c36242850418a8bf372961ab3d9ec0ca234" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/79f87b0a5eaf8c0e074c09baec9470d5370fd476", - "reference": "79f87b0a5eaf8c0e074c09baec9470d5370fd476", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/ee473c36242850418a8bf372961ab3d9ec0ca234", + "reference": "ee473c36242850418a8bf372961ab3d9ec0ca234", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#7b055d8634d2143a909f2c5141ec70c82b8b9254", + "jetbrains/phpstorm-stubs": "dev-master#217ed9356d07ef89109d3cd7d8c5df10aab4b0d4", "nikic/php-parser": "^4.18.0", "php": "^7.2 || ^8.0" }, @@ -2119,11 +2199,11 @@ }, "require-dev": { "doctrine/coding-standard": "^12.0.0", - "phpstan/phpstan": "^1.10.50", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpunit/phpunit": "^10.5.5", + "phpstan/phpstan": "^1.10.60", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpunit/phpunit": "^10.5.12", "rector/rector": "0.14.3", - "vimeo/psalm": "5.18.0" + "vimeo/psalm": "5.23.0" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" @@ -2162,22 +2242,22 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.21.0" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.13" }, - "time": "2024-01-04T17:00:26+00:00" + "time": "2024-08-03T11:36:12+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.3.82", + "version": "0.3.95", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "b9ba35e299a7ef357f0d9426edc4b3a65bf9421d" + "reference": "1e2422fdfc9da3e96bc1038eaf42728025d24756" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/b9ba35e299a7ef357f0d9426edc4b3a65bf9421d", - "reference": "b9ba35e299a7ef357f0d9426edc4b3a65bf9421d", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/1e2422fdfc9da3e96bc1038eaf42728025d24756", + "reference": "1e2422fdfc9da3e96bc1038eaf42728025d24756", "shasum": "" }, "type": "library", @@ -2194,22 +2274,22 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.82" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.95" }, - "time": "2023-12-04T00:15:44+00:00" + "time": "2024-08-12T00:18:17+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -2241,9 +2321,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.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "psr/container", @@ -2295,25 +2375,25 @@ }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -2342,36 +2422,36 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", - "version": "1.1.3", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2381,7 +2461,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -2392,22 +2472,22 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/2.0.0" }, - "time": "2020-03-23T09:12:05+00:00" + "time": "2021-07-14T16:41:46+00:00" }, { "name": "react/async", - "version": "v3.0.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/async.git", - "reference": "3c3b812be77aec14bf8300b052ba589c9a5bc95b" + "reference": "bc3ef672b33e95bf814fe8377731e46888ed4b54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/async/zipball/3c3b812be77aec14bf8300b052ba589c9a5bc95b", - "reference": "3c3b812be77aec14bf8300b052ba589c9a5bc95b", + "url": "https://api.github.com/repos/reactphp/async/zipball/bc3ef672b33e95bf814fe8377731e46888ed4b54", + "reference": "bc3ef672b33e95bf814fe8377731e46888ed4b54", "shasum": "" }, "require": { @@ -2416,7 +2496,8 @@ "react/promise": "^3.0 || ^2.8 || ^1.2.1" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^7.5" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { @@ -2457,19 +2538,15 @@ ], "support": { "issues": "https://github.com/reactphp/async/issues", - "source": "https://github.com/reactphp/async/tree/v3.0.0" + "source": "https://github.com/reactphp/async/tree/v3.2.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-07-11T14:17:23+00:00" + "time": "2023-11-22T16:21:11+00:00" }, { "name": "react/cache", @@ -2624,33 +2701,33 @@ }, { "name": "react/dns", - "version": "v1.10.0", + "version": "v1.13.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "a5427e7dfa47713e438016905605819d101f238c" + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/a5427e7dfa47713e438016905605819d101f238c", - "reference": "a5427e7dfa47713e438016905605819d101f238c", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", "shasum": "" }, "require": { "php": ">=5.3.0", "react/cache": "^1.0 || ^0.6 || ^0.5", "react/event-loop": "^1.2", - "react/promise": "^3.0 || ^2.7 || ^1.2.1", - "react/promise-timer": "^1.9" + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^4.8.35", - "react/async": "^4 || ^3 || ^2" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Dns\\": "src" + "React\\Dns\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2688,32 +2765,28 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.10.0" + "source": "https://github.com/reactphp/dns/tree/v1.13.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-08T12:22:46+00:00" + "time": "2024-06-13T14:18:03+00:00" }, { "name": "react/event-loop", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05" + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6e7e587714fff7a83dcc7025aee42ab3b265ae05", - "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { @@ -2764,7 +2837,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.4.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, "funding": [ { @@ -2772,20 +2845,20 @@ "type": "open_collective" } ], - "time": "2023-05-05T10:11:24+00:00" + "time": "2023-11-13T13:48:05+00:00" }, { "name": "react/http", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/reactphp/http.git", - "reference": "bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0" + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0", - "reference": "bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0", + "url": "https://api.github.com/repos/reactphp/http/zipball/8111281ee57f22b7194f5dba225e609ba7ce4d20", + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20", "shasum": "" }, "require": { @@ -2796,14 +2869,13 @@ "react/event-loop": "^1.2", "react/promise": "^3 || ^2.3 || ^1.2.1", "react/socket": "^1.12", - "react/stream": "^1.2", - "ringcentral/psr7": "^1.2" + "react/stream": "^1.2" }, "require-dev": { "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" @@ -2856,7 +2928,7 @@ ], "support": { "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.9.0" + "source": "https://github.com/reactphp/http/tree/v1.10.0" }, "funding": [ { @@ -2864,27 +2936,28 @@ "type": "open_collective" } ], - "time": "2023-04-26T10:29:24+00:00" + "time": "2024-03-27T17:20:46+00:00" }, { "name": "react/promise", - "version": "v2.10.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", - "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { @@ -2928,7 +3001,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.10.0" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { @@ -2936,123 +3009,40 @@ "type": "open_collective" } ], - "time": "2023-05-02T15:15:43+00:00" - }, - { - "name": "react/promise-timer", - "version": "v1.9.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-timer.git", - "reference": "aa7a73c74b8d8c0f622f5982ff7b0351bc29e495" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/aa7a73c74b8d8c0f622f5982ff7b0351bc29e495", - "reference": "aa7a73c74b8d8c0f622f5982ff7b0351bc29e495", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/event-loop": "^1.2", - "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\Timer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", - "homepage": "https://github.com/reactphp/promise-timer", - "keywords": [ - "async", - "event-loop", - "promise", - "reactphp", - "timeout", - "timer" - ], - "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.9.0" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2022-06-13T13:41:03+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "react/socket", - "version": "v1.12.0", + "version": "v1.15.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "81e1b4d7f5450ebd8d2e9a95bb008bb15ca95a7b" + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/81e1b4d7f5450ebd8d2e9a95bb008bb15ca95a7b", - "reference": "81e1b4d7f5450ebd8d2e9a95bb008bb15ca95a7b", + "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/dns": "^1.8", + "react/dns": "^1.11", "react/event-loop": "^1.2", "react/promise": "^3 || ^2.6 || ^1.2.1", - "react/promise-timer": "^1.9", "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2", - "react/promise-stream": "^1.4" + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.10" }, "type": "library", "autoload": { "psr-4": { - "React\\Socket\\": "src" + "React\\Socket\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3091,32 +3081,28 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.12.0" + "source": "https://github.com/reactphp/socket/tree/v1.15.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-08-25T12:32:25+00:00" + "time": "2023-12-15T11:02:10+00:00" }, { "name": "react/stream", - "version": "v1.2.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/reactphp/stream.git", - "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9" + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/7a423506ee1903e89f1e08ec5f0ed430ff784ae9", - "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { @@ -3126,12 +3112,12 @@ }, "require-dev": { "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { "psr-4": { - "React\\Stream\\": "src" + "React\\Stream\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3173,93 +3159,28 @@ ], "support": { "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.2.0" + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2021-07-11T12:37:55+00:00" - }, - { - "name": "ringcentral/psr7", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/ringcentral/psr7.git", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ringcentral/psr7/zipball/360faaec4b563958b673fb52bbe94e37f14bc686", - "reference": "360faaec4b563958b673fb52bbe94e37f14bc686", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "RingCentral\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "description": "PSR-7 message implementation", - "keywords": [ - "http", - "message", - "stream", - "uri" - ], - "support": { - "source": "https://github.com/ringcentral/psr7/tree/master" - }, - "time": "2018-05-29T20:21:04+00:00" + "time": "2024-06-11T12:45:25+00:00" }, { "name": "symfony/console", - "version": "v5.4.28", + "version": "v5.4.41", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f4f71842f24c2023b91237c72a365306f3c58827" + "reference": "6473d441a913cb997123b59ff2dbe3d1cf9e11ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827", - "reference": "f4f71842f24c2023b91237c72a365306f3c58827", + "url": "https://api.github.com/repos/symfony/console/zipball/6473d441a913cb997123b59ff2dbe3d1cf9e11ba", + "reference": "6473d441a913cb997123b59ff2dbe3d1cf9e11ba", "shasum": "" }, "require": { @@ -3329,7 +3250,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.28" + "source": "https://github.com/symfony/console/tree/v5.4.41" }, "funding": [ { @@ -3345,20 +3266,20 @@ "type": "tidelift" } ], - "time": "2023-08-07T06:12:30+00:00" + "time": "2024-06-28T07:48:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -3367,7 +3288,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -3396,7 +3317,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.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -3412,20 +3333,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/finder", - "version": "v5.4.27", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d" + "reference": "f51cff4687547641c7d8180d74932ab40b2205ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d", - "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d", + "url": "https://api.github.com/repos/symfony/finder/zipball/f51cff4687547641c7d8180d74932ab40b2205ce", + "reference": "f51cff4687547641c7d8180d74932ab40b2205ce", "shasum": "" }, "require": { @@ -3459,7 +3380,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.27" + "source": "https://github.com/symfony/finder/tree/v5.4.40" }, "funding": [ { @@ -3475,20 +3396,20 @@ "type": "tidelift" } ], - "time": "2023-07-31T08:02:31+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -3502,9 +3423,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3541,7 +3459,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -3557,20 +3475,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -3581,9 +3499,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3622,7 +3537,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -3638,20 +3553,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -3662,9 +3577,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3706,7 +3618,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -3722,20 +3634,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -3749,9 +3661,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3789,7 +3698,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -3805,20 +3714,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" + "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", "shasum": "" }, "require": { @@ -3826,9 +3735,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3868,7 +3774,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" }, "funding": [ { @@ -3884,20 +3790,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php74", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php74.git", - "reference": "8b755b41a155c89f1af29cc33305538499fa05ea" + "reference": "37f1d1a2fb3ebc494f9f9b0f7e92064b43332321" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/8b755b41a155c89f1af29cc33305538499fa05ea", - "reference": "8b755b41a155c89f1af29cc33305538499fa05ea", + "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/37f1d1a2fb3ebc494f9f9b0f7e92064b43332321", + "reference": "37f1d1a2fb3ebc494f9f9b0f7e92064b43332321", "shasum": "" }, "require": { @@ -3905,9 +3811,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3948,7 +3851,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php74/tree/v1.30.0" }, "funding": [ { @@ -3964,20 +3867,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -3985,9 +3888,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4031,7 +3931,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -4047,20 +3947,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" + "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", - "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", + "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", "shasum": "" }, "require": { @@ -4068,9 +3968,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4110,7 +4007,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" }, "funding": [ { @@ -4126,20 +4023,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/process", - "version": "v5.4.28", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", - "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "shasum": "" }, "require": { @@ -4172,7 +4069,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.28" + "source": "https://github.com/symfony/process/tree/v5.4.40" }, "funding": [ { @@ -4188,20 +4085,20 @@ "type": "tidelift" } ], - "time": "2023-08-07T10:36:04+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", "shasum": "" }, "require": { @@ -4255,7 +4152,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" }, "funding": [ { @@ -4271,20 +4168,20 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:29+00:00" + "time": "2023-04-21T15:04:16+00:00" }, { "name": "symfony/string", - "version": "v5.4.26", + "version": "v5.4.41", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1181fe9270e373537475e826873b5867b863883c" + "reference": "065a9611e0b1fd2197a867e1fb7f2238191b7096" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", - "reference": "1181fe9270e373537475e826873b5867b863883c", + "url": "https://api.github.com/repos/symfony/string/zipball/065a9611e0b1fd2197a867e1fb7f2238191b7096", + "reference": "065a9611e0b1fd2197a867e1fb7f2238191b7096", "shasum": "" }, "require": { @@ -4341,7 +4238,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.26" + "source": "https://github.com/symfony/string/tree/v5.4.41" }, "funding": [ { @@ -4357,7 +4254,7 @@ "type": "tidelift" } ], - "time": "2023-06-28T12:46:07+00:00" + "time": "2024-06-28T09:20:55+00:00" } ], "packages-dev": [ @@ -4692,16 +4589,16 @@ }, { "name": "ondrejmirtes/simple-downgrader", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/simple-downgrader.git", - "reference": "6a8d0f0c1a4fae734f03161d3d19d7437058ca0b" + "reference": "832aaae53dcfe358f63180494de8734244773d46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/6a8d0f0c1a4fae734f03161d3d19d7437058ca0b", - "reference": "6a8d0f0c1a4fae734f03161d3d19d7437058ca0b", + "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/832aaae53dcfe358f63180494de8734244773d46", + "reference": "832aaae53dcfe358f63180494de8734244773d46", "shasum": "" }, "require": { @@ -4735,9 +4632,9 @@ "description": "Simple Downgrader", "support": { "issues": "https://github.com/ondrejmirtes/simple-downgrader/issues", - "source": "https://github.com/ondrejmirtes/simple-downgrader/tree/1.0.1" + "source": "https://github.com/ondrejmirtes/simple-downgrader/tree/1.0.2" }, - "time": "2024-01-07T07:49:43+00:00" + "time": "2024-02-12T19:22:32+00:00" }, { "name": "phar-io/manifest", @@ -4852,16 +4749,16 @@ }, { "name": "php-parallel-lint/php-parallel-lint", - "version": "v1.3.2", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", - "reference": "6483c9832e71973ed29cf71bd6b3f4fde438a9de" + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6483c9832e71973ed29cf71bd6b3f4fde438a9de", - "reference": "6483c9832e71973ed29cf71bd6b3f4fde438a9de", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", "shasum": "" }, "require": { @@ -4899,26 +4796,30 @@ "email": "ahoj@jakubonderka.cz" } ], - "description": "This tool check syntax of PHP files about 20x faster than serial check.", + "description": "This tool checks the syntax of PHP files about 20x faster than serial check.", "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", + "keywords": [ + "lint", + "static analysis" + ], "support": { "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", - "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.3.2" + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0" }, - "time": "2022-02-21T12:50:22+00:00" + "time": "2024-03-27T12:14:49+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.2.x-dev", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "788ea1bd84f7848abf27ba29b92c6c9d285dfc95" + "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/788ea1bd84f7848abf27ba29b92c6c9d285dfc95", - "reference": "788ea1bd84f7848abf27ba29b92c6c9d285dfc95", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/fa8cce7720fa782899a0aa97b6a41225d1bb7b26", + "reference": "fa8cce7720fa782899a0aa97b6a41225d1bb7b26", "shasum": "" }, "require": { @@ -4930,7 +4831,6 @@ "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^9.5" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -4951,27 +4851,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.x" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.0" }, - "time": "2023-09-19T08:17:29+00:00" + "time": "2024-04-20T06:39:48+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "1.2.9", + "version": "1.3.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "0e3a6805917811d685e59bb83c2286315f2f6d78" + "reference": "bc74b8b208b47f163fe55708fcf1a0333247fa79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/0e3a6805917811d685e59bb83c2286315f2f6d78", - "reference": "0e3a6805917811d685e59bb83c2286315f2f6d78", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/bc74b8b208b47f163fe55708fcf1a0333247fa79", + "reference": "bc74b8b208b47f163fe55708fcf1a0333247fa79", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.11.11" }, "conflict": { "nette/application": "<2.3.0", @@ -4987,10 +4887,9 @@ "nette/utils": "^2.3.0 || ^3.0.0", "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-php-parser": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "~9.5.28" }, "type": "phpstan-extension", "extra": { @@ -5013,27 +4912,27 @@ "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.2.9" + "source": "https://github.com/phpstan/phpstan-nette/tree/1.3.8" }, - "time": "2023-04-12T14:11:53+00:00" + "time": "2024-08-25T12:11:12+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.15", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" + "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", - "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f3ea021866f4263f07ca3636bf22c64be9610c11", + "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.11" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -5065,22 +4964,22 @@ "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.15" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.0" }, - "time": "2023-10-09T18:58:39+00:00" + "time": "2024-04-20T06:39:00+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.6.x-dev", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "a3b0404c40197996b6ed32b2613e5a337fcbefd4" + "reference": "363f921dd8441777d4fc137deb99beb486c77df1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a3b0404c40197996b6ed32b2613e5a337fcbefd4", - "reference": "a3b0404c40197996b6ed32b2613e5a337fcbefd4", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/363f921dd8441777d4fc137deb99beb486c77df1", + "reference": "363f921dd8441777d4fc137deb99beb486c77df1", "shasum": "" }, "require": { @@ -5094,7 +4993,6 @@ "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^9.5" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -5115,9 +5013,9 @@ "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.x" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.0" }, - "time": "2023-10-30T14:35:14+00:00" + "time": "2024-04-20T06:37:51+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6500,21 +6398,88 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "shipmonk/composer-dependency-analyser", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/bca862b2830a453734aee048eb0cdab82e5c9da3", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "ext-dom": "*", + "ext-libxml": "*", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.10.63", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/name-collision-detector": "^2.0.0", + "slevomat/coding-standard": "^8.0.1" + }, + "bin": [ + "bin/composer-dependency-analyser" + ], + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", + "keywords": [ + "analyser", + "composer", + "composer dependency", + "dead code", + "dead dependency", + "detector", + "dev", + "misplaced dependency", + "shadow dependency", + "static analysis", + "unused code", + "unused dependency" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.7.0" + }, + "time": "2024-08-08T08:12:32+00:00" + }, { "name": "shipmonk/name-collision-detector", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/shipmonk-rnd/name-collision-detector.git", - "reference": "322993a0b057457ab363929c3ca37bce6eb4affb" + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/shipmonk-rnd/name-collision-detector/zipball/322993a0b057457ab363929c3ca37bce6eb4affb", - "reference": "322993a0b057457ab363929c3ca37bce6eb4affb", + "url": "https://api.github.com/repos/shipmonk-rnd/name-collision-detector/zipball/e8c8267a9a3774450b64f4cbf0bb035108e78f07", + "reference": "e8c8267a9a3774450b64f4cbf0bb035108e78f07", "shasum": "" }, "require": { + "ext-json": "*", "ext-tokenizer": "*", "nette/schema": "^1.1.0", "php": "^7.2 || ^8.0" @@ -6526,6 +6491,7 @@ "phpstan/phpstan-phpunit": "^1.1.1", "phpstan/phpstan-strict-rules": "^1.2.3", "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/composer-dependency-analyser": "^1.0.0", "slevomat/coding-standard": "^8.0.1" }, "bin": [ @@ -6552,9 +6518,9 @@ ], "support": { "issues": "https://github.com/shipmonk-rnd/name-collision-detector/issues", - "source": "https://github.com/shipmonk-rnd/name-collision-detector/tree/2.1.0" + "source": "https://github.com/shipmonk-rnd/name-collision-detector/tree/2.1.1" }, - "time": "2023-10-09T12:15:58+00:00" + "time": "2024-03-01T13:26:32+00:00" }, { "name": "theseer/tokenizer", diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 948b35d9cc..a924d8222a 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -7,6 +7,7 @@ parameters: explicitMixedViaIsArray: true arrayFilter: true arrayUnpacking: true + arrayValues: true nodeConnectingVisitorCompatibility: false nodeConnectingVisitorRule: true disableCheckMissingIterableValueType: true @@ -29,6 +30,7 @@ parameters: alwaysCheckTooWideReturnTypeFinalMethods: true duplicateStubs: true logicalXor: true + betterNoop: true invarianceComposition: true alwaysTrueAlwaysReported: true disableUnreachableBranchesRules: true @@ -45,8 +47,18 @@ parameters: 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 diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 08a2153f0a..8052e5f5de 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -24,6 +24,12 @@ conditionalTags: 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% rules: - PHPStan\Rules\Api\ApiInstantiationRule @@ -201,6 +207,7 @@ services: tags: - phpstan.rules.rule arguments: + bleedingEdge: %featureToggles.bleedingEdge% checkThisOnly: %checkThisOnly% - @@ -293,3 +300,12 @@ services: - class: PHPStan\Rules\Constants\MagicConstantContextRule + + - + class: PHPStan\Rules\Functions\UselessFunctionReturnValueRule + + - + class: PHPStan\Rules\Functions\PrintfArrayParametersRule + + - + class: PHPStan\Rules\Regexp\RegularExpressionQuotingRule diff --git a/conf/config.level2.neon b/conf/config.level2.neon index a4487cfd1d..907c83e394 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -24,13 +24,12 @@ rules: - 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\InvalidBinaryOperationRule - - PHPStan\Rules\Operators\InvalidUnaryOperationRule - PHPStan\Rules\Operators\InvalidComparisonOperationRule - PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule - PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule @@ -61,6 +60,10 @@ conditionalTags: 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% services: - @@ -128,3 +131,20 @@ services: 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 diff --git a/conf/config.level3.neon b/conf/config.level3.neon index 286f456be6..5540500714 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -8,6 +8,10 @@ conditionalTags: 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 @@ -92,3 +96,9 @@ services: - 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 81752faae9..cb79c9cca5 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -8,7 +8,6 @@ rules: - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule - PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\CallToMethodStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\CallToStaticMethodStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\NullsafeMethodCallRule @@ -27,6 +26,34 @@ conditionalTags: 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% parameters: checkAdvancedIsset: true @@ -70,10 +97,43 @@ services: - class: PHPStan\Rules\DeadCode\NoopRule arguments: - logicalXor: %featureToggles.logicalXor% + 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: @@ -138,6 +198,9 @@ services: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + - + class: PHPStan\Rules\DeadCode\BetterNoopRule + - class: PHPStan\Rules\Comparison\MatchExpressionRule arguments: @@ -208,6 +271,13 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule + arguments: + reportNoConstructor: %featureToggles.pure% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule arguments: @@ -237,3 +307,9 @@ services: reportUncheckedExceptionDeadCatch: %exceptions.reportUncheckedExceptionDeadCatch% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\TooWideTypehints\TooWideFunctionParameterOutTypeRule + + - + class: PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 842cd90f5b..184cee83b8 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -8,12 +8,19 @@ parameters: 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% rules: - PHPStan\Rules\DateTimeInstantiationRule - - PHPStan\Rules\Functions\ImplodeFunctionRule services: - @@ -28,5 +35,22 @@ services: 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 diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 05f3616832..545fac6ad2 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -9,8 +9,21 @@ parameters: rules: - PHPStan\Rules\Constants\MissingClassConstantTypehintRule - - PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule - PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule - - PHPStan\Rules\Methods\MissingMethodParameterTypehintRule - PHPStan\Rules\Methods\MissingMethodReturnTypehintRule - PHPStan\Rules\Properties\MissingPropertyTypehintRule + +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 diff --git a/conf/config.neon b/conf/config.neon index 5baed2912b..96e073cffb 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -41,6 +41,7 @@ parameters: explicitMixedViaIsArray: false arrayFilter: false arrayUnpacking: false + arrayValues: false nodeConnectingVisitorCompatibility: true nodeConnectingVisitorRule: false illegalConstructorMethodCall: false @@ -64,6 +65,7 @@ parameters: alwaysCheckTooWideReturnTypeFinalMethods: false duplicateStubs: false logicalXor: false + betterNoop: false invarianceComposition: false alwaysTrueAlwaysReported: false disableUnreachableBranchesRules: false @@ -80,9 +82,19 @@ parameters: 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 fileExtensions: - php checkAdvancedIsset: false @@ -122,6 +134,9 @@ parameters: reportMaybesInPropertyPhpDocTypes: false reportStaticMethodSignatures: false reportWrongPhpDocTypeInVarTag: false + reportAnyTypeWideningInVarTag: false + reportPossiblyNonexistentGeneralArrayOffset: false + reportPossiblyNonexistentConstantArrayOffset: false checkMissingOverrideMethodAttribute: false mixinExcludeClasses: [] scanFiles: [] @@ -255,6 +270,7 @@ extensions: conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension + validateExcludePaths: PHPStan\DependencyInjection\ValidateExcludePathsExtension rules: - PHPStan\Rules\Debug\DumpTypeRule @@ -275,6 +291,12 @@ conditionalTags: 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% services: - @@ -289,6 +311,11 @@ services: options: preserveOriginalNames: true + - + class: PHPStan\Parser\AnonymousClassVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\ArrayFilterArgVisitor tags: @@ -309,6 +336,16 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ClosureBindToVarVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ClosureBindArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\CurlSetOptArgVisitor @@ -432,11 +469,16 @@ services: tags: - phpstan.stubFilesExtension + - + class: PHPStan\PhpDoc\SocketSelectStubFilesExtension + tags: + - phpstan.stubFilesExtension + - class: PHPStan\PhpDoc\DefaultStubFilesProvider arguments: stubFiles: %stubFiles% - currentWorkingDirectory: %currentWorkingDirectory% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% autowired: - PHPStan\PhpDoc\StubFilesProvider @@ -455,11 +497,18 @@ services: arguments: internalErrorsCountLimit: %internalErrorsCountLimit% + - + class: PHPStan\Analyser\AnalyserResultFinalizer + arguments: + reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% + - class: PHPStan\Analyser\FileAnalyser arguments: parser: @defaultAnalysisParser - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% + + - + class: PHPStan\Analyser\LocalIgnoresProcessor - class: PHPStan\Analyser\RuleErrorTransformer @@ -496,6 +545,8 @@ services: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% detectDeadTypeInMultiCatch: %featureToggles.detectDeadTypeInMultiCatch% universalObjectCratesClasses: %universalObjectCratesClasses% + paramOutType: %featureToggles.paramOutType% + preciseMissingReturn: %featureToggles.preciseMissingReturn% - class: PHPStan\Analyser\ConstantResolver @@ -537,8 +588,6 @@ services: - class: PHPStan\Command\AnalyseApplication - arguments: - internalErrorsCountLimit: %internalErrorsCountLimit% - class: PHPStan\Command\AnalyserRunner @@ -554,6 +603,8 @@ services: allConfigFiles: %allConfigFiles% cliAutoloadFile: %cliAutoloadFile% bootstrapFiles: %bootstrapFiles% + editorUrl: %editorUrl% + usedLevel: %usedLevel% - class: PHPStan\Dependency\DependencyResolver @@ -601,6 +652,10 @@ services: class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider + - + class: PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider + factory: PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider + - class: PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyExpressionTypeResolverExtensionRegistryProvider @@ -613,6 +668,10 @@ services: class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider + - + class: PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider + factory: PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider + - class: PHPStan\File\FileHelper arguments: @@ -626,6 +685,8 @@ services: - implement: PHPStan\File\FileExcluderRawFactory + arguments: + noImplicitWildcard: %featureToggles.noImplicitWildcard% fileExcluderAnalyse: class: PHPStan\File\FileExcluder @@ -674,6 +735,8 @@ services: jobSize: %parallel.jobSize% maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% minimumNumberOfJobsPerProcess: %parallel.minimumNumberOfJobsPerProcess% + tags: + - phpstan.diagnoseExtension - class: PHPStan\Parser\FunctionCallStatementFinder @@ -833,12 +896,20 @@ services: 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: @@ -904,6 +975,8 @@ services: - class: PHPStan\Rules\FunctionReturnTypeCheck + - + class: PHPStan\Rules\ParameterCastableToStringCheck - class: PHPStan\Rules\Generics\CrossCheckInterfacesHelper @@ -975,6 +1048,9 @@ services: - class: PHPStan\Rules\Constants\LazyAlwaysUsedClassConstantsExtensionProvider + - + class: PHPStan\Rules\Methods\LazyAlwaysUsedMethodExtensionProvider + - class: PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper @@ -984,10 +1060,14 @@ services: - 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 @@ -1001,6 +1081,9 @@ services: - class: PHPStan\Rules\Properties\PropertyReflectionFinder + - + class: PHPStan\Rules\Pure\FunctionPurityCheck + - class: PHPStan\Rules\RuleLevelHelper arguments: @@ -1015,6 +1098,9 @@ services: - class: PHPStan\Rules\UnusedFunctionParametersCheck + - + class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck + - class: PHPStan\Type\FileTypeMapper arguments: @@ -1033,6 +1119,11 @@ services: - class: PHPStan\Type\BitwiseFlagHelper + - + class: PHPStan\Type\Php\AbsFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension tags: @@ -1253,6 +1344,9 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\DateFunctionReturnTypeHelper + - class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension tags: @@ -1307,6 +1401,11 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\DateTimeModifyMethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + - class: PHPStan\Type\Php\DateTimeZoneConstructorThrowTypeExtension tags: @@ -1360,6 +1459,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GetDebugTypeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: @@ -1394,6 +1498,24 @@ services: 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: @@ -1530,6 +1652,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\SetTypeFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\StrCaseFunctionsReturnTypeExtension tags: @@ -1788,6 +1915,9 @@ services: - class: PHPStan\Type\Constant\OversizedArrayBuilder + - + class: PHPStan\Rules\Functions\PrintfHelper + exceptionTypeResolver: class: PHPStan\Rules\Exceptions\ExceptionTypeResolver factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver @@ -1976,10 +2106,13 @@ services: - PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber - - class: PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory::create() autowired: - PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber + - + class: PHPStan\Reflection\BetterReflection\SourceStubber\ReflectionSourceStubberFactory + php8Lexer: class: PhpParser\Lexer\Emulative factory: @PHPStan\Parser\LexerFactory::createEmulative() @@ -2005,6 +2138,13 @@ services: php8Parser: @php8Parser autowired: false + phpstanDiagnoseExtension: + class: PHPStan\Diagnose\PHPStanDiagnoseExtension + arguments: + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + allConfigFiles: %allConfigFiles% + autowired: false + # Error formatters - diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index cdbe34bf56..a217bfc4e0 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -36,6 +36,7 @@ parametersSchema: explicitMixedViaIsArray: bool(), arrayFilter: bool(), arrayUnpacking: bool(), + arrayValues: bool(), nodeConnectingVisitorCompatibility: bool(), nodeConnectingVisitorRule: bool(), illegalConstructorMethodCall: bool(), @@ -59,6 +60,7 @@ parametersSchema: alwaysCheckTooWideReturnTypeFinalMethods: bool() duplicateStubs: bool() logicalXor: bool() + betterNoop: bool() invarianceComposition: bool() alwaysTrueAlwaysReported: bool() disableUnreachableBranchesRules: bool() @@ -75,9 +77,19 @@ parametersSchema: 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() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() @@ -119,6 +131,9 @@ parametersSchema: reportMaybesInPropertyPhpDocTypes: bool() reportStaticMethodSignatures: bool() reportWrongPhpDocTypeInVarTag: bool() + reportAnyTypeWideningInVarTag: bool() + reportPossiblyNonexistentGeneralArrayOffset: bool() + reportPossiblyNonexistentConstantArrayOffset: bool() checkMissingOverrideMethodAttribute: bool() parallel: structure([ jobSize: int(), @@ -208,7 +223,7 @@ parametersSchema: dnsServers: schema(listOf(string()), min(1)), tmpDir: string() ]) - env: arrayOf(string(), string()) + env: arrayOf(string(), anyOf(int(), string())) sysGetTempDir: string() # irrelevant Nette parameters diff --git a/e2e/bad-exclude-paths/excludePaths.neon b/e2e/bad-exclude-paths/excludePaths.neon new file mode 100644 index 0000000000..14ed59cad4 --- /dev/null +++ b/e2e/bad-exclude-paths/excludePaths.neon @@ -0,0 +1,9 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + excludePaths: + - tests diff --git a/e2e/bad-exclude-paths/ignore.neon b/e2e/bad-exclude-paths/ignore.neon new file mode 100644 index 0000000000..26d56b4b57 --- /dev/null +++ b/e2e/bad-exclude-paths/ignore.neon @@ -0,0 +1,11 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + ignoreErrors: + - + message: '#aaa#' + path: tests diff --git a/e2e/bad-exclude-paths/ignoreNonexistentExcludePath.neon b/e2e/bad-exclude-paths/ignoreNonexistentExcludePath.neon new file mode 100644 index 0000000000..b78c536f97 --- /dev/null +++ b/e2e/bad-exclude-paths/ignoreNonexistentExcludePath.neon @@ -0,0 +1,10 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - . + excludePaths: + - node_modules (?) + - tmp-node-modules diff --git a/e2e/bad-exclude-paths/ignoreReportUnmatchedFalse.neon b/e2e/bad-exclude-paths/ignoreReportUnmatchedFalse.neon new file mode 100644 index 0000000000..2206595e61 --- /dev/null +++ b/e2e/bad-exclude-paths/ignoreReportUnmatchedFalse.neon @@ -0,0 +1,12 @@ +includes: + - ../../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + reportUnmatchedIgnoredErrors: false + ignoreErrors: + - + message: '#aaa#' + path: tests diff --git a/e2e/bad-exclude-paths/phpneon.php b/e2e/bad-exclude-paths/phpneon.php new file mode 100644 index 0000000000..92ebd989a1 --- /dev/null +++ b/e2e/bad-exclude-paths/phpneon.php @@ -0,0 +1,17 @@ + [ + __DIR__ . '/../../conf/bleedingEdge.neon', + ], + 'parameters' => [ + 'level' => '8', + 'paths' => [__DIR__ . '/src'], + 'ignoreErrors' => [ + [ + 'message' => '#aaa#', + 'path' => 'src/test.php', // not absolute path - invalid in .php config + ], + ], + ], +]; diff --git a/e2e/bad-exclude-paths/phpneon2.php b/e2e/bad-exclude-paths/phpneon2.php new file mode 100644 index 0000000000..4c06f1f310 --- /dev/null +++ b/e2e/bad-exclude-paths/phpneon2.php @@ -0,0 +1,16 @@ + [ + __DIR__ . '/../../conf/bleedingEdge.neon', + ], + 'parameters' => [ + 'level' => '8', + 'paths' => [__DIR__ . '/src'], + 'excludePaths' => [ + 'analyse' => [ + 'src/test.php', // not absolute path - invalid in .php config + ], + ], + ], +]; diff --git a/e2e/bad-exclude-paths/src/test.php b/e2e/bad-exclude-paths/src/test.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bad-exclude-paths/tmp-node-modules/test.php b/e2e/bad-exclude-paths/tmp-node-modules/test.php new file mode 100644 index 0000000000..c24b518fb0 --- /dev/null +++ b/e2e/bad-exclude-paths/tmp-node-modules/test.php @@ -0,0 +1,3 @@ + $query + * + * @return T + */ + public function handle(QueryInterface $query); +} \ No newline at end of file diff --git a/e2e/bug10449/src/Bus/QueryHandlerInterface.php b/e2e/bug10449/src/Bus/QueryHandlerInterface.php new file mode 100644 index 0000000000..88faa0164a --- /dev/null +++ b/e2e/bug10449/src/Bus/QueryHandlerInterface.php @@ -0,0 +1,9 @@ +queryBus->handle(new Query\ExampleQuery()); + $this->needsString($value); + return $value; + } + + private function needsString(string $s):void {} +} \ No newline at end of file diff --git a/e2e/bug10449/src/Query/ExampleQuery.php b/e2e/bug10449/src/Query/ExampleQuery.php new file mode 100644 index 0000000000..a5c7637793 --- /dev/null +++ b/e2e/bug10449/src/Query/ExampleQuery.php @@ -0,0 +1,15 @@ + + */ +final class ExampleQuery implements QueryInterface +{ +} \ No newline at end of file diff --git a/e2e/bug10449/src/Query/ExampleQueryHandler.php b/e2e/bug10449/src/Query/ExampleQueryHandler.php new file mode 100644 index 0000000000..e9bc1d59f0 --- /dev/null +++ b/e2e/bug10449/src/Query/ExampleQueryHandler.php @@ -0,0 +1,19 @@ + $query + * + * @return T + */ + public function handle(QueryInterface $query); +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Bus/QueryHandlerInterface.php b/e2e/bug10449b/src/Bus/QueryHandlerInterface.php new file mode 100644 index 0000000000..88faa0164a --- /dev/null +++ b/e2e/bug10449b/src/Bus/QueryHandlerInterface.php @@ -0,0 +1,9 @@ +queryBus->handle($x); + $this->needsString($value); + return $value; + } + + return 'hello'; + } + + private function needsString(string $s):void {} +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Query/ExampleQuery.php b/e2e/bug10449b/src/Query/ExampleQuery.php new file mode 100644 index 0000000000..a5c7637793 --- /dev/null +++ b/e2e/bug10449b/src/Query/ExampleQuery.php @@ -0,0 +1,15 @@ + + */ +final class ExampleQuery implements QueryInterface +{ +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Query/ExampleQueryHandler.php b/e2e/bug10449b/src/Query/ExampleQueryHandler.php new file mode 100644 index 0000000000..e9bc1d59f0 --- /dev/null +++ b/e2e/bug10449b/src/Query/ExampleQueryHandler.php @@ -0,0 +1,19 @@ +parentIssue; + } + + public function getParentLesson(): ?Lesson + { + return $this->parentLesson; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getType(): string + { + return $this->type; + } + + public function getNavigationVisible(): bool + { + return $this->navigationVisible; + } + + public function getNavigationColor(): string + { + return $this->navigationColor; + } +} + diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/Issue.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Issue.php new file mode 100644 index 0000000000..8343551215 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Issue.php @@ -0,0 +1,45 @@ +parentSchoolYear; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getStartDate(): int + { + return $this->startDate; + } + + public function getHolidayTitle(): string + { + return $this->holidayTitle; + } +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/Lesson.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Lesson.php new file mode 100644 index 0000000000..e00f2f0efb --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/Lesson.php @@ -0,0 +1,29 @@ +schoolLevel; + } + + public function getParentIssue(): ?Issue + { + return $this->parentIssue; + } + + public function getLessonNumber(): int + { + return $this->lessonNumber; + } +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolLevel.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolLevel.php new file mode 100644 index 0000000000..5c326ca596 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolLevel.php @@ -0,0 +1,15 @@ +title; + } +} diff --git a/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolYear.php b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolYear.php new file mode 100644 index 0000000000..a86f5bf575 --- /dev/null +++ b/e2e/discussion-11362/packages/site/Classes/Domain/Model/SchoolYear.php @@ -0,0 +1,40 @@ +startDate; + } + + public function getEndDate(): int + { + return $this->endDate; + } + + public function getIntroStartDate(): int + { + return $this->introStartDate; + } + + public function getIntroEndDate(): int + { + return $this->introEndDate; + } +} diff --git a/e2e/discussion-11362/packages/site/composer.json b/e2e/discussion-11362/packages/site/composer.json new file mode 100644 index 0000000000..ca413c1c8d --- /dev/null +++ b/e2e/discussion-11362/packages/site/composer.json @@ -0,0 +1,11 @@ +{ + "autoload": { + "psr-4": { + "Repro\\Site\\": "Classes" + } + }, + "name": "repro/site", + "require": { + "php": "^8.1" + } +} diff --git a/e2e/discussion-11362/phpstan.neon b/e2e/discussion-11362/phpstan.neon new file mode 100644 index 0000000000..d9a4bd0ab3 --- /dev/null +++ b/e2e/discussion-11362/phpstan.neon @@ -0,0 +1,11 @@ +parameters: + excludePaths: + analyseAndScan: + - .git + analyse: + - vendor + + level: 1 + + paths: + - . diff --git a/e2e/env-int-key/test.php b/e2e/env-int-key/test.php new file mode 100644 index 0000000000..b3d9bbc7f3 --- /dev/null +++ b/e2e/env-int-key/test.php @@ -0,0 +1 @@ + + */ +class CustomRule implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/build/CustomRule2.php b/e2e/result-cache-8/build/CustomRule2.php new file mode 100644 index 0000000000..413d37b54d --- /dev/null +++ b/e2e/result-cache-8/build/CustomRule2.php @@ -0,0 +1,24 @@ + + */ +class CustomRule2 implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/composer.json b/e2e/result-cache-8/composer.json new file mode 100644 index 0000000000..d29ab86e75 --- /dev/null +++ b/e2e/result-cache-8/composer.json @@ -0,0 +1,14 @@ +{ + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-webmozart-assert": "^1.2" + }, + "autoload": { + "classmap": ["src"] + }, + "autoload-dev": { + "classmap": [ + "build" + ] + } +} diff --git a/e2e/result-cache-8/composer.lock b/e2e/result-cache-8/composer.lock new file mode 100644 index 0000000000..23a7f311ea --- /dev/null +++ b/e2e/result-cache-8/composer.lock @@ -0,0 +1,132 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "964b13a11680dbf7fa5291f0baa6d10c", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "1.10.63", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad12836d9ca227301f5fb9960979574ed8628339", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2024-03-18T16:53:53+00:00" + }, + { + "name": "phpstan/phpstan-webmozart-assert", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-webmozart-assert.git", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-webmozart-assert/zipball/d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "webmozart/assert": "^1.11.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan webmozart/assert extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-webmozart-assert/issues", + "source": "https://github.com/phpstan/phpstan-webmozart-assert/tree/1.2.4" + }, + "time": "2023-02-21T20:34:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/e2e/result-cache-8/phpstan.neon b/e2e/result-cache-8/phpstan.neon new file mode 100644 index 0000000000..c7fd83c756 --- /dev/null +++ b/e2e/result-cache-8/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - vendor/phpstan/phpstan-webmozart-assert/extension.neon + +parameters: + paths: + - src + level: 8 + +rules: + - ResultCache8E2E\CustomRule + - ResultCache8E2E\CustomRule3 + +services: + - + class: ResultCache8E2E\CustomRule2 + tags: + - phpstan.rules.rule diff --git a/e2e/result-cache-8/src/CustomRule3.php b/e2e/result-cache-8/src/CustomRule3.php new file mode 100644 index 0000000000..1f0ca326e1 --- /dev/null +++ b/e2e/result-cache-8/src/CustomRule3.php @@ -0,0 +1,24 @@ + + */ +class CustomRule3 implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/src/Foo.php b/e2e/result-cache-8/src/Foo.php new file mode 100644 index 0000000000..0082675fde --- /dev/null +++ b/e2e/result-cache-8/src/Foo.php @@ -0,0 +1,8 @@ +=5.3" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { @@ -46,7 +46,7 @@ } ], "description": "A simple and modern approach to stream filtering in PHP", - "homepage": "https://github.com/clue/php-stream-filter", + "homepage": "https://github.com/clue/stream-filter", "keywords": [ "bucket brigade", "callback", @@ -58,7 +58,7 @@ ], "support": { "issues": "https://github.com/clue/stream-filter/issues", - "source": "https://github.com/clue/stream-filter/tree/v1.6.0" + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" }, "funding": [ { @@ -70,7 +70,7 @@ "type": "github" } ], - "time": "2022-02-21T13:15:14+00:00" + "time": "2023-12-20T15:40:13+00:00" }, { "name": "dflydev/dot-access-data", @@ -149,22 +149,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.7.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", - "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0", - "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -173,11 +173,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -255,7 +255,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, "funding": [ { @@ -271,28 +271,28 @@ "type": "tidelift" } ], - "time": "2023-05-21T14:04:53+00:00" + "time": "2023-12-03T20:35:24+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6" + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6", - "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "type": "library", "extra": { @@ -338,7 +338,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.0" + "source": "https://github.com/guzzle/promises/tree/2.0.2" }, "funding": [ { @@ -354,20 +354,20 @@ "type": "tidelift" } ], - "time": "2023-05-21T13:50:22+00:00" + "time": "2023-12-03T20:19:20+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.5.0", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", "shasum": "" }, "require": { @@ -381,9 +381,9 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -454,7 +454,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.5.0" + "source": "https://github.com/guzzle/psr7/tree/2.6.2" }, "funding": [ { @@ -470,26 +470,26 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:11:26+00:00" + "time": "2023-12-03T20:05:35+00:00" }, { "name": "knplabs/github-api", - "version": "v3.11.0", + "version": "v3.14.1", "source": { "type": "git", "url": "https://github.com/KnpLabs/php-github-api.git", - "reference": "c68b874ac3267c3cc0544b726dbb4e49a72a9920" + "reference": "71fec50e228737ec23c0b69801b85bf596fbdaca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/c68b874ac3267c3cc0544b726dbb4e49a72a9920", - "reference": "c68b874ac3267c3cc0544b726dbb4e49a72a9920", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/71fec50e228737ec23c0b69801b85bf596fbdaca", + "reference": "71fec50e228737ec23c0b69801b85bf596fbdaca", "shasum": "" }, "require": { "ext-json": "*", "php": "^7.2.5 || ^8.0", - "php-http/cache-plugin": "^1.7.1", + "php-http/cache-plugin": "^1.7.1|^2.0", "php-http/client-common": "^2.3", "php-http/discovery": "^1.12", "php-http/httplug": "^2.2", @@ -497,7 +497,7 @@ "psr/cache": "^1.0|^2.0|^3.0", "psr/http-client-implementation": "^1.0", "psr/http-factory-implementation": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.0|^2.0", "symfony/deprecation-contracts": "^2.2|^3.0", "symfony/polyfill-php80": "^1.17" }, @@ -517,7 +517,7 @@ "extra": { "branch-alias": { "dev-2.x": "2.20.x-dev", - "dev-master": "3.10.x-dev" + "dev-master": "3.14-dev" } }, "autoload": { @@ -550,7 +550,7 @@ ], "support": { "issues": "https://github.com/KnpLabs/php-github-api/issues", - "source": "https://github.com/KnpLabs/php-github-api/tree/v3.11.0" + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.14.1" }, "funding": [ { @@ -558,20 +558,20 @@ "type": "github" } ], - "time": "2023-03-10T11:40:14+00:00" + "time": "2024-03-24T18:21:15+00:00" }, { "name": "league/commonmark", - "version": "2.4.0", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048" + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048", - "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", "shasum": "" }, "require": { @@ -584,7 +584,7 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", + "commonmark/cmark": "0.30.3", "commonmark/commonmark.js": "0.30.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", @@ -594,10 +594,10 @@ "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -664,7 +664,7 @@ "type": "tidelift" } ], - "time": "2023-03-24T15:16:10+00:00" + "time": "2024-02-02T11:59:32+00:00" }, { "name": "league/config", @@ -750,21 +750,21 @@ }, { "name": "nette/neon", - "version": "v3.4.0", + "version": "v3.4.3", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "372d945c156ee7f35c953339fb164538339e6283" + "reference": "c8481c104431c8d94cc88424a1e21f47f8c93280" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/372d945c156ee7f35c953339fb164538339e6283", - "reference": "372d945c156ee7f35c953339fb164538339e6283", + "url": "https://api.github.com/repos/nette/neon/zipball/c8481c104431c8d94cc88424a1e21f47f8c93280", + "reference": "c8481c104431c8d94cc88424a1e21f47f8c93280", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=8.0 <8.3" + "php": "8.0 - 8.3" }, "require-dev": { "nette/tester": "^2.4", @@ -812,27 +812,27 @@ ], "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.3" }, - "time": "2023-01-13T03:08:29+00:00" + "time": "2024-06-26T14:53:59+00:00" }, { "name": "nette/schema", - "version": "v1.2.3", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", - "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", "shasum": "" }, "require": { "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.3" + "php": "7.1 - 8.3" }, "require-dev": { "nette/tester": "^2.3 || ^2.4", @@ -874,26 +874,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.3" + "source": "https://github.com/nette/schema/tree/v1.2.5" }, - "time": "2022-10-13T01:24:26+00:00" + "time": "2023-10-05T20:37:59+00:00" }, { "name": "nette/utils", - "version": "v3.2.9", + "version": "v3.2.10", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c91bac3470c34b2ecd5400f6e6fdf0b64a836a5c" + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c91bac3470c34b2ecd5400f6e6fdf0b64a836a5c", - "reference": "c91bac3470c34b2ecd5400f6e6fdf0b64a836a5c", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", "shasum": "" }, "require": { - "php": ">=7.2 <8.3" + "php": ">=7.2 <8.4" }, "conflict": { "nette/di": "<3.0.6" @@ -960,40 +960,36 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.9" + "source": "https://github.com/nette/utils/tree/v3.2.10" }, - "time": "2023-01-18T03:26:20+00:00" + "time": "2023-07-30T15:38:18+00:00" }, { "name": "php-http/cache-plugin", - "version": "1.7.5", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-http/cache-plugin.git", - "reference": "63bc3f7242825c9a817db8f78e4c9703b0c471e2" + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/63bc3f7242825c9a817db8f78e4c9703b0c471e2", - "reference": "63bc3f7242825c9a817db8f78e4c9703b0c471e2", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-http/client-common": "^1.9 || ^2.0", - "php-http/message-factory": "^1.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "psr/http-factory-implementation": "^1.0", + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.0" + "nyholm/psr7": "^1.6.1", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\Common\\Plugin\\": "src/" @@ -1019,33 +1015,32 @@ ], "support": { "issues": "https://github.com/php-http/cache-plugin/issues", - "source": "https://github.com/php-http/cache-plugin/tree/1.7.5" + "source": "https://github.com/php-http/cache-plugin/tree/2.0.0" }, - "time": "2022-01-18T12:24:56+00:00" + "time": "2024-02-19T17:02:14+00:00" }, { "name": "php-http/client-common", - "version": "2.6.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/php-http/client-common.git", - "reference": "45db684cd4e186dcdc2b9c06b22970fe123796c0" + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/45db684cd4e186dcdc2b9c06b22970fe123796c0", - "reference": "45db684cd4e186dcdc2b9c06b22970fe123796c0", + "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-http/httplug": "^2.0", "php-http/message": "^1.6", - "php-http/message-factory": "^1.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", "symfony/polyfill-php80": "^1.17" }, "require-dev": { @@ -1054,7 +1049,7 @@ "nyholm/psr7": "^1.2", "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", "phpspec/prophecy": "^1.10.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" }, "suggest": { "ext-json": "To detect JSON responses with the ContentTypePlugin", @@ -1064,11 +1059,6 @@ "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\Common\\": "src/" @@ -1094,22 +1084,22 @@ ], "support": { "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.6.0" + "source": "https://github.com/php-http/client-common/tree/2.7.1" }, - "time": "2022-09-29T09:59:43+00:00" + "time": "2023-11-30T10:31:25+00:00" }, { "name": "php-http/discovery", - "version": "1.15.2", + "version": "1.19.4", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "5cc428320191ac1d0b6520034c2dc0698628ced5" + "reference": "0700efda8d7526335132360167315fdab3aeb599" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/5cc428320191ac1d0b6520034c2dc0698628ced5", - "reference": "5cc428320191ac1d0b6520034c2dc0698628ced5", + "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", + "reference": "0700efda8d7526335132360167315fdab3aeb599", "shasum": "" }, "require": { @@ -1117,7 +1107,8 @@ "php": "^7.1 || ^8.0" }, "conflict": { - "nyholm/psr7": "<1.0" + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" }, "provide": { "php-http/async-client-implementation": "*", @@ -1132,7 +1123,8 @@ "php-http/httplug": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "symfony/phpunit-bridge": "^6.2" + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" }, "type": "composer-plugin", "extra": { @@ -1142,7 +1134,10 @@ "autoload": { "psr-4": { "Http\\Discovery\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1168,40 +1163,35 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.15.2" + "source": "https://github.com/php-http/discovery/tree/1.19.4" }, - "time": "2023-02-11T08:28:41+00:00" + "time": "2024-03-29T13:00:05+00:00" }, { "name": "php-http/httplug", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/php-http/httplug.git", - "reference": "f640739f80dfa1152533976e3c112477f69274eb" + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/f640739f80dfa1152533976e3c112477f69274eb", - "reference": "f640739f80dfa1152533976e3c112477f69274eb", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-http/promise": "^1.1", "psr/http-client": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.1", - "phpspec/phpspec": "^5.1 || ^6.0" + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\": "src/" @@ -1230,29 +1220,28 @@ ], "support": { "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/2.3.0" + "source": "https://github.com/php-http/httplug/tree/2.4.0" }, - "time": "2022-02-21T09:52:22+00:00" + "time": "2023-04-14T15:10:03+00:00" }, { "name": "php-http/message", - "version": "1.13.0", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "7886e647a30a966a1a8d1dad1845b71ca8678361" + "reference": "5997f3289332c699fa2545c427826272498a2088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/7886e647a30a966a1a8d1dad1845b71ca8678361", - "reference": "7886e647a30a966a1a8d1dad1845b71ca8678361", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", "shasum": "" }, "require": { "clue/stream-filter": "^1.5", - "php": "^7.1 || ^8.0", - "php-http/message-factory": "^1.0.2", - "psr/http-message": "^1.0" + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" }, "provide": { "php-http/message-factory-implementation": "1.0" @@ -1260,8 +1249,9 @@ "require-dev": { "ergebnis/composer-normalize": "^2.6", "ext-zlib": "*", - "guzzlehttp/psr7": "^1.0", - "laminas/laminas-diactoros": "^2.0", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", "slim/slim": "^3.0" }, @@ -1272,11 +1262,6 @@ "slim/slim": "Used with Slim Framework PSR-7 implementation" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, "autoload": { "files": [ "src/filters.php" @@ -1304,96 +1289,36 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.13.0" + "source": "https://github.com/php-http/message/tree/1.16.1" }, - "time": "2022-02-11T13:41:14+00:00" - }, - { - "name": "php-http/message-factory", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org/", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" - }, - "time": "2015-12-19T14:08:53+00:00" + "time": "2024-03-07T13:22:09+00:00" }, { "name": "php-http/multipart-stream-builder", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/php-http/multipart-stream-builder.git", - "reference": "11c1d31f72e01c738bbce9e27649a7cca829c30e" + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/11c1d31f72e01c738bbce9e27649a7cca829c30e", - "reference": "11c1d31f72e01c738bbce9e27649a7cca829c30e", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "php-http/discovery": "^1.7", - "php-http/message-factory": "^1.0.2", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" }, "require-dev": { "nyholm/psr7": "^1.0", "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Message\\MultipartStream\\": "src/" @@ -1420,37 +1345,32 @@ ], "support": { "issues": "https://github.com/php-http/multipart-stream-builder/issues", - "source": "https://github.com/php-http/multipart-stream-builder/tree/1.2.0" + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0" }, - "time": "2021-05-21T08:32:01+00:00" + "time": "2023-04-28T14:10:22+00:00" }, { "name": "php-http/promise", - "version": "1.1.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "phpspec/phpspec": "^5.1.2 || ^6.2" + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, "autoload": { "psr-4": { "Http\\Promise\\": "src/" @@ -1477,9 +1397,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.1.0" + "source": "https://github.com/php-http/promise/tree/1.3.1" }, - "time": "2020-07-07T09:29:14+00:00" + "time": "2024-03-15T13:55:21+00:00" }, { "name": "phpstan/phpstan", @@ -1487,12 +1407,12 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8d675d986bfb4b7431d3527366e38b6b16c8cd88" + "reference": "7a2e524c7bdc18295d62b0ed598cec1166da80ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8d675d986bfb4b7431d3527366e38b6b16c8cd88", - "reference": "8d675d986bfb4b7431d3527366e38b6b16c8cd88", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7a2e524c7bdc18295d62b0ed598cec1166da80ab", + "reference": "7a2e524c7bdc18295d62b0ed598cec1166da80ab", "shasum": "" }, "require": { @@ -1536,26 +1456,22 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2023-06-14T19:35:03+00:00" + "time": "2024-04-19T14:55:18+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.5.x-dev", + "version": "1.6.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "b7edb14296bae350401afef889c0243958174975" + "reference": "c7b4d283fbffd23b9405c01d1f68124739d698f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/b7edb14296bae350401afef889c0243958174975", - "reference": "b7edb14296bae350401afef889c0243958174975", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/c7b4d283fbffd23b9405c01d1f68124739d698f6", + "reference": "c7b4d283fbffd23b9405c01d1f68124739d698f6", "shasum": "" }, "require": { @@ -1590,9 +1506,9 @@ "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.5.x" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.x" }, - "time": "2023-06-09T16:01:59+00:00" + "time": "2024-04-19T14:52:46+00:00" }, { "name": "psr/cache", @@ -1748,16 +1664,16 @@ }, { "name": "psr/http-client", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", - "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { @@ -1794,9 +1710,9 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/1.0.2" + "source": "https://github.com/php-fig/http-client" }, - "time": "2023-04-10T20:12:12+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", @@ -1855,16 +1771,16 @@ }, { "name": "psr/http-message", - "version": "1.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", - "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { @@ -1873,7 +1789,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1888,7 +1804,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -1902,9 +1818,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/1.1" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2023-04-04T09:50:52+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "ralouphie/getallheaders", @@ -1952,16 +1868,16 @@ }, { "name": "symfony/console", - "version": "v6.3.0", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7" + "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", - "reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7", + "url": "https://api.github.com/repos/symfony/console/zipball/6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", + "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", "shasum": "" }, "require": { @@ -1969,7 +1885,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0" + "symfony/string": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/dependency-injection": "<5.4", @@ -1983,12 +1899,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" + "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": { @@ -2022,7 +1942,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.0" + "source": "https://github.com/symfony/console/tree/v6.4.9" }, "funding": [ { @@ -2038,20 +1958,20 @@ "type": "tidelift" } ], - "time": "2023-05-29T12:49:39+00:00" + "time": "2024-06-28T09:49:33+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -2060,7 +1980,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -2089,7 +2009,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.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -2105,27 +2025,27 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/finder", - "version": "v6.3.0", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2" + "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d9b01ba073c44cef617c7907ce2419f8d00d75e2", - "reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2", + "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", + "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "symfony/filesystem": "^6.0" + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", "autoload": { @@ -2153,7 +2073,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/finder/tree/v6.3.0" + "source": "https://github.com/symfony/finder/tree/v6.4.8" }, "funding": [ { @@ -2169,25 +2089,25 @@ "type": "tidelift" } ], - "time": "2023-04-02T01:25:41+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.2.7", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629" + "reference": "700ff4096e346f54cb628ea650767c8130f1001f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/aa0e85b53bbb2b4951960efd61d295907eacd629", - "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -2220,7 +2140,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.2.7" + "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" }, "funding": [ { @@ -2236,20 +2156,20 @@ "type": "tidelift" } ], - "time": "2023-02-14T08:44:56+00:00" + "time": "2023-08-08T10:20:21+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "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/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -2263,9 +2183,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2302,7 +2219,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -2318,20 +2235,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "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/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -2342,9 +2259,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2383,7 +2297,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.30.0" }, "funding": [ { @@ -2399,20 +2313,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "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/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -2423,9 +2337,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2467,7 +2378,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.30.0" }, "funding": [ { @@ -2483,20 +2394,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "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/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -2510,9 +2421,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2550,7 +2458,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -2566,20 +2474,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -2587,9 +2495,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2633,7 +2538,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -2649,25 +2554,26 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.3.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", - "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "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" @@ -2675,7 +2581,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -2715,7 +2621,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -2731,24 +2637,24 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", - "version": "v6.3.0", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f" + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f2e190ee75ff0f5eced645ec0be5c66fac81f51f", - "reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f", + "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", "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", @@ -2758,11 +2664,12 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0", - "symfony/http-client": "^5.4|^6.0", - "symfony/intl": "^6.2", + "symfony/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": "^5.4|^6.0" + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -2801,7 +2708,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.3.0" + "source": "https://github.com/symfony/string/tree/v7.1.2" }, "funding": [ { @@ -2817,7 +2724,7 @@ "type": "tidelift" } ], - "time": "2023-03-21T21:06:29+00:00" + "time": "2024-06-28T09:27:18+00:00" } ], "packages-dev": [ @@ -2952,25 +2859,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.5", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", - "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "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": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -2978,7 +2887,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -3002,26 +2911,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2023-05-19T20:20:00+00:00" + "time": "2024-03-05T20:51:40+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", @@ -3062,9 +2972,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", @@ -3119,23 +3035,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.26", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -3184,7 +3100,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -3192,7 +3109,7 @@ "type": "github" } ], - "time": "2023-03-06T12:58:08+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3437,16 +3354,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.8", + "version": "9.6.19", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e" + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e", - "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", "shasum": "" }, "require": { @@ -3461,7 +3378,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-code-coverage": "^9.2.28", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -3520,7 +3437,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.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" }, "funding": [ { @@ -3536,20 +3453,20 @@ "type": "tidelift" } ], - "time": "2023-05-11T05:14:45+00:00" + "time": "2024-04-05T04:35:58+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": { @@ -3584,7 +3501,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": [ { @@ -3592,7 +3509,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -3781,20 +3698,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": { @@ -3826,7 +3743,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": [ { @@ -3834,20 +3751,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": { @@ -3892,7 +3809,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": [ { @@ -3900,7 +3817,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -3967,16 +3884,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": { @@ -4032,7 +3949,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": [ { @@ -4040,20 +3957,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.5", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "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/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -4096,7 +4013,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -4104,24 +4021,24 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+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": { @@ -4153,7 +4070,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": [ { @@ -4161,7 +4078,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -4340,16 +4257,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": { @@ -4361,7 +4278,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4382,8 +4299,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": [ { @@ -4391,7 +4307,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -4504,16 +4420,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": { @@ -4542,7 +4458,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": [ { @@ -4550,7 +4466,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -4561,8 +4477,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1" + "php": "^8.3" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/issue-bot/src/Playground/TabCreator.php b/issue-bot/src/Playground/TabCreator.php index a74da457a4..544fcb1461 100644 --- a/issue-bot/src/Playground/TabCreator.php +++ b/issue-bot/src/Playground/TabCreator.php @@ -15,7 +15,7 @@ class TabCreator { /** - * @param array> $versionedErrors $versionedErrors + * @param array> $versionedErrors * @return list */ public function create(array $versionedErrors): array diff --git a/patches/DependencyChecker.patch b/patches/DependencyChecker.patch new file mode 100644 index 0000000000..4902922537 --- /dev/null +++ b/patches/DependencyChecker.patch @@ -0,0 +1,13 @@ +--- src/DI/DependencyChecker.php 2023-10-02 21:58:38 ++++ src/DI/DependencyChecker.php 2024-07-07 09:24:35 +@@ -147,7 +147,9 @@ + $flip = array_flip($classes); + foreach ($functions as $name) { + if (strpos($name, '::')) { +- $method = new ReflectionMethod($name); ++ $method = PHP_VERSION_ID < 80300 ++ ? new ReflectionMethod($name) ++ : ReflectionMethod::createFromMethodName($name); + $class = $method->getDeclaringClass(); + if (isset($flip[$class->name])) { + continue; diff --git a/patches/HoaException.patch b/patches/HoaException.patch new file mode 100644 index 0000000000..bada616504 --- /dev/null +++ b/patches/HoaException.patch @@ -0,0 +1,11 @@ +--- Exception/Exception.php 2024-06-24 15:17:26 ++++ Exception/Exception.php 2024-06-24 15:17:51 +@@ -37,7 +37,7 @@ + namespace Hoa\Compiler\Exception; + + use Hoa\Consistency; +-use Hoa\Exception as HoaException; ++use Hoa\Exception\Exception as HoaException; + + /** + * Class \Hoa\Compiler\Exception. diff --git a/patches/Lexer.patch b/patches/Lexer.patch new file mode 100644 index 0000000000..b2d84ed6e9 --- /dev/null +++ b/patches/Lexer.patch @@ -0,0 +1,12 @@ +diff --git a/Llk/Lexer.php b/Llk/Lexer.php +index 6851367..b8acf98 100644 +--- a/Llk/Lexer.php ++++ b/Llk/Lexer.php +@@ -281,7 +281,7 @@ class Lexer + $offset + ); + +- if (0 === $preg) { ++ if (0 === $preg || $preg === false) { + return null; + } diff --git a/patches/cloudflare-ca.patch b/patches/cloudflare-ca.patch new file mode 100644 index 0000000000..57f6cf5a77 --- /dev/null +++ b/patches/cloudflare-ca.patch @@ -0,0 +1,27 @@ +--- ca-bundle/res/cacert.pem 2024-06-18 13:50:00 ++++ ca-bundle/res/cacert.pem 2024-06-18 13:50:29 +@@ -3579,3 +3579,24 @@ + HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 + o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= + -----END CERTIFICATE----- ++ ++Cloudflare CA ++================================== ++-----BEGIN CERTIFICATE----- ++MIIC6zCCAkygAwIBAgIUI7b68p0pPrCBoW4ptlyvVcPItscwCgYIKoZIzj0EAwQw ++gY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T ++YW4gRnJhbmNpc2NvMRgwFgYDVQQKEw9DbG91ZGZsYXJlLCBJbmMxNzA1BgNVBAMT ++LkNsb3VkZmxhcmUgZm9yIFRlYW1zIEVDQyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw ++HhcNMjAwMjA0MTYwNTAwWhcNMjUwMjAyMTYwNTAwWjCBjTELMAkGA1UEBhMCVVMx ++EzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGDAW ++BgNVBAoTD0Nsb3VkZmxhcmUsIEluYzE3MDUGA1UEAxMuQ2xvdWRmbGFyZSBmb3Ig ++VGVhbXMgRUNDIENlcnRpZmljYXRlIEF1dGhvcml0eTCBmzAQBgcqhkjOPQIBBgUr ++gQQAIwOBhgAEAVdXsX8tpA9NAQeEQalvUIcVaFNDvGsR69ysZxOraRWNGHLfq1mi ++P6o3wtmtx/C2OXG01Cw7UFJbKl5MEDxnT2KoAdFSynSJOF2NDoe5LoZHbUW+yR3X ++FDl+MF6JzZ590VLGo6dPBf06UsXbH7PvHH2XKtFt8bBXVNMa5a21RdmpD0Pho0Uw ++QzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU ++YBcQng1AEMMNteuRDAMG0/vgFe0wCgYIKoZIzj0EAwQDgYwAMIGIAkIBQU5OTA2h ++YqmFk8paan5ezHVLcmcucsfYw4L/wmeEjCkczRmCVNm6L86LjhWU0v0wER0e+lHO ++3efvjbsu8gIGSagCQgEBnyYMP9gwg8l96QnQ1khFA1ljFlnqc2XgJHDSaAJC0gdz +++NV3JMeWaD2Rb32jc9r6/a7xY0u0ByqxBQ1OQ0dt7A== ++-----END CERTIFICATE----- diff --git a/patches/dom_c.patch b/patches/dom_c.patch new file mode 100644 index 0000000000..9e542c826d --- /dev/null +++ b/patches/dom_c.patch @@ -0,0 +1,17 @@ +--- dom/dom_c.php 2024-01-02 12:04:54 ++++ dom/dom_c.php 2024-01-21 10:41:56 +@@ -1347,6 +1347,14 @@ + */ + class DOMNamedNodeMap implements IteratorAggregate, Countable + { ++ ++ /** ++ * The number of nodes in the map. The range of valid child node indices is 0 to length - 1 inclusive. ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $length; + /** + * Retrieves a node specified by name + * @link https://php.net/manual/en/domnamednodemap.getnameditem.php diff --git a/patches/xmlreader.patch b/patches/xmlreader.patch new file mode 100644 index 0000000000..7be168133f --- /dev/null +++ b/patches/xmlreader.patch @@ -0,0 +1,122 @@ +--- xmlreader/xmlreader.php 2024-01-21 10:44:31 ++++ xmlreader/xmlreader.php 2024-01-21 10:48:24 +@@ -28,7 +28,119 @@ + */ + class XMLReader + { ++ /** ++ * The number of attributes on the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $attributeCount; ++ ++ /** ++ * The base URI of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $baseURI; ++ ++ /** ++ * Depth of the node in the tree, starting at 0 ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $depth; ++ ++ /** ++ * Indicates if node has attributes ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasAttributes; ++ ++ /** ++ * Indicates if node has a text value ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasValue; ++ ++ /** ++ * Indicates if attribute is defaulted from DTD ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isDefault; ++ ++ /** ++ * Indicates if node is an empty element tag ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isEmptyElement; ++ ++ /** ++ * The local name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $localName; ++ + /** ++ * The qualified name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $name; ++ ++ /** ++ * The URI of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $namespaceURI; ++ ++ /** ++ * The node type for the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $nodeType; ++ ++ /** ++ * The prefix of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $prefix; ++ ++ /** ++ * The text value of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $value; ++ ++ /** ++ * The xml:lang scope which the node resides ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $xmlLang; ++ ++ /** + * No node type + */ + public const NONE = 0; diff --git a/phpcs.xml b/phpcs.xml index 7ef56aaeb0..8a8a89df38 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -6,7 +6,6 @@ - bin src @@ -204,14 +203,17 @@ - tests/*/data - tests/*/Fixture + compiler/tests/*/data/ + tests/*/Fixture/ + tests/*/cache/ + tests/*/data/ + tests/*/traits/ + tests/PHPStan/Analyser/nsrt/ + tests/e2e/anon-class/ + tests/e2e/magic-setter/ tests/e2e/resultCache_1.php tests/e2e/resultCache_2.php tests/e2e/resultCache_3.php - tests/*/traits - tests/tmp - tests/notAutoloaded - src/Reflection/SignatureMap/functionMap.php - src/Reflection/SignatureMap/functionMetadata.php + tests/notAutoloaded/ + tests/tmp/ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ed69257845..cdf3372afc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,10 @@ parameters: ignoreErrors: + - + message: "#^Method PHPStan\\\\Analyser\\\\AnalyserResultFinalizer\\:\\:finalize\\(\\) throws checked exception Throwable but it's missing from the PHPDoc @throws tag\\.$#" + count: 1 + path: src/Analyser/AnalyserResultFinalizer.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 @@ -47,7 +52,7 @@ parameters: path: src/Analyser/MutatingScope.php - - message: "#^Only numeric types are allowed in pre\\-increment, bool\\|float\\|int\\|string\\|null given\\.$#" + message: "#^Only numeric types are allowed in pre\\-increment, float\\|int\\|string\\|null given\\.$#" count: 1 path: src/Analyser/MutatingScope.php @@ -139,21 +144,6 @@ parameters: count: 1 path: src/Command/ErrorsConsoleStyle.php - - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" - count: 1 - path: src/Command/FixerApplication.php - - - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" - count: 1 - path: src/Command/FixerWorkerCommand.php - - - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" - count: 1 - path: src/Command/WorkerCommand.php - - message: "#^Variable method call on Nette\\\\Schema\\\\Elements\\\\AnyOf\\|Nette\\\\Schema\\\\Elements\\\\Structure\\|Nette\\\\Schema\\\\Elements\\\\Type\\.$#" count: 1 @@ -169,6 +159,16 @@ parameters: count: 1 path: src/DependencyInjection/NeonAdapter.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\\.$#" + count: 1 + path: src/Diagnose/PHPStanDiagnoseExtension.php + + - + message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#" + count: 1 + path: src/Diagnose/PHPStanDiagnoseExtension.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 @@ -267,6 +267,11 @@ parameters: count: 2 path: src/PhpDoc/TypeNodeResolver.php + - + message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\CallableTypeNode\\:\\:\\$templateTypes \\(array\\\\) on left side of \\?\\? is not nullable\\.$#" + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" count: 3 @@ -370,16 +375,6 @@ parameters: count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - - message: "#^Binary operation \"\\<\\<\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\<0, max\\>\\|string\\|null results in an error\\.$#" - count: 1 - path: src/Reflection/InitializerExprTypeResolver.php - - - - message: "#^Binary operation \"\\>\\>\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\<0, max\\>\\|string\\|null results in an error\\.$#" - 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\\.$#" count: 1 @@ -666,6 +661,11 @@ parameters: 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 @@ -701,6 +701,11 @@ parameters: 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\\.$#" + count: 2 + path: src/Testing/TypeInferenceTestCase.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 @@ -838,17 +843,12 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 4 + count: 3 path: src/Type/Constant/ConstantArrayType.php - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" - count: 1 - path: src/Type/Constant/ConstantArrayType.php - - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" - count: 1 + count: 2 path: src/Type/Constant/ConstantArrayType.php - @@ -1518,8 +1518,8 @@ parameters: path: src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php - - message: "#^Casting to string something that's already string\\.$#" - count: 1 + message: "#^Cannot access offset int\\<0, max\\> on \\(float\\|int\\)\\.$#" + count: 2 path: src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php - @@ -1589,7 +1589,7 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 4 + count: 5 path: src/Type/TypeCombinator.php - @@ -1632,6 +1632,16 @@ parameters: count: 1 path: src/Type/TypeCombinator.php + - + message: "#^Instanceof between PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType and PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType will always evaluate to true\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Result of \\|\\| is always true\\.$#" + 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 @@ -1692,6 +1702,11 @@ parameters: 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\\.$#" + 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\\.$#" count: 4 @@ -1809,6 +1824,38 @@ parameters: count: 1 path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + - + message: """ + #^Instantiation of deprecated class PHPStan\\\\Rules\\\\DeadCode\\\\NoopRule\\: + Replaced by PHPStan\\\\Rules\\\\DeadCode\\\\BetterNoopRule$# + """ + count: 1 + path: tests/PHPStan/Rules/DeadCode/NoopRuleTest.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$# + """ + count: 1 + path: tests/PHPStan/Rules/DeadCode/NoopRuleTest.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: """ + #^Return type of method PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRule\\: + Replaced by PHPStan\\\\Rules\\\\Functions\\\\ImplodeParameterCastableToStringRuleTest$# + """ + count: 1 + path: tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.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\\.$#" count: 1 diff --git a/phpunit.xml b/phpunit.xml index 03fcbf718f..b083450096 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,8 +4,10 @@ bootstrap="tests/bootstrap.php" cacheResult="false" colors="true" + executionOrder="random" failOnRisky="true" failOnWarning="true" + failOnEmptyTestSuite="true" beStrictAboutChangesToGlobalState="true" beStrictAboutCoversAnnotation="true" beStrictAboutOutputDuringTests="true" diff --git a/resources/RegexGrammar.pp b/resources/RegexGrammar.pp new file mode 100644 index 0000000000..b8bea027d3 --- /dev/null +++ b/resources/RegexGrammar.pp @@ -0,0 +1,215 @@ +// +// Hoa +// +// +// @license +// +// New BSD License +// +// Copyright © 2007-2017, Hoa community. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the Hoa nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Grammar \Hoa\Regex\Grammar. +// +// Provide grammar of PCRE (Perl Compatible Regular Expression)for the LL(k) +// parser. More informations at http://pcre.org/pcre.txt, sections pcrepattern & +// pcresyntax. +// +// @copyright Copyright © 2007-2017 Hoa community. +// @license New BSD License +// + +// Character classes. +%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]+}) +%token class:character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token class:literal \\.|.|\n + +// Internal options. +// See https://www.regular-expressions.info/refmodifiers.html +%token internal_option \(\?([imsxnJUX^]|xx)?-?([imsxnJUX^]|xx)\) + +// Lookahead and lookbehind assertions. +%token lookahead_ \(\?= +%token negative_lookahead_ \(\?! +%token lookbehind_ \(\?<= +%token negative_lookbehind_ \(\? nc +%token absolute_reference_ \(\?\((?=\d) -> c +%token relative_reference_ \(\?\((?=[\+\-]) -> c +%token c:index [\+\-]?\d+ -> default +%token assertion_reference_ \(\?\( + +// Comments. +%token comment_ \(\?# -> co +%token co:_comment \) -> default +%token co:comment .*?(?=(? mark +%token mark:name [^)]+ +%token mark:_marker \) -> default + +// Capturing group. +%token named_capturing_ \(\?P?< -> nc +%token nc:_named_capturing > -> default +%token nc:capturing_name .+?(?=(?) +%token non_capturing_ \(\?: +%token non_capturing_internal_option \(\?([imsxnJUX^]|xx)?-?([imsxnJUX^]|xx): +%token non_capturing_reset_ \(\?\| +%token atomic_group_ \(\?> +%token capturing_ \( +%token _capturing \) + +// Quantifiers (by default, greedy). +%token zero_or_one_possessive \?\+ +%token zero_or_one_lazy \?\? +%token zero_or_one \? +%token zero_or_more_possessive \*\+ +%token zero_or_more_lazy \*\? +%token zero_or_more \* +%token one_or_more_possessive \+\+ +%token one_or_more_lazy \+\? +%token one_or_more \+ +%token exactly_n \{[0-9]+\} +%token n_to_m_possessive \{[0-9]+,[0-9]+\}\+ +%token n_to_m_lazy \{[0-9]+,[0-9]+\}\? +%token n_to_m \{[0-9]+,[0-9]+\} +%token n_or_more_possessive \{[0-9]+,\}\+ +%token n_or_more_lazy \{[0-9]+,\}\? +%token n_or_more \{[0-9]+,\} + +// Alternation. +%token alternation \| + +// Literal. +%token character \\([aefnrt]|c[\x00-\x7f]) +%token dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +// Please, see PCRESYNTAX(3), General Category properties, PCRE special category +// properties and script names for \p{} and \P{}. +%token character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token anchor \\([bBAZzG])|\^|\$ +%token match_point_reset \\K +%token literal \\.|.|\n + + +// Rules. + +#expression: + alternation() + +alternation: + concatenation() ( ::alternation:: concatenation() #alternation )* + +concatenation: + ( internal_options() | assertion() | quantification() | condition() ) + ( ( internal_options() | assertion() | quantification() | condition() ) #concatenation )* + +#internal_options: + + +#condition: + ( + ::named_reference_:: ::_named_capturing:: #namedcondition + | ( + ::relative_reference_:: #relativecondition + | ::absolute_reference_:: #absolutecondition + ) + + | ::assertion_reference_:: alternation() #assertioncondition + ) + ::_capturing:: concatenation()? + ( ::alternation:: concatenation()? )? + ::_capturing:: + +assertion: + ( + ::lookahead_:: #lookahead + | ::negative_lookahead_:: #negativelookahead + | ::lookbehind_:: #lookbehind + | ::negative_lookbehind_:: #negativelookbehind + ) + alternation() ::_capturing:: + +quantification: + ( class() | simple() ) ( quantifier() #quantification )? + +quantifier: + | | + | | | + | | | + | + | | | + | | | + +#class: + ( + ::negative_class_:: #negativeclass + | ::class_:: + ) + ( | <_class_literal> )? ( | | range() | literal() | )* ? + ::_class:: + +#range: + literal() ::range:: literal() + +simple: + capturing() + | literal() + +#capturing: + ::marker_:: ::_marker:: #mark + | ::comment_:: ? ::_comment:: #comment + | ( + ::named_capturing_:: ::_named_capturing:: #namedcapturing + | ::non_capturing_:: #noncapturing + | non_capturing_internal_options() #noncapturing + | ::non_capturing_reset_:: #noncapturingreset + | ::atomic_group_:: #atomicgroup + | ::capturing_:: + ) + alternation() ::_capturing:: + +non_capturing_internal_options: + + +literal: + + | + | + | + | + | diff --git a/resources/functionMap.php b/resources/functionMap.php index 193b03052a..558096b86b 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -69,105 +69,105 @@ 'acosh' => ['float', 'number'=>'float'], 'addcslashes' => ['string', 'str'=>'string', 'charlist'=>'string'], 'addslashes' => ['string', 'str'=>'string'], -'AMQPChannel::__construct' => ['void', 'amqp_connection'=>'AMQPConnection'], -'AMQPChannel::basicRecover' => ['', 'requeue='=>'bool|true'], -'AMQPChannel::commitTransaction' => ['bool'], -'AMQPChannel::getChannelId' => ['int'], +'AMQPChannel::__construct' => ['void', 'connection'=>'AMQPConnection'], +'AMQPChannel::basicRecover' => ['void', 'requeue='=>'bool'], +'AMQPChannel::commitTransaction' => ['void'], +'AMQPChannel::getChannelId' => ['int<1, 65535>'], 'AMQPChannel::getConnection' => ['AMQPConnection'], -'AMQPChannel::getPrefetchCount' => ['int'], -'AMQPChannel::getPrefetchSize' => ['int'], +'AMQPChannel::getPrefetchCount' => ['int<0, 65535>'], +'AMQPChannel::getPrefetchSize' => ['int<0, max>'], 'AMQPChannel::isConnected' => ['bool'], -'AMQPChannel::qos' => ['bool', 'size'=>'int', 'count'=>'int'], -'AMQPChannel::rollbackTransaction' => ['bool'], -'AMQPChannel::setPrefetchCount' => ['bool', 'count'=>'int'], -'AMQPChannel::setPrefetchSize' => ['bool', 'size'=>'int'], -'AMQPChannel::startTransaction' => ['bool'], +'AMQPChannel::qos' => ['void', 'size'=>'int', 'count'=>'int', 'global='=>'bool'], +'AMQPChannel::rollbackTransaction' => ['void'], +'AMQPChannel::setPrefetchCount' => ['void', 'count'=>'int'], +'AMQPChannel::setPrefetchSize' => ['void', 'size'=>'int'], +'AMQPChannel::startTransaction' => ['void'], 'AMQPConnection::__construct' => ['void', 'credentials='=>'array'], -'AMQPConnection::connect' => ['bool'], -'AMQPConnection::disconnect' => ['bool'], +'AMQPConnection::connect' => ['void'], +'AMQPConnection::disconnect' => ['void'], 'AMQPConnection::getHost' => ['string'], 'AMQPConnection::getLogin' => ['string'], -'AMQPConnection::getMaxChannels' => ['int|null'], +'AMQPConnection::getMaxChannels' => ['int<1, 65535>'], 'AMQPConnection::getPassword' => ['string'], -'AMQPConnection::getPort' => ['int'], +'AMQPConnection::getPort' => ['int<1, 65535>'], 'AMQPConnection::getReadTimeout' => ['float'], 'AMQPConnection::getTimeout' => ['float'], -'AMQPConnection::getUsedChannels' => ['int'], +'AMQPConnection::getUsedChannels' => ['int<1, 65535>'], 'AMQPConnection::getVhost' => ['string'], 'AMQPConnection::getWriteTimeout' => ['float'], 'AMQPConnection::isConnected' => ['bool'], -'AMQPConnection::isPersistent' => ['bool|null'], -'AMQPConnection::pconnect' => ['bool'], -'AMQPConnection::pdisconnect' => ['bool'], -'AMQPConnection::preconnect' => ['bool'], -'AMQPConnection::reconnect' => ['bool'], -'AMQPConnection::setHost' => ['bool', 'host'=>'string'], -'AMQPConnection::setLogin' => ['bool', 'login'=>'string'], -'AMQPConnection::setPassword' => ['bool', 'password'=>'string'], -'AMQPConnection::setPort' => ['bool', 'port'=>'int'], -'AMQPConnection::setReadTimeout' => ['bool', 'timeout'=>'int'], -'AMQPConnection::setTimeout' => ['bool', 'timeout'=>'int'], -'AMQPConnection::setVhost' => ['bool', 'vhost'=>'string'], -'AMQPConnection::setWriteTimeout' => ['bool', 'timeout'=>'int'], -'AMQPEnvelope::getAppId' => ['string'], +'AMQPConnection::isPersistent' => ['bool'], +'AMQPConnection::pconnect' => ['void'], +'AMQPConnection::pdisconnect' => ['void'], +'AMQPConnection::preconnect' => ['void'], +'AMQPConnection::reconnect' => ['void'], +'AMQPConnection::setHost' => ['void', 'host'=>'string'], +'AMQPConnection::setLogin' => ['void', 'login'=>'string'], +'AMQPConnection::setPassword' => ['void', 'password'=>'string'], +'AMQPConnection::setPort' => ['void', 'port'=>'int'], +'AMQPConnection::setReadTimeout' => ['void', 'timeout'=>'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'], -'AMQPEnvelope::getContentType' => ['string'], -'AMQPEnvelope::getCorrelationId' => ['string'], +'AMQPEnvelope::getContentEncoding' => ['string|null'], +'AMQPEnvelope::getContentType' => ['string|null'], +'AMQPEnvelope::getCorrelationId' => ['string|null'], 'AMQPEnvelope::getDeliveryMode' => ['int'], -'AMQPEnvelope::getDeliveryTag' => ['string'], -'AMQPEnvelope::getExchangeName' => ['string'], -'AMQPEnvelope::getExpiration' => ['string'], -'AMQPEnvelope::getHeader' => ['bool|string', 'header_key'=>'string'], -'AMQPEnvelope::getHeaders' => ['array'], -'AMQPEnvelope::getMessageId' => ['string'], -'AMQPEnvelope::getPriority' => ['int'], -'AMQPEnvelope::getReplyTo' => ['string'], +'AMQPEnvelope::getDeliveryTag' => ['int|null'], +'AMQPEnvelope::getExchangeName' => ['string|null'], +'AMQPEnvelope::getExpiration' => ['string|null'], +'AMQPEnvelope::getHeader' => ['mixed', 'headerName'=>'string'], +'AMQPEnvelope::getHeaders' => ['array'], +'AMQPEnvelope::getMessageId' => ['string|null'], +'AMQPEnvelope::getPriority' => ['int<0, max>'], +'AMQPEnvelope::getReplyTo' => ['string|null'], 'AMQPEnvelope::getRoutingKey' => ['string'], -'AMQPEnvelope::getTimeStamp' => ['string'], -'AMQPEnvelope::getType' => ['string'], -'AMQPEnvelope::getUserId' => ['string'], +'AMQPEnvelope::getTimestamp' => ['int|null'], +'AMQPEnvelope::getType' => ['string|null'], +'AMQPEnvelope::getUserId' => ['string|null'], 'AMQPEnvelope::isRedelivery' => ['bool'], -'AMQPExchange::__construct' => ['void', 'amqp_channel'=>'AMQPChannel'], -'AMQPExchange::bind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], -'AMQPExchange::declareExchange' => ['bool'], -'AMQPExchange::delete' => ['bool', 'exchangeName='=>'string', 'flags='=>'int'], -'AMQPExchange::getArgument' => ['bool|int|string', 'key'=>'string'], -'AMQPExchange::getArguments' => ['array'], +'AMQPExchange::__construct' => ['void', 'channel'=>'AMQPChannel'], +'AMQPExchange::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPExchange::declareExchange' => ['void'], +'AMQPExchange::delete' => ['void', 'exchangeName='=>'string', 'flags='=>'int'], +'AMQPExchange::getArgument' => ['scalar|null', 'argumentName'=>'string'], +'AMQPExchange::getArguments' => ['array'], 'AMQPExchange::getChannel' => ['AMQPChannel'], 'AMQPExchange::getConnection' => ['AMQPConnection'], 'AMQPExchange::getFlags' => ['int'], -'AMQPExchange::getName' => ['string'], -'AMQPExchange::getType' => ['string'], -'AMQPExchange::publish' => ['bool', 'message'=>'string', 'routing_key='=>'string', 'flags='=>'int', 'attributes='=>'array'], -'AMQPExchange::setArgument' => ['bool', 'key'=>'string', 'value'=>'int|string'], -'AMQPExchange::setArguments' => ['bool', 'arguments'=>'array'], -'AMQPExchange::setFlags' => ['bool', 'flags'=>'int'], -'AMQPExchange::setName' => ['bool', 'exchange_name'=>'string'], -'AMQPExchange::setType' => ['bool', 'exchange_type'=>'string'], -'AMQPExchange::unbind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], -'AMQPQueue::__construct' => ['void', 'amqp_channel'=>'AMQPChannel'], -'AMQPQueue::ack' => ['bool', 'delivery_tag'=>'string', 'flags='=>'int'], -'AMQPQueue::bind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], -'AMQPQueue::cancel' => ['bool', 'consumer_tag='=>'string'], -'AMQPQueue::consume' => ['void', 'callback='=>'?callable', 'flags='=>'int', 'consumerTag='=>'string'], +'AMQPExchange::getName' => ['string|null'], +'AMQPExchange::getType' => ['string|null'], +'AMQPExchange::publish' => ['void', 'message'=>'string', 'routingKey='=>'string|null', 'flags='=>'int|null', 'header='=>'array'], +'AMQPExchange::setArgument' => ['void', 'argumentName'=>'string', 'argumentValue'=>'scalar|null'], +'AMQPExchange::setArguments' => ['void', 'arguments'=>'array'], +'AMQPExchange::setFlags' => ['void', 'flags'=>'int|null'], +'AMQPExchange::setName' => ['void', 'exchangeName'=>'string|null'], +'AMQPExchange::setType' => ['void', 'exchangeType'=>'string|null'], +'AMQPExchange::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPQueue::__construct' => ['void', 'channel'=>'AMQPChannel'], +'AMQPQueue::ack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::bind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], +'AMQPQueue::cancel' => ['void', 'consumerTag='=>'string'], +'AMQPQueue::consume' => ['void', 'callback='=>'null|callable(AMQPEnvelope, AMQPQueue): mixed', 'flags='=>'int|null', 'consumerTag='=>'string|null'], 'AMQPQueue::declareQueue' => ['int'], -'AMQPQueue::delete' => ['int', 'flags='=>'int'], -'AMQPQueue::get' => ['AMQPEnvelope|bool', 'flags='=>'int'], -'AMQPQueue::getArgument' => ['bool|int|string', 'key'=>'string'], +'AMQPQueue::delete' => ['int', 'flags='=>'int|null'], +'AMQPQueue::get' => ['AMQPEnvelope|null', 'flags='=>'int|null'], +'AMQPQueue::getArgument' => ['scalar|null|array|AMQPValue|AMQPDecimal|AMQPTimestamp', 'argumentName'=>'string'], 'AMQPQueue::getArguments' => ['array'], 'AMQPQueue::getChannel' => ['AMQPChannel'], 'AMQPQueue::getConnection' => ['AMQPConnection'], 'AMQPQueue::getFlags' => ['int'], -'AMQPQueue::getName' => ['string'], -'AMQPQueue::nack' => ['bool', 'delivery_tag'=>'string', 'flags='=>'int'], -'AMQPQueue::purge' => ['bool'], -'AMQPQueue::reject' => ['bool', 'delivery_tag'=>'string', 'flags='=>'int'], -'AMQPQueue::setArgument' => ['bool', 'key'=>'string', 'value'=>'mixed'], -'AMQPQueue::setArguments' => ['bool', 'arguments'=>'array'], -'AMQPQueue::setFlags' => ['bool', 'flags'=>'int'], -'AMQPQueue::setName' => ['bool', 'queue_name'=>'string'], -'AMQPQueue::unbind' => ['bool', 'exchange_name'=>'string', 'routing_key='=>'string', 'arguments='=>'array'], +'AMQPQueue::getName' => ['string|null'], +'AMQPQueue::nack' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::purge' => ['int'], +'AMQPQueue::reject' => ['void', 'deliveryTag'=>'int', 'flags='=>'int|null'], +'AMQPQueue::setArgument' => ['void', 'argumentName'=>'string', 'argumentValue'=>'scalar|null|array|AMQPValue|AMQPDecimal|AMQPTimestamp'], +'AMQPQueue::setArguments' => ['void', 'arguments'=>'array'], +'AMQPQueue::setFlags' => ['void', 'flags'=>'int|null'], +'AMQPQueue::setName' => ['void', 'name'=>'string'], +'AMQPQueue::unbind' => ['void', 'exchangeName'=>'string', 'routingKey='=>'string|null', 'arguments='=>'array'], 'apache_child_terminate' => ['bool'], 'apache_get_modules' => ['array'], 'apache_get_version' => ['string|false'], @@ -395,7 +395,7 @@ 'BadFunctionCallException::getLine' => ['int'], 'BadFunctionCallException::getMessage' => ['string'], 'BadFunctionCallException::getPrevious' => ['(?Throwable)|(?BadFunctionCallException)'], -'BadFunctionCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'BadFunctionCallException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'BadFunctionCallException::getTraceAsString' => ['string'], 'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?BadMethodCallException)'], @@ -405,7 +405,7 @@ 'BadMethodCallException::getLine' => ['int'], 'BadMethodCallException::getMessage' => ['string'], 'BadMethodCallException::getPrevious' => ['(?Throwable)|(?BadMethodCallException)'], -'BadMethodCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'BadMethodCallException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'BadMethodCallException::getTraceAsString' => ['string'], 'base64_decode' => ['string', 'str'=>'string', 'strict='=>'false'], 'base64_decode\'1' => ['string|false', 'str'=>'string', 'strict='=>'true'], @@ -933,7 +933,7 @@ 'CallbackFilterIterator::next' => ['void'], 'CallbackFilterIterator::rewind' => ['void'], 'CallbackFilterIterator::valid' => ['bool'], -'ceil' => ['float|false', 'number'=>'float'], +'ceil' => ['__benevolent', 'number'=>'float'], 'chdb::__construct' => ['void', 'pathname'=>'string'], 'chdb::get' => ['string', 'key'=>'string'], 'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'], @@ -990,7 +990,7 @@ 'ClosedGeneratorException::getLine' => ['int'], 'ClosedGeneratorException::getMessage' => ['string'], 'ClosedGeneratorException::getPrevious' => ['Throwable|ClosedGeneratorException|null'], -'ClosedGeneratorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'ClosedGeneratorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'ClosedGeneratorException::getTraceAsString' => ['string'], 'closedir' => ['void', 'dir_handle='=>'resource'], 'closelog' => ['bool'], @@ -1501,7 +1501,7 @@ 'curl_multi_exec' => ['int', 'mh'=>'resource', '&w_still_running'=>'int'], 'curl_multi_getcontent' => ['string', 'ch'=>'resource'], 'curl_multi_info_read' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], -'curl_multi_init' => ['resource|false'], +'curl_multi_init' => ['resource'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_select' => ['int', 'mh'=>'resource', 'timeout='=>'float'], 'curl_multi_setopt' => ['bool', 'mh'=>'resource', 'option'=>'int', 'value'=>'mixed'], @@ -1552,7 +1552,7 @@ 'date_parse' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'date'=>'string'], 'date_parse_from_format' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'format'=>'string', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], -'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], +'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_sunrise' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], 'date_sunset' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'', 'hour'=>'', 'minute'=>'', 'second='=>'', 'microseconds='=>''], @@ -1564,7 +1564,7 @@ 'datefmt_format' => ['string|false', 'fmt'=>'IntlDateFormatter', 'value'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int|false', 'fmt'=>'IntlDateFormatter'], -'datefmt_get_calendar_object' => ['IntlCalendar|false', 'fmt'=>'IntlDateFormatter'], +'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'fmt'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'fmt'=>'IntlDateFormatter'], @@ -1574,10 +1574,10 @@ 'datefmt_get_timezone' => ['IntlTimeZone|false'], 'datefmt_get_timezone_id' => ['string|false', 'fmt'=>'IntlDateFormatter'], 'datefmt_is_lenient' => ['bool', 'fmt'=>'IntlDateFormatter'], -'datefmt_localtime' => ['array|bool|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], -'datefmt_parse' => ['int|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], +'datefmt_localtime' => ['array|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], +'datefmt_parse' => ['int|float|false', 'fmt'=>'IntlDateFormatter', 'text_to_parse='=>'string', '&rw_parse_pos='=>'int'], 'datefmt_set_calendar' => ['bool', 'fmt'=>'IntlDateFormatter', 'which'=>'int'], -'datefmt_set_lenient' => ['?bool', 'fmt'=>'IntlDateFormatter', 'lenient'=>'bool'], +'datefmt_set_lenient' => ['void', 'fmt'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'fmt'=>'IntlDateFormatter', 'pattern'=>'string'], 'datefmt_set_timezone' => ['bool', 'zone'=>'mixed'], 'datefmt_set_timezone_id' => ['bool', 'fmt'=>'IntlDateFormatter', 'zone'=>'string'], @@ -1659,8 +1659,8 @@ 'db2_escape_string' => ['string', 'string_literal'=>'string'], 'db2_exec' => ['resource|false', 'connection'=>'resource', 'statement'=>'string', 'options='=>'array'], 'db2_execute' => ['bool', 'stmt'=>'resource', 'parameters='=>'array'], -'db2_fetch_array' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], -'db2_fetch_assoc' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], +'db2_fetch_array' => ['non-empty-list|false', 'stmt'=>'resource', 'row_number='=>'int'], +'db2_fetch_assoc' => ['non-empty-array|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_both' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_object' => ['stdClass|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_row' => ['bool', 'stmt'=>'resource', 'row_number='=>'int'], @@ -1884,7 +1884,7 @@ 'DomainException::getLine' => ['int'], 'DomainException::getMessage' => ['string'], 'DomainException::getPrevious' => ['Throwable|DomainException|null'], -'DomainException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'DomainException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'DomainException::getTraceAsString' => ['string'], 'DOMAttr::__construct' => ['void', 'name'=>'string', 'value='=>'string'], 'DOMAttr::isId' => ['bool'], @@ -1914,10 +1914,10 @@ 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode', 'importednode'=>'DOMNode', 'deep='=>'bool'], -'DOMDocument::load' => ['mixed', 'filename'=>'string', 'options='=>'int'], +'DOMDocument::load' => ['bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'string', 'options='=>'int'], 'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'], -'DOMDocument::loadXML' => ['mixed', 'source'=>'string', 'options='=>'int'], +'DOMDocument::loadXML' => ['bool', 'source'=>'string', 'options='=>'int'], 'DOMDocument::normalizeDocument' => ['void'], 'DOMDocument::registerNodeClass' => ['bool', 'baseclass'=>'string', 'extendedclass'=>'string'], 'DOMDocument::relaxNGValidate' => ['bool', 'filename'=>'string'], @@ -2332,7 +2332,7 @@ 'Error::getLine' => ['int'], 'Error::getMessage' => ['string'], 'Error::getPrevious' => ['Throwable|Error|null'], -'Error::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'Error::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'Error::getTraceAsString' => ['string'], 'error_clear_last' => ['void'], 'error_get_last' => ['?array{type:int,message:string,file:string,line:int}'], @@ -2347,7 +2347,7 @@ 'ErrorException::getMessage' => ['string'], 'ErrorException::getPrevious' => ['Throwable|ErrorException|null'], 'ErrorException::getSeverity' => ['int'], -'ErrorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'ErrorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'ErrorException::getTraceAsString' => ['string'], 'escapeshellarg' => ['string', 'arg'=>'string'], 'escapeshellcmd' => ['string', 'command'=>'string'], @@ -2626,9 +2626,9 @@ 'Exception::getLine' => ['int'], 'Exception::getMessage' => ['string'], 'Exception::getPrevious' => ['(?Throwable)|(?Exception)'], -'Exception::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'Exception::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'Exception::getTraceAsString' => ['string'], -'exec' => ['string', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], +'exec' => ['string|false', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], 'exif_imagetype' => ['int|false', 'imagefile'=>'string'], 'exif_read_data' => ['array|false', 'filename'=>'string|resource', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'exif_tagname' => ['string|false', 'index'=>'int'], @@ -2991,7 +2991,7 @@ 'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'], 'floatval' => ['float', 'var'=>'scalar|array|resource|null'], 'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'], -'floor' => ['float|false', 'number'=>'float'], +'floor' => ['__benevolent', 'number'=>'float'], 'flush' => ['void'], 'fmod' => ['float', 'x'=>'float', 'y'=>'float'], 'fnmatch' => ['bool', 'pattern'=>'string', 'filename'=>'string', 'flags='=>'int'], @@ -3003,7 +3003,7 @@ 'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'fputcsv' => ['0|positive-int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], 'fputs' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], -'fread' => ['string|false', 'fp'=>'resource', 'length'=>'0|positive-int'], +'fread' => ['string', 'fp'=>'resource', 'length'=>'positive-int'], 'frenchtojd' => ['int', 'month'=>'int', 'day'=>'int', 'year'=>'int'], 'fribidi_log2vis' => ['string', 'str'=>'string', 'direction'=>'string', 'charset'=>'int'], 'fscanf' => ['list|int|false', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], @@ -3299,7 +3299,7 @@ 'get_called_class' => ['class-string'], 'get_cfg_var' => ['mixed', 'option_name'=>'string'], 'get_class' => ['class-string', 'object='=>'object'], -'get_class_methods' => ['list', 'class'=>'mixed'], +'get_class_methods' => ['list', 'class'=>'mixed'], 'get_class_vars' => ['array', 'class_name'=>'string'], 'get_current_user' => ['string'], 'get_declared_classes' => ['list'], @@ -3311,7 +3311,7 @@ 'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int', 'context='=>'resource'], 'get_html_translation_table' => ['array', 'table='=>'int', 'flags='=>'int', 'encoding='=>'string'], -'get_include_path' => ['string|false'], +'get_include_path' => ['__benevolent'], 'get_included_files' => ['list'], 'get_loaded_extensions' => ['list', 'zend_extensions='=>'bool'], 'get_magic_quotes_gpc' => ['false'], @@ -3331,8 +3331,8 @@ 'gethostbyname' => ['string', 'hostname'=>'string'], 'gethostbynamel' => ['list|false', 'hostname'=>'string'], 'gethostname' => ['string|false'], -'getimagesize' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'imagefile'=>'string', '&w_info='=>'array'], -'getimagesizefromstring' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'data'=>'string', '&w_info='=>'array'], +'getimagesize' => ['array{0: 0|positive-int, 1: 0|positive-int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'imagefile'=>'string', '&w_info='=>'array'], +'getimagesizefromstring' => ['array{0: 0|positive-int, 1: 0|positive-int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'data'=>'string', '&w_info='=>'array'], 'getlastmod' => ['int|false'], 'getmxrr' => ['bool', 'hostname'=>'string', '&w_mxhosts'=>'array', '&w_weight='=>'array'], 'getmygid' => ['int|false'], @@ -3610,7 +3610,7 @@ 'gnupg::getprotocol' => ['int'], 'gnupg::gettrustlist' => ['array', 'pattern'=>'string'], 'gnupg::import' => ['array|false', 'keydata'=>'string'], -'gnupg::init' => ['resource'], +'gnupg::init' => ['resource', 'options'=>'?array{file_name?:string,home_dir?:string}'], 'gnupg::keyinfo' => ['array|false', 'pattern'=>'string'], 'gnupg::listsignatures' => ['?array', 'keyid'=>'string'], 'gnupg::setarmor' => ['bool', 'armor'=>'int'], @@ -3635,7 +3635,7 @@ 'gnupg_getprotocol' => ['int', 'identifier'=>'resource'], 'gnupg_gettrustlist' => ['array', 'identifier'=>'resource', 'pattern'=>'string'], 'gnupg_import' => ['array|false', 'identifier'=>'resource', 'keydata'=>'string'], -'gnupg_init' => ['resource'], +'gnupg_init' => ['resource', 'options='=>'?array{file_name?:string,home_dir?:string}'], 'gnupg_keyinfo' => ['array|false', 'identifier'=>'resource', 'pattern'=>'string'], 'gnupg_listsignatures' => ['?array', 'identifier'=>'resource', 'keyid'=>'string'], 'gnupg_setarmor' => ['bool', 'identifier'=>'resource', 'armor'=>'int'], @@ -4544,21 +4544,21 @@ 'imagebmp' => ['bool', 'image'=>'resource', 'to='=>'string|resource|null', 'compressed='=>'bool'], 'imagechar' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'c'=>'string', 'col'=>'int'], 'imagecharup' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'c'=>'string', 'col'=>'int'], -'imagecolorallocate' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorallocatealpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorat' => ['int|false', 'im'=>'resource', 'x'=>'int', 'y'=>'int'], -'imagecolorclosest' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorclosestalpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], -'imagecolorclosesthwb' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], +'imagecolorallocate' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], +'imagecolorallocatealpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], +'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'], 'imagecolordeallocate' => ['bool', 'im'=>'resource', 'index'=>'int'], -'imagecolorexact' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorexactalpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], +'imagecolorexact' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], +'imagecolorexactalpha' => ['int<0, max>|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], 'imagecolormatch' => ['bool', 'im1'=>'resource', 'im2'=>'resource'], -'imagecolorresolve' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], -'imagecolorresolvealpha' => ['int|false', 'im'=>'resource', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'alpha'=>'int'], +'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'], -'imagecolorsforindex' => ['array|false', 'im'=>'resource', 'col'=>'int'], -'imagecolorstotal' => ['int', 'im'=>'resource'], +'imagecolorsforindex' => ['array{red: int<0, 255>, green: int<0, 255>, blue: int<0, 255>, alpha: int<0, 127>}', 'im'=>'resource', 'col'=>'int'], +'imagecolorstotal' => ['int<0, 256>', 'im'=>'resource'], 'imagecolortransparent' => ['int', 'im'=>'resource', 'col='=>'int'], 'imageconvolution' => ['bool', 'src_im'=>'resource', 'matrix3x3'=>'array', 'div'=>'float', 'offset'=>'float'], 'imagecopy' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'src_w'=>'int', 'src_h'=>'int'], @@ -4566,7 +4566,7 @@ 'imagecopymergegray' => ['bool', 'src_im'=>'resource', 'dst_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'src_w'=>'int', 'src_h'=>'int', 'pct'=>'int'], 'imagecopyresampled' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'dst_w'=>'int', 'dst_h'=>'int', 'src_w'=>'int', 'src_h'=>'int'], 'imagecopyresized' => ['bool', 'dst_im'=>'resource', 'src_im'=>'resource', 'dst_x'=>'int', 'dst_y'=>'int', 'src_x'=>'int', 'src_y'=>'int', 'dst_w'=>'int', 'dst_h'=>'int', 'src_w'=>'int', 'src_h'=>'int'], -'imagecreate' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], +'imagecreate' => ['__benevolent', 'x_size'=>'int', 'y_size'=>'int'], 'imagecreatefrombmp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromgd' => ['resource|false', 'filename'=>'string'], 'imagecreatefromgd2' => ['resource|false', 'filename'=>'string'], @@ -4579,7 +4579,7 @@ 'imagecreatefromwebp' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxbm' => ['resource|false', 'filename'=>'string'], 'imagecreatefromxpm' => ['resource|false', 'filename'=>'string'], -'imagecreatetruecolor' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], +'imagecreatetruecolor' => ['__benevolent', 'x_size'=>'int', 'y_size'=>'int'], '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'], @@ -4639,8 +4639,8 @@ 'imagesettile' => ['bool', 'image'=>'resource', 'tile'=>'resource'], 'imagestring' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'str'=>'string', 'col'=>'int'], 'imagestringup' => ['bool', 'im'=>'resource', 'font'=>'int', 'x'=>'int', 'y'=>'int', 'str'=>'string', 'col'=>'int'], -'imagesx' => ['int|false', 'im'=>'resource'], -'imagesy' => ['int|false', 'im'=>'resource'], +'imagesx' => ['int<1, max>', 'im'=>'resource'], +'imagesy' => ['int<1, max>', 'im'=>'resource'], 'imagetruecolortopalette' => ['bool', 'im'=>'resource', 'ditherflag'=>'bool', 'colorswanted'=>'int'], 'imagettfbbox' => ['array|false', 'size'=>'float', 'angle'=>'float', 'font_file'=>'string', 'text'=>'string'], 'imagettftext' => ['array|false', 'im'=>'resource', 'size'=>'float', 'angle'=>'float', 'x'=>'int', 'y'=>'int', 'col'=>'int', 'font_file'=>'string', 'text'=>'string'], @@ -4831,7 +4831,7 @@ 'Imagick::hasNextImage' => ['bool'], 'Imagick::hasPreviousImage' => ['bool'], 'Imagick::identifyFormat' => ['string|false', 'embedText'=>'string'], -'Imagick::identifyImage' => ['array{width:0|positive-int,height:0|positive-int}', 'appendrawoutput='=>'bool'], +'Imagick::identifyImage' => ['array{imageName:string,mimetype:string,format:string,units:string,colorSpace:string,type:string,compression:string,fileSize:string,geometry:array{width:0|positive-int,height:0|positive-int},resolution:array{x:float,y:float},signature:string}', 'appendrawoutput='=>'bool'], 'Imagick::identifyImageType' => ['int'], 'Imagick::implodeImage' => ['bool', 'radius'=>'float'], 'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int', 'pixels'=>'array'], @@ -4905,7 +4905,7 @@ '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'], +'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'], @@ -5519,22 +5519,22 @@ 'IntlDateFormatter::__construct' => ['void', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'null|int|IntlCalendar', 'pattern='=>'string'], 'IntlDateFormatter::create' => ['IntlDateFormatter|null', 'locale'=>'?string', 'datetype'=>'?int', 'timetype'=>'?int', 'timezone='=>'null|string|IntlTimeZone|DateTimeZone', 'calendar='=>'int|IntlCalendar', 'pattern='=>'string'], 'IntlDateFormatter::format' => ['string|false', 'args'=>''], -'IntlDateFormatter::formatObject' => ['string', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], -'IntlDateFormatter::getCalendar' => ['int'], -'IntlDateFormatter::getCalendarObject' => ['IntlCalendar'], -'IntlDateFormatter::getDateType' => ['int'], +'IntlDateFormatter::formatObject' => ['string|false', 'object'=>'object', 'format='=>'mixed', 'locale='=>'string'], +'IntlDateFormatter::getCalendar' => ['int|false'], +'IntlDateFormatter::getCalendarObject' => ['IntlCalendar|false|null'], +'IntlDateFormatter::getDateType' => ['int|false'], 'IntlDateFormatter::getErrorCode' => ['int'], 'IntlDateFormatter::getErrorMessage' => ['string'], -'IntlDateFormatter::getLocale' => ['string'], -'IntlDateFormatter::getPattern' => ['string'], -'IntlDateFormatter::getTimeType' => ['int'], -'IntlDateFormatter::getTimeZone' => ['IntlTimeZone'], -'IntlDateFormatter::getTimeZoneId' => ['string'], +'IntlDateFormatter::getLocale' => ['string|false'], +'IntlDateFormatter::getPattern' => ['string|false'], +'IntlDateFormatter::getTimeType' => ['int|false'], +'IntlDateFormatter::getTimeZone' => ['IntlTimeZone|false'], +'IntlDateFormatter::getTimeZoneId' => ['string|false'], 'IntlDateFormatter::isLenient' => ['bool'], -'IntlDateFormatter::localtime' => ['array', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], -'IntlDateFormatter::parse' => ['int|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], +'IntlDateFormatter::localtime' => ['array|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], +'IntlDateFormatter::parse' => ['int|float|false', 'text_to_parse'=>'string', '&w_parse_pos='=>'int'], 'IntlDateFormatter::setCalendar' => ['bool', 'calendar'=>''], -'IntlDateFormatter::setLenient' => ['bool', 'lenient'=>'bool'], +'IntlDateFormatter::setLenient' => ['void', 'lenient'=>'bool'], 'IntlDateFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'IntlDateFormatter::setTimeZone' => ['bool', 'timezone'=>''], 'IntlDateFormatter::setTimeZoneId' => ['bool', 'zone'=>'string', 'fmt='=>'IntlDateFormatter'], @@ -5625,7 +5625,7 @@ 'InvalidArgumentException::getLine' => ['int'], 'InvalidArgumentException::getMessage' => ['string'], 'InvalidArgumentException::getPrevious' => ['Throwable|InvalidArgumentException|null'], -'InvalidArgumentException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'InvalidArgumentException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'InvalidArgumentException::getTraceAsString' => ['string'], 'ip2long' => ['int|false', 'ip_address'=>'string'], 'iptcembed' => ['string|bool', 'iptcdata'=>'string', 'jpeg_file_name'=>'string', 'spool='=>'int'], @@ -5922,7 +5922,7 @@ 'LengthException::getLine' => ['int'], 'LengthException::getMessage' => ['string'], 'LengthException::getPrevious' => ['Throwable|LengthException|null'], -'LengthException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'LengthException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'LengthException::getTraceAsString' => ['string'], 'levenshtein' => ['int', 'str1'=>'string', 'str2'=>'string'], 'levenshtein\'1' => ['int', 'str1'=>'string', 'str2'=>'string', 'cost_ins'=>'int', 'cost_rep'=>'int', 'cost_del'=>'int'], @@ -5953,41 +5953,41 @@ 'linkinfo' => ['int|false', 'filename'=>'string'], 'litespeed_request_headers' => ['array'], 'litespeed_response_headers' => ['array|false'], -'Locale::acceptFromHttp' => ['string|false', 'header'=>'string'], -'Locale::canonicalize' => ['string', 'locale'=>'string'], -'Locale::composeLocale' => ['string', 'subtags'=>'array'], -'Locale::filterMatches' => ['bool', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'Locale::getAllVariants' => ['array', 'locale'=>'string'], -'Locale::getDefault' => ['string'], -'Locale::getDisplayLanguage' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayName' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayRegion' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayScript' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getDisplayVariant' => ['string', 'locale'=>'string', 'in_locale='=>'string'], -'Locale::getKeywords' => ['array|false', 'locale'=>'string'], -'Locale::getPrimaryLanguage' => ['string', 'locale'=>'string'], -'Locale::getRegion' => ['string', 'locale'=>'string'], -'Locale::getScript' => ['string', 'locale'=>'string'], -'Locale::lookup' => ['string', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'default='=>'string'], -'Locale::parseLocale' => ['array', 'locale'=>'string'], +'Locale::acceptFromHttp' => ['non-empty-string|false', 'header'=>'string'], +'Locale::canonicalize' => ['non-empty-string|null', 'locale'=>'string'], +'Locale::composeLocale' => ['string|false', 'subtags'=>'array'], +'Locale::filterMatches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'Locale::getAllVariants' => ['array|null', 'locale'=>'string'], +'Locale::getDefault' => ['non-empty-string'], +'Locale::getDisplayLanguage' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayName' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayRegion' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayScript' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getDisplayVariant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'Locale::getKeywords' => ['array|null', 'locale'=>'string'], +'Locale::getPrimaryLanguage' => ['non-empty-string|null', 'locale'=>'string'], +'Locale::getRegion' => ['string|null', 'locale'=>'string'], +'Locale::getScript' => ['string|null', 'locale'=>'string'], +'Locale::lookup' => ['string|null', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'Locale::parseLocale' => ['array|null', 'locale'=>'string'], 'Locale::setDefault' => ['bool', 'locale'=>'string'], -'locale_accept_from_http' => ['string|false', 'header'=>'string'], -'locale_canonicalize' => ['', 'arg1'=>''], +'locale_accept_from_http' => ['non-empty-string|false', 'header'=>'string'], +'locale_canonicalize' => ['non-empty-string|null', 'locale'=>'string'], 'locale_compose' => ['string|false', 'subtags'=>'array'], -'locale_filter_matches' => ['bool', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'locale_get_all_variants' => ['array', 'locale'=>'string'], -'locale_get_default' => ['string'], -'locale_get_display_language' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_name' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_region' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_script' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_display_variant' => ['string|false', 'locale'=>'string', 'in_locale='=>'string'], -'locale_get_keywords' => ['array|false', 'locale'=>'string'], -'locale_get_primary_language' => ['string', 'locale'=>'string'], -'locale_get_region' => ['string', 'locale'=>'string'], -'locale_get_script' => ['string', 'locale'=>'string'], -'locale_lookup' => ['string', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'default='=>'string'], -'locale_parse' => ['array', 'locale'=>'string'], +'locale_filter_matches' => ['bool|null', 'langtag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'locale_get_all_variants' => ['array|null', 'locale'=>'string'], +'locale_get_default' => ['non-empty-string'], +'locale_get_display_language' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_name' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_region' => ['non-empty-string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], +'locale_get_keywords' => ['array|null', 'locale'=>'string'], +'locale_get_primary_language' => ['non-empty-string|null', 'locale'=>'string'], +'locale_get_region' => ['string|null', 'locale'=>'string'], +'locale_get_script' => ['string|null', 'locale'=>'string'], +'locale_lookup' => ['string|null', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'locale_parse' => ['array|null', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], 'localeconv' => ['array'], 'localtime' => ['array', 'timestamp='=>'int', 'associative_array='=>'bool'], @@ -6002,7 +6002,7 @@ 'LogicException::getLine' => ['int'], 'LogicException::getMessage' => ['string'], 'LogicException::getPrevious' => ['Throwable|LogicException|null'], -'LogicException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'LogicException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'LogicException::getTraceAsString' => ['string'], 'long2ip' => ['string|false', 'proper_address'=>'int'], 'lstat' => ['array|false', 'filename'=>'string'], @@ -6494,8 +6494,8 @@ 'MemcachePool::set' => ['bool', 'key'=>'string', 'var'=>'mixed', 'flag='=>'int', 'expire='=>'int'], 'MemcachePool::setCompressThreshold' => ['bool', 'threshold'=>'int', 'min_savings='=>'float'], 'MemcachePool::setServerParams' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'int', 'retry_interval='=>'int', 'status='=>'bool', 'failure_callback='=>'callable'], -'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'], -'memory_get_usage' => ['int', 'real_usage='=>'bool'], +'memory_get_peak_usage' => ['positive-int', 'real_usage='=>'bool'], +'memory_get_usage' => ['positive-int', 'real_usage='=>'bool'], 'MessageFormatter::__construct' => ['void', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::format' => ['false|string', 'args'=>'array'], @@ -6525,7 +6525,7 @@ 'ming_useconstants' => ['void', 'use'=>'int'], 'ming_useswfversion' => ['void', 'version'=>'int'], 'mkdir' => ['bool', 'pathname'=>'string', 'mode='=>'int', 'recursive='=>'bool', 'context='=>'resource'], -'mktime' => ['int|false', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], +'mktime' => ['__benevolent', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'Mongo::__construct' => ['void', 'server='=>'string', 'options='=>'array', 'driver_options='=>'array'], 'Mongo::__get' => ['MongoDB', 'dbname'=>'string'], @@ -6668,7 +6668,7 @@ 'MongoCursorException::getLine' => ['int'], 'MongoCursorException::getMessage' => ['string'], 'MongoCursorException::getPrevious' => ['Exception|Throwable'], -'MongoCursorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'MongoCursorException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoCursorException::getTraceAsString' => ['string'], 'MongoCursorInterface::__construct' => ['void'], 'MongoCursorInterface::batchSize' => ['MongoCursorInterface', 'batchSize'=>'int'], @@ -7082,7 +7082,7 @@ 'MongoException::getLine' => ['int'], 'MongoException::getMessage' => ['string'], 'MongoException::getPrevious' => ['Exception|Throwable'], -'MongoException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'MongoException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoException::getTraceAsString' => ['string'], 'MongoGridFS::__construct' => ['void', 'db'=>'MongoDB', 'prefix='=>'string', 'chunks='=>'mixed'], 'MongoGridFS::__get' => ['MongoCollection', 'name'=>'string'], @@ -7193,7 +7193,7 @@ 'MongoResultException::getLine' => ['int'], 'MongoResultException::getMessage' => ['string'], 'MongoResultException::getPrevious' => ['Exception|Throwable'], -'MongoResultException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'MongoResultException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoResultException::getTraceAsString' => ['string'], 'MongoTimestamp::__construct' => ['void', 'sec='=>'int', 'inc='=>'int'], 'MongoTimestamp::__toString' => ['string'], @@ -7213,7 +7213,7 @@ 'MongoWriteConcernException::getLine' => ['int'], 'MongoWriteConcernException::getMessage' => ['string'], 'MongoWriteConcernException::getPrevious' => ['Exception|Throwable'], -'MongoWriteConcernException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'MongoWriteConcernException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'MongoWriteConcernException::getTraceAsString' => ['string'], 'monitor_custom_event' => ['void', 'class'=>'string', 'text'=>'string', 'severe='=>'int', 'user_data='=>'mixed'], 'monitor_httperror_event' => ['void', 'error_code'=>'int', 'url'=>'string', 'severe='=>'int'], @@ -7420,7 +7420,7 @@ 'mysqli::get_charset' => ['object'], 'mysqli::get_client_info' => ['string'], 'mysqli::get_connection_stats' => ['array|false'], -'mysqli::get_warnings' => ['mysqli_warning'], +'mysqli::get_warnings' => ['mysqli_warning|false'], 'mysqli::init' => ['mysqli'], 'mysqli::kill' => ['bool', 'processid'=>'int'], 'mysqli::more_results' => ['bool'], @@ -7475,14 +7475,15 @@ 'mysqli_errno' => ['int', 'link'=>'mysqli'], 'mysqli_error' => ['string|null', 'link'=>'mysqli'], 'mysqli_error_list' => ['array', 'connection'=>'mysqli'], -'mysqli_fetch_all' => ['array|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], +'mysqli_fetch_all' => ['array', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_array' => ['array|null|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], -'mysqli_fetch_assoc' => ['array|null', 'result'=>'mysqli_result'], -'mysqli_fetch_field' => ['object|false', 'result'=>'mysqli_result'], -'mysqli_fetch_field_direct' => ['object|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], -'mysqli_fetch_fields' => ['array', 'result'=>'mysqli_result'], +'mysqli_fetch_assoc' => ['array|null|false', 'result'=>'mysqli_result'], +'mysqli_fetch_column' => ['null|int|float|string|false', 'result' => 'mysqli_result', 'column'=>'int'], +'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], +'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], +'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], 'mysqli_fetch_lengths' => ['array|false', 'result'=>'mysqli_result'], -'mysqli_fetch_object' => ['object|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], +'mysqli_fetch_object' => ['object|false|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], 'mysqli_fetch_row' => ['array|null', 'result'=>'mysqli_result'], 'mysqli_field_count' => ['int', 'link'=>'mysqli'], 'mysqli_field_seek' => ['bool', 'result'=>'mysqli_result', 'fieldnr'=>'int'], @@ -7527,11 +7528,12 @@ 'mysqli_result::close' => ['void'], 'mysqli_result::data_seek' => ['bool', 'offset'=>'int'], 'mysqli_result::fetch_all' => ['array', 'resulttype='=>'int'], -'mysqli_result::fetch_array' => ['array|null', 'resulttype='=>'int'], -'mysqli_result::fetch_assoc' => ['array|null'], -'mysqli_result::fetch_field' => ['object|false'], -'mysqli_result::fetch_field_direct' => ['object|false', 'fieldnr'=>'int'], -'mysqli_result::fetch_fields' => ['array'], +'mysqli_result::fetch_array' => ['array|null|false', 'resulttype='=>'int'], +'mysqli_result::fetch_assoc' => ['array|null|false'], +'mysqli_result::fetch_column' => ['null|int|float|string|false', 'column'=>'int'], +'mysqli_result::fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false'], +'mysqli_result::fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'fieldnr'=>'int'], +'mysqli_result::fetch_fields' => ['list'], 'mysqli_result::fetch_object' => ['object|null', 'class_name='=>'string', 'params='=>'array'], 'mysqli_result::fetch_row' => ['array|null'], 'mysqli_result::field_seek' => ['bool', 'fieldnr'=>'int'], @@ -7563,7 +7565,7 @@ 'mysqli_stmt::fetch' => ['bool|null'], 'mysqli_stmt::free_result' => ['void'], 'mysqli_stmt::get_result' => ['mysqli_result|false'], -'mysqli_stmt::get_warnings' => ['object'], +'mysqli_stmt::get_warnings' => ['mysqli_warning|false'], 'mysqli_stmt::more_results' => ['bool'], 'mysqli_stmt::next_result' => ['bool'], 'mysqli_stmt::num_rows' => ['int<0,max>|numeric-string'], @@ -7587,7 +7589,7 @@ 'mysqli_stmt_field_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_free_result' => ['void', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_get_result' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_get_warnings' => ['object|false', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_get_warnings' => ['mysqli_warning|false', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_init' => ['mysqli_stmt|false', 'link'=>'mysqli'], 'mysqli_stmt_insert_id' => ['', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_more_results' => ['bool', 'stmt'=>'mysqli_stmt'], @@ -8382,7 +8384,7 @@ 'OutOfBoundsException::getLine' => ['int'], 'OutOfBoundsException::getMessage' => ['string'], 'OutOfBoundsException::getPrevious' => ['Throwable|OutOfBoundsException|null'], -'OutOfBoundsException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'OutOfBoundsException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OutOfBoundsException::getTraceAsString' => ['string'], 'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?OutOfRangeException)'], @@ -8392,7 +8394,7 @@ 'OutOfRangeException::getLine' => ['int'], 'OutOfRangeException::getMessage' => ['string'], 'OutOfRangeException::getPrevious' => ['Throwable|OutOfRangeException|null'], -'OutOfRangeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'OutOfRangeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OutOfRangeException::getTraceAsString' => ['string'], 'output_add_rewrite_var' => ['bool', 'name'=>'string', 'value'=>'string'], 'output_reset_rewrite_vars' => ['bool'], @@ -8404,7 +8406,7 @@ 'OverflowException::getLine' => ['int'], 'OverflowException::getMessage' => ['string'], 'OverflowException::getPrevious' => ['Throwable|OverflowException|null'], -'OverflowException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'OverflowException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'OverflowException::getTraceAsString' => ['string'], 'overload' => ['', 'class_name'=>'string'], 'override_function' => ['bool', 'function_name'=>'string', 'function_args'=>'string', 'function_code'=>'string'], @@ -8480,14 +8482,14 @@ 'ParseError::getLine' => ['int'], 'ParseError::getMessage' => ['string'], 'ParseError::getPrevious' => ['Throwable|ParseError|null'], -'ParseError::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'ParseError::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'ParseError::getTraceAsString' => ['string'], 'parsekit_compile_file' => ['array', 'filename'=>'string', 'errors='=>'array', 'options='=>'int'], 'parsekit_compile_string' => ['array', 'phpcode'=>'string', 'errors='=>'array', 'options='=>'int'], 'parsekit_func_arginfo' => ['array', 'function'=>'mixed'], 'passthru' => ['void', 'command'=>'string', '&w_return_value='=>'int'], 'password_get_info' => ['array', 'hash'=>'string'], -'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'int', 'options='=>'array'], +'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'password_make_salt' => ['bool', 'password'=>'string', 'hash'=>'string'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'int', 'options='=>'array'], 'password_verify' => ['bool', 'password'=>'string', 'hash'=>'string'], @@ -8711,7 +8713,7 @@ 'PDOException::getLine' => [''], 'PDOException::getMessage' => [''], 'PDOException::getPrevious' => [''], -'PDOException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'PDOException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'PDOException::getTraceAsString' => [''], 'PDOStatement::__sleep' => ['list'], 'PDOStatement::__wakeup' => ['void'], @@ -8764,15 +8766,15 @@ 'pg_escape_string\'1' => ['string', 'data'=>'string'], 'pg_execute' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'params'=>'array'], 'pg_execute\'1' => ['resource|false', 'stmtname'=>'string', 'params'=>'array'], -'pg_fetch_all' => ['array|false', 'result'=>'resource', 'result_type='=>'int'], +'pg_fetch_all' => ['array>', 'result'=>'resource', 'result_type='=>'int'], 'pg_fetch_all_columns' => ['array|false', 'result'=>'resource', 'column_number='=>'int'], 'pg_fetch_array' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], -'pg_fetch_assoc' => ['array|false', 'result'=>'resource', 'row='=>'?int'], +'pg_fetch_assoc' => ['non-empty-array|false', 'result'=>'resource', 'row='=>'?int'], 'pg_fetch_object' => ['object|false', 'result'=>'', 'row='=>'?int', 'result_type='=>'int'], 'pg_fetch_object\'1' => ['object', 'result'=>'', 'row='=>'?int', 'class_name='=>'string', 'ctor_params='=>'array'], 'pg_fetch_result' => ['', 'result'=>'', 'field_name'=>'string|int'], 'pg_fetch_result\'1' => ['', 'result'=>'', 'row_number'=>'int', 'field_name'=>'string|int'], -'pg_fetch_row' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], +'pg_fetch_row' => ['non-empty-list|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], 'pg_field_is_null' => ['int|false', 'result'=>'', 'field_name_or_number'=>'string|int'], 'pg_field_is_null\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'], 'pg_field_name' => ['string|false', 'result'=>'resource', 'field_number'=>'int'], @@ -8962,10 +8964,10 @@ 'phdfs::tell' => ['int', 'path'=>'string'], 'phdfs::write' => ['bool', 'path'=>'string', 'buffer'=>'string', 'mode='=>'string'], 'php_check_syntax' => ['bool', 'filename'=>'string', 'error_message='=>'string'], -'php_ini_loaded_file' => ['string|false'], +'php_ini_loaded_file' => ['non-empty-string|false'], 'php_ini_scanned_files' => ['string|false'], 'php_logo_guid' => ['string'], -'php_sapi_name' => ['string|false'], +'php_sapi_name' => ['__benevolent'], 'php_strip_whitespace' => ['string', 'file_name'=>'string'], 'php_uname' => ['string', 'mode='=>'string'], 'php_user_filter::filter' => ['int', 'in'=>'resource', 'out'=>'resource', '&rw_consumed'=>'int', 'closing'=>'bool'], @@ -9049,7 +9051,7 @@ 'posix_getpid' => ['int'], 'posix_getppid' => ['int'], 'posix_getpwnam' => ['array|false', 'groupname'=>'string'], -'posix_getpwuid' => ['array|false', 'uid'=>'int'], +'posix_getpwuid' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'uid'=>'int'], 'posix_getrlimit' => ['array|false'], 'posix_getsid' => ['int|false', 'pid'=>'int'], 'posix_getuid' => ['int'], @@ -9313,7 +9315,7 @@ 'RangeException::getLine' => ['int'], 'RangeException::getMessage' => ['string'], 'RangeException::getPrevious' => ['Throwable|RangeException|null'], -'RangeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'RangeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RangeException::getTraceAsString' => ['string'], 'rar_allow_broken_set' => ['bool', 'rarfile'=>'RarArchive', 'allow_broken'=>'bool'], 'rar_broken_is' => ['bool', 'rarfile'=>'RarArchive'], @@ -9356,7 +9358,7 @@ 'RarException::getLine' => ['int'], 'RarException::getMessage' => ['string'], 'RarException::getPrevious' => ['Exception|Throwable'], -'RarException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'RarException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RarException::getTraceAsString' => ['string'], 'RarException::isUsingExceptions' => ['bool'], 'RarException::setUsingExceptions' => ['void', 'using_exceptions'=>'bool'], @@ -9549,7 +9551,7 @@ 'Redis::georadiusbymember_ro' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], 'Redis::geosearch' => ['__benevolent>', 'key'=>'string', 'position'=>'array|string', 'shape'=>'array|int|float', 'unit'=>'string', 'options='=>'array'], 'Redis::geosearchstore' => ['__benevolent|int|false>', 'dst'=>'string', 'src'=>'string', 'position'=>'array|string', 'shape'=>'array|int|float', 'unit'=>'string', 'options='=>'array'], -'Redis::get' => ['string|false', 'key'=>'string'], +'Redis::get' => ['mixed', 'key'=>'string'], 'Redis::getAuth' => ['string|false|null'], 'Redis::getBit' => ['__benevolent', 'key'=>'string', 'idx'=>'int'], 'Redis::getEx' => ['__benevolent', 'key'=>'string', 'options'=>'?array{EX?:int,PX?:int,EXAT?:int,PXAT?:int,PERSIST?:bool}'], @@ -9746,7 +9748,7 @@ 'RedisArray::_hosts' => ['array'], 'RedisArray::_rehash' => ['', 'callable='=>'callable'], 'RedisArray::_target' => ['string', 'key'=>'string'], -'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds'=>'array', 'timeout='=>'float', 'readTimeout='=>'float', 'persistent='=>'bool|false', 'auth='=>'string|array|null'], +'RedisCluster::__construct' => ['void', 'name'=>'string|null', 'seeds='=>'string[]|null', 'timeout='=>'int|float', 'read_timeout='=>'int|float', 'persistent='=>'bool', 'auth='=>'mixed', 'context='=>'array|null'], 'RedisCluster::_masters' => ['array'], 'RedisCluster::_prefix' => ['string', 'value'=>'mixed'], 'RedisCluster::_serialize' => ['mixed', 'value'=>'mixed'], @@ -10008,7 +10010,7 @@ 'ReflectionFunction::__construct' => ['void', 'name'=>'string|Closure'], 'ReflectionFunction::__toString' => ['string'], 'ReflectionFunction::export' => ['string|null', 'name'=>'string', 'return='=>'bool'], -'ReflectionFunction::getClosure' => ['?Closure'], +'ReflectionFunction::getClosure' => ['Closure'], 'ReflectionFunction::getClosureScopeClass' => ['ReflectionClass'], 'ReflectionFunction::getClosureThis' => ['bool'], 'ReflectionFunction::getDocComment' => ['string|false'], @@ -10042,8 +10044,8 @@ 'ReflectionFunctionAbstract::getClosureThis' => ['object|null'], 'ReflectionFunctionAbstract::getDocComment' => ['string|false'], 'ReflectionFunctionAbstract::getEndLine' => ['int|false'], -'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension'], -'ReflectionFunctionAbstract::getExtensionName' => ['string'], +'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension|null'], +'ReflectionFunctionAbstract::getExtensionName' => ['string|false'], 'ReflectionFunctionAbstract::getFileName' => ['string|false'], 'ReflectionFunctionAbstract::getName' => ['non-empty-string'], 'ReflectionFunctionAbstract::getNamespaceName' => ['string'], @@ -10069,12 +10071,12 @@ 'ReflectionGenerator::getExecutingLine' => ['int'], 'ReflectionGenerator::getFunction' => ['ReflectionFunctionAbstract'], 'ReflectionGenerator::getThis' => ['object'], -'ReflectionGenerator::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}', 'options'=>'int'], +'ReflectionGenerator::getTrace' => ['list\',args?:mixed[],object?:object}>', 'options'=>'int'], 'ReflectionMethod::__construct' => ['void', 'class'=>'string|object', 'name'=>'string'], 'ReflectionMethod::__construct\'1' => ['void', 'class_method'=>'string'], 'ReflectionMethod::__toString' => ['string'], 'ReflectionMethod::export' => ['string|null', 'class'=>'string', 'name'=>'string', 'return='=>'bool'], -'ReflectionMethod::getClosure' => ['?Closure', 'object'=>'?object'], +'ReflectionMethod::getClosure' => ['Closure', 'object'=>'?object'], 'ReflectionMethod::getDeclaringClass' => ['ReflectionClass'], 'ReflectionMethod::getModifiers' => ['int'], 'ReflectionMethod::getPrototype' => ['ReflectionMethod'], @@ -10182,7 +10184,7 @@ 'rewind' => ['bool', 'fp'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'], -'round' => ['float|false', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], +'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], @@ -10250,7 +10252,7 @@ 'RuntimeException::getLine' => ['int'], 'RuntimeException::getMessage' => ['string'], 'RuntimeException::getPrevious' => ['Throwable|RuntimeException|null'], -'RuntimeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'RuntimeException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'RuntimeException::getTraceAsString' => ['string'], 'SAMConnection::commit' => ['bool'], 'SAMConnection::connect' => ['bool', 'protocol'=>'string', 'properties='=>'array'], @@ -10630,7 +10632,7 @@ 'socket_recv' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_recvfrom' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int', '&w_name'=>'string', '&w_port='=>'int'], 'socket_recvmsg' => ['int|false', 'socket'=>'resource', '&w_message'=>'string', 'flags='=>'int'], -'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], +'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'socket_send' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_sendmsg' => ['int|false', 'socket'=>'resource', 'message'=>'array', 'flags'=>'int'], 'socket_sendto' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int', 'addr'=>'string', 'port='=>'int'], @@ -11107,7 +11109,7 @@ 'SolrException::getLine' => ['int'], 'SolrException::getMessage' => ['string'], 'SolrException::getPrevious' => ['Exception|Throwable'], -'SolrException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'SolrException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrException::getTraceAsString' => ['string'], 'SolrGenericResponse::__construct' => ['void'], 'SolrGenericResponse::__destruct' => [''], @@ -11132,7 +11134,7 @@ 'SolrIllegalArgumentException::getLine' => ['int'], 'SolrIllegalArgumentException::getMessage' => ['string'], 'SolrIllegalArgumentException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalArgumentException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'SolrIllegalArgumentException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalArgumentException::getTraceAsString' => ['string'], 'SolrIllegalOperationException::__clone' => ['void'], 'SolrIllegalOperationException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], @@ -11144,7 +11146,7 @@ 'SolrIllegalOperationException::getLine' => ['int'], 'SolrIllegalOperationException::getMessage' => ['string'], 'SolrIllegalOperationException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalOperationException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'SolrIllegalOperationException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrIllegalOperationException::getTraceAsString' => ['string'], 'SolrInputDocument::__clone' => ['void'], 'SolrInputDocument::__construct' => ['void'], @@ -11452,7 +11454,7 @@ 'SolrServerException::getLine' => ['int'], 'SolrServerException::getMessage' => ['string'], 'SolrServerException::getPrevious' => ['Exception|Throwable'], -'SolrServerException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'SolrServerException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'SolrServerException::getTraceAsString' => ['string'], 'SolrUpdateResponse::__construct' => ['void'], 'SolrUpdateResponse::__destruct' => [''], @@ -11579,7 +11581,7 @@ 'SplFileObject::fgetc' => ['string|false'], // Do not believe https://www.php.net/manual/en/splfileobject.fgetcsv#refsect1-splfileobject.fgetcsv-returnvalues 'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'SplFileObject::fgets' => ['string|false'], +'SplFileObject::fgets' => ['string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'SplFileObject::flock' => ['bool', 'operation'=>'int', '&w_wouldblock='=>'int'], 'SplFileObject::fpassthru' => ['int'], @@ -11593,7 +11595,7 @@ 'SplFileObject::fwrite' => ['int', 'str'=>'string', 'length='=>'int'], 'SplFileObject::getChildren' => ['null'], 'SplFileObject::getCsvControl' => ['array'], -'SplFileObject::getCurrentLine' => ['string|false'], +'SplFileObject::getCurrentLine' => ['string'], 'SplFileObject::getFlags' => ['int'], 'SplFileObject::getMaxLineLen' => ['int'], 'SplFileObject::hasChildren' => ['false'], @@ -12012,7 +12014,7 @@ 'stream_get_contents' => ['string|false', 'source'=>'resource', 'maxlen='=>'int', 'offset='=>'int'], 'stream_get_filters' => ['list'], 'stream_get_line' => ['string|false', 'stream'=>'resource', 'maxlen'=>'int', 'ending='=>'string'], -'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], +'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri?:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], 'stream_get_transports' => ['list'], 'stream_get_wrappers' => ['list'], 'stream_is_local' => ['bool', 'stream'=>'resource|string'], @@ -12480,7 +12482,7 @@ 'tan' => ['float', 'number'=>'float'], 'tanh' => ['float', 'number'=>'float'], 'tcpwrap_check' => ['bool', 'daemon'=>'string', 'address'=>'string', 'user='=>'string', 'nodns='=>'bool'], -'tempnam' => ['string|false', 'dir'=>'string', 'prefix'=>'string'], +'tempnam' => ['__benevolent', 'dir'=>'string', 'prefix'=>'string'], 'textdomain' => ['string', 'domain'=>'string'], 'Thread::__construct' => ['void'], 'Thread::chunk' => ['array', 'size'=>'int', 'preserve'=>'bool'], @@ -12531,7 +12533,7 @@ 'Throwable::getLine' => ['int'], 'Throwable::getMessage' => ['string'], 'Throwable::getPrevious' => ['Throwable|null'], -'Throwable::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'Throwable::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'Throwable::getTraceAsString' => ['string'], 'tidy::__construct' => ['void', 'filename='=>'string', 'config='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], 'tidy::body' => ['tidyNode'], @@ -12604,7 +12606,7 @@ 'timezone_open' => ['DateTimeZone|false', 'timezone'=>'string'], 'timezone_transitions_get' => ['list|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'timezone_version_get' => ['string'], -'tmpfile' => ['resource|false'], +'tmpfile' => ['__benevolent'], 'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], 'token_name' => ['string', 'type'=>'int'], 'TokyoTyrant::__construct' => ['void', 'host='=>'string', 'port='=>'int', 'options='=>'array'], @@ -12851,7 +12853,7 @@ 'TypeError::getLine' => ['int'], 'TypeError::getMessage' => ['string'], 'TypeError::getPrevious' => ['Throwable|TypeError|null'], -'TypeError::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'TypeError::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'TypeError::getTraceAsString' => ['string'], 'uasort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'ucfirst' => ['string', 'str'=>'string'], @@ -12912,7 +12914,7 @@ 'UnderflowException::getLine' => ['int'], 'UnderflowException::getMessage' => ['string'], 'UnderflowException::getPrevious' => ['Throwable|UnderflowException|null'], -'UnderflowException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'UnderflowException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'UnderflowException::getTraceAsString' => ['string'], 'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnexpectedValueException)'], @@ -12922,7 +12924,7 @@ 'UnexpectedValueException::getLine' => ['int'], 'UnexpectedValueException::getMessage' => ['string'], 'UnexpectedValueException::getPrevious' => ['Throwable|UnexpectedValueException|null'], -'UnexpectedValueException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'UnexpectedValueException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'UnexpectedValueException::getTraceAsString' => ['string'], 'uniqid' => ['non-empty-string', 'prefix='=>'string', 'more_entropy='=>'bool'], 'unixtojd' => ['int|false', 'timestamp='=>'int'], @@ -13017,7 +13019,7 @@ 'V8JsScriptException::getLine' => ['int'], 'V8JsScriptException::getMessage' => ['string'], 'V8JsScriptException::getPrevious' => ['Exception|Throwable'], -'V8JsScriptException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], +'V8JsScriptException::getTrace' => ['list\',args?:mixed[],object?:object}>'], 'V8JsScriptException::getTraceAsString' => ['string'], 'var_dump' => ['void', 'var'=>'mixed', '...args='=>'mixed'], 'var_export' => ['string|null', 'var'=>'mixed', 'return='=>'bool'], diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php index c13badb685..9f2230dd6f 100644 --- a/resources/functionMap_bleedingEdge.php +++ b/resources/functionMap_bleedingEdge.php @@ -2,6 +2,8 @@ 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_*'], @@ -106,6 +108,18 @@ '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'], @@ -125,6 +139,9 @@ '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 b154d13e65..b37fe5a7a5 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -38,10 +38,10 @@ 'FFI::string' => ['string', '&ptr'=>'FFI\CData', 'size='=>'int'], 'FFI::typeof' => ['FFI\CType', '&ptr'=>'FFI\CData'], 'FFI::type' => ['FFI\CType', 'type'=>'string'], + 'fread' => ['string|false', 'fp'=>'resource', 'length'=>'positive-int'], 'get_mangled_object_vars' => ['array', 'obj'=>'object'], 'mb_str_split' => ['list|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], 'password_algos' => ['list'], - 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|null', 'options='=>'array'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'string|null', 'options='=>'array'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index 07ef193a87..6ec7d665f7 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -28,6 +28,7 @@ 'bcmod' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], + 'ceil' => ['float', 'number'=>'float'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], 'count_chars' => ['array|string', 'input'=>'string', 'mode='=>'int'], 'date_add' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], @@ -37,12 +38,13 @@ 'date_isodate_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array', 'date'=>'string'], 'date_sub' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['array{sunrise: int|bool,sunset: int|bool,transit: int|bool,civil_twilight_begin: int|bool,civil_twilight_end: int|bool,nautical_twilight_begin: int|bool,nautical_twilight_end: int|bool,astronomical_twilight_begin: int|bool,astronomical_twilight_end: int|bool}', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'floor' => ['float', 'number'=>'float'], 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'get_debug_type' => ['string', 'var'=>'mixed'], 'get_resource_id' => ['int', 'res'=>'resource'], @@ -53,7 +55,7 @@ 'hash_hmac' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], - 'imagecreate' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecreate' => ['__benevolent', 'width'=>'int', 'height'=>'int'], 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], 'imagecreatefromgd' => ['false|object', 'filename'=>'string'], 'imagecreatefromgd2' => ['false|object', 'filename'=>'string'], @@ -66,7 +68,7 @@ 'imagecreatefromwebp' => ['false|object', 'filename'=>'string'], 'imagecreatefromxbm' => ['false|object', 'filename'=>'string'], 'imagecreatefromxpm' => ['false|object', 'filename'=>'string'], - 'imagecreatetruecolor' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], + 'imagecreatetruecolor' => ['__benevolent', 'width'=>'int', 'height'=>'int'], 'imagecrop' => ['false|object', 'im'=>'resource', 'rect'=>'array'], 'imagecropauto' => ['false|object', 'im'=>'resource', 'mode'=>'int', 'threshold'=>'float', 'color'=>'int'], 'imagegetclip' => ['array', 'im'=>'resource'], @@ -83,7 +85,7 @@ 'mktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result'=>'array'], - 'password_hash' => ['string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], + 'password_hash' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], 'PDOStatement::fetchAll' => ['array', 'how='=>'int', 'fetch_argument='=>'int|string|callable', 'ctor_args='=>'?array'], 'PhpToken::tokenize' => ['list', 'code'=>'string', 'flags='=>'int'], 'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'], @@ -93,14 +95,14 @@ 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', 'process'=>'resource'], 'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int):bool', 'error_types='=>'int'], 'socket_addrinfo_lookup' => ['AddressInfo[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], - 'socket_select' => ['int|false', '&rw_read'=>'Socket[]|null', '&rw_write'=>'Socket[]|null', '&rw_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], + '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'], 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_split' => ['non-empty-list', 'str'=>'string', 'split_length='=>'positive-int'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_starts_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], @@ -108,6 +110,7 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'], + 'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], 'xml_parser_create' => ['XMLParser', 'encoding='=>'string'], 'xml_parser_create_ns' => ['XMLParser', 'encoding='=>'string', 'sep='=>'string'], @@ -162,6 +165,7 @@ 'bcdiv' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['?string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], + 'ceil' => ['__benevolent', 'number'=>'float'], 'convert_cyr_string' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'bool'], 'count_chars' => ['array|false|string', 'input'=>'string', 'mode='=>'int'], @@ -172,13 +176,14 @@ 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array|false', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array|false', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], 'each' => ['array{0:int|string,key:int|string,1:mixed,value:mixed}', '&r_arr'=>'array'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], + 'floor' => ['__benevolent', 'number'=>'float'], 'get_magic_quotes_gpc' => ['false'], 'gmdate' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], @@ -226,16 +231,17 @@ 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result='=>'array'], - 'password_hash' => ['string|false|null', 'password'=>'string', 'algo'=>'?string|?int', 'options='=>'array'], + 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'png2wbmp' => ['bool', 'pngname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'restore_include_path' => ['void'], - 'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], + 'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], + 'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['?string|?false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int'], diff --git a/resources/functionMap_php80delta_bleedingEdge.php b/resources/functionMap_php80delta_bleedingEdge.php index 990aafe691..82e9b720a8 100644 --- a/resources/functionMap_php80delta_bleedingEdge.php +++ b/resources/functionMap_php80delta_bleedingEdge.php @@ -5,8 +5,10 @@ '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-empty-string', 'algo'=>'non-falsy-string', 'key'=>'string', 'length='=>'0|positive-int', 'info='=>'string', 'salt='=>'string'], + '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 6d55567897..54a3199934 100644 --- a/resources/functionMap_php81delta.php +++ b/resources/functionMap_php81delta.php @@ -21,7 +21,13 @@ */ return [ 'new' => [ - + 'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], + 'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], + 'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], + 'mysqli_result::fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false'], + 'mysqli_result::fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'fieldnr'=>'int'], + 'mysqli_result::fetch_fields' => ['list'], + 'UnitEnum::cases' => ['list'], ], 'old' => [ 'pg_escape_bytea' => ['string', 'connection'=>'resource', 'data'=>'string'], diff --git a/resources/functionMap_php83delta.php b/resources/functionMap_php83delta.php index 3961b6a613..bcc70d4b0f 100644 --- a/resources/functionMap_php83delta.php +++ b/resources/functionMap_php83delta.php @@ -21,8 +21,12 @@ */ return [ 'new' => [ + 'DateTime::modify' => ['static', 'modify'=>'string'], + 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'], 'str_increment' => ['non-falsy-string', 'string'=>'non-empty-string'], + 'gc_status' => ['array{running:bool,protected:bool,full:bool,runs:int,collected:int,threshold:int,buffer_size:int,roots:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], + 'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], ], 'old' => [ diff --git a/resources/functionMap_php84delta.php b/resources/functionMap_php84delta.php new file mode 100644 index 0000000000..fe719fce15 --- /dev/null +++ b/resources/functionMap_php84delta.php @@ -0,0 +1,26 @@ + [ + 'http_get_last_response_headers' => ['list|null'], + 'http_clear_last_response_headers' => ['void'], + 'mb_lcfirst' => ['string', 'string'=>'string', 'encoding='=>'string'], + 'mb_ucfirst' => ['string', 'string'=>'string', 'encoding='=>'string'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index ac2385703c..4ba615393d 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -634,6 +634,18 @@ 'SimpleXMLIterator::hasChildren' => ['hasSideEffects' => false], 'SimpleXMLIterator::valid' => ['hasSideEffects' => false], 'SoapFault::__construct' => ['hasSideEffects' => false], + 'SplFileObject::fflush' => ['hasSideEffects' => true], + 'SplFileObject::fgetc' => ['hasSideEffects' => true], + 'SplFileObject::fgetcsv' => ['hasSideEffects' => true], + 'SplFileObject::fgets' => ['hasSideEffects' => true], + 'SplFileObject::fgetss' => ['hasSideEffects' => true], + 'SplFileObject::fpassthru' => ['hasSideEffects' => true], + 'SplFileObject::fputcsv' => ['hasSideEffects' => true], + 'SplFileObject::fread' => ['hasSideEffects' => true], + 'SplFileObject::fscanf' => ['hasSideEffects' => true], + 'SplFileObject::fseek' => ['hasSideEffects' => true], + 'SplFileObject::ftruncate' => ['hasSideEffects' => true], + 'SplFileObject::fwrite' => ['hasSideEffects' => true], 'Spoofchecker::__construct' => ['hasSideEffects' => false], 'StringBackedEnum::from' => ['hasSideEffects' => false], 'StringBackedEnum::tryFrom' => ['hasSideEffects' => false], @@ -861,6 +873,7 @@ 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], 'error_get_last' => ['hasSideEffects' => false], + 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], 'escapeshellcmd' => ['hasSideEffects' => false], 'exp' => ['hasSideEffects' => false], @@ -877,7 +890,7 @@ 'fgetss' => ['hasSideEffects' => true], 'file' => ['hasSideEffects' => false], 'file_exists' => ['hasSideEffects' => false], - 'file_get_contents' => ['hasSideEffects' => false], + 'file_get_contents' => ['hasSideEffects' => true], 'file_put_contents' => ['hasSideEffects' => true], 'fileatime' => ['hasSideEffects' => false], 'filectime' => ['hasSideEffects' => false], diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index b30a741a44..ed161671c6 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -11,7 +11,6 @@ use function array_merge; use function count; use function memory_get_peak_usage; -use function sprintf; class Analyser { @@ -49,10 +48,17 @@ public function analyse( /** @var list $errors */ $errors = []; + /** @var list $filteredPhpErrors */ + $filteredPhpErrors = []; + /** @var list $allPhpErrors */ + $allPhpErrors = []; /** @var list $locallyIgnoredErrors */ $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; + /** @var list $collectedData */ $collectedData = []; @@ -74,7 +80,12 @@ public function analyse( null, ); $errors = array_merge($errors, $fileAnalyserResult->getErrors()); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + $locallyIgnoredErrors = array_merge($locallyIgnoredErrors, $fileAnalyserResult->getLocallyIgnoredErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $collectedData = array_merge($collectedData, $fileAnalyserResult->getCollectedData()); $dependencies[$file] = $fileAnalyserResult->getDependencies(); @@ -87,14 +98,12 @@ public function analyse( throw $t; } $internalErrorsCount++; - $internalErrorMessage = sprintf('Internal error: %s', $t->getMessage()); - $internalErrorMessage .= sprintf( - '%sRun PHPStan with --debug option and post the stack trace to:%s%s', - "\n", - "\n", - 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml', - ); - $errors[] = (new Error($internalErrorMessage, $file, null, $t))->withIdentifier('phpstan.internal'); + $errors[] = (new Error($t->getMessage(), $file, null, $t)) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($t), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $t->getTraceAsString(), + ]); if ($internalErrorsCount >= $this->internalErrorsCountLimit) { $reachedInternalErrorsCountLimit = true; break; @@ -110,7 +119,11 @@ public function analyse( return new AnalyserResult( $errors, + $filteredPhpErrors, + $allPhpErrors, $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, [], $collectedData, $internalErrorsCount === 0 ? $dependencies : null, diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 01c4fccfa3..903ab9c5dc 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -6,6 +6,9 @@ use PHPStan\Dependency\RootExportedNode; use function usort; +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ class AnalyserResult { @@ -14,15 +17,23 @@ class AnalyserResult /** * @param list $unorderedErrors + * @param list $filteredPhpErrors + * @param list $allPhpErrors * @param list $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores * @param list $collectedData - * @param list $internalErrors + * @param list $internalErrors * @param array>|null $dependencies * @param array> $exportedNodes */ public function __construct( private array $unorderedErrors, + private array $filteredPhpErrors, + private array $allPhpErrors, private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, private array $internalErrors, private array $collectedData, private ?array $dependencies, @@ -65,6 +76,22 @@ public function getErrors(): array return $this->errors; } + /** + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } + + /** + * @return list + */ + public function getAllPhpErrors(): array + { + return $this->allPhpErrors; + } + /** * @return list */ @@ -74,7 +101,23 @@ public function getLocallyIgnoredErrors(): array } /** - * @return list + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + + /** + * @return list */ public function getInternalErrors(): array { diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php new file mode 100644 index 0000000000..707ac02005 --- /dev/null +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -0,0 +1,222 @@ +getCollectedData()) === 0) { + return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); + } + + $hasInternalErrors = count($analyserResult->getInternalErrors()) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); + if ($hasInternalErrors) { + return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); + } + + $nodeType = CollectedDataNode::class; + $node = new CollectedDataNode($analyserResult->getCollectedData(), $onlyFiles); + + $file = 'N/A'; + $scope = $this->scopeFactory->create(ScopeContext::create($file)); + $tempCollectorErrors = []; + $internalErrors = $analyserResult->getInternalErrors(); + foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { + try { + $ruleErrors = $rule->processNode($node, $scope); + } catch (AnalysedCodeException $e) { + $tempCollectorErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (IdentifierNotFound $e) { + $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (UnableToCompileNode | CircularReference $e) { + $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); + continue; + } catch (Throwable $t) { + if ($debug) { + throw $t; + } + + $internalErrors[] = new InternalError( + $t->getMessage(), + sprintf('running CollectedDataNode rule %s', get_class($rule)), + InternalError::prepareTrace($t), + $t->getTraceAsString(), + true, + ); + continue; + } + + foreach ($ruleErrors as $ruleError) { + $tempCollectorErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + } + } + + $errors = $analyserResult->getUnorderedErrors(); + $locallyIgnoredErrors = $analyserResult->getLocallyIgnoredErrors(); + $allLinesToIgnore = $analyserResult->getLinesToIgnore(); + $allUnmatchedLineIgnores = $analyserResult->getUnmatchedLineIgnores(); + $collectorErrors = []; + $locallyIgnoredCollectorErrors = []; + foreach ($tempCollectorErrors as $tempCollectorError) { + $file = $tempCollectorError->getFilePath(); + $linesToIgnore = $allLinesToIgnore[$file] ?? []; + $unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? []; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + [$tempCollectorError], + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $error) { + $errors[] = $error; + $collectorErrors[] = $error; + } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; + $locallyIgnoredCollectorErrors[] = $locallyIgnoredError; + } + $allLinesToIgnore[$file] = $localIgnoresProcessorResult->getLinesToIgnore(); + $allUnmatchedLineIgnores[$file] = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); + } + + return $this->addUnmatchedIgnoredErrors(new AnalyserResult( + array_merge($errors, $analyserResult->getFilteredPhpErrors()), + [], + $analyserResult->getAllPhpErrors(), + $locallyIgnoredErrors, + $allLinesToIgnore, + $allUnmatchedLineIgnores, + $internalErrors, + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), $collectorErrors, $locallyIgnoredCollectorErrors); + } + + private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult + { + return new AnalyserResult( + array_merge($analyserResult->getUnorderedErrors(), $analyserResult->getFilteredPhpErrors()), + [], + $analyserResult->getAllPhpErrors(), + $analyserResult->getLocallyIgnoredErrors(), + $analyserResult->getLinesToIgnore(), + $analyserResult->getUnmatchedLineIgnores(), + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ); + } + + /** + * @param list $collectorErrors + * @param list $locallyIgnoredCollectorErrors + */ + private function addUnmatchedIgnoredErrors( + AnalyserResult $analyserResult, + array $collectorErrors, + array $locallyIgnoredCollectorErrors, + ): FinalizerResult + { + if (!$this->reportUnmatchedIgnoredErrors) { + return new FinalizerResult($analyserResult, $collectorErrors, $locallyIgnoredCollectorErrors); + } + + $errors = $analyserResult->getUnorderedErrors(); + foreach ($analyserResult->getUnmatchedLineIgnores() as $file => $data) { + foreach ($data as $ignoredFile => $lines) { + if ($ignoredFile !== $file) { + continue; + } + + foreach ($lines as $line => $identifiers) { + if ($identifiers === null) { + $errors[] = (new Error( + sprintf('No error to ignore is reported on line %d.', $line), + $file, + $line, + false, + $file, + ))->withIdentifier('ignore.unmatchedLine'); + continue; + } + + foreach ($identifiers as $identifier) { + $errors[] = (new Error( + sprintf('No error with identifier %s is reported on line %d.', $identifier, $line), + $file, + $line, + false, + $file, + ))->withIdentifier('ignore.unmatchedIdentifier'); + } + } + } + } + + return new FinalizerResult( + new AnalyserResult( + $errors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), + $analyserResult->getLocallyIgnoredErrors(), + $analyserResult->getLinesToIgnore(), + $analyserResult->getUnmatchedLineIgnores(), + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), + $collectorErrors, + $locallyIgnoredCollectorErrors, + ); + } + +} diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index ff5f12089b..6c6db632ac 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser; use PhpParser\Node\Arg; -use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; @@ -85,7 +84,7 @@ public static function reorderFuncArguments( FuncCall $functionCall, ): ?FuncCall { - $reorderedArgs = self::reorderArgs($parametersAcceptor, $functionCall); + $reorderedArgs = self::reorderArgs($parametersAcceptor, $functionCall->getArgs()); if ($reorderedArgs === null) { return null; @@ -103,7 +102,7 @@ public static function reorderMethodArguments( MethodCall $methodCall, ): ?MethodCall { - $reorderedArgs = self::reorderArgs($parametersAcceptor, $methodCall); + $reorderedArgs = self::reorderArgs($parametersAcceptor, $methodCall->getArgs()); if ($reorderedArgs === null) { return null; @@ -122,7 +121,7 @@ public static function reorderStaticCallArguments( StaticCall $staticCall, ): ?StaticCall { - $reorderedArgs = self::reorderArgs($parametersAcceptor, $staticCall); + $reorderedArgs = self::reorderArgs($parametersAcceptor, $staticCall->getArgs()); if ($reorderedArgs === null) { return null; @@ -141,7 +140,7 @@ public static function reorderNewArguments( New_ $new, ): ?New_ { - $reorderedArgs = self::reorderArgs($parametersAcceptor, $new); + $reorderedArgs = self::reorderArgs($parametersAcceptor, $new->getArgs()); if ($reorderedArgs === null) { return null; @@ -155,17 +154,17 @@ public static function reorderNewArguments( } /** + * @param Arg[] $callArgs * @return ?array */ - private static function reorderArgs(ParametersAcceptor $parametersAcceptor, CallLike $callLike): ?array + public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array { - $signatureParameters = $parametersAcceptor->getParameters(); - $callArgs = $callLike->getArgs(); - if (count($callArgs) === 0) { return []; } + $signatureParameters = $parametersAcceptor->getParameters(); + $hasNamedArgs = false; foreach ($callArgs as $arg) { if ($arg->name !== null) { @@ -191,6 +190,7 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call $reorderedArgs = []; $additionalNamedArgs = []; + $appendArgs = []; foreach ($callArgs as $i => $arg) { if ($arg->name === null) { // add regular args as is @@ -209,7 +209,16 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call ); } else { if (!$hasVariadic) { - return null; + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $appendArgs[] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + continue; } $attributes = $arg->getAttributes(); @@ -235,7 +244,10 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call } if (count($reorderedArgs) === 0) { - return []; + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + return $reorderedArgs; } // fill up all holes with default values until the last given argument @@ -269,6 +281,10 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call ksort($reorderedArgs); + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + return $reorderedArgs; } diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index 19cf21bbf2..d8df652c42 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -6,7 +6,7 @@ use PHPStan\Reflection\NamespaceAnswerer; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -73,7 +73,7 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type if ($resolvedConstantName === 'PHP_VERSION') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { @@ -106,7 +106,7 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type if ($resolvedConstantName === 'PHP_OS') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_OS_FAMILY') { @@ -132,7 +132,7 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type new ConstantStringType('phpdbg'), new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]), ]); } @@ -165,61 +165,61 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type if ($resolvedConstantName === 'PHP_EXTENSION_DIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_PREFIX') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_BINDIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_BINARY') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_MANDIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_LIBDIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_DATADIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_SYSCONFDIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_LOCALSTATEDIR') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_CONFIG_FILE_PATH') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } if ($resolvedConstantName === 'PHP_SHLIB_SUFFIX') { @@ -232,7 +232,7 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type return IntegerRangeType::fromInterval(1, null); } if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { - return IntegerRangeType::fromInterval(0, null); + return IntegerRangeType::fromInterval(1, null); } // core other, https://www.php.net/manual/en/info.constants.php if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_MAJOR') { @@ -261,7 +261,7 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type if ($resolvedConstantName === 'ICONV_IMPL') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } // libxml, https://www.php.net/manual/en/libxml.constants.php @@ -271,13 +271,22 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type if ($resolvedConstantName === 'LIBXML_DOTTED_VERSION') { return new IntersectionType([ new StringType(), - new AccessoryNonEmptyStringType(), + new AccessoryNonFalsyStringType(), ]); } // openssl, https://www.php.net/manual/en/openssl.constants.php if ($resolvedConstantName === 'OPENSSL_VERSION_NUMBER') { return IntegerRangeType::fromInterval(1, null); } + + // pcre, https://www.php.net/manual/en/pcre.constants.php + if ($resolvedConstantName === 'PCRE_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + } + if (in_array($resolvedConstantName, ['STDIN', 'STDOUT', 'STDERR'], true)) { return new ResourceType(); } diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 07d7fbe09c..c662e1d9a0 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -46,7 +46,7 @@ public function __construct( * @param array $expressionTypes * @param array $nativeExpressionTypes * @param array $conditionalExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions */ diff --git a/src/Analyser/EndStatementResult.php b/src/Analyser/EndStatementResult.php new file mode 100644 index 0000000000..32d0809bef --- /dev/null +++ b/src/Analyser/EndStatementResult.php @@ -0,0 +1,27 @@ +statement; + } + + public function getResult(): StatementResult + { + return $this->result; + } + +} diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 35b567fbca..f378d912d9 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -184,6 +184,30 @@ public function withIdentifier(string $identifier): self ); } + /** + * @param mixed[] $metadata + */ + public function withMetadata(array $metadata): self + { + if ($this->metadata !== []) { + throw new ShouldNotHappenException('Error already has metadata'); + } + + return new self( + $this->message, + $this->file, + $this->line, + $this->canBeIgnored, + $this->filePath, + $this->traitFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, + $this->identifier, + $metadata, + ); + } + public function getNodeLine(): ?int { return $this->nodeLine; diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 93a3bdca25..cae75b1609 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -17,6 +17,7 @@ class ExpressionResult /** * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ @@ -24,6 +25,7 @@ public function __construct( private MutatingScope $scope, private bool $hasYield, private array $throwPoints, + private array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ) @@ -50,6 +52,14 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getTruthyScope(): MutatingScope { if ($this->truthyScopeCallback === null) { diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index fe795c92ff..13ee1ba1b6 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -15,26 +15,35 @@ use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; use PHPStan\Rules\Registry as RuleRegistry; -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 error_reporting; use function get_class; -use function is_array; use function is_dir; use function is_file; use function restore_error_handler; use function set_error_handler; use function sprintf; use const E_DEPRECATED; +use const E_ERROR; +use const E_NOTICE; +use const E_PARSE; +use const E_STRICT; +use const E_USER_ERROR; +use const E_USER_NOTICE; +use const E_USER_WARNING; +use const E_WARNING; class FileAnalyser { /** @var list */ - private array $collectedErrors = []; + private array $allPhpErrors = []; + + /** @var list */ + private array $filteredPhpErrors = []; public function __construct( private ScopeFactory $scopeFactory, @@ -42,7 +51,7 @@ public function __construct( private Parser $parser, private DependencyResolver $dependencyResolver, private RuleErrorTransformer $ruleErrorTransformer, - private bool $reportUnmatchedIgnoredErrors, + private LocalIgnoresProcessor $localIgnoresProcessor, ) { } @@ -70,6 +79,8 @@ public function analyseFile( $fileDependencies = []; $exportedNodes = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; if (is_file($file)) { try { $this->collectErrors($analysedFiles); @@ -104,13 +115,28 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } catch (UnableToCompileNode | CircularReference $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } @@ -128,13 +154,28 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } catch (UnableToCompileNode | CircularReference $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); continue; } @@ -173,102 +214,47 @@ public function analyseFile( $scope, $nodeCallback, ); - foreach ($temporaryFileErrors as $tmpFileError) { - $line = $tmpFileError->getLine(); - if ( - $line !== null - && $tmpFileError->canBeIgnored() - && array_key_exists($tmpFileError->getFile(), $linesToIgnore) - && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) - ) { - $identifiers = $linesToIgnore[$tmpFileError->getFile()][$line]; - if ($identifiers === null) { - $locallyIgnoredErrors[] = $tmpFileError; - unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); - continue; - } - - if ($tmpFileError->getIdentifier() === null) { - $fileErrors[] = $tmpFileError; - continue; - } - - foreach ($identifiers as $i => $ignoredIdentifier) { - if ($ignoredIdentifier !== $tmpFileError->getIdentifier()) { - continue; - } - unset($identifiers[$i]); - $linesToIgnore[$tmpFileError->getFile()][$line] = array_values($identifiers); - - if ( - array_key_exists($tmpFileError->getFile(), $unmatchedLineIgnores) - && array_key_exists($line, $unmatchedLineIgnores[$tmpFileError->getFile()]) - ) { - $unmatchedIgnoredIdentifiers = $unmatchedLineIgnores[$tmpFileError->getFile()][$line]; - if (is_array($unmatchedIgnoredIdentifiers)) { - foreach ($unmatchedIgnoredIdentifiers as $j => $unmatchedIgnoredIdentifier) { - if ($ignoredIdentifier !== $unmatchedIgnoredIdentifier) { - continue; - } - - unset($unmatchedIgnoredIdentifiers[$j]); - $unmatchedLineIgnores[$tmpFileError->getFile()][$line] = array_values($unmatchedIgnoredIdentifiers); - break; - } - } - } - - $locallyIgnoredErrors[] = $tmpFileError; - continue 2; - } - } - - $fileErrors[] = $tmpFileError; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + $temporaryFileErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $fileError) { + $fileErrors[] = $fileError; } - - if ($this->reportUnmatchedIgnoredErrors) { - foreach ($unmatchedLineIgnores as $ignoredFile => $lines) { - if ($ignoredFile !== $file) { - continue; - } - - foreach ($lines as $line => $identifiers) { - if ($identifiers === null) { - $fileErrors[] = (new Error( - sprintf('No error to ignore is reported on line %d.', $line), - $scope->getFileDescription(), - $line, - false, - $scope->getFile(), - ))->withIdentifier('ignore.unmatchedLine'); - continue; - } - - foreach ($identifiers as $identifier) { - $fileErrors[] = (new Error( - sprintf('No error with identifier %s is reported on line %d.', $identifier, $line), - $scope->getFileDescription(), - $line, - false, - $scope->getFile(), - ))->withIdentifier('ignore.unmatchedIdentifier'); - } - } - } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; } + $linesToIgnore = $localIgnoresProcessorResult->getLinesToIgnore(); + $unmatchedLineIgnores = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); } catch (\PhpParser\Error $e) { - $fileErrors[] = (new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); + $fileErrors[] = (new Error($e->getRawMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } catch (ParserErrorsException $e) { foreach ($e->getErrors() as $error) { $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } } catch (AnalysedCodeException $e) { - $fileErrors[] = (new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); + $fileErrors[] = (new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip())) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } catch (IdentifierNotFound $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } catch (UnableToCompileNode | CircularReference $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e))->withIdentifier('phpstan.reflection'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e)) + ->withIdentifier('phpstan.reflection') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } } elseif (is_dir($file)) { $fileErrors[] = (new Error(sprintf('File %s is a directory.', $file), $file, null, false))->withIdentifier('phpstan.path'); @@ -278,9 +264,33 @@ public function analyseFile( $this->restoreCollectErrorsHandler(); - $fileErrors = array_merge($fileErrors, $this->collectedErrors); + foreach ($linesToIgnore as $fileKey => $lines) { + if (count($lines) > 0) { + continue; + } + + unset($linesToIgnore[$fileKey]); + } + + foreach ($unmatchedLineIgnores as $fileKey => $lines) { + if (count($lines) > 0) { + continue; + } + + unset($unmatchedLineIgnores[$fileKey]); + } - return new FileAnalyserResult($fileErrors, $locallyIgnoredErrors, $fileCollectedData, array_values(array_unique($fileDependencies)), $exportedNodes); + return new FileAnalyserResult( + $fileErrors, + $this->filteredPhpErrors, + $this->allPhpErrors, + $locallyIgnoredErrors, + $fileCollectedData, + array_values(array_unique($fileDependencies)), + $exportedNodes, + $linesToIgnore, + $unmatchedLineIgnores, + ); } /** @@ -302,13 +312,18 @@ private function getLinesToIgnoreFromTokens(array $nodes): array */ private function collectErrors(array $analysedFiles): void { - $this->collectedErrors = []; + $this->filteredPhpErrors = []; + $this->allPhpErrors = []; set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($analysedFiles): bool { if ((error_reporting() & $errno) === 0) { // silence @ operator return true; } + $errorMessage = sprintf('%s: %s', $this->getErrorLabel($errno), $errstr); + + $this->allPhpErrors[] = (new Error($errorMessage, $errfile, $errline, true))->withIdentifier('phpstan.php'); + if ($errno === E_DEPRECATED) { return true; } @@ -317,7 +332,7 @@ private function collectErrors(array $analysedFiles): void return true; } - $this->collectedErrors[] = (new Error($errstr, $errfile, $errline, true))->withIdentifier('phpstan.php'); + $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, true))->withIdentifier('phpstan.php'); return true; }); @@ -328,4 +343,28 @@ private function restoreCollectErrorsHandler(): void restore_error_handler(); } + private function getErrorLabel(int $errno): string + { + switch ($errno) { + case E_ERROR: + return 'Fatal error'; + case E_WARNING: + return 'Warning'; + case E_PARSE: + return 'Parse error'; + case E_NOTICE: + return 'Notice'; + case E_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_STRICT: + return 'Strict error (E_STRICT)'; + } + + return 'Unknown PHP error'; + } + } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index 0033368bfa..23e34a9278 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -5,22 +5,33 @@ use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; +/** + * @phpstan-type LinesToIgnore = array|null>> + */ class FileAnalyserResult { /** * @param list $errors + * @param list $filteredPhpErrors + * @param list $allPhpErrors * @param list $locallyIgnoredErrors * @param list $collectedData * @param list $dependencies * @param list $exportedNodes + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores */ public function __construct( private array $errors, + private array $filteredPhpErrors, + private array $allPhpErrors, private array $locallyIgnoredErrors, private array $collectedData, private array $dependencies, private array $exportedNodes, + private array $linesToIgnore, + private array $unmatchedLineIgnores, ) { } @@ -33,6 +44,22 @@ public function getErrors(): array return $this->errors; } + /** + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } + + /** + * @return list + */ + public function getAllPhpErrors(): array + { + return $this->allPhpErrors; + } + /** * @return list */ @@ -65,4 +92,20 @@ public function getExportedNodes(): array return $this->exportedNodes; } + /** + * @return LinesToIgnore + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return LinesToIgnore + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + } diff --git a/src/Analyser/FinalizerResult.php b/src/Analyser/FinalizerResult.php new file mode 100644 index 0000000000..dfc7761743 --- /dev/null +++ b/src/Analyser/FinalizerResult.php @@ -0,0 +1,49 @@ + $collectorErrors + * @param list $locallyIgnoredCollectorErrors + */ + public function __construct( + private AnalyserResult $analyserResult, + private array $collectorErrors, + private array $locallyIgnoredCollectorErrors, + ) + { + } + + /** + * @return list + */ + public function getErrors(): array + { + return $this->analyserResult->getErrors(); + } + + public function getAnalyserResult(): AnalyserResult + { + return $this->analyserResult; + } + + /** + * @return list + */ + public function getCollectorErrors(): array + { + return $this->collectorErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredCollectorErrors(): array + { + return $this->locallyIgnoredCollectorErrors; + } + +} diff --git a/src/Analyser/Ignore/IgnoreLexer.php b/src/Analyser/Ignore/IgnoreLexer.php index baa0cf7af4..bcaf00ec10 100644 --- a/src/Analyser/Ignore/IgnoreLexer.php +++ b/src/Analyser/Ignore/IgnoreLexer.php @@ -11,7 +11,7 @@ final class IgnoreLexer { public const TOKEN_WHITESPACE = 1; - public const TOKEN_EOL = 2; + public const TOKEN_END = 2; public const TOKEN_IDENTIFIER = 3; public const TOKEN_COMMA = 4; public const TOKEN_OPEN_PARENTHESIS = 5; @@ -20,9 +20,9 @@ final class IgnoreLexer private const LABELS = [ self::TOKEN_WHITESPACE => 'T_WHITESPACE', - self::TOKEN_EOL => 'T_EOL', - self::TOKEN_IDENTIFIER => 'T_IDENTIFIER', - self::TOKEN_COMMA => 'T_COMMA', + self::TOKEN_END => 'end', + self::TOKEN_IDENTIFIER => 'identifier', + self::TOKEN_COMMA => 'comma (,)', self::TOKEN_OPEN_PARENTHESIS => 'T_OPEN_PARENTHESIS', self::TOKEN_CLOSE_PARENTHESIS => 'T_CLOSE_PARENTHESIS', self::TOKEN_OTHER => 'T_OTHER', @@ -51,13 +51,17 @@ public function tokenize(string $input): array /** @var self::TOKEN_* $type */ $type = (int) $match['MARK']; $tokens[] = [$match[0], $type, $line]; - if ($type !== self::TOKEN_EOL) { + if ($type !== self::TOKEN_END) { continue; } $line++; } + if (($type ?? null) !== self::TOKEN_END) { + $tokens[] = ['', self::TOKEN_END, $line]; // ensure ending token is present + } + return $tokens; } @@ -73,14 +77,14 @@ private function generateRegexp(): string { $patterns = [ self::TOKEN_WHITESPACE => '[\\x09\\x20]++', - self::TOKEN_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?', + self::TOKEN_END => '(\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?|\\*/)', self::TOKEN_IDENTIFIER => Error::PATTERN_IDENTIFIER, self::TOKEN_COMMA => ',', self::TOKEN_OPEN_PARENTHESIS => '\\(', self::TOKEN_CLOSE_PARENTHESIS => '\\)', - // everything except whitespaces, TOKEN_CLOSE_PARENTHESIS - self::TOKEN_OTHER => '(?:(?!\\))[^\\s])++', + // everything except whitespaces and parentheses + self::TOKEN_OTHER => '([^\\s\\)\\(])++', ]; foreach ($patterns as $type => &$pattern) { diff --git a/src/Analyser/Ignore/IgnoredError.php b/src/Analyser/Ignore/IgnoredError.php index 00cc1e7df8..ecbe6e1059 100644 --- a/src/Analyser/Ignore/IgnoredError.php +++ b/src/Analyser/Ignore/IgnoredError.php @@ -4,6 +4,7 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Error; +use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\File\FileExcluder; use PHPStan\File\FileHelper; use PHPStan\ShouldNotHappenException; @@ -85,7 +86,7 @@ public static function shouldIgnore( } if ($path !== null) { - $fileExcluder = new FileExcluder($fileHelper, [$path]); + $fileExcluder = new FileExcluder($fileHelper, [$path], BleedingEdgeToggle::isBleedingEdge()); $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 068114767c..bff07c1085 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelper.php +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -68,9 +68,9 @@ public function initialize(): IgnoredErrorHelperResult continue; } - $key = ''; + $key = $ignoreError['path']; if (isset($ignoreError['message'])) { - $key = sprintf("%s\n%s", $ignoreError['message'], $ignoreError['path']); + $key = sprintf("%s\n%s", $key, $ignoreError['message']); } if (isset($ignoreError['identifier'])) { $key = sprintf("%s\n%s", $key, $ignoreError['identifier']); diff --git a/src/Analyser/ImpurePoint.php b/src/Analyser/ImpurePoint.php new file mode 100644 index 0000000000..e87e8866b0 --- /dev/null +++ b/src/Analyser/ImpurePoint.php @@ -0,0 +1,60 @@ +scope; + } + + /** + * @return Node\Expr|Node\Stmt|VirtualNode + */ + public function getNode() + { + return $this->node; + } + + /** + * @return ImpurePointIdentifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isCertain(): bool + { + return $this->certain; + } + +} diff --git a/src/Analyser/InternalError.php b/src/Analyser/InternalError.php new file mode 100644 index 0000000000..9a3ddd2820 --- /dev/null +++ b/src/Analyser/InternalError.php @@ -0,0 +1,104 @@ + + */ +class InternalError implements JsonSerializable +{ + + public const STACK_TRACE_METADATA_KEY = 'stackTrace'; + + public const STACK_TRACE_AS_STRING_METADATA_KEY = 'stackTraceAsString'; + + /** + * @param Trace $trace + */ + public function __construct( + private string $message, + private string $contextDescription, + private array $trace, + private ?string $traceAsString, + private bool $shouldReportBug, + ) + { + } + + /** + * @return Trace + */ + public static function prepareTrace(Throwable $exception): array + { + $trace = array_map(static fn (array $trace) => [ + 'file' => $trace['file'] ?? null, + 'line' => $trace['line'] ?? null, + ], $exception->getTrace()); + + array_unshift($trace, [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]); + + return $trace; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getContextDescription(): string + { + return $this->contextDescription; + } + + /** + * @return Trace + */ + public function getTrace(): array + { + return $this->trace; + } + + public function getTraceAsString(): ?string + { + return $this->traceAsString; + } + + public function shouldReportBug(): bool + { + return $this->shouldReportBug; + } + + /** + * @param mixed[] $json + */ + public static function decode(array $json): self + { + return new self($json['message'], $json['contextDescription'], $json['trace'], $json['traceAsString'], $json['shouldReportBug']); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'message' => $this->message, + 'contextDescription' => $this->contextDescription, + 'trace' => $this->trace, + 'traceAsString' => $this->traceAsString, + 'shouldReportBug' => $this->shouldReportBug, + ]; + } + +} diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index 7d3414f297..fdacebc9ec 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -17,7 +17,7 @@ interface InternalScopeFactory * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 0d74666e85..028600646c 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -42,7 +42,7 @@ public function __construct( * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LocalIgnoresProcessor.php b/src/Analyser/LocalIgnoresProcessor.php new file mode 100644 index 0000000000..04368960b0 --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessor.php @@ -0,0 +1,101 @@ + $temporaryFileErrors + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function process( + array $temporaryFileErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, + ): LocalIgnoresProcessorResult + { + $fileErrors = []; + $locallyIgnoredErrors = []; + foreach ($temporaryFileErrors as $tmpFileError) { + $line = $tmpFileError->getLine(); + if ( + $line !== null + && $tmpFileError->canBeIgnored() + && array_key_exists($tmpFileError->getFile(), $linesToIgnore) + && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) + ) { + $identifiers = $linesToIgnore[$tmpFileError->getFile()][$line]; + if ($identifiers === null) { + $locallyIgnoredErrors[] = $tmpFileError; + unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); + continue; + } + + if ($tmpFileError->getIdentifier() === null) { + $fileErrors[] = $tmpFileError; + continue; + } + + foreach ($identifiers as $i => $ignoredIdentifier) { + if ($ignoredIdentifier !== $tmpFileError->getIdentifier()) { + continue; + } + + unset($identifiers[$i]); + + if (count($identifiers) > 0) { + $linesToIgnore[$tmpFileError->getFile()][$line] = array_values($identifiers); + } else { + unset($linesToIgnore[$tmpFileError->getFile()][$line]); + } + + if ( + array_key_exists($tmpFileError->getFile(), $unmatchedLineIgnores) + && array_key_exists($line, $unmatchedLineIgnores[$tmpFileError->getFile()]) + ) { + $unmatchedIgnoredIdentifiers = $unmatchedLineIgnores[$tmpFileError->getFile()][$line]; + if (is_array($unmatchedIgnoredIdentifiers)) { + foreach ($unmatchedIgnoredIdentifiers as $j => $unmatchedIgnoredIdentifier) { + if ($ignoredIdentifier !== $unmatchedIgnoredIdentifier) { + continue; + } + + unset($unmatchedIgnoredIdentifiers[$j]); + + if (count($unmatchedIgnoredIdentifiers) > 0) { + $unmatchedLineIgnores[$tmpFileError->getFile()][$line] = array_values($unmatchedIgnoredIdentifiers); + } else { + unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); + } + break; + } + } + } + + $locallyIgnoredErrors[] = $tmpFileError; + continue 2; + } + } + + $fileErrors[] = $tmpFileError; + } + + return new LocalIgnoresProcessorResult( + $fileErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + } + +} diff --git a/src/Analyser/LocalIgnoresProcessorResult.php b/src/Analyser/LocalIgnoresProcessorResult.php new file mode 100644 index 0000000000..8c7a36287f --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessorResult.php @@ -0,0 +1,58 @@ + $fileErrors + * @param list $locallyIgnoredErrors + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function __construct( + private array $fileErrors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + ) + { + } + + /** + * @return list + */ + public function getFileErrors(): array + { + return $this->fileErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return LinesToIgnore + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return LinesToIgnore + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index db4cf5d49f..6632716bd7 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -30,20 +30,30 @@ use PhpParser\NodeFinder; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; +use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\IssetExpr; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; @@ -87,6 +97,7 @@ use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; +use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; @@ -110,7 +121,6 @@ use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; @@ -120,6 +130,7 @@ use stdClass; use Throwable; use function abs; +use function array_filter; use function array_key_exists; use function array_key_first; use function array_keys; @@ -133,9 +144,11 @@ use function get_class; use function implode; use function in_array; +use function is_bool; use function is_numeric; use function is_string; use function ltrim; +use function md5; use function sprintf; use function str_starts_with; use function strlen; @@ -161,12 +174,15 @@ class MutatingScope implements Scope /** @var array */ private array $falseyScopes = []; + /** @var non-empty-string|null */ private ?string $namespace; private ?self $scopeOutOfFirstLevelStatement = null; private ?self $scopeWithPromotedNativeTypes = null; + private static int $resolveClosureTypeDepth = 0; + /** * @param array $expressionTypes * @param array $conditionalExpressions @@ -174,7 +190,7 @@ class MutatingScope implements Scope * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function __construct( private InternalScopeFactory $scopeFactory, @@ -560,17 +576,7 @@ public function getDefinedVariables(): array private function isGlobalVariable(string $variableName): bool { - return in_array($variableName, [ - 'GLOBALS', - '_SERVER', - '_GET', - '_POST', - '_FILES', - '_COOKIE', - '_SESSION', - '_REQUEST', - '_ENV', - ], true); + return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); } /** @api */ @@ -639,12 +645,24 @@ public function getType(Expr $node): Type if ($node instanceof GetOffsetValueTypeExpr) { return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim())); } + if ($node instanceof ExistingArrayDimFetch) { + return $this->getType(new Expr\ArrayDimFetch($node->getVar(), $node->getDim())); + } + if ($node instanceof UnsetOffsetExpr) { + return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim())); + } if ($node instanceof SetOffsetValueTypeExpr) { return $this->getType($node->getVar())->setOffsetValueType( $node->getDim() !== null ? $this->getType($node->getDim()) : null, $this->getType($node->getValue()), ); } + if ($node instanceof SetExistingOffsetValueTypeExpr) { + return $this->getType($node->getVar())->setExistingOffsetValueType( + $this->getType($node->getDim()), + $this->getType($node->getValue()), + ); + } if ($node instanceof TypeExpr) { return $node->getExprType(); } @@ -685,6 +703,30 @@ private function getNodeKey(Expr $node): string return $key; } + private function getClosureScopeCacheKey(): string + { + $parts = []; + foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) { + $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache())); + } + $parts[] = '---'; + foreach ($this->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache())); + } + + $parts[] = sprintf(':%d', count($this->inFunctionCallsStack)); + foreach ($this->inFunctionCallsStack as [$method, $parameter]) { + if ($parameter === null) { + $parts[] = ',null'; + continue; + } + + $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache())); + } + + return md5(implode("\n", $parts)); + } + private function resolveType(string $exprString, Expr $node): Type { foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) { @@ -1112,8 +1154,10 @@ private function resolveType(string $exprString, Expr $node): Type if ($node instanceof FuncCall) { if ($node->name instanceof Name) { if ($this->reflectionProvider->hasFunction($node->name, $this)) { + $function = $this->reflectionProvider->getFunction($node->name, $this); return $this->createFirstClassCallable( - $this->reflectionProvider->getFunction($node->name, $this)->getVariants(), + $function, + $function->getVariants(), ); } @@ -1126,6 +1170,7 @@ private function resolveType(string $exprString, Expr $node): Type } return $this->createFirstClassCallable( + null, $callableType->getCallableParametersAcceptors($this), ); } @@ -1141,7 +1186,10 @@ private function resolveType(string $exprString, Expr $node): Type return new ObjectType(Closure::class); } - return $this->createFirstClassCallable($method->getVariants()); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } if ($node instanceof Expr\StaticCall) { @@ -1159,7 +1207,11 @@ private function resolveType(string $exprString, Expr $node): Type return new ObjectType(Closure::class); } - return $this->createFirstClassCallable($classType->getMethod($methodName, $this)->getVariants()); + $method = $classType->getMethod($methodName, $this); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } if ($node instanceof New_) { @@ -1209,6 +1261,14 @@ private function resolveType(string $exprString, Expr $node): Type foreach ($arrayMapArgs as $funcCallArg) { $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); } + } else { + $inFunctionCallsStackCount = count($this->inFunctionCallsStack); + if ($inFunctionCallsStackCount > 0) { + [, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1]; + if ($inParameter !== null) { + $callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType()); + } + } } if ($node instanceof Expr\ArrowFunction) { @@ -1244,48 +1304,146 @@ private function resolveType(string $exprString, Expr $node): Type } else { $returnType = $arrowScope->getKeepVoidType($node->expr); if ($node->returnType !== null) { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); } } + + $arrowFunctionImpurePoints = []; + $invalidateExpressions = []; + $arrowFunctionExprResult = $this->nodeScopeResolver->processExprNode( + new Node\Stmt\Expression($node->expr), + $node->expr, + $arrowScope, + static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $arrowScope->getAnonymousFunctionReflection()) { + return; + } + + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if (!$node instanceof PropertyAssignNode) { + return; + } + + $arrowFunctionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + }, + ExpressionContext::createDeep(), + ); + $throwPoints = $arrowFunctionExprResult->getThrowPoints(); + $impurePoints = array_merge($arrowFunctionImpurePoints, $arrowFunctionExprResult->getImpurePoints()); + $usedVariables = []; } else { + $cachedTypes = $node->getAttribute('phpstanCachedTypes', []); + $cacheKey = $this->getClosureScopeCacheKey(); + if (array_key_exists($cacheKey, $cachedTypes)) { + $cachedClosureData = $cachedTypes[$cacheKey]; + + return new ClosureType( + $parameters, + $cachedClosureData['returnType'], + $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + [], + $cachedClosureData['throwPoints'], + $cachedClosureData['impurePoints'], + $cachedClosureData['invalidateExpressions'], + $cachedClosureData['usedVariables'], + ); + } + if (self::$resolveClosureTypeDepth >= 2) { + return new ClosureType( + $parameters, + $this->getFunctionType($node->returnType, false, false), + $isVariadic, + ); + } + + self::$resolveClosureTypeDepth++; + $closureScope = $this->enterAnonymousFunctionWithoutReflection($node, $callableParameters); $closureReturnStatements = []; $closureYieldStatements = []; - $closureExecutionEnds = []; - $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds): void { - if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { - return; - } + $onlyNeverExecutionEnds = null; + $closureImpurePoints = []; + $invalidateExpressions = []; + + try { + $closureStatementResult = $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$onlyNeverExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { + return; + } - if ($node instanceof ExecutionEndNode) { - if ($node->getStatementResult()->isAlwaysTerminating()) { - foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { - if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { - continue; + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + + if ($node instanceof ExecutionEndNode) { + if ($node->getStatementResult()->isAlwaysTerminating()) { + foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { + if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) { + $onlyNeverExecutionEnds = false; + continue; + } + + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + + break; } - $closureExecutionEnds[] = $node; - break; + if (count($node->getStatementResult()->getExitPoints()) === 0) { + if ($onlyNeverExecutionEnds === null) { + $onlyNeverExecutionEnds = true; + } + } + } else { + $onlyNeverExecutionEnds = false; } - if (count($node->getStatementResult()->getExitPoints()) === 0) { - $closureExecutionEnds[] = $node; - } + return; } - return; - } + if ($node instanceof Node\Stmt\Return_) { + $closureReturnStatements[] = [$node, $scope]; + } - if ($node instanceof Node\Stmt\Return_) { - $closureReturnStatements[] = [$node, $scope]; - } + if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) { + return; + } - if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) { - return; - } + $closureYieldStatements[] = [$node, $scope]; + }, StatementContext::createTopLevel()); + } finally { + self::$resolveClosureTypeDepth--; + } - $closureYieldStatements[] = [$node, $scope]; - }, StatementContext::createTopLevel()); + $throwPoints = $closureStatementResult->getThrowPoints(); + $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints()); $returnTypes = []; $hasNull = false; @@ -1299,13 +1457,13 @@ private function resolveType(string $exprString, Expr $node): Type } if (count($returnTypes) === 0) { - if (count($closureExecutionEnds) > 0 && !$hasNull) { + if ($onlyNeverExecutionEnds === true && !$hasNull) { $returnType = new NonAcceptingNeverType(); } else { $returnType = new VoidType(); } } else { - if (count($closureExecutionEnds) > 0) { + if ($onlyNeverExecutionEnds === true) { $returnTypes[] = new NonAcceptingNeverType(); } if ($hasNull) { @@ -1346,14 +1504,76 @@ private function resolveType(string $exprString, Expr $node): Type $returnType, ]); } else { - $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); + if ($node->returnType !== null) { + $nativeReturnType = $this->getFunctionType($node->returnType, false, false); + $returnType = self::intersectButNotNever($nativeReturnType, $returnType); + } + } + + $usedVariables = []; + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $usedVariables[] = $use->var->name; + } + + foreach ($node->uses as $use) { + if (!$use->byRef) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref use', + true, + ); + break; } } + foreach ($parameters as $parameter) { + if ($parameter->passedByReference()->no()) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref parameter', + true, + ); + } + + $throwPointsForClosureType = array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable()) : SimpleThrowPoint::createImplicit(), $throwPoints); + $impurePointsForClosureType = array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints); + + $cachedTypes = $node->getAttribute('phpstanCachedTypes', []); + $cachedTypes[$this->getClosureScopeCacheKey()] = [ + 'returnType' => $returnType, + 'throwPoints' => $throwPointsForClosureType, + 'impurePoints' => $impurePointsForClosureType, + 'invalidateExpressions' => $invalidateExpressions, + 'usedVariables' => $usedVariables, + ]; + $node->setAttribute('phpstanCachedTypes', $cachedTypes); + return new ClosureType( $parameters, $returnType, $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + [], + $throwPointsForClosureType, + $impurePointsForClosureType, + $invalidateExpressions, + $usedVariables, ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -1445,13 +1665,15 @@ private function resolveType(string $exprString, Expr $node): Type } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) { $varType = $this->getType($node->var); $varScalars = $varType->getConstantScalarValues(); - $stringType = new StringType(); + if (count($varScalars) > 0) { $newTypes = []; foreach ($varScalars as $varValue) { if ($node instanceof Expr\PreInc) { - ++$varValue; + if (!is_bool($varValue)) { + ++$varValue; + } } elseif (is_numeric($varValue)) { --$varValue; } @@ -1461,9 +1683,24 @@ private function resolveType(string $exprString, Expr $node): Type return TypeCombinator::union(...$newTypes); } elseif ($varType->isString()->yes()) { if ($varType->isLiteralString()->yes()) { - return new IntersectionType([$stringType, new AccessoryLiteralStringType()]); + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); } - return $stringType; + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); } if ($node instanceof Expr\PreInc) { @@ -1494,10 +1731,92 @@ private function resolveType(string $exprString, Expr $node): Type return $generatorReturnType; } elseif ($node instanceof Expr\Match_) { $cond = $node->cond; + $condType = $this->getType($cond); $types = []; $matchScope = $this; - foreach ($node->arms as $arm) { + $arms = $node->arms; + if ($condType->isEnum()->yes()) { + // enum match analysis would work even without this if branch + // but would be much slower + // this avoids using ObjectType::$subtractedType which is slow for huge enums + // because of repeated union type normalization + $enumCases = $condType->getEnumCases(); + if (count($enumCases) > 0) { + $indexedEnumCases = []; + foreach ($enumCases as $enumCase) { + $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase; + } + $unusedIndexedEnumCases = $indexedEnumCases; + + foreach ($arms as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + $conditionCases = []; + foreach ($arm->conds as $armCond) { + if (!$armCond instanceof Expr\ClassConstFetch) { + continue 2; + } + if (!$armCond->class instanceof Name) { + continue 2; + } + if (!$armCond->name instanceof Node\Identifier) { + continue 2; + } + $fetchedClassName = $this->resolveName($armCond->class); + $loweredFetchedClassName = strtolower($fetchedClassName); + if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) { + continue 2; + } + + $caseName = $armCond->name->toString(); + if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) { + continue 2; + } + + $conditionCases[] = $indexedEnumCases[$loweredFetchedClassName][$caseName]; + unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + } + + $conditionCasesCount = count($conditionCases); + if ($conditionCasesCount === 0) { + throw new ShouldNotHappenException(); + } elseif ($conditionCasesCount === 1) { + $conditionCaseType = $conditionCases[0]; + } else { + $conditionCaseType = new UnionType($conditionCases); + } + + $types[] = $matchScope->addTypeToExpression( + $cond, + $conditionCaseType, + )->getType($arm->body); + unset($arms[$i]); + } + + $remainingCases = []; + foreach ($unusedIndexedEnumCases as $cases) { + foreach ($cases as $case) { + $remainingCases[] = $case; + } + } + + $remainingCasesCount = count($remainingCases); + if ($remainingCasesCount === 0) { + $remainingType = new NeverType(); + } elseif ($remainingCasesCount === 1) { + $remainingType = $remainingCases[0]; + } else { + $remainingType = new UnionType($remainingCases); + } + + $matchScope = $matchScope->addTypeToExpression($cond, $remainingType); + } + } + + foreach ($arms as $arm) { if ($arm->conds === null) { if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); @@ -1787,7 +2106,7 @@ private function resolveType(string $exprString, Expr $node): Type if ($node->class instanceof Name) { $staticMethodCalledOnType = $this->resolveTypeByName($node->class); } else { - $staticMethodCalledOnType = $this->getType($node->class)->getObjectTypeOrClassStringObjectType(); + $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } $returnType = $this->methodCallReturnType( @@ -1879,7 +2198,7 @@ private function resolveType(string $exprString, Expr $node): Type if ($node->class instanceof Name) { $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); } else { - $staticPropertyFetchedOnType = $this->getType($node->class)->getObjectTypeOrClassStringObjectType(); + $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } $returnType = $this->propertyFetchType( @@ -2176,14 +2495,61 @@ private function issetCheckUndefined(Expr $expr): ?bool /** * @param ParametersAcceptor[] $variants */ - private function createFirstClassCallable(array $variants): Type + private function createFirstClassCallable( + FunctionReflection|ExtendedMethodReflection|null $function, + array $variants, + ): Type { $closureTypes = []; + foreach ($variants as $variant) { $returnType = $variant->getReturnType(); if ($variant instanceof ParametersAcceptorWithPhpDocs) { $returnType = $this->nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType; } + + $templateTags = []; + foreach ($variant->getTemplateTypeMap()->getTypes() as $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + $templateTags[$templateType->getName()] = new TemplateTag( + $templateType->getName(), + $templateType->getBound(), + $templateType->getVariance(), + ); + } + + $throwPoints = []; + $impurePoints = []; + if ($variant instanceof CallableParametersAcceptor) { + $throwPoints = $variant->getThrowPoints(); + $impurePoints = $variant->getImpurePoints(); + } elseif ($function !== null) { + $returnTypeForThrow = $variant->getReturnType(); + $throwType = $function->getThrowType(); + if ($throwType === null) { + if ($returnTypeForThrow instanceof NeverType && $returnTypeForThrow->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant); + if ($impurePoint !== null) { + $impurePoints[] = $impurePoint; + } + } + $parameters = $variant->getParameters(); $closureTypes[] = new ClosureType( $parameters, @@ -2192,6 +2558,11 @@ private function createFirstClassCallable(array $variants): Type $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $templateTags, + $throwPoints, + $impurePoints, + [], + [], ); } @@ -2325,7 +2696,8 @@ public function resolveName(Name $name): string { $originalClass = (string) $name; if ($this->isInClass()) { - if (in_array(strtolower($originalClass), [ + $lowerClass = strtolower($originalClass); + if (in_array($lowerClass, [ 'self', 'static', ], true)) { @@ -2333,7 +2705,7 @@ public function resolveName(Name $name): string return $this->inClosureBindScopeClasses[0]; } return $this->getClassReflection()->getName(); - } elseif ($originalClass === 'parent') { + } elseif ($lowerClass === 'parent') { $currentClassReflection = $this->getClassReflection(); if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); @@ -2409,14 +2781,14 @@ public function hasExpressionType(Expr $node): TrinaryLogic } /** - * @param MethodReflection|FunctionReflection $reflection + * @param MethodReflection|FunctionReflection|null $reflection */ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self { $stack = $this->inFunctionCallsStack; $stack[] = [$reflection, $parameter]; - $scope = $this->scopeFactory->create( + return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->getFunction(), @@ -2434,11 +2806,6 @@ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter) $this->parentScope, $this->nativeTypesPromoted, ); - $scope->resolvedTypes = $this->resolvedTypes; - $scope->truthyScopes = $this->truthyScopes; - $scope->falseyScopes = $this->falseyScopes; - - return $scope; } public function popInFunctionCall(): self @@ -2446,7 +2813,7 @@ public function popInFunctionCall(): self $stack = $this->inFunctionCallsStack; array_pop($stack); - $scope = $this->scopeFactory->create( + return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->getFunction(), @@ -2464,11 +2831,6 @@ public function popInFunctionCall(): self $this->parentScope, $this->nativeTypesPromoted, ); - $scope->resolvedTypes = $this->resolvedTypes; - $scope->truthyScopes = $this->truthyScopes; - $scope->falseyScopes = $this->falseyScopes; - - return $scope; } /** @api */ @@ -2496,12 +2858,18 @@ public function isInClassExists(string $className): bool public function getFunctionCallStack(): array { - return array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack); + return array_values(array_filter( + array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack), + static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null, + )); } public function getFunctionCallStackWithParameters(): array { - return $this->inFunctionCallsStack; + return array_values(array_filter( + $this->inFunctionCallsStack, + static fn ($item) => $item[0] !== null, + )); } /** @api */ @@ -2567,6 +2935,8 @@ public function enterTrait(ClassReflection $traitReflection): self * @api * @param Type[] $phpDocParameterTypes * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2584,6 +2954,8 @@ public function enterClassMethod( ?Type $selfOutType = null, ?string $phpDocComment = null, array $parameterOutTypes = [], + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): self { if (!$this->isInClass()) { @@ -2597,10 +2969,10 @@ public function enterClassMethod( $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($classMethod), - array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes), $this->getRealParameterDefaultValues($classMethod), $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), - $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, + $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null, $throwType, $deprecatedDescription, $isDeprecated, @@ -2611,7 +2983,9 @@ public function enterClassMethod( $asserts ?? Assertions::createEmpty(), $selfOutType, $phpDocComment, - array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), + 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), ), !$classMethod->isStatic(), ); @@ -2648,7 +3022,7 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array } $realParameterTypes[$parameter->var->name] = $this->getFunctionType( $parameter->type, - $this->isParameterValueNullable($parameter), + $this->isParameterValueNullable($parameter) && $parameter->flags === 0, false, ); } @@ -2679,6 +3053,8 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): * @api * @param Type[] $phpDocParameterTypes * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2695,6 +3071,8 @@ public function enterFunction( ?Assertions $asserts = null, ?string $phpDocComment = null, array $parameterOutTypes = [], + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): self { return $this->enterFunctionLike( @@ -2717,6 +3095,8 @@ public function enterFunction( $asserts ?? Assertions::createEmpty(), $phpDocComment, array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ), false, ); @@ -2771,6 +3151,10 @@ private function enterFunctionLike( $parameterNode = new Variable($parameter->getName()); $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType); + $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName()); + $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr); + $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType); + $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { @@ -2780,6 +3164,7 @@ private function enterFunctionLike( } } $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType); + $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType); } if ($preserveThis && array_key_exists('$this', $this->expressionTypes)) { @@ -2876,6 +3261,58 @@ public function restoreOriginalScopeAfterClosureBind(self $originalScope): self ); } + public function restoreThis(self $restoreThisScope): self + { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + + if ($restoreThisScope->isInClass()) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $expressionTypes[$exprString] = $expressionTypeHolder; + } + + foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $nativeExpressionTypes[$exprString] = $expressionTypeHolder; + } + } else { + unset($expressionTypes['$this']); + unset($nativeExpressionTypes['$this']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + public function enterClosureCall(Type $thisType, Type $nativeThisType): self { $expressionTypes = $this->expressionTypes; @@ -2958,16 +3395,16 @@ private function enterAnonymousFunctionWithoutReflection( $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); @@ -3134,16 +3571,16 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu if ($callableParameters !== null) { if (isset($callableParameters[$i])) { - $parameterType = TypehintHelper::decideType($parameterType, $callableParameters[$i]->getType()); + $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); } elseif (count($callableParameters) > 0) { $lastParameter = $callableParameters[count($callableParameters) - 1]; if ($lastParameter->isVariadic()) { - $parameterType = TypehintHelper::decideType($parameterType, $lastParameter->getType()); + $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } else { - $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); + $parameterType = self::intersectButNotNever($parameterType, new MixedType()); } } @@ -3166,7 +3603,7 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu $arrowFunctionScope->nativeExpressionTypes, $arrowFunctionScope->conditionalExpressions, $arrowFunctionScope->inClosureBindScopeClasses, - null, + new TrivialParametersAcceptor(), true, [], [], @@ -3228,18 +3665,34 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); } + private static function intersectButNotNever(Type $nativeType, Type $inferredType): Type + { + if ($nativeType->isSuperTypeOf($inferredType)->no()) { + return $nativeType; + } + + $result = TypeCombinator::intersect($nativeType, $inferredType); + if (TypeCombinator::containsNull($nativeType)) { + return TypeCombinator::addNull($result); + } + + return $result; + } + public function enterMatch(Expr\Match_ $expr): self { if ($expr->cond instanceof Variable) { return $this; } if ($expr->cond instanceof AlwaysRememberedExpr) { - return $this; + $cond = $expr->cond->expr; + } else { + $cond = $expr->cond; } - $type = $this->getType($expr->cond); - $nativeType = $this->getNativeType($expr->cond); - $condExpr = new AlwaysRememberedExpr($expr->cond, $type, $nativeType); + $type = $this->getType($cond); + $nativeType = $this->getNativeType($cond); + $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType); $expr->cond = $condExpr; return $this->assignExpression($condExpr, $type, $nativeType); @@ -3462,6 +3915,10 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp } } + $parameterOriginalValueExprString = $this->getNodeKey(new ParameterVariableOriginalValueExpr($variableName)); + unset($scope->expressionTypes[$parameterOriginalValueExprString]); + unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]); + return $scope; } @@ -3563,7 +4020,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $nativeTypes = $scope->nativeExpressionTypes; $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->getFunction(), @@ -3581,6 +4038,12 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $this->parentScope, $this->nativeTypesPromoted, ); + + if ($expr instanceof AlwaysRememberedExpr) { + return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty); + } + + return $scope; } public function assignExpression(Expr $expr, Type $type, ?Type $nativeType = null): self @@ -3795,7 +4258,7 @@ private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): se ); } - private function addTypeToExpression(Expr $expr, Type $type): self + public function addTypeToExpression(Expr $expr, Type $type): self { $originalExprType = $this->getType($expr); $nativeType = $this->getNativeType($expr); @@ -4954,6 +5417,9 @@ public function debug(): array return $descriptions; } + /** + * @param non-empty-string $className + */ private function exactInstantiation(New_ $node, string $className): ?Type { $resolvedClassName = $this->resolveExactName(new Name($className)); @@ -5141,6 +5607,17 @@ 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); + if ($type === null) { + return null; + } + + return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod(); + } + /** * @param MethodCall|Node\Expr\StaticCall $methodCall */ diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index af3688e107..e083daf91b 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -24,6 +24,7 @@ class NameScope /** * @api + * @param non-empty-string|null $namespace * @param array $uses alias(string) => fullName(string) * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap @@ -171,6 +172,20 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self ); } + public function withClassName(string $className): self + { + return new self( + $this->namespace, + $this->uses, + $className, + $this->functionName, + $this->templateTypeMap, + $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, + ); + } + public function unsetTemplateType(string $name): self { $map = $this->templateTypeMap; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index aba73c290a..64d0dd9ec1 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -20,6 +20,7 @@ use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\Coalesce; use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Cast; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\ErrorSuppress; @@ -43,6 +44,7 @@ use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\If_; +use PhpParser\Node\Stmt\InlineHTML; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Static_; use PhpParser\Node\Stmt\Switch_; @@ -50,6 +52,10 @@ use PhpParser\Node\Stmt\TryCatch; use PhpParser\Node\Stmt\Unset_; use PhpParser\Node\Stmt\While_; +use PhpParser\NodeFinder; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\CloningVisitor; +use PhpParser\NodeVisitorAbstract; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -57,6 +63,8 @@ use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Node\BooleanAndNode; @@ -72,12 +80,16 @@ use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionReturnStatementsNode; @@ -89,6 +101,7 @@ use PHPStan\Node\InFunctionNode; use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\InTraitNode; +use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Node\MatchExpressionArm; @@ -97,10 +110,12 @@ use PHPStan\Node\MatchExpressionNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodReturnStatementsNode; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Node\PropertyAssignNode; use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\UnreachableStatementNode; +use PHPStan\Node\VariableAssignNode; use PHPStan\Node\VarTagChangedExpressionTypeNode; use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; @@ -111,12 +126,17 @@ use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; 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; @@ -129,6 +149,7 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\ClosureType; @@ -144,19 +165,23 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\ResourceType; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use ReflectionProperty; use Throwable; use Traversable; use TypeError; @@ -171,17 +196,20 @@ use function array_pop; use function array_reverse; use function array_slice; +use function array_values; use function base64_decode; use function count; use function in_array; use function is_array; use function is_int; use function is_string; +use function ksort; use function sprintf; use function str_starts_with; use function strtolower; use function trim; use const PHP_VERSION_ID; +use const SORT_NUMERIC; class NodeScopeResolver { @@ -211,6 +239,7 @@ public function __construct( private readonly InitializerExprTypeResolver $initializerExprTypeResolver, private readonly Reflector $reflector, private readonly ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider, private readonly Parser $parser, private readonly FileTypeMapper $fileTypeMapper, private readonly StubPhpDocProvider $stubPhpDocProvider, @@ -221,6 +250,7 @@ public function __construct( private readonly TypeSpecifier $typeSpecifier, private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, + private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider, private readonly ScopeFactory $scopeFactory, private readonly bool $polluteScopeWithLoopInitialAssignments, private readonly bool $polluteScopeWithAlwaysIterableForeach, @@ -230,6 +260,8 @@ public function __construct( private readonly bool $implicitThrows, private readonly bool $treatPhpDocTypesAsCertain, private readonly bool $detectDeadTypeInMultiCatch, + private readonly bool $paramOutType, + private readonly bool $preciseMissingReturn, ) { $earlyTerminatingMethodNames = []; @@ -261,24 +293,28 @@ public function processNodes( callable $nodeCallback, ): void { + $alreadyTerminated = false; foreach ($nodes as $i => $node) { - if (!$node instanceof Node\Stmt) { + if ( + !$node instanceof Node\Stmt + || ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) + ) { continue; } $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel()); $scope = $statementResult->getScope(); - if (!$statementResult->isAlwaysTerminating()) { + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; } + $alreadyTerminated = true; $nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true); if (!$nextStmt instanceof Node\Stmt) { continue; } $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); - break; } } @@ -300,6 +336,7 @@ public function processStmtNodes( } $exitPoints = []; $throwPoints = []; + $impurePoints = []; $alreadyTerminated = false; $hasYield = false; $stmtCount = count($stmts); @@ -307,6 +344,10 @@ public function processStmtNodes( || $parentNode instanceof Node\Stmt\ClassMethod || $parentNode instanceof Expr\Closure; foreach ($stmts as $i => $stmt) { + if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) { + continue; + } + $isLast = $i === $stmtCount - 1; $statementResult = $this->processStmtNode( $stmt, @@ -320,42 +361,68 @@ public function processStmtNodes( if ($shouldCheckLastStatement && $isLast) { /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ $parentNode = $parentNode; - $nodeCallback(new ExecutionEndNode( - $stmt, - new StatementResult( - $scope, - $hasYield, - $statementResult->isAlwaysTerminating(), - $statementResult->getExitPoints(), - $statementResult->getThrowPoints(), - ), - $parentNode->returnType !== null, - ), $scope); + + $endStatements = $statementResult->getEndStatements(); + if ($this->preciseMissingReturn && count($endStatements) > 0) { + foreach ($endStatements as $endStatement) { + $endStatementResult = $endStatement->getResult(); + $nodeCallback(new ExecutionEndNode( + $endStatement->getStatement(), + new StatementResult( + $endStatementResult->getScope(), + $hasYield, + $endStatementResult->isAlwaysTerminating(), + $endStatementResult->getExitPoints(), + $endStatementResult->getThrowPoints(), + $endStatementResult->getImpurePoints(), + ), + $parentNode->returnType !== null, + ), $endStatementResult->getScope()); + } + } else { + $nodeCallback(new ExecutionEndNode( + $stmt, + new StatementResult( + $scope, + $hasYield, + $statementResult->isAlwaysTerminating(), + $statementResult->getExitPoints(), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + ), + $parentNode->returnType !== null, + ), $scope); + } } $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints()); - if (!$statementResult->isAlwaysTerminating()) { + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; } $alreadyTerminated = true; $nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); - if ($nextStmt !== null) { - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + if ($nextStmt === null) { + continue; } - break; + $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); } - $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints); + $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); if ($stmtCount === 0 && $shouldCheckLastStatement) { /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ $parentNode = $parentNode; + $returnTypeNode = $parentNode->returnType; + if ($parentNode instanceof Expr\Closure) { + $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); + } $nodeCallback(new ExecutionEndNode( $parentNode, $statementResult, - $parentNode->returnType !== null, + $returnTypeNode !== null, ), $scope); } @@ -394,12 +461,12 @@ private function processStmtNode( ) { $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); if ($methodReflection instanceof NativeMethodReflection) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } if ($methodReflection instanceof PhpMethodReflection) { $declaringTrait = $methodReflection->getDeclaringTrait(); if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } } } @@ -417,6 +484,7 @@ private function processStmtNode( if ($stmt instanceof Node\Stmt\Declare_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $alwaysTerminating = false; $exitPoints = []; foreach ($stmt->declares as $declare) { @@ -438,16 +506,18 @@ private function processStmtNode( $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $alwaysTerminating = $result->isAlwaysTerminating(); $exitPoints = $result->getExitPoints(); } - return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints); + return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Function_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($stmt, $param, $scope, $nodeCallback); @@ -472,6 +542,8 @@ private function processStmtNode( $asserts, $phpDocComment, $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $functionReflection = $functionScope->getFunction(); if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { @@ -483,7 +555,8 @@ private function processStmtNode( $gatheredReturnStatements = []; $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds): void { + $functionImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -491,6 +564,16 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + $functionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; @@ -511,13 +594,15 @@ private function processStmtNode( $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $functionImpurePoints), $functionReflection, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($stmt, $param, $scope, $nodeCallback); @@ -543,6 +628,8 @@ private function processStmtNode( $selfOutType, $phpDocComment, $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); if (!$scope->isInClass()) { @@ -550,7 +637,7 @@ private function processStmtNode( } $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; - if ($stmt->name->toLowerString() === '__construct' || $isFromTrait) { + if ($isFromTrait || $stmt->name->toLowerString() === '__construct') { foreach ($stmt->params as $param) { if ($param->flags === 0) { continue; @@ -595,7 +682,8 @@ private function processStmtNode( $gatheredReturnStatements = []; $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds): void { + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -603,6 +691,25 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + if ( + $node->getPropertyFetch() instanceof Expr\PropertyFetch + && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection + && $scope->getFunction()->getDeclaringClass()->hasConstructor() + && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName() + && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null + ) { + return; + } + $methodImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; @@ -620,7 +727,7 @@ private function processStmtNode( $classReflection = $scope->getClassReflection(); $methodReflection = $methodScope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { + if (!$methodReflection instanceof ExtendedMethodReflection) { throw new ShouldNotHappenException(); } @@ -630,6 +737,7 @@ private function processStmtNode( $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $methodImpurePoints), $classReflection, $methodReflection, ), $methodScope); @@ -645,37 +753,70 @@ private function processStmtNode( } $throwPoints = $overridingThrowPoints ?? $throwPoints; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'echo', 'echo', true), + ]; } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); $hasYield = $result->hasYield(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) { if ($stmt->num !== null) { $result = $this->processExprNode($stmt, $stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Expression) { $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); - $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel()); + $hasAssign = false; + $currentScope = $scope; + $result = $this->processExprNode($stmt, $stmt->expr, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { + $nodeCallback($node, $scope); + if ($scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()) { + return; + } + if ($scope->getFunction() !== $currentScope->getFunction()) { + return; + } + if (!$node instanceof VariableAssignNode && !$node instanceof PropertyAssignNode) { + return; + } + + $hasAssign = true; + }, ExpressionContext::createTopLevel()); + $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); + if ( + count($result->getImpurePoints()) === 0 + && count($throwPoints) === 0 + && !$stmt->expr instanceof Expr\PostInc + && !$stmt->expr instanceof Expr\PreInc + && !$stmt->expr instanceof Expr\PostDec + && !$stmt->expr instanceof Expr\PreDec + ) { + $nodeCallback(new NoopExpressionNode($stmt->expr, $hasAssign), $scope); + } $scope = $result->getScope(); $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( $scope, @@ -684,12 +825,13 @@ private function processStmtNode( )); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($earlyTerminationExpr !== null) { return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } - return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints); + return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Namespace_) { if ($stmt->name !== null) { $scope = $scope->enterNamespace($stmt->name->toString()); @@ -698,11 +840,13 @@ private function processStmtNode( $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope(); $hasYield = false; $throwPoints = []; + $impurePoints = []; } elseif ($stmt instanceof Node\Stmt\Trait_) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } elseif ($stmt instanceof Node\Stmt\ClassLike) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if (isset($stmt->namespacedName)) { $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); @@ -711,7 +855,7 @@ private function processStmtNode( if ($stmt->name === null) { throw new ShouldNotHappenException(); } - if ($stmt->getAttribute('anonymousClass', false) === false) { + if (!$stmt->isAnonymous()) { $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); } else { $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); @@ -734,6 +878,7 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\Property) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->props as $prop) { @@ -741,7 +886,7 @@ private function processStmtNode( if ($prop->default !== null) { $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep()); } - [,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt); + [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt); if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } @@ -780,15 +925,18 @@ private function processStmtNode( $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); + ], $throwPoints, $impurePoints); } elseif ($stmt instanceof If_) { $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); $ifAlwaysTrue = $conditionType->isTrue()->yes(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); + $endStatements = []; $finalScope = null; $alwaysTerminating = true; $hasYield = $condResult->hasYield(); @@ -798,9 +946,17 @@ private function processStmtNode( if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt, $branchScopeStatementResult); + } $hasYield = $branchScopeStatementResult->hasYield() || $hasYield; } @@ -813,6 +969,7 @@ private function processStmtNode( $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $condScope = $condResult->getScope(); $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); @@ -828,9 +985,17 @@ private function processStmtNode( ) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($elseif->stmts) > 0) { + $endStatements[] = new EndStatementResult($elseif->stmts[count($elseif->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($elseif, $branchScopeStatementResult); + } $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } @@ -856,9 +1021,17 @@ private function processStmtNode( if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if (count($branchScopeStatementResult->getEndStatements()) > 0) { + $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements()); + } elseif (count($stmt->else->stmts) > 0) { + $endStatements[] = new EndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult); + } else { + $endStatements[] = new EndStatementResult($stmt->else, $branchScopeStatementResult); + } $hasYield = $hasYield || $branchScopeStatementResult->hasYield(); } } @@ -867,14 +1040,20 @@ private function processStmtNode( $finalScope = $scope; } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints); + if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { + $endStatements[] = new EndStatementResult($stmt, new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints)); + } + + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements); } elseif ($stmt instanceof Node\Stmt\TraitUse) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processTraitUse($stmt, $scope, $nodeCallback); } elseif ($stmt instanceof Foreach_) { $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $scope = $condResult->getScope(); $arrayComparisonExpr = new BinaryOp\NotIdentical( $stmt->expr, @@ -926,19 +1105,15 @@ private function processStmtNode( $exprType = $scope->getType($stmt->expr); $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) { - if ($this->polluteScopeWithAlwaysIterableForeach) { - $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( - new BinaryOp\Identical( - $stmt->expr, - new Array_([]), - ), - new FuncCall(new Name\FullyQualified('is_object'), [ - new Arg($stmt->expr), - ]), - ))); - } else { - $finalScope = $finalScope->mergeWith($scope); - } + $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( + new BinaryOp\Identical( + $stmt->expr, + new Array_([]), + ), + new FuncCall(new Name\FullyQualified('is_object'), [ + new Arg($stmt->expr), + ]), + ))); } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { @@ -948,6 +1123,7 @@ private function processStmtNode( if (!$isIterableAtLeastOnce->no()) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) { $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr); @@ -959,6 +1135,7 @@ private function processStmtNode( $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(), $finalScopeResult->getExitPointsForOuterLoop(), $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof While_) { $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, static function (): void { @@ -994,19 +1171,23 @@ private function processStmtNode( $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); - foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); + $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel(); + if (!$alwaysIterates) { + foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + } } + $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); foreach ($breakExitPoints as $breakExitPoint) { $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); } $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); - $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); - $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel(); $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan); if ($alwaysIterates) { @@ -1025,8 +1206,10 @@ private function processStmtNode( } $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); if (!$neverIterates) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } return new StatementResult( @@ -1035,6 +1218,7 @@ private function processStmtNode( $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof Do_) { $finalScope = null; @@ -1042,6 +1226,7 @@ private function processStmtNode( $count = 0; $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($context->isTopLevel()) { do { @@ -1096,6 +1281,7 @@ private function processStmtNode( $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $finalScope = $condResult->getFalseyScope(); } else { $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); @@ -1110,16 +1296,19 @@ private function processStmtNode( $alwaysTerminating, $bodyScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $bodyScopeResult->getThrowPoints()), + array_merge($impurePoints, $bodyScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof For_) { $initScope = $scope; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->init as $initExpr) { $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); $initScope = $initResult->getScope(); $hasYield = $hasYield || $initResult->hasYield(); $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints()); } $bodyScope = $initScope; @@ -1138,6 +1327,7 @@ private function processStmtNode( $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary); $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $bodyScope = $condResult->getTruthyScope(); } @@ -1162,6 +1352,7 @@ private function processStmtNode( $bodyScope = $exprResult->getScope(); $hasYield = $hasYield || $exprResult->hasYield(); $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); } if ($bodyScope->equals($prevScope)) { @@ -1224,6 +1415,7 @@ private function processStmtNode( false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, $finalScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $finalScopeResult->getThrowPoints()), + array_merge($impurePoints, $finalScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof Switch_) { $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -1236,6 +1428,7 @@ private function processStmtNode( $hasYield = $condResult->hasYield(); $exitPointsForOuterLoop = []; $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); foreach ($stmt->cases as $caseNode) { if ($caseNode->cond !== null) { $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); @@ -1243,6 +1436,7 @@ private function processStmtNode( $scopeForBranches = $caseResult->getScope(); $hasYield = $hasYield || $caseResult->hasYield(); $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints()); $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; @@ -1263,6 +1457,7 @@ private function processStmtNode( } $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop()); $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints()); if ($branchScopeResult->isAlwaysTerminating()) { $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); $prevScope = null; @@ -1292,7 +1487,7 @@ private function processStmtNode( $finalScope = $scope->mergeWith($finalScope); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints); } elseif ($stmt instanceof TryCatch) { $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); @@ -1320,6 +1515,7 @@ private function processStmtNode( } $throwPoints = $branchScopeResult->getThrowPoints(); + $impurePoints = $branchScopeResult->getImpurePoints(); $throwPointsForLater = []; $pastCatchTypes = new NeverType(); @@ -1451,6 +1647,7 @@ private function processStmtNode( $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); $hasYield = $hasYield || $catchScopeResult->hasYield(); $catchThrowPoints = $catchScopeResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints()); $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints); if ($finallyScope !== null) { @@ -1492,6 +1689,7 @@ private function processStmtNode( $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); $hasYield = $hasYield || $finallyResult->hasYield(); $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints()); $finallyScope = $finallyResult->getScope(); $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); if (count($finallyResult->getExitPoints()) > 0) { @@ -1503,32 +1701,99 @@ private function processStmtNode( $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater)); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints); } elseif ($stmt instanceof Unset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->vars as $var) { $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $scope = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + $exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $exprResult->getScope(); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); - $scope = $scope->unsetExpression($var); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + if ($var instanceof ArrayDimFetch && $var->dim !== null) { + $cloningTraverser = new NodeTraverser(); + $cloningTraverser->addVisitor(new CloningVisitor()); + + /** @var Expr $clonedVar */ + [$clonedVar] = $cloningTraverser->traverse([$var->var]); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class () extends NodeVisitorAbstract { + + public function leaveNode(Node $node): ?ExistingArrayDimFetch + { + if (!$node instanceof ArrayDimFetch || $node->dim === null) { + return null; + } + + return new ExistingArrayDimFetch($node->var, $node->dim); + } + + }); + + /** @var Expr $clonedVar */ + [$clonedVar] = $traverser->traverse([$clonedVar]); + $scope = $this->processAssignVar( + $scope, + $stmt, + $clonedVar, + new UnsetOffsetExpr($var->var, $var->dim), + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + ExpressionContext::createDeep(), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + false, + )->getScope(); + } elseif ($var instanceof PropertyFetch) { + $scope = $scope->invalidateExpression($var); + $impurePoints[] = new ImpurePoint( + $scope, + $var, + 'propertyUnset', + 'property unset', + true, + ); + } else { + $scope = $scope->invalidateExpression($var); + } + } } elseif ($stmt instanceof Node\Stmt\Use_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->uses as $use) { $nodeCallback($use, $scope); } } elseif ($stmt instanceof Node\Stmt\Global_) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'global', + 'global variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { if (!$var instanceof Variable) { throw new ShouldNotHappenException(); } $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $varResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); if (!is_string($var->name)) { @@ -1542,6 +1807,15 @@ private function processStmtNode( } elseif ($stmt instanceof Static_) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'static', + 'static variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { @@ -1550,11 +1824,13 @@ private function processStmtNode( } if ($var->default !== null) { - $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints()); } $scope = $scope->enterExpressionAssign($var->var); - $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $varResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $scope->exitExpressionAssign($var->var); $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType()); @@ -1565,12 +1841,17 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\Const_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); if ($const->namespacedName !== null) { $constantName = new Name\FullyQualified($const->namespacedName->toString()); } else { + 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)); @@ -1578,10 +1859,12 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); if ($scope->getClassReflection() === null) { throw new ShouldNotHappenException(); } @@ -1595,24 +1878,35 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $impurePoints = []; if ($stmt->expr !== null) { - $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = $exprResult->getImpurePoints(); } + } elseif ($stmt instanceof InlineHTML) { + $hasYield = false; + $throwPoints = []; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true), + ]; } elseif ($stmt instanceof Node\Stmt\Nop) { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; } elseif ($stmt instanceof Node\Stmt\GroupUse) { $hasYield = false; $throwPoints = []; foreach ($stmt->uses as $use) { $nodeCallback($use, $scope); } + $impurePoints = []; } else { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; } - return new StatementResult($scope, $hasYield, false, [], $throwPoints); + return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints); } /** @@ -1895,8 +2189,11 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop if ($expr instanceof Variable) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->name instanceof Expr) { return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); } } elseif ($expr instanceof Assign || $expr instanceof AssignRef) { $result = $this->processAssignVar( @@ -1907,7 +2204,23 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop $nodeCallback, $context, function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + $impurePoints = []; if ($expr instanceof AssignRef) { + $referencedExpr = $expr->expr; + while ($referencedExpr instanceof ArrayDimFetch) { + $referencedExpr = $referencedExpr->var; + } + + if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'propertyAssignByRef', + 'property assignment by reference', + false, + ); + } + $scope = $scope->enterExpressionAssign($expr->expr); } @@ -1922,19 +2235,21 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); if ($expr instanceof AssignRef) { $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); }, true, ); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $vars = $this->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; @@ -1965,6 +2280,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $result->getScope()->mergeWith($originalScope), $result->hasYield(), $result->getThrowPoints(), + $result->getImpurePoints(), ); } @@ -1975,6 +2291,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() @@ -1985,9 +2302,10 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $parametersAcceptor = null; $functionReflection = null; $throwPoints = []; + $impurePoints = []; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); - if ($nameType->isCallable()->yes()) { + if (!$nameType->isCallable()->no()) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), @@ -1997,7 +2315,9 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp } $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); + $impurePoints = $nameResult->getImpurePoints(); if ( $nameType->isObject()->yes() && $nameType->isCallable()->yes() @@ -2012,8 +2332,17 @@ static function (): void { $context->enterDeep(), ); $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); + } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); + + $scope = $this->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); } - $scope = $nameResult->getScope(); } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( @@ -2022,15 +2351,28 @@ static function (): void { $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants(), ); + $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'functionCall', + 'call to unknown function', + false, + ); } if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; } - $result = $this->processArgs($stmt, $functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); if ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); @@ -2094,7 +2436,7 @@ static function (): void { $functionReflection !== null && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) ) { - $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType()), new ArrayType(new IntegerType(), new StringType())); + $scope = $scope->assignVariable('http_response_header', AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())), new ArrayType(new IntegerType(), new StringType())); } if ( @@ -2128,6 +2470,32 @@ static function (): void { ); } + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); + } + if ( $functionReflection !== null && $functionReflection->getName() === 'extract' @@ -2200,6 +2568,7 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if (isset($closureCallScope)) { $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); @@ -2229,15 +2598,41 @@ static function (): void { } } + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; } - $result = $this->processArgs($stmt, $methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + $result = $this->processArgs( + $stmt, + $methodReflection, + $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, + $parametersAcceptor, + $expr, + $scope, + $nodeCallback, + $context, + ); $scope = $result->getScope(); if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { + $nodeCallback(new InvalidateExprNode($expr->var), $scope); $scope = $scope->invalidateExpression($expr->var, true); } if ($parametersAcceptor !== null) { @@ -2277,6 +2672,7 @@ static function (): void { } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof Expr\NullsafeMethodCall) { $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); @@ -2286,12 +2682,14 @@ static function (): void { $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), + $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->class instanceof Expr) { $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); if (count($objectClasses) !== 1) { @@ -2307,6 +2705,7 @@ static function (): void { $classResult = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -2319,79 +2718,91 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr->class instanceof Name) { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { - $classReflection = $this->reflectionProvider->getClass($className); - $methodName = $expr->name->name; - if ($classReflection->hasMethod($methodName)) { - $methodReflection = $classReflection->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), - ); + $classType = $scope->resolveTypeByName($expr->class); + $methodName = $expr->name->name; + if ($classType->hasMethod($methodName)->yes()) { + $methodReflection = $classType->getMethod($methodName, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); - if ($methodThrowPoint !== null) { - $throwPoints[] = $methodThrowPoint; - } - if ( - $classReflection->getName() === 'Closure' - && strtolower($methodName) === 'bind' - ) { - $thisType = null; - $nativeThisType = null; - if (isset($expr->getArgs()[1])) { - $argType = $scope->getType($expr->getArgs()[1]->value); - if ($argType->isNull()->yes()) { - $thisType = null; - } else { - $thisType = $argType; - } + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; + } - $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value); - if ($nativeArgType->isNull()->yes()) { - $nativeThisType = null; - } else { - $nativeThisType = $nativeArgType; - } + $declaringClass = $methodReflection->getDeclaringClass(); + if ( + $declaringClass->getName() === 'Closure' + && strtolower($methodName) === 'bind' + ) { + $thisType = null; + $nativeThisType = null; + if (isset($expr->getArgs()[1])) { + $argType = $scope->getType($expr->getArgs()[1]->value); + if ($argType->isNull()->yes()) { + $thisType = null; + } else { + $thisType = $argType; } - $scopeClasses = ['static']; - if (isset($expr->getArgs()[2])) { - $argValue = $expr->getArgs()[2]->value; - $argValueType = $scope->getType($argValue); - - $scopeClasses = []; - $directClassNames = $argValueType->getObjectClassNames(); - if (count($directClassNames) > 0) { - $scopeClasses = $directClassNames; - $thisTypes = []; - foreach ($directClassNames as $directClassName) { - $thisTypes[] = new ObjectType($directClassName); - } - $thisType = TypeCombinator::union(...$thisTypes); - } else { - $thisType = $argValueType->getClassStringObjectType(); - $scopeClasses = $thisType->getObjectClassNames(); + + $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value); + if ($nativeArgType->isNull()->yes()) { + $nativeThisType = null; + } else { + $nativeThisType = $nativeArgType; + } + } + $scopeClasses = ['static']; + if (isset($expr->getArgs()[2])) { + $argValue = $expr->getArgs()[2]->value; + $argValueType = $scope->getType($argValue); + + $directClassNames = $argValueType->getObjectClassNames(); + if (count($directClassNames) > 0) { + $scopeClasses = $directClassNames; + $thisTypes = []; + foreach ($directClassNames as $directClassName) { + $thisTypes[] = new ObjectType($directClassName); } + $thisType = TypeCombinator::union(...$thisTypes); + } else { + $thisType = $argValueType->getClassStringObjectType(); + $scopeClasses = $thisType->getObjectClassNames(); } - $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } - } else { - $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; } - $result = $this->processArgs($stmt, $methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); + $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); @@ -2413,16 +2824,40 @@ static function (): void { $scope = $scope->invalidateExpression(new Variable('this'), true); } + if ( + $methodReflection !== null + && !$methodReflection->isStatic() + && $methodReflection->getName() === '__construct' + && $scopeFunction instanceof MethodReflection + && !$scopeFunction->isStatic() + && $scope->isInClass() + && $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName()) + ) { + $thisType = $scope->getType(new Variable('this')); + $methodClassReflection = $methodReflection->getDeclaringClass(); + foreach ($methodClassReflection->getNativeReflection()->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) { + if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $methodClassReflection->getName()) { + continue; + } + + $scope = $scope->assignInitializedProperty($thisType, $property->getName()); + } + } + $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof PropertyFetch) { $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\NullsafePropertyFetch) { @@ -2434,69 +2869,99 @@ static function (): void { $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), + $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticPropertyFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->class instanceof Expr) { $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { - return $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + + return new ExpressionResult( + $processClosureResult->getScope(), + false, + [], + [], + ); } elseif ($expr instanceof Expr\ArrowFunction) { - return $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, $context, null); + $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); + return new ExpressionResult( + $result->getScope(), + $result->hasYield(), + [], + [], + ); } elseif ($expr instanceof ErrorSuppress) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; $throwPoints = []; + $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT); + $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit'; + $impurePoints = [ + new ImpurePoint($scope, $expr, $identifier, $identifier, true), + ]; if ($expr->expr !== null) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Node\Scalar\Encapsed) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($expr->parts as $part) { $result = $this->processExprNode($stmt, $part, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof ArrayDimFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->dim !== null) { $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr instanceof Array_) { $itemNodes = []; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); if ($arrayItem === null) { @@ -2507,12 +2972,14 @@ static function (): void { $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); $scope = $keyResult->getScope(); } $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); $scope = $valueResult->getScope(); } $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); @@ -2532,6 +2999,7 @@ static function (): void { $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); @@ -2551,6 +3019,7 @@ static function (): void { $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr), ); @@ -2572,11 +3041,13 @@ static function (): void { $hasYield = $condResult->hasYield() || $rightResult->hasYield(); $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); + $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); } elseif ($expr instanceof BinaryOp) { $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && @@ -2587,22 +3058,60 @@ static function (): void { $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof Expr\Include_) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + true, + ); $hasYield = $result->hasYield(); $scope = $result->getScope()->afterExtractCall(); + } elseif ($expr instanceof Expr\Print_) { + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'print', 'print', true); + $hasYield = $result->hasYield(); + + $scope = $result->getScope(); + } elseif ($expr instanceof Cast\String_) { + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $hasYield = $result->hasYield(); + + $exprType = $scope->getType($expr->expr); + $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); + if ($toStringMethod !== null) { + if (!$toStringMethod->hasSideEffects()->no()) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()), + $toStringMethod->isPure()->no(), + ); + } + } + + $scope = $result->getScope(); } elseif ( $expr instanceof Expr\BitwiseNot || $expr instanceof Cast || $expr instanceof Expr\Clone_ - || $expr instanceof Expr\Print_ || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus ) { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $hasYield = $result->hasYield(); $scope = $result->getScope(); @@ -2610,6 +3119,8 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true); $hasYield = $result->hasYield(); $scope = $result->getScope(); @@ -2617,6 +3128,14 @@ static function (): void { $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'yieldFrom', + 'yield from', + true, + ); $hasYield = true; $scope = $result->getScope(); @@ -2625,14 +3144,17 @@ static function (): void { $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } elseif ($expr instanceof Expr\ClassConstFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; 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(); } } elseif ($expr instanceof Expr\Empty_) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr); @@ -2641,11 +3163,13 @@ static function (): void { $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); } elseif ($expr instanceof Expr\Isset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $nonNullabilityResults = []; foreach ($expr->vars as $var) { $nonNullabilityResult = $this->ensureNonNullability($scope, $var); @@ -2654,6 +3178,7 @@ static function (): void { $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $nonNullabilityResults[] = $nonNullabilityResult; } foreach (array_reverse($expr->vars) as $var) { @@ -2667,43 +3192,50 @@ static function (): void { $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); 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()); } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, false, []); + return new ExpressionResult($scope, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; $hasYield = false; $throwPoints = []; - if ($expr->class instanceof Expr) { - $objectClasses = $scope->getType($expr)->getObjectClassNames(); - if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void { - }, $context->enterDeep()); - $additionalThrowPoints = $objectExprResult->getThrowPoints(); + $impurePoints = []; + $className = null; + if ($expr->class instanceof Expr || $expr->class instanceof Name) { + if ($expr->class instanceof Expr) { + $objectClasses = $scope->getType($expr)->getObjectClassNames(); + if (count($objectClasses) === 1) { + $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void { + }, $context->enterDeep()); + $className = $objectClasses[0]; + $additionalThrowPoints = $objectExprResult->getThrowPoints(); + } else { + $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + } + + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + foreach ($additionalThrowPoints as $throwPoint) { + $throwPoints[] = $throwPoint; + } } else { - $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + $className = $scope->resolveName($expr->class); } - $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - foreach ($additionalThrowPoints as $throwPoint) { - $throwPoints[] = $throwPoint; - } - } elseif ($expr->class instanceof Class_) { - $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name - $this->processStmtNode($expr->class, $scope, $nodeCallback, StatementContext::createTopLevel()); - } else { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { + $classReflection = null; + if ($className !== null && $this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); @@ -2713,7 +3245,7 @@ static function (): void { $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), ); - $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, $expr->class, $expr->getArgs(), $scope); + $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope); if ($constructorThrowPoint !== null) { $throwPoints[] = $constructorThrowPoint; } @@ -2721,25 +3253,86 @@ static function (): void { } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } - } - if ($parametersAcceptor !== null) { - $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - } - $result = $this->processArgs($stmt, $constructorReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); - $scope = $result->getScope(); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - } elseif ( - $expr instanceof Expr\PreInc - || $expr instanceof Expr\PostInc - || $expr instanceof Expr\PreDec - || $expr instanceof Expr\PostDec + if ($constructorReflection !== null) { + if (!$constructorReflection->hasSideEffects()->no()) { + $certain = $constructorReflection->isPure()->no(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + sprintf('instantiation of class %s', $constructorReflection->getDeclaringClass()->getDisplayName()), + $certain, + ); + } + } elseif ($classReflection === null) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + 'instantiation of unknown class', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + } + + } else { + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name + $constructorResult = null; + $this->processStmtNode($expr->class, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { + $nodeCallback($node, $scope); + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + if ($constructorResult !== null) { + return; + } + $currentClassReflection = $node->getClassReflection(); + if ($currentClassReflection->getName() !== $classReflection->getName()) { + return; + } + if (!$currentClassReflection->hasConstructor()) { + return; + } + if ($currentClassReflection->getConstructor()->getName() !== $node->getMethodReflection()->getName()) { + return; + } + $constructorResult = $node; + }, StatementContext::createTopLevel()); + if ($constructorResult !== null) { + $throwPoints = array_merge($throwPoints, $constructorResult->getStatementResult()->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $constructorResult->getImpurePoints()); + } + if ($classReflection->hasConstructor()) { + $constructorReflection = $classReflection->getConstructor(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); + } + } + + $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); + $scope = $result->getScope(); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } elseif ( + $expr instanceof Expr\PreInc + || $expr instanceof Expr\PostInc + || $expr instanceof Expr\PreDec + || $expr instanceof Expr\PostDec ) { $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); - $throwPoints = []; + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $newExpr = $expr; if ($expr instanceof Expr\PostInc) { @@ -2754,31 +3347,34 @@ static function (): void { $expr->var, $newExpr, static function (Node $node, Scope $scope) use ($nodeCallback): void { - if (!$node instanceof PropertyAssignNode) { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; } $nodeCallback($node, $scope); }, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), false, )->getScope(); } elseif ($expr instanceof Ternary) { $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $ternaryCondResult->getThrowPoints(); + $impurePoints = $ternaryCondResult->getImpurePoints(); $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); $ifTrueType = null; if ($expr->if !== null) { $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); $ifTrueType = $ifTrueScope->getType($expr->if); } $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); $ifFalseScope = $elseResult->getScope(); $condType = $scope->getType($expr->cond); @@ -2804,6 +3400,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $finalScope, $ternaryCondResult->hasYield(), $throwPoints, + $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); @@ -2812,36 +3409,182 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints = [ ThrowPoint::createImplicit($scope, $expr), ]; + $impurePoints = [ + new ImpurePoint( + $scope, + $expr, + 'yield', + 'yield', + true, + ), + ]; if ($expr->key !== null) { $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); } if ($expr->value !== null) { $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); $scope = $valueResult->getScope(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); } $hasYield = true; } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); + $condType = $scope->getType($expr->cond); $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $matchScope = $scope->enterMatch($expr); $armNodes = []; $hasDefaultCond = false; $hasAlwaysTrueCond = false; - foreach ($expr->arms as $arm) { + $arms = $expr->arms; + if ($condType->isEnum()->yes()) { + // enum match analysis would work even without this if branch + // but would be much slower + // this avoids using ObjectType::$subtractedType which is slow for huge enums + // because of repeated union type normalization + $enumCases = $condType->getEnumCases(); + if (count($enumCases) > 0) { + $indexedEnumCases = []; + foreach ($enumCases as $enumCase) { + $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase; + } + $unusedIndexedEnumCases = $indexedEnumCases; + foreach ($arms as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + $condNodes = []; + $conditionCases = []; + foreach ($arm->conds as $cond) { + if (!$cond instanceof Expr\ClassConstFetch) { + continue 2; + } + if (!$cond->class instanceof Name) { + continue 2; + } + if (!$cond->name instanceof Node\Identifier) { + continue 2; + } + $fetchedClassName = $scope->resolveName($cond->class); + $loweredFetchedClassName = strtolower($fetchedClassName); + if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) { + continue 2; + } + + if (!array_key_exists($loweredFetchedClassName, $unusedIndexedEnumCases)) { + throw new ShouldNotHappenException(); + } + + $caseName = $cond->name->toString(); + if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) { + continue 2; + } + + $enumCase = $indexedEnumCases[$loweredFetchedClassName][$caseName]; + $conditionCases[] = $enumCase; + $armConditionScope = $matchScope; + if (!array_key_exists($caseName, $unusedIndexedEnumCases[$loweredFetchedClassName])) { + // force "always false" + $armConditionScope = $armConditionScope->removeTypeFromExpression( + $expr->cond, + $enumCase, + ); + } else { + $unusedCasesCount = 0; + foreach ($unusedIndexedEnumCases as $cases) { + $unusedCasesCount += count($cases); + } + if ($unusedCasesCount === 1) { + $hasAlwaysTrueCond = true; + + // force "always true" + $armConditionScope = $armConditionScope->addTypeToExpression( + $expr->cond, + $enumCase, + ); + } + } + + $this->processExprNode($stmt, $cond, $armConditionScope, $nodeCallback, $deepContext); + + $condNodes[] = new MatchExpressionArmCondition( + $cond, + $armConditionScope, + $cond->getStartLine(), + ); + + unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + } + + $conditionCasesCount = count($conditionCases); + if ($conditionCasesCount === 0) { + throw new ShouldNotHappenException(); + } elseif ($conditionCasesCount === 1) { + $conditionCaseType = $conditionCases[0]; + } else { + $conditionCaseType = new UnionType($conditionCases); + } + + $matchArmBodyScope = $matchScope->addTypeToExpression( + $expr->cond, + $conditionCaseType, + ); + $matchArmBody = new MatchExpressionArmBody($matchArmBodyScope, $arm->body); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); + + $armResult = $this->processExprNode( + $stmt, + $arm->body, + $matchArmBodyScope, + $nodeCallback, + ExpressionContext::createTopLevel(), + ); + $armScope = $armResult->getScope(); + $scope = $scope->mergeWith($armScope); + $hasYield = $hasYield || $armResult->hasYield(); + $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + + unset($arms[$i]); + } + + $remainingCases = []; + foreach ($unusedIndexedEnumCases as $cases) { + foreach ($cases as $case) { + $remainingCases[] = $case; + } + } + + $remainingCasesCount = count($remainingCases); + if ($remainingCasesCount === 0) { + $remainingType = new NeverType(); + } elseif ($remainingCasesCount === 1) { + $remainingType = $remainingCases[0]; + } else { + $remainingType = new UnionType($remainingCases); + } + + $matchScope = $matchScope->addTypeToExpression($expr->cond, $remainingType); + } + } + foreach ($arms as $i => $arm) { if ($arm->conds === null) { $hasDefaultCond = true; $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); - $armNodes[] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $scope = $scope->mergeWith($matchScope); continue; } @@ -2858,6 +3601,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); $armCondResultScope = $armCondResult->getScope(); $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); @@ -2888,7 +3632,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void { }, $deepContext)->getTruthyScope(); $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body); - $armNodes[] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); + $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); $armResult = $this->processExprNode( $stmt, @@ -2901,6 +3645,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope = $scope->mergeWith($armScope); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $matchScope = $matchScope->filterByFalseyValue($filteringExpr); } @@ -2909,73 +3654,94 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(UnhandledMatchError::class), $expr, false); } - $nodeCallback(new MatchExpressionNode($expr->cond, $armNodes, $expr, $matchScope), $scope); + ksort($armNodes, SORT_NUMERIC); + + $nodeCallback(new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope); } elseif ($expr instanceof AlwaysRememberedExpr) { $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $nodeCallback, $context); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\Throw_) { $hasYield = false; $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false); } elseif ($expr instanceof FunctionCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getName() instanceof Expr) { $result = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } } elseif ($expr instanceof MethodCallableNode) { $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($expr->getName() instanceof Expr) { $nameResult = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); } } elseif ($expr instanceof StaticMethodCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getClass() instanceof Expr) { $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); } if ($expr->getName() instanceof Expr) { $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); } } elseif ($expr instanceof InstantiationCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getClass() instanceof Expr) { $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); } } elseif ($expr instanceof Node\Scalar) { $hasYield = false; $throwPoints = []; + $impurePoints = []; + } elseif ($expr instanceof ConstFetch) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $nodeCallback($expr->name, $scope); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new ExpressionResult( $scope, $hasYield, $throwPoints, + $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); @@ -3051,9 +3817,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) { $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType()); + $isList = $constantArray->isList()->yes(); $constantArray = $constantArray->isIterableAtLeastOnce()->yes() ? TypeCombinator::intersect($array, new NonEmptyArrayType()) : $array; + $constantArray = $isList + ? AccessoryArrayListType::intersectWith($constantArray) + : $constantArray; } $newArrayTypes[] = $constantArray; @@ -3079,6 +3849,68 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } + private function getArraySortPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if (!$type instanceof ArrayType) { + return $type; + } + + $newArrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $type->getIterableValueType())); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + + private function getArraySortDoNotPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $types = []; + foreach ($constantArrays as $constantArray) { + $types[] = new ConstantArrayType( + $constantArray->getKeyTypes(), + $constantArray->getValueTypes(), + $constantArray->getNextAutoIndexes(), + $constantArray->getOptionalKeys(), + $constantArray->isList()->and(TrinaryLogic::createMaybe()), + ); + } + + return TypeCombinator::union(...$types); + } + + $newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType()); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, @@ -3314,7 +4146,7 @@ private function processClosureNode( callable $nodeCallback, ExpressionContext $context, ?Type $passedToType, - ): ExpressionResult + ): ProcessClosureResult { foreach ($expr->params as $param) { $this->processParamNode($stmt, $param, $scope, $nodeCallback); @@ -3322,43 +4154,13 @@ private function processClosureNode( $byRefUses = []; - $callableParameters = null; $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); - - if ($closureCallArgs !== null) { - $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - - foreach ($callableParameters as $index => $callableParameter) { - if (!isset($closureCallArgs[$index])) { - continue; - } - - $type = $scope->getType($closureCallArgs[$index]->value); - $callableParameters[$index] = new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $type, - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ); - } - } - } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { - if ($passedToType instanceof UnionType) { - $passedToType = TypeCombinator::union(...array_filter( - $passedToType->getTypes(), - static fn (Type $type) => $type->isCallable()->yes(), - )); - } - - $acceptors = $passedToType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - } - } + $callableParameters = $this->createCallableParameters( + $scope, + $expr, + $closureCallArgs, + $passedToType, + ); $useScope = $scope; foreach ($expr->uses as $use) { @@ -3421,15 +4223,31 @@ private function processClosureNode( $executionEnds = []; $gatheredReturnStatements = []; $gatheredYieldStatements = []; - $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { $gatheredYieldStatements[] = $node; } @@ -3447,9 +4265,10 @@ private function processClosureNode( $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope, false, []); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); } $count = 0; @@ -3480,9 +4299,43 @@ private function processClosureNode( $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []); + return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + } + + /** + * @param InvalidateExprNode[] $invalidatedExpressions + * @param string[] $uses + */ + private function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope + { + if ($scope->isInClass()) { + $uses[] = 'this'; + } + + $finder = new NodeFinder(); + foreach ($invalidatedExpressions as $invalidateExpression) { + $found = false; + foreach ($uses as $use) { + $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && $node->name === $use); + if ($result === null) { + continue; + } + + $found = true; + break; + } + + if (!$found) { + continue; + } + + $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), true); + } + + return $scope; } /** @@ -3493,7 +4346,6 @@ private function processArrowFunctionNode( Expr\ArrowFunction $expr, MutatingScope $scope, callable $nodeCallback, - ExpressionContext $context, ?Type $passedToType, ): ExpressionResult { @@ -3504,20 +4356,47 @@ private function processArrowFunctionNode( $nodeCallback($expr->returnType, $scope); } - $callableParameters = null; $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); + $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters( + $scope, + $expr, + $arrowFunctionCallArgs, + $passedToType, + )); + $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); + if (!$arrowFunctionType instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); + + return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + } + + /** + * @param Node\Arg[] $args + * @return ParameterReflection[]|null + */ + public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + { + $callableParameters = null; + if ($args !== null) { + $closureType = $scope->getType($closureExpr); + + if ($closureType->isCallable()->no()) { + return null; + } - if ($arrowFunctionCallArgs !== null) { - $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); + $acceptors = $closureType->getCallableParametersAcceptors($scope); if (count($acceptors) === 1) { $callableParameters = $acceptors[0]->getParameters(); foreach ($callableParameters as $index => $callableParameter) { - if (!isset($arrowFunctionCallArgs[$index])) { + if (!isset($args[$index])) { continue; } - $type = $scope->getType($arrowFunctionCallArgs[$index]->value); + $type = $scope->getType($args[$index]->value); $callableParameters[$index] = new NativeParameterReflection( $callableParameter->getName(), $callableParameter->isOptional(), @@ -3534,23 +4413,50 @@ private function processArrowFunctionNode( $passedToType->getTypes(), static fn (Type $type) => $type->isCallable()->yes(), )); + + if ($passedToType->isCallable()->no()) { + return null; + } } $acceptors = $passedToType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - } - } + if (count($acceptors) > 0) { + foreach ($acceptors as $acceptor) { + if ($callableParameters === null) { + $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + continue; + } - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters); - $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); - if (!$arrowFunctionType instanceof ClosureType) { - throw new ShouldNotHappenException(); + $newParameters = []; + foreach ($acceptor->getParameters() as $i => $callableParameter) { + if (!array_key_exists($i, $callableParameters)) { + $newParameters[] = $callableParameter; + continue; + } + + $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + )); + } + + $callableParameters = $newParameters; + } + } } - $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); - $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, []); + return $callableParameters; } /** @@ -3600,63 +4506,71 @@ private function processAttributeGroups( /** * @param MethodReflection|FunctionReflection|null $calleeReflection - * @param Node\Arg[] $args * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArgs( Node\Stmt $stmt, $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, ?ParametersAcceptor $parametersAcceptor, - array $args, + CallLike $callLike, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, ?MutatingScope $closureBindScope = null, ): ExpressionResult { - $paramOutTypes = []; + $args = $callLike->getArgs(); + if ($parametersAcceptor !== null) { $parameters = $parametersAcceptor->getParameters(); - - foreach ($parameters as $parameter) { - if (!$parameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - - if ($parameter->getOutType() === null) { - continue; - } - - $paramOutTypes[$parameter->getName()] = TemplateTypeHelper::resolveTemplateTypes( - $parameter->getOutType(), - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), - TemplateTypeVariance::createCovariant(), - ); - } } $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($args as $i => $arg) { $assignByReference = false; $parameter = null; + $parameterType = null; + $parameterNativeType = null; if (isset($parameters) && $parametersAcceptor !== null) { if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); + + if ($parameters[$i] instanceof ParameterReflectionWithPhpDocs) { + $parameterNativeType = $parameters[$i]->getNativeType(); + } $parameter = $parameters[$i]; } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { $lastParameter = $parameters[count($parameters) - 1]; $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); + + if ($lastParameter instanceof ParameterReflectionWithPhpDocs) { + $parameterNativeType = $lastParameter->getNativeType(); + } $parameter = $lastParameter; } } + $lookForUnset = false; if ($assignByReference) { if ($arg->value instanceof Variable) { - $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $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; + } } } @@ -3673,28 +4587,126 @@ private function processArgs( $scopeToPass = $closureBindScope; } + if ($parameter instanceof ParameterReflectionWithPhpDocs) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + if ($parameterCallImmediately->maybe()) { + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + } else { + $callCallbackImmediately = $parameterCallImmediately->yes(); + } + } else { + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + } if ($arg->value instanceof Expr\Closure) { + $restoreThisScope = null; + if ( + $closureBindScope === null + && $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && !$arg->value->static + ) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType()); + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); + } + + $uses = []; + foreach ($arg->value->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $uses[] = $use->var->name; + } + + $scope = $closureResult->getScope(); + $invalidateExpressions = $closureResult->getInvalidateExpressions(); + if ($restoreThisScope !== null) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($invalidateExpressions as $j => $invalidateExprNode) { + $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); + if ($foundThis === null) { + continue; + } + + unset($invalidateExpressions[$j]); + } + $invalidateExpressions = array_values($invalidateExpressions); + $scope = $scope->restoreThis($restoreThisScope); + } + + $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); } elseif ($arg->value instanceof Expr\ArrowFunction) { + if ( + $closureBindScope === null + && $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && !$arg->value->static + ) { + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType()); + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); + } } else { - $result = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); - } - $scope = $result->getScope(); - if ($assignByReference) { - if ($arg->value instanceof Variable) { - $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + $exprType = $scope->getType($arg->value); + $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + $scope = $exprResult->getScope(); + $hasYield = $hasYield || $exprResult->hasYield(); + + if ($exprType->isCallable()->yes()) { + $acceptors = $exprType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $scope = $this->processImmediatelyCalledCallable($scope, $acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()); + if ($callCallbackImmediately) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); + } + } } } + if ($assignByReference && $lookForUnset) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + } + if ($calleeReflection !== null) { $scope = $scope->popInFunctionCall(); } - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); if ($i !== 0 || $closureBindScope === null) { continue; } @@ -3708,35 +4720,167 @@ private function processArgs( $byRefType = new MixedType(); $assignByReference = false; + $currentParameter = null; if (isset($parameters[$i])) { - $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); - if (isset($paramOutTypes[$parameters[$i]->getName()])) { - $byRefType = $paramOutTypes[$parameters[$i]->getName()]; - } + $currentParameter = $parameters[$i]; } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { - $lastParameter = $parameters[count($parameters) - 1]; - $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); - if (isset($paramOutTypes[$lastParameter->getName()])) { - $byRefType = $paramOutTypes[$lastParameter->getName()]; + $currentParameter = $parameters[count($parameters) - 1]; + } + + if ($currentParameter !== null) { + $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); + if ($assignByReference) { + if ($currentParameter instanceof ParameterReflectionWithPhpDocs && $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(); + } } } if ($assignByReference) { + if ($currentParameter === null) { + throw new ShouldNotHappenException(); + } + $argValue = $arg->value; if ($argValue instanceof Variable && is_string($argValue->name)) { - $scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType()); + 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()); + } } else { $scope = $scope->invalidateExpression($argValue); } } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { $argType = $scope->getType($arg->value); - if (!$argType->isObject()->no() || !(new ResourceType())->isSuperTypeOf($argType)->no()) { + if (!$argType->isObject()->no()) { + $nakedReturnType = null; + if ($nakedMethodReflection !== null) { + $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $args, + $nakedMethodReflection->getVariants(), + $nakedMethodReflection->getNamedArgumentsVariants(), + ); + $nakedReturnType = $nakedParametersAcceptor->getReturnType(); + } + if ( + $nakedReturnType === null + || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() + || $nakedMethodReflection->isPure()->no() + ) { + $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $scope = $scope->invalidateExpression($arg->value, true); + } + } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { + $nodeCallback(new InvalidateExprNode($arg->value), $scope); $scope = $scope->invalidateExpression($arg->value, true); } } } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type + { + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) { + if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { + return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($calleeReflection instanceof MethodReflection) { + if ($callLike instanceof StaticCall) { + foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($callLike instanceof MethodCall) { + foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) { + if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { + return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } + } + + return null; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type + { + $paramOutTypes = []; + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->parameterOutTypeExtensionProvider->getFunctionParameterOutTypeExtensions() as $functionParameterOutTypeExtension) { + if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterOutTypeExtensionProvider->getMethodParameterOutTypeExtensions() as $methodParameterOutTypeExtension) { + if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterOutTypeExtensionProvider->getStaticMethodParameterOutTypeExtensions() as $staticMethodParameterOutTypeExtension) { + if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } + + if (count($paramOutTypes) === 1) { + return $paramOutTypes[0]; + } + + if (count($paramOutTypes) > 1) { + return TypeCombinator::union(...$paramOutTypes); + } + + return null; } /** @@ -3757,11 +4901,16 @@ private function processAssignVar( $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); $hasYield = false; $throwPoints = []; + $impurePoints = []; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable && is_string($var->name)) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $var, 'superglobal', 'assign to superglobal variable', true); + } $assignedExpr = $this->unwrapAssign($assignedExpr); $type = $scope->getType($assignedExpr); @@ -3802,6 +4951,7 @@ 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)); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions($exprString, $holders); @@ -3831,6 +4981,7 @@ private function processAssignVar( $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); @@ -3880,6 +5031,7 @@ private function processAssignVar( $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); $varType = $scope->getType($var); @@ -3958,6 +5110,7 @@ private function processAssignVar( 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); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { @@ -3984,7 +5137,9 @@ private function processAssignVar( } } } else { - if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + if ($var instanceof Variable) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); @@ -4006,6 +5161,7 @@ static function (): void { $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context); $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); + $impurePoints = $objectResult->getImpurePoints(); $scope = $objectResult->getScope(); $propertyName = null; @@ -4015,12 +5171,14 @@ static function (): void { $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); $hasYield = $hasYield || $propertyNameResult->hasYield(); $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints()); $scope = $propertyNameResult->getScope(); } $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); $propertyHolderType = $scope->getType($var->var); @@ -4072,18 +5230,18 @@ static function (): void { $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; - $hasYield = false; - $throwPoints = []; } else { $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); $hasYield = $propertyNameResult->hasYield(); $throwPoints = $propertyNameResult->getThrowPoints(); + $impurePoints = $propertyNameResult->getImpurePoints(); $scope = $propertyNameResult->getScope(); } $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); if ($propertyName !== null) { @@ -4103,6 +5261,7 @@ static function (): void { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { @@ -4119,12 +5278,14 @@ static function (): void { $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); $itemScope = $keyResult->getScope(); } $valueResult = $this->processExprNode($stmt, $arrayItem->value, $itemScope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); if ($arrayItem->key === null) { $dimExpr = new Node\Scalar\LNumber($i); @@ -4138,16 +5299,84 @@ 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, [], []), $enterExpressionAssign, ); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } + } elseif ($var instanceof ExistingArrayDimFetch) { + $dimFetchStack = []; + $assignedPropertyExpr = $assignedExpr; + while ($var instanceof ExistingArrayDimFetch) { + $varForSetOffsetValue = $var->getVar(); + if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { + $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + } + $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( + $varForSetOffsetValue, + $var->getDim(), + $assignedPropertyExpr, + ); + $dimFetchStack[] = $var; + $var = $var->getVar(); + } + + $offsetTypes = []; + $offsetNativeTypes = []; + foreach (array_reverse($dimFetchStack) as $dimFetch) { + $dimExpr = $dimFetch->getDim(); + $offsetTypes[] = $scope->getType($dimExpr); + $offsetNativeTypes[] = $scope->getNativeType($dimExpr); + } + + $valueToWrite = $scope->getType($assignedExpr); + $nativeValueToWrite = $scope->getNativeType($assignedExpr); + $varType = $scope->getType($var); + $varNativeType = $scope->getNativeType($var); + + $offsetValueType = $varType; + $offsetNativeValueType = $varNativeType; + $offsetValueTypeStack = [$offsetValueType]; + $offsetValueNativeTypeStack = [$offsetNativeValueType]; + foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + $offsetValueTypeStack[] = $offsetValueType; + } + foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) { + $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType); + $offsetValueNativeTypeStack[] = $offsetNativeValueType; + } + + foreach (array_reverse($offsetTypes) as $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite); + } + foreach (array_reverse($offsetNativeTypes) as $offsetNativeType) { + /** @var Type $offsetNativeValueType */ + $offsetNativeValueType = array_pop($offsetValueNativeTypeStack); + $nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite); + } + + if ($var instanceof Variable && is_string($var->name)) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite); + } else { + if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } + $scope = $scope->assignExpression( + $var, + $valueToWrite, + $nativeValueToWrite, + ); } } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); } private function unwrapAssign(Expr $expr): Expr @@ -4388,7 +5617,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, [], []), true, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); @@ -4406,7 +5635,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, [], []), true, )->getScope(); $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); @@ -4706,19 +5935,21 @@ private function processNodesForCalledMethod($node, string $fileName, MethodRefl } /** - * @return array{TemplateTypeMap, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; + $phpDocImmediatelyInvokedCallableParameters = []; + $phpDocClosureThisTypeParameters = []; $phpDocReturnType = null; $phpDocThrowType = null; $deprecatedDescription = null; $isDeprecated = false; $isInternal = false; $isFinal = false; - $isPure = false; + $isPure = null; $isAllowedPrivateMutation = false; $acceptsNamedArguments = true; $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable(); @@ -4809,6 +6040,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $varTags = []; if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { continue; @@ -4819,6 +6051,17 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } $phpDocParameterTypes[$paramName] = $paramType; } + foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) { + if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) { + continue; + } + $paramClosureThisType = $paramClosureThisTag->getType(); + if ($scope->isInClass()) { + $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType); + } + $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType; + } + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); } @@ -4837,13 +6080,16 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $isPure = $resolvedPhpDoc->isPure(); $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); + if ($acceptsNamedArguments && $scope->isInClass()) { + $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments(); + } $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; $varTags = $resolvedPhpDoc->getVarTags(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type @@ -4893,7 +6139,7 @@ private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Nod if ($node instanceof Node\Stmt\Nop) { continue; } - if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) { + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { continue; } return $node; diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php new file mode 100644 index 0000000000..7423ac220d --- /dev/null +++ b/src/Analyser/ProcessClosureResult.php @@ -0,0 +1,53 @@ +scope; + } + + /** + * @return ThrowPoint[] + */ + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + /** + * @return InvalidateExprNode[] + */ + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + +} diff --git a/src/Analyser/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index 409f123007..8b592f68fe 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -3,9 +3,13 @@ namespace PHPStan\Analyser\ResultCache; use PHPStan\Analyser\Error; +use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ class ResultCache { @@ -14,9 +18,12 @@ class ResultCache * @param mixed[] $meta * @param array> $errors * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores * @param array> $collectedData * @param array> $dependencies * @param array> $exportedNodes + * @param array $projectExtensionFiles */ public function __construct( private array $filesToAnalyse, @@ -25,9 +32,12 @@ public function __construct( private array $meta, private array $errors, private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, private array $collectedData, private array $dependencies, private array $exportedNodes, + private array $projectExtensionFiles, ) { } @@ -74,6 +84,22 @@ public function getLocallyIgnoredErrors(): array return $this->locallyIgnoredErrors; } + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + /** * @return array> */ @@ -98,4 +124,12 @@ public function getExportedNodes(): array return $this->exportedNodes; } + /** + * @return array + */ + public function getProjectExtensionFiles(): array + { + return $this->projectExtensionFiles; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 08af6df0d0..416ce41108 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,16 +2,18 @@ namespace PHPStan\Analyser\ResultCache; -use Nette\DI\Definitions\Statement; use Nette\Neon\Neon; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; +use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Collectors\CollectedData; use PHPStan\Command\Output; use PHPStan\Dependency\ExportedNodeFetcher; use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\ProjectConfigHelper; use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileFinder; +use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; use PHPStan\Internal\ComposerHelper; use PHPStan\PhpDoc\StubFilesProvider; @@ -23,7 +25,6 @@ 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; @@ -31,20 +32,25 @@ use function implode; use function is_array; use function is_file; -use function is_string; use function ksort; +use function microtime; +use function round; use function sha1_file; use function sort; use function sprintf; +use function str_starts_with; use function time; use function unlink; use function var_export; use const PHP_VERSION_ID; +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ class ResultCacheManager { - private const CACHE_VERSION = 'v11-locallyIgnoredErrors'; + private const CACHE_VERSION = 'v12-linesToIgnore'; /** @var array */ private array $fileHashes = []; @@ -64,6 +70,7 @@ public function __construct( private FileFinder $scanFileFinder, private ReflectionProvider $reflectionProvider, private StubFilesProvider $stubFilesProvider, + private FileHelper $fileHelper, private string $cacheFilePath, private array $analysedPaths, private array $composerAutoloaderProjectPaths, @@ -83,17 +90,18 @@ public function __construct( */ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output): ResultCache { + $startTime = microtime(true); if ($debug) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache not used because of debug mode.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } if ($onlyFiles) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache not used because only files were passed as analysed paths.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } $cacheFilePath = $this->cacheFilePath; @@ -101,7 +109,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? if ($output->isDebug()) { $output->writeLineFormatted('Result cache not used because the cache file does not exist.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } try { @@ -113,7 +121,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? @unlink($cacheFilePath); - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } if (!is_array($data)) { @@ -122,7 +130,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $output->writeLineFormatted('Result cache not used because the cache file is corrupted.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } $meta = $this->getMeta($allAnalysedFiles, $projectConfigArray); @@ -131,7 +139,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $diffs = $this->getMetaKeyDifferences($data['meta'], $meta); $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * 7) { @@ -139,15 +147,22 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $output->writeLineFormatted('Result cache not used because it\'s more than 7 days since last full analysis.'); } // run full analysis if the result cache is older than 7 days - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } - foreach ($data['projectExtensionFiles'] as $extensionFile => $fileHash) { + /** + * @var string $fileHash + * @var bool $isAnalysed + */ + foreach ($data['projectExtensionFiles'] as $extensionFile => [$fileHash, $isAnalysed]) { + if (!$isAnalysed) { + continue; + } if (!is_file($extensionFile)) { if ($output->isDebug()) { $output->writeLineFormatted(sprintf('Result cache not used because extension file %s was not found.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } if ($this->getFileHash($extensionFile) === $fileHash) { @@ -158,7 +173,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $output->writeLineFormatted(sprintf('Result cache not used because extension file %s hash does not match.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } $invertedDependencies = $data['dependencies']; @@ -167,10 +182,14 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $invertedDependenciesToReturn = []; $errors = $data['errorsCallback'](); $locallyIgnoredErrors = $data['locallyIgnoredErrorsCallback'](); + $linesToIgnore = $data['linesToIgnore']; + $unmatchedLineIgnores = $data['unmatchedLineIgnores']; $collectedData = $data['collectedDataCallback'](); $exportedNodes = $data['exportedNodesCallback'](); $filteredErrors = []; $filteredLocallyIgnoredErrors = []; + $filteredLinesToIgnore = []; + $filteredUnmatchedLineIgnores = []; $filteredCollectedData = []; $filteredExportedNodes = []; $newFileAppeared = false; @@ -190,6 +209,12 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? if (array_key_exists($analysedFile, $locallyIgnoredErrors)) { $filteredLocallyIgnoredErrors[$analysedFile] = $locallyIgnoredErrors[$analysedFile]; } + if (array_key_exists($analysedFile, $linesToIgnore)) { + $filteredLinesToIgnore[$analysedFile] = $linesToIgnore[$analysedFile]; + } + if (array_key_exists($analysedFile, $unmatchedLineIgnores)) { + $filteredUnmatchedLineIgnores[$analysedFile] = $unmatchedLineIgnores[$analysedFile]; + } if (array_key_exists($analysedFile, $collectedData)) { $filteredCollectedData[$analysedFile] = $collectedData[$analysedFile]; } @@ -259,7 +284,24 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } } - return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes); + $filesToAnalyse = array_unique($filesToAnalyse); + $filesToAnalyseCount = count($filesToAnalyse); + + if ($output->isDebug()) { + $elapsed = microtime(true) - $startTime; + $elapsedString = $elapsed > 5 + ? sprintf(' in %f seconds', round($elapsed, 1)) + : ''; + + $output->writeLineFormatted(sprintf( + 'Result cache restored%s. %d %s will be reanalysed.', + $elapsedString, + $filesToAnalyseCount, + $filesToAnalyseCount === 1 ? 'file' : 'files', + )); + } + + return new ResultCache($filesToAnalyse, false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredLinesToIgnore, $filteredUnmatchedLineIgnores, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles']); } /** @@ -364,7 +406,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } $meta = $resultCache->getMeta(); - $doSave = function (array $errorsByFile, array $locallyIgnoredErrorsByFile, $collectedDataByFile, ?array $dependencies, array $exportedNodes) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { + $projectConfigArray = $meta['projectConfig']; + if ($projectConfigArray !== null) { + $meta['projectConfig'] = Neon::encode($projectConfigArray); + } + $doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { if ($onlyFiles) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache was not saved because only files were passed as analysed paths.'); @@ -399,7 +445,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $collectedDataByFile, $dependencies, $exportedNodes, $meta); + $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles, $meta); if ($output->isDebug()) { $output->writeLineFormatted('Result cache is saved.'); @@ -410,8 +456,12 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache if ($resultCache->isFullAnalysis()) { $saved = false; - if ($save) { - $saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes()); + if ($save !== false) { + $projectExtensionFiles = []; + if ($analyserResult->getDependencies() !== null) { + $projectExtensionFiles = $this->getProjectExtensionFiles($projectConfigArray, $analyserResult->getDependencies()); + } + $saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); } else { if ($output->isDebug()) { $output->writeLineFormatted('Result cache was not saved because it was not requested.'); @@ -426,10 +476,32 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $collectedDataByFile = $this->mergeCollectedData($resultCache, $freshCollectedDataByFile); $dependencies = $this->mergeDependencies($resultCache, $analyserResult->getDependencies()); $exportedNodes = $this->mergeExportedNodes($resultCache, $analyserResult->getExportedNodes()); + $linesToIgnore = $this->mergeLinesToIgnore($resultCache, $analyserResult->getLinesToIgnore()); + $unmatchedLineIgnores = $this->mergeUnmatchedLineIgnores($resultCache, $analyserResult->getUnmatchedLineIgnores()); $saved = false; - if ($save) { - $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $collectedDataByFile, $dependencies, $exportedNodes); + if ($save !== false) { + $projectExtensionFiles = []; + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + // keep the same file hashes from the old run + // so that the message "When you edit them and re-run PHPStan, the result cache will get stale." + // keeps being shown on subsequent runs + $projectExtensionFiles[$file] = [$hash, false, $className]; + } + if ($dependencies !== null) { + foreach ($this->getProjectExtensionFiles($projectConfigArray, $dependencies) as $file => [$hash, $isAnalysed, $className]) { + if (!$isAnalysed) { + continue; + } + + $projectExtensionFiles[$file] = [$hash, true, $className]; + } + } + $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -455,7 +527,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache return new ResultCacheProcessResult(new AnalyserResult( $flatErrors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), $flatLocallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, $internalErrors, $flatCollectedData, $dependencies, @@ -579,21 +655,65 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport return $newExportedNodes; } + /** + * @param array $freshLinesToIgnore + * @return array + */ + private function mergeLinesToIgnore(ResultCache $resultCache, array $freshLinesToIgnore): array + { + $newLinesToIgnore = $resultCache->getLinesToIgnore(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshLinesToIgnore)) { + unset($newLinesToIgnore[$file]); + continue; + } + + $newLinesToIgnore[$file] = $freshLinesToIgnore[$file]; + } + + return $newLinesToIgnore; + } + + /** + * @param array $freshUnmatchedLineIgnores + * @return array + */ + private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $freshUnmatchedLineIgnores): array + { + $newUnmatchedLineIgnores = $resultCache->getUnmatchedLineIgnores(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshUnmatchedLineIgnores)) { + unset($newUnmatchedLineIgnores[$file]); + continue; + } + + $newUnmatchedLineIgnores[$file] = $freshUnmatchedLineIgnores[$file]; + } + + return $newUnmatchedLineIgnores; + } + /** * @param array> $errors * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores * @param array> $collectedData * @param array> $dependencies * @param array> $exportedNodes + * @param array $projectExtensionFiles * @param mixed[] $meta */ private function save( int $lastFullAnalysisTime, array $errors, array $locallyIgnoredErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, array $collectedData, array $dependencies, array $exportedNodes, + array $projectExtensionFiles, array $meta, ): void { @@ -629,6 +749,8 @@ private function save( ksort($errors); ksort($locallyIgnoredErrors); + ksort($linesToIgnore); + ksort($unmatchedLineIgnores); ksort($collectedData); ksort($invertedDependencies); @@ -641,10 +763,6 @@ private function save( ksort($exportedNodes); $file = $this->cacheFilePath; - $projectConfigArray = $meta['projectConfig']; - if ($projectConfigArray !== null) { - $meta['projectConfig'] = Neon::encode($projectConfigArray); - } FileWriter::write( $file, @@ -653,9 +771,11 @@ private function save( return [ 'lastFullAnalysisTime' => " . var_export($lastFullAnalysisTime, true) . ", 'meta' => " . var_export($meta, true) . ", - 'projectExtensionFiles' => " . var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true) . ", + 'projectExtensionFiles' => " . var_export($projectExtensionFiles, true) . ", 'errorsCallback' => static function (): array { return " . var_export($errors, true) . "; }, 'locallyIgnoredErrorsCallback' => static function (): array { return " . var_export($locallyIgnoredErrors, true) . "; }, + 'linesToIgnore' => " . var_export($linesToIgnore, true) . ", + 'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ", 'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; }, 'dependencies' => " . var_export($invertedDependencies, true) . ", 'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; }, @@ -667,75 +787,62 @@ private function save( /** * @param mixed[]|null $projectConfig * @param array $dependencies - * @return array + * @return array */ private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array { $this->alreadyProcessed = []; $projectExtensionFiles = []; if ($projectConfig !== null) { - $services = array_merge( - $projectConfig['services'] ?? [], - $projectConfig['rules'] ?? [], - ); - foreach ($services as $service) { - $classes = $this->getClassesFromConfigDefinition($service); - if (is_array($service)) { - foreach (['class', 'factory', 'implement'] as $key) { - if (!isset($service[$key])) { - continue; - } + $vendorDirs = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloaderProjectPath) { + $composer = ComposerHelper::getComposerConfig($autoloaderProjectPath); + if ($composer === null) { + continue; + } + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($autoloaderProjectPath, $composer); + $vendorDirs[] = $this->fileHelper->normalizePath($vendorDirectory); + } - $classes = array_merge($classes, $this->getClassesFromConfigDefinition($service[$key])); - } + $classes = ProjectConfigHelper::getServiceClassNames($projectConfig); + foreach ($classes as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + continue; } - foreach (array_unique($classes) as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - continue; - } + $classReflection = $this->reflectionProvider->getClass($class); + $fileName = $classReflection->getFileName(); + if ($fileName === null) { + continue; + } - $classReflection = $this->reflectionProvider->getClass($class); - $fileName = $classReflection->getFileName(); - if ($fileName === null) { - continue; - } + if (str_starts_with($fileName, 'phar://')) { + continue; + } - $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); - foreach ($allServiceFiles as $serviceFile) { - if (array_key_exists($serviceFile, $projectExtensionFiles)) { - continue; + $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); + if (count($allServiceFiles) === 0) { + $normalizedFileName = $this->fileHelper->normalizePath($fileName); + foreach ($vendorDirs as $vendorDir) { + if (str_starts_with($normalizedFileName, $vendorDir)) { + continue 2; } - - $projectExtensionFiles[$serviceFile] = $this->getFileHash($serviceFile); } + $projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class]; + continue; } - } - } - - return $projectExtensionFiles; - } - /** - * @param mixed $definition - * @return string[] - */ - private function getClassesFromConfigDefinition($definition): array - { - if (is_string($definition)) { - return [$definition]; - } + foreach ($allServiceFiles as $serviceFile) { + if (array_key_exists($serviceFile, $projectExtensionFiles)) { + continue; + } - if ($definition instanceof Statement) { - $entity = $definition->entity; - if (is_string($entity)) { - return [$entity]; - } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { - return [$entity[0]]; + $projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class]; + } } } - return []; + return $projectExtensionFiles; } /** @@ -782,6 +889,7 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a 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']); diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 30d5caf4e8..e26aa8853a 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -24,6 +24,18 @@ interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer { + public const SUPERGLOBAL_VARIABLES = [ + 'GLOBALS', + '_SERVER', + '_GET', + '_POST', + '_FILES', + '_COOKIE', + '_SESSION', + '_REQUEST', + '_ENV', + ]; + public function getFile(): string; public function getFileDescription(): string; diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index be2e8aef4b..20365299c0 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -12,6 +12,8 @@ class StatementResult /** * @param StatementExitPoint[] $exitPoints * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param EndStatementResult[] $endStatements */ public function __construct( private MutatingScope $scope, @@ -19,6 +21,8 @@ public function __construct( private bool $isAlwaysTerminating, private array $exitPoints, private array $throwPoints, + private array $impurePoints, + private array $endStatements = [], ) { } @@ -52,14 +56,14 @@ public function filterOutLoopExitPoints(): self $num = $statement->num; if (!$num instanceof LNumber) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } if ($num->value !== 1) { continue; } - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } return $this; @@ -155,4 +159,33 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + /** + * Top-level StatementResult represents the state of the code + * at the end of control flow statements like If_ or TryCatch. + * + * It shows how Scope etc. looks like after If_ no matter + * which code branch was executed. + * + * For If_, "end statements" contain the state of the code + * at the end of each branch - if, elseifs, else, including the last + * statement node in each branch. + * + * For nested ifs, end statements try to contain the last non-control flow + * statement like Return_ or Throw_, instead of If_, TryCatch, or Foreach_. + * + * @return EndStatementResult[] + */ + public function getEndStatements(): array + { + return $this->endStatements; + } + } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4e7b61cc25..f35b81a939 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use Countable; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; @@ -79,6 +80,7 @@ use function is_string; use function strtolower; use function substr; +use const COUNT_NORMAL; class TypeSpecifier { @@ -208,13 +210,13 @@ public function specifyTypesInCondition( if ( $expr->left instanceof FuncCall - && count($expr->left->getArgs()) === 1 + && count($expr->left->getArgs()) >= 1 && $expr->left->name instanceof Name - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen'], true) + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true) && ( !$expr->right instanceof FuncCall || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen'], true) + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -237,19 +239,63 @@ public function specifyTypesInCondition( if ( !$context->null() && $expr->right instanceof FuncCall - && count($expr->right->getArgs()) === 1 + && count($expr->right->getArgs()) >= 1 && $expr->right->name instanceof Name && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) && $leftType->isInteger()->yes() ) { + $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; + } + + $narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); + if ($narrowed !== null) { + return $narrowed; + } + } + if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); + if ($context->truthy() && $argType->isArray()->maybe()) { + $countables = []; + if ($argType instanceof UnionType) { + $countableInterface = new ObjectType(Countable::class); + foreach ($argType->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); + $countables[] = $innerType; + } + + if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $countables[] = $innerType; + } + } + + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); + + return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, false, $scope, $rootExpr); + } + } + if ($argType->isArray()->yes()) { $newType = new NonEmptyArrayType(); - if ($context->truthy() && $argType->isList()->yes()) { + if ($context->true() && $argType->isList()->yes()) { $newType = AccessoryArrayListType::intersectWith($newType); } @@ -265,17 +311,18 @@ public function specifyTypesInCondition( && $expr->right instanceof FuncCall && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name - && strtolower((string) $expr->right->name) === 'strlen' + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) && $leftType->isInteger()->yes() ) { if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); if ($argType->isString()->yes()) { $accessory = new AccessoryNonEmptyStringType(); - if ($leftType instanceof ConstantIntegerType && $leftType->getValue() >= 2) { + + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { $accessory = new AccessoryNonFalsyStringType(); } @@ -866,6 +913,34 @@ public function specifyTypesInCondition( $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); + } elseif ( + $expr instanceof Expr\New_ + && $expr->class instanceof Name + && $this->reflectionProvider->hasClass($expr->class->toString()) + ) { + $classReflection = $this->reflectionProvider->getClass($expr->class->toString()); + + if ($classReflection->hasConstructor()) { + $methodReflection = $classReflection->getConstructor(); + $asserts = $methodReflection->getAsserts(); + + if ($asserts->getAll() !== []) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + } } elseif (!$context->null()) { return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); } @@ -873,6 +948,112 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], [], false, [], $rootExpr); } + private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?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()); + } + + if ( + $isNormalCount->yes() + && $argType->isConstantArray()->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; + } + + 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()); + } + + 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)); + } + 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++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); + } + } else { + for ($i = $sizeType->getMin();; $i++) { + $offsetType = new ConstantIntegerType($i); + $hasOffset = $type->hasOffsetValueType($offsetType); + if ($hasOffset->no()) { + break; + } + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes()); + } + + } + return $valueTypesBuilder->getArray(); + } + + return null; + } + private function specifyTypesForConstantBinaryExpression( Expr $exprNode, ConstantScalarType $constantType, @@ -916,26 +1097,39 @@ private function specifyTypesForConstantBinaryExpression( if ( !$context->null() && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 + && 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(); } - $argType = $scope->getType($exprNode->getArgs()[0]->value); + 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); - if ($argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); - $itemType = $argType->getIterableValueType(); - for ($i = 0; $i < $constantType->getValue(); $i++) { - $valueTypesBuilder->setOffsetValueType(new ConstantIntegerType($i), $itemType); - } - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $valueTypesBuilder->getArray(), $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); } @@ -949,7 +1143,7 @@ private function specifyTypesForConstantBinaryExpression( && $exprNode instanceof FuncCall && count($exprNode->getArgs()) === 1 && $exprNode->name instanceof Name - && strtolower((string) $exprNode->name) === 'strlen' + && in_array(strtolower((string) $exprNode->name), ['strlen', 'mb_strlen'], true) && $constantType instanceof ConstantIntegerType ) { if ($context->truthy() || $constantType->getValue() === 0) { @@ -988,7 +1182,11 @@ private function specifyTypesForConstantStringBinaryExpression( $context->truthy() && $exprNode instanceof FuncCall && $exprNode->name instanceof Name - && in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana'], true) + && 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() !== '' ) { @@ -1812,7 +2010,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif if ( $exprNode instanceof FuncCall && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'gettype' + && in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true) && isset($exprNode->getArgs()[0]) && $constantType->isString()->yes() ) { @@ -1820,11 +2018,11 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif } if ( - $exprNode instanceof FuncCall + $context->true() + && $exprNode instanceof FuncCall && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'get_class' - && isset($exprNode->getArgs()[0]) - && $constantType->isString()->yes() + && $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); } @@ -1916,11 +2114,27 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $unwrappedRightExpr = $rightExpr->getExpr(); } $rightType = $scope->getType($rightExpr); + + if ( + $context->true() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && $unwrappedLeftExpr->name->toLowerString() === 'preg_match' + && (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes() + ) { + return $this->specifyTypesInCondition( + $scope, + $leftExpr, + $context, + $rootExpr, + ); + } + if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall && $unwrappedLeftExpr->name instanceof Name - && strtolower($unwrappedLeftExpr->name->toString()) === 'get_class' + && in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true) && isset($unwrappedLeftExpr->getArgs()[0]) ) { if ($rightType->getClassStringObjectType()->isObject()->yes()) { @@ -1963,11 +2177,16 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $exprNode = $expressions[0]; $constantType = $expressions[1]; - $specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); + $unwrappedExprNode = $exprNode; + if ($exprNode instanceof AlwaysRememberedExpr) { + $unwrappedExprNode = $exprNode->getExpr(); + } + + $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedExprNode, $constantType, $context, $scope, $rootExpr); if ($specifiedType !== null) { - if ($exprNode instanceof AlwaysRememberedExpr) { - $specifiedType->unionWith( - $this->create($exprNode->getExpr(), $constantType, $context, false, $scope, $rootExpr), + if ($exprNode !== $unwrappedExprNode) { + $specifiedType = $specifiedType->unionWith( + $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr), ); } return $specifiedType; @@ -1981,6 +2200,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $unwrappedLeftExpr->name instanceof Node\Identifier && $unwrappedRightExpr instanceof ClassConstFetch && $rightType instanceof ConstantStringType && + $rightType->getValue() !== '' && strtolower($unwrappedLeftExpr->name->toString()) === 'class' ) { return $this->specifyTypesInCondition( @@ -2002,6 +2222,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $unwrappedRightExpr->name instanceof Node\Identifier && $unwrappedLeftExpr instanceof ClassConstFetch && $leftType instanceof ConstantStringType && + $leftType->getValue() !== '' && strtolower($unwrappedRightExpr->name->toString()) === 'class' ) { return $this->specifyTypesInCondition( diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index 20e3087b00..a09a15e760 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\File\FileHelper; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\ShouldNotHappenException; use function md5; use function sprintf; @@ -32,9 +33,17 @@ public function getAnonymousClassName( $this->fileHelper->normalizePath($filename, '/'), ); + /** @var int|null $lineIndex */ + $lineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); + if ($lineIndex === null) { + $hash = md5(sprintf('%s:%s', $filename, $classNode->getStartLine())); + } else { + $hash = md5(sprintf('%s:%s:%d', $filename, $classNode->getStartLine(), $lineIndex)); + } + return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $classNode->getStartLine())), + $hash, ); } diff --git a/src/Classes/ForbiddenClassNameExtension.php b/src/Classes/ForbiddenClassNameExtension.php new file mode 100644 index 0000000000..7d545d83d4 --- /dev/null +++ b/src/Classes/ForbiddenClassNameExtension.php @@ -0,0 +1,32 @@ + */ + public function getClassPrefixes(): array; + +} diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 5a6209c941..85acf2d88c 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -2,29 +2,21 @@ namespace PHPStan\Command; -use PHPStan\AnalysedCodeException; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\Error; +use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; -use PHPStan\Analyser\RuleErrorTransformer; -use PHPStan\Analyser\ScopeContext; -use PHPStan\Analyser\ScopeFactory; -use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use PHPStan\BetterReflection\Reflection\Exception\CircularReference; -use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\Collectors\CollectedData; use PHPStan\Internal\BytesHelper; -use PHPStan\Node\CollectedDataNode; use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\PhpDoc\StubValidator; -use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\InputInterface; use function array_merge; use function count; +use function is_file; use function memory_get_peak_usage; use function microtime; +use function sha1_file; use function sprintf; class AnalyseApplication @@ -32,14 +24,11 @@ class AnalyseApplication public function __construct( private AnalyserRunner $analyserRunner, + private AnalyserResultFinalizer $analyserResultFinalizer, private StubValidator $stubValidator, private ResultCacheManagerFactory $resultCacheManagerFactory, private IgnoredErrorHelper $ignoredErrorHelper, - private int $internalErrorsCountLimit, private StubFilesProvider $stubFilesProvider, - private RuleRegistry $ruleRegistry, - private ScopeFactory $scopeFactory, - private RuleErrorTransformer $ruleErrorTransformer, ) { } @@ -65,7 +54,6 @@ public function analyse( $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); $fileSpecificErrors = []; - $notFileSpecificErrors = []; if (count($ignoredErrorHelperResult->getErrors()) > 0) { $notFileSpecificErrors = $ignoredErrorHelperResult->getErrors(); $internalErrors = []; @@ -75,6 +63,7 @@ public function analyse( if ($errorOutput->isDebug()) { $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); } + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); $intermediateAnalyserResult = $this->runAnalyser( @@ -98,7 +87,11 @@ public function analyse( $stubErrors = $this->stubValidator->validate($projectStubFiles, $debug); $intermediateAnalyserResult = new AnalyserResult( array_merge($intermediateAnalyserResult->getUnorderedErrors(), $stubErrors), + $intermediateAnalyserResult->getFilteredPhpErrors(), + $intermediateAnalyserResult->getAllPhpErrors(), $intermediateAnalyserResult->getLocallyIgnoredErrors(), + $intermediateAnalyserResult->getLinesToIgnore(), + $intermediateAnalyserResult->getUnmatchedLineIgnores(), $intermediateAnalyserResult->getInternalErrors(), $intermediateAnalyserResult->getCollectedData(), $intermediateAnalyserResult->getDependencies(), @@ -109,27 +102,47 @@ public function analyse( } $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); - $analyserResult = $resultCacheResult->getAnalyserResult(); + $analyserResult = $this->analyserResultFinalizer->finalize($resultCacheResult->getAnalyserResult(), $onlyFiles, $debug)->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); - $errors = $analyserResult->getErrors(); + $errors = array_merge( + $analyserResult->getErrors(), + $analyserResult->getFilteredPhpErrors(), + ); $hasInternalErrors = count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); $memoryUsageBytes = $analyserResult->getPeakMemoryUsageBytes(); $isResultCacheUsed = !$resultCache->isFullAnalysis(); - if (!$hasInternalErrors) { - foreach ($this->getCollectedDataErrors($analyserResult->getCollectedData(), $onlyFiles) as $error) { - $errors[] = $error; + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; + if ( + $isResultCacheUsed + && $resultCacheResult->isSaved() + && !$onlyFiles + && $projectConfigArray !== null + ) { + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + if (!is_file($file)) { + $changedProjectExtensionFilesOutsideOfAnalysedPaths[$file] = $className; + continue; + } + + $newHash = sha1_file($file); + if ($newHash === $hash) { + continue; + } + + $changedProjectExtensionFilesOutsideOfAnalysedPaths[$file] = $className; } } + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $files, $hasInternalErrors); $fileSpecificErrors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); $notFileSpecificErrors = $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(); $collectedData = $analyserResult->getCollectedData(); $savedResultCache = $resultCacheResult->isSaved(); - if ($analyserResult->hasReachedInternalErrorsCountLimit()) { - $notFileSpecificErrors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); - } - $notFileSpecificErrors = array_merge($notFileSpecificErrors, $internalErrors); } return new AnalysisResult( @@ -143,42 +156,10 @@ public function analyse( $savedResultCache, $memoryUsageBytes, $isResultCacheUsed, + $changedProjectExtensionFilesOutsideOfAnalysedPaths, ); } - /** - * @param CollectedData[] $collectedData - * @return Error[] - */ - private function getCollectedDataErrors(array $collectedData, bool $onlyFiles): array - { - $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($collectedData, $onlyFiles); - $file = 'N/A'; - $scope = $this->scopeFactory->create(ScopeContext::create($file)); - $errors = []; - foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { - try { - $ruleErrors = $rule->processNode($node, $scope); - } catch (AnalysedCodeException $e) { - $errors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); - continue; - } catch (IdentifierNotFound $e) { - $errors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); - continue; - } catch (UnableToCompileNode | CircularReference $e) { - $errors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); - continue; - } - - foreach ($ruleErrors as $ruleError) { - $errors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); - } - } - - return $errors; - } - /** * @param string[] $files * @param string[] $allAnalysedFiles @@ -199,7 +180,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) { diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 9c49194aab..6ecfba9105 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,12 +3,15 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; +use PHPStan\Analyser\InternalError; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\DependencyInjection\Container; +use PHPStan\Diagnose\DiagnoseExtension; +use PHPStan\Diagnose\PHPStanDiagnoseExtension; use PHPStan\File\CouldNotWriteFileException; use PHPStan\File\FileReader; use PHPStan\File\FileWriter; @@ -28,6 +31,8 @@ use Symfony\Component\Console\Output\StreamOutput; use Throwable; use function array_intersect; +use function array_key_exists; +use function array_keys; use function array_map; use function array_unique; use function array_values; @@ -65,6 +70,7 @@ class AnalyseCommand extends Command */ public function __construct( private array $composerAutoloaderProjectPaths, + private float $analysisStartTime, ) { parent::__construct(); @@ -166,7 +172,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($generateBaselineFile === null && $allowEmptyBaseline) { $inceptionResult->getStdOutput()->getStyle()->error('You must pass the --generate-baseline option alongside --allow-empty-baseline.'); - return $inceptionResult->handleReturn(1, null); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } $errorOutput = $inceptionResult->getErrorOutput(); @@ -209,40 +215,47 @@ protected function execute(InputInterface $input, OutputInterface $output): int $baselineExtension = pathinfo($generateBaselineFile, PATHINFO_EXTENSION); if ($baselineExtension === '') { $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename must have an extension, %s provided instead.', pathinfo($generateBaselineFile, PATHINFO_BASENAME))); - return $inceptionResult->handleReturn(1, null); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } if (!in_array($baselineExtension, ['neon', 'php'], true)) { $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon or .php, .%s was used instead.', $baselineExtension)); - return $inceptionResult->handleReturn(1, null); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } } try { [$files, $onlyFiles] = $inceptionResult->getFiles(); } catch (PathNotFoundException $e) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; } catch (InceptionNotSuccessfulException) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); return 1; } 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); + return $inceptionResult->handleReturn(0, null, $this->analysisStartTime); } $inceptionResult->getErrorOutput()->getStyle()->error('No files found to analyse.'); - return $inceptionResult->handleReturn(1, null); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } $analysedConfigFiles = array_intersect($files, $container->getParameter('allConfigFiles')); + /** @var RelativePathHelper $relativePathHelper */ + $relativePathHelper = $container->getService('relativePathHelper'); foreach ($analysedConfigFiles as $analysedConfigFile) { $fileSize = @filesize($analysedConfigFile); if ($fileSize === false) { @@ -253,8 +266,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - /** @var RelativePathHelper $relativePathHelper */ - $relativePathHelper = $container->getService('relativePathHelper'); $inceptionResult->getErrorOutput()->getStyle()->warning(sprintf( 'Configuration file %s (%s) is too big and might slow down PHPStan. Consider adding it to excludePaths.', $relativePathHelper->getRelativePath($analysedConfigFile), @@ -263,9 +274,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($fix) { + if ($generateBaselineFile !== null) { + $inceptionResult->getStdOutput()->getStyle()->error('You cannot pass the --generate-baseline option when running PHPStan Pro.'); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); + } + return $this->runFixer($inceptionResult, $container, $onlyFiles, $input, $output, $files); } + /** @var AnalyseApplication $application */ $application = $container->getByType(AnalyseApplication::class); $debug = $input->getOption('debug'); @@ -315,27 +332,217 @@ protected function execute(InputInterface $input, OutputInterface $output): int $previous = $previous->getPrevious(); } - return $inceptionResult->handleReturn(1, null); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } throw $t; } + /** + * Variable $internalErrorsTuples contains both "internal errors" + * and "errors with non-ignorable exception" as InternalError objects. + */ + $internalErrorsTuples = []; + $internalFileSpecificErrors = []; + foreach ($analysisResult->getInternalErrorObjects() as $internalError) { + $internalErrorsTuples[$internalError->getMessage()] = [new InternalError( + $internalError->getTraceAsString() !== null ? sprintf('Internal error: %s', $internalError->getMessage()) : $internalError->getMessage(), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ), false]; + } + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } + + $message = $fileSpecificError->getMessage(); + $metadata = $fileSpecificError->getMetadata(); + $hasStackTrace = false; + if ( + $fileSpecificError->getIdentifier() === 'phpstan.internal' + && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) + ) { + $message = sprintf('Internal error: %s', $message); + $hasStackTrace = true; + } + + if (!$hasStackTrace) { + if (!array_key_exists($fileSpecificError->getMessage(), $internalFileSpecificErrors)) { + $internalFileSpecificErrors[$fileSpecificError->getMessage()] = $fileSpecificError; + } + } + + $internalErrorsTuples[$fileSpecificError->getMessage()] = [new InternalError( + $message, + sprintf('analysing file %s', $fileSpecificError->getTraitFilePath() ?? $fileSpecificError->getFilePath()), + $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], + $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, + true, + ), !$hasStackTrace]; + } + + $internalErrorsTuples = array_values($internalErrorsTuples); + $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml'; + + /** + * Variable $internalErrors only contains non-file-specific "internal errors". + */ + $internalErrors = []; + foreach ($internalErrorsTuples as [$internalError, $isInFileSpecificErrors]) { + if ($isInFileSpecificErrors) { + 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, + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ); + } + if ($generateBaselineFile !== null) { + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + if (count($internalErrorsTuples) > 0) { + foreach ($internalErrorsTuples as [$internalError]) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + } + + $inceptionResult->getStdOutput()->getStyle()->error(sprintf( + '%s occurred. Baseline could not be generated.', + count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', + )); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); + } + return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache); } /** @var ErrorFormatter $errorFormatter */ $errorFormatter = $container->getService($errorFormatterServiceName); + if (count($internalErrorsTuples) > 0) { + $analysisResult = new AnalysisResult( + array_values($internalFileSpecificErrors), + array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $internalErrors), + [], + [], + [], + $analysisResult->isDefaultLevelUsed(), + $analysisResult->getProjectConfigFile(), + $analysisResult->isResultCacheSaved(), + $analysisResult->getPeakMemoryUsageBytes(), + $analysisResult->isResultCacheUsed(), + $analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths(), + ); + + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + + $errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️'); + $errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan'); + $errorOutput->writeLineFormatted(' to get all reported errors.'); + $errorOutput->writeLineFormatted(''); + + return $inceptionResult->handleReturn( + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + $this->analysisStartTime, + ); + } + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { $exitCode = 2; } + if ( + $analysisResult->isResultCacheUsed() + && $analysisResult->isResultCacheSaved() + && !$onlyFiles + && $inceptionResult->getProjectConfigArray() !== null + ) { + $projectServicesNotInAnalysedPaths = array_values(array_unique($analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths())); + $projectServiceFileNamesNotInAnalysedPaths = array_keys($analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths()); + + if (count($projectServicesNotInAnalysedPaths) > 0) { + $one = count($projectServicesNotInAnalysedPaths) === 1; + $errorOutput->writeLineFormatted('Result cache might not behave correctly.'); + $errorOutput->writeLineFormatted(sprintf('You\'re using custom %s in your project config', $one ? 'extension' : 'extensions')); + $errorOutput->writeLineFormatted(sprintf('but %s not part of analysed paths:', $one ? 'this extension is' : 'these extensions are')); + $errorOutput->writeLineFormatted(''); + foreach ($projectServicesNotInAnalysedPaths as $service) { + $errorOutput->writeLineFormatted(sprintf('- %s', $service)); + } + + $errorOutput->writeLineFormatted(''); + + $errorOutput->writeLineFormatted('When you edit them and re-run PHPStan, the result cache will get stale.'); + + $directoriesToAdd = []; + foreach ($projectServiceFileNamesNotInAnalysedPaths as $path) { + $directoriesToAdd[] = dirname($relativePathHelper->getRelativePath($path)); + } + + $directoriesToAdd = array_unique($directoriesToAdd); + $oneDirectory = count($directoriesToAdd) === 1; + + $errorOutput->writeLineFormatted(sprintf('Add %s to your analysed paths to get rid of this problem:', $oneDirectory ? 'this directory' : 'these directories')); + + $errorOutput->writeLineFormatted(''); + + foreach ($directoriesToAdd as $directory) { + $errorOutput->writeLineFormatted(sprintf('- %s', $directory)); + } + + $errorOutput->writeLineFormatted(''); + + $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.'); + } + } + + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + return $inceptionResult->handleReturn( $exitCode, $analysisResult->getPeakMemoryUsageBytes(), + $this->analysisStartTime, ); } @@ -354,36 +561,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); - } - if ($analysisResult->hasInternalErrors()) { - $internalErrors = array_values(array_unique($analysisResult->getInternalErrors())); - - foreach ($internalErrors as $internalError) { - $inceptionResult->getStdOutput()->writeLineFormatted($internalError); - $inceptionResult->getStdOutput()->writeLineFormatted(''); - } - - $inceptionResult->getStdOutput()->getStyle()->error(sprintf( - '%s occurred. Baseline could not be generated.', - count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', - )); - - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); - } - - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->hasNonIgnorableException()) { - continue; - } - - $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated.'); - - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); - $inceptionResult->getStdOutput()->writeLineFormatted(''); - - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } $streamOutput = $this->createStreamOutput(); @@ -413,7 +591,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult } catch (DirectoryCreatorException $e) { $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } try { @@ -421,7 +599,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult } catch (CouldNotWriteFileException $e) { $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } $errorsCount = 0; @@ -430,7 +608,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult if (!$fileSpecificError->canBeIgnored()) { $unignorableCount++; if ($output->isVeryVerbose()) { - $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable could not be added to the baseline:'); + $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable errors could not be added to the baseline:'); $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); $inceptionResult->getStdOutput()->writeLineFormatted(''); @@ -449,7 +627,11 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult ) { $inceptionResult->getStdOutput()->getStyle()->success($message); } else { - $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them."); + if ($output->isVeryVerbose()) { + $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline."); + } else { + $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan with \"-vv\" and fix them."); + } } $exitCode = 0; @@ -457,7 +639,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult $exitCode = 2; } - return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes()); + return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime); } /** @@ -469,7 +651,7 @@ private function runFixer(InceptionResult $inceptionResult, Container $container if ($ciDetector->isCiDetected()) { $inceptionResult->getStdOutput()->writeLineFormatted('PHPStan Pro can\'t run in CI environment yet. Stay tuned!'); - return $inceptionResult->handleReturn(1, null); + return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } /** @var FixerApplication $fixerApplication */ @@ -484,4 +666,22 @@ private function runFixer(InceptionResult $inceptionResult, Container $container ); } + private function runDiagnoseExtensions(Container $container, Output $errorOutput): void + { + if (!$errorOutput->isDebug()) { + return; + } + + /** @var PHPStanDiagnoseExtension $phpstanDiagnoseExtension */ + $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); + + // not using tag for this extension to make sure it's always first + $phpstanDiagnoseExtension->print($errorOutput); + + /** @var DiagnoseExtension $extension */ + foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { + $extension->print($errorOutput); + } + } + } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 6e5a92467d..da6eb42962 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -47,7 +47,7 @@ public function runAnalyser( { $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); diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index b589665f8b..f7c4424284 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -3,7 +3,9 @@ namespace PHPStan\Command; use PHPStan\Analyser\Error; +use PHPStan\Analyser\InternalError; use PHPStan\Collectors\CollectedData; +use function array_map; use function count; use function usort; @@ -17,9 +19,10 @@ class AnalysisResult /** * @param list $fileSpecificErrors * @param list $notFileSpecificErrors - * @param list $internalErrors + * @param list $internalErrors * @param list $warnings * @param list $collectedData + * @param array $changedProjectExtensionFilesOutsideOfAnalysedPaths */ public function __construct( array $fileSpecificErrors, @@ -32,6 +35,7 @@ public function __construct( private bool $savedResultCache, private int $peakMemoryUsageBytes, private bool $isResultCacheUsed, + private array $changedProjectExtensionFilesOutsideOfAnalysedPaths, ) { usort( @@ -77,9 +81,18 @@ public function getNotFileSpecificErrors(): array } /** + * @deprecated Use getInternalErrorObjects * @return list */ public function getInternalErrors(): array + { + return array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $this->internalErrors); + } + + /** + * @return list + */ + public function getInternalErrorObjects(): array { return $this->internalErrors; } @@ -135,4 +148,12 @@ public function isResultCacheUsed(): bool return $this->isResultCacheUsed; } + /** + * @return array + */ + public function getChangedProjectExtensionFilesOutsideOfAnalysedPaths(): array + { + return $this->changedProjectExtensionFilesOutsideOfAnalysedPaths; + } + } diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 1261814cef..dee834e30b 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +use Composer\Semver\Semver; use Composer\XdebugHandler\XdebugHandler; use Nette\DI\Helpers; use Nette\DI\InvalidConfigurationException; @@ -17,12 +18,15 @@ use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\DuplicateIncludedFilesException; +use PHPStan\DependencyInjection\InvalidExcludePathsException; use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException; use PHPStan\DependencyInjection\LoaderFactory; use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileExcluder; use PHPStan\File\FileFinder; use PHPStan\File\FileHelper; +use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\Internal\ComposerHelper; use PHPStan\Internal\DirectoryCreator; use PHPStan\Internal\DirectoryCreatorException; use PHPStan\PhpDoc\StubFilesProvider; @@ -279,6 +283,43 @@ public static function begin( $additionalConfigFiles[] = $includedFilePath; } } + + if ( + count($additionalConfigFiles) > 0 + && $generatedConfigReflection->hasConstant('PHPSTAN_VERSION_CONSTRAINT') + ) { + $generatedConfigPhpStanVersionConstraint = $generatedConfigReflection->getConstant('PHPSTAN_VERSION_CONSTRAINT'); + if ($generatedConfigPhpStanVersionConstraint !== null) { + $phpstanSemverVersion = ComposerHelper::getPhpStanVersion(); + if ( + $phpstanSemverVersion !== ComposerHelper::UNKNOWN_VERSION + && !str_contains($phpstanSemverVersion, '@') + && !Semver::satisfies($phpstanSemverVersion, $generatedConfigPhpStanVersionConstraint) + ) { + $errorOutput->writeLineFormatted('Running PHPStan with incompatible extensions'); + $errorOutput->writeLineFormatted('You\'re running PHPStan from a different Composer project'); + $errorOutput->writeLineFormatted('than the one where you installed extensions.'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted(sprintf('Your PHPStan version is: %s', $phpstanSemverVersion)); + $errorOutput->writeLineFormatted(sprintf('Installed PHPStan extensions support: %s', $generatedConfigPhpStanVersionConstraint)); + + $errorOutput->writeLineFormatted(''); + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $mainScript = $_SERVER['argv'][0]; + $errorOutput->writeLineFormatted(sprintf('PHPStan is running from: %s', $currentWorkingDirectoryFileHelper->absolutizePath(dirname($mainScript)))); + } + + $errorOutput->writeLineFormatted(sprintf('Extensions were installed in: %s', dirname($generatedConfigDirectory, 3))); + $errorOutput->writeLineFormatted(''); + + $simpleRelativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); + $errorOutput->writeLineFormatted(sprintf('Run PHPStan with %s to fix this problem.', $simpleRelativePathHelper->getRelativePath(dirname($generatedConfigDirectory, 3) . '/bin/phpstan'))); + + $errorOutput->writeLineFormatted(''); + throw new InceptionNotSuccessfulException(); + } + } + } } if ( @@ -314,6 +355,23 @@ public static function begin( $errorOutput->writeLineFormatted($error); $errorOutput->writeLineFormatted(''); } + + $errorOutput->writeLineFormatted('To ignore non-existent paths in ignoreErrors,'); + $errorOutput->writeLineFormatted('set reportUnmatchedIgnoredErrors: false in your configuration file.'); + $errorOutput->writeLineFormatted(''); + + throw new InceptionNotSuccessfulException(); + } catch (InvalidExcludePathsException $e) { + $errorOutput->writeLineFormatted(sprintf('Invalid %s in excludePaths:', count($e->getErrors()) === 1 ? 'entry' : 'entries')); + foreach ($e->getErrors() as $error) { + $errorOutput->writeLineFormatted($error); + $errorOutput->writeLineFormatted(''); + } + + $errorOutput->writeLineFormatted('If the excluded path can sometimes exist, append (?)'); + $errorOutput->writeLineFormatted('to its config entry to mark it as optional.'); + $errorOutput->writeLineFormatted(''); + throw new InceptionNotSuccessfulException(); } catch (ValidationException $e) { foreach ($e->getMessages() as $message) { @@ -479,6 +537,50 @@ public static function begin( $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); @@ -499,7 +601,7 @@ public static function begin( $pathRoutingParser->setAnalysedFiles($files); - $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles()); + $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles(), true); $files = array_values(array_filter($files, static fn (string $file) => !$stubFilesExcluder->isExcludedFromAnalysing($file))); diff --git a/src/Command/DiagnoseCommand.php b/src/Command/DiagnoseCommand.php new file mode 100644 index 0000000000..608cf732f1 --- /dev/null +++ b/src/Command/DiagnoseCommand.php @@ -0,0 +1,104 @@ +setName(self::NAME) + ->setDescription('Shows diagnose information about PHPStan and extensions') + ->setDefinition([ + new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), + new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), + new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), + new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - do not catch internal errors'), + new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + ]); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + if ((bool) $input->getOption('debug')) { + $application = $this->getApplication(); + if ($application === null) { + throw new ShouldNotHappenException(); + } + $application->setCatchExceptions(false); + return; + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $memoryLimit = $input->getOption('memory-limit'); + $autoloadFile = $input->getOption('autoload-file'); + $configuration = $input->getOption('configuration'); + $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + + if ( + (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_string($autoloadFile) && $autoloadFile !== null) + || (!is_string($configuration) && $configuration !== null) + || (!is_string($level) && $level !== null) + ) { + throw new ShouldNotHappenException(); + } + + try { + $inceptionResult = CommandHelper::begin( + $input, + $output, + [], + $memoryLimit, + $autoloadFile, + $this->composerAutoloaderProjectPaths, + $configuration, + null, + $level, + false, + ); + } catch (InceptionNotSuccessfulException) { + return 1; + } + + $container = $inceptionResult->getContainer(); + $output = $inceptionResult->getStdOutput(); + + /** @var PHPStanDiagnoseExtension $phpstanDiagnoseExtension */ + $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); + + // not using tag for this extension to make sure it's always first + $phpstanDiagnoseExtension->print($output); + + /** @var DiagnoseExtension $extension */ + foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { + $extension->print($output); + } + + return 0; + } + +} diff --git a/src/Command/ErrorFormatter/JunitErrorFormatter.php b/src/Command/ErrorFormatter/JunitErrorFormatter.php index 6e684ad942..b7562f4733 100644 --- a/src/Command/ErrorFormatter/JunitErrorFormatter.php +++ b/src/Command/ErrorFormatter/JunitErrorFormatter.php @@ -37,16 +37,16 @@ public function formatErrors( $result .= $this->createTestCase( sprintf('%s:%s', $fileName, (string) $fileSpecificError->getLine()), 'ERROR', - $this->escape($fileSpecificError->getMessage()), + $fileSpecificError->getMessage(), ); } foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { - $result .= $this->createTestCase('General error', 'ERROR', $this->escape($notFileSpecificError)); + $result .= $this->createTestCase('General error', 'ERROR', $notFileSpecificError); } foreach ($analysisResult->getWarnings() as $warning) { - $result .= $this->createTestCase('Warning', 'WARNING', $this->escape($warning)); + $result .= $this->createTestCase('Warning', 'WARNING', $warning); } if (!$analysisResult->hasErrors()) { diff --git a/src/Command/ErrorFormatter/RawErrorFormatter.php b/src/Command/ErrorFormatter/RawErrorFormatter.php index 4625983275..add31fa48f 100644 --- a/src/Command/ErrorFormatter/RawErrorFormatter.php +++ b/src/Command/ErrorFormatter/RawErrorFormatter.php @@ -19,13 +19,20 @@ public function formatErrors( $output->writeLineFormatted(''); } + $outputIdentifiers = $output->isVerbose(); foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + $identifier = ''; + if ($outputIdentifiers && $fileSpecificError->getIdentifier() !== null) { + $identifier = sprintf(' [identifier=%s]', $fileSpecificError->getIdentifier()); + } + $output->writeRaw( sprintf( - '%s:%d:%s', + '%s:%d:%s%s', $fileSpecificError->getFile(), $fileSpecificError->getLine() ?? '?', $fileSpecificError->getMessage(), + $identifier, ), ); $output->writeLineFormatted(''); diff --git a/src/Command/ErrorFormatter/TableErrorFormatter.php b/src/Command/ErrorFormatter/TableErrorFormatter.php index 16d10956d3..eb040735d0 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -153,12 +153,12 @@ public function formatErrors( } if (count($analysisResult->getNotFileSpecificErrors()) > 0) { - $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', $error], $analysisResult->getNotFileSpecificErrors())); + $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', OutputFormatter::escape($error)], $analysisResult->getNotFileSpecificErrors())); } $warningsCount = count($analysisResult->getWarnings()); if ($warningsCount > 0) { - $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', $warning], $analysisResult->getWarnings())); + $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', OutputFormatter::escape($warning)], $analysisResult->getWarnings())); } $finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount()); diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index c3be97c7ed..75caa6420c 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -11,6 +11,7 @@ use Nette\Utils\Json; use Phar; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\InternalError; use PHPStan\File\FileMonitor; use PHPStan\File\FileMonitorResult; use PHPStan\File\FileReader; @@ -19,6 +20,8 @@ use PHPStan\Internal\DirectoryCreator; use PHPStan\Internal\DirectoryCreatorException; use PHPStan\PhpDoc\StubFilesProvider; +use PHPStan\Process\ProcessCanceledException; +use PHPStan\Process\ProcessCrashedException; use PHPStan\Process\ProcessHelper; use PHPStan\Process\ProcessPromise; use PHPStan\ShouldNotHappenException; @@ -29,8 +32,7 @@ use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; use React\Http\Browser; -use React\Promise\CancellablePromiseInterface; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\TcpServer; @@ -64,8 +66,10 @@ class FixerApplication { - /** @var (ExtendedPromiseInterface&CancellablePromiseInterface)|null */ - private $processInProgress; + /** @var PromiseInterface|null */ + private PromiseInterface|null $processInProgress = null; + + private bool $fileMonitorActive = true; /** * @param string[] $analysedPaths @@ -86,6 +90,8 @@ public function __construct( private array $allConfigFiles, private ?string $cliAutoloadFile, private array $bootstrapFiles, + private ?string $editorUrl, + private string $usedLevel, ) { } @@ -118,8 +124,10 @@ public function run( 'projectConfigFile' => $projectConfigFile, 'filesCount' => $filesCount, 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'editorUrl' => $this->editorUrl, + 'ruleLevel' => $this->usedLevel, ]]); - $decoder->on('data', static function (array $data) use ( + $decoder->on('data', function (array $data) use ( $output, ): void { if ($data['action'] === 'webPort') { @@ -127,6 +135,14 @@ public function run( $output->writeln('Press [Ctrl-C] to quit.'); return; } + if ($data['action'] === 'resumeFileMonitor') { + $this->fileMonitorActive = true; + return; + } + if ($data['action'] === 'pauseFileMonitor') { + $this->fileMonitorActive = false; + return; + } }); $this->fileMonitor->initialize(array_merge( @@ -284,7 +300,7 @@ private function downloadPhar( ): void { $currentVersion = null; - $branch = 'main'; + $branch = '1.1.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); @@ -336,7 +352,7 @@ private function downloadPhar( throw new ShouldNotHappenException(sprintf('Could not open file %s for writing.', $pharPath)); } $progressBar = new ProgressBar($output); - $client->requestStreaming('GET', $latestInfo['url'])->done(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { + $client->requestStreaming('GET', $latestInfo['url'])->then(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { $body = $response->getBody(); if (!$body instanceof ReadableStreamInterface) { throw new ShouldNotHappenException(); @@ -396,6 +412,14 @@ private function writeInfoFile(string $infoPath, string $version, string $branch private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCallback): void { $callback = function () use (&$callback, $loop, $hasChangesCallback): void { + if (!$this->fileMonitorActive) { + $loop->addTimer(1.0, $callback); + return; + } + if ($this->processInProgress !== null) { + $loop->addTimer(1.0, $callback); + return; + } $changes = $this->fileMonitor->getChanges(); if ($changes->hasAnyChanges()) { @@ -450,17 +474,35 @@ private function analyse( )); $this->processInProgress = $process->run(); - $this->processInProgress->done(function () use ($server): void { + $this->processInProgress->then(function () use ($server): void { $this->processInProgress = null; $server->close(); - }, function (Throwable $e) use ($server, $output, $phpstanFixerEncoder): void { + }, function (Throwable $e) use ($server, $phpstanFixerEncoder): void { $this->processInProgress = null; $server->close(); - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); + + if ($e instanceof ProcessCanceledException) { + return; + } + + if ($e instanceof ProcessCrashedException) { + $message = 'Analysis crashed'; + $traceAsString = $e->getMessage(); + $trace = []; + } else { + $message = $e->getMessage(); + $traceAsString = $e->getTraceAsString(); + $trace = InternalError::prepareTrace($e); + } $phpstanFixerEncoder->write(['action' => 'analysisCrash', 'data' => [ - 'errors' => [$e->getMessage()], + 'internalErrors' => [new InternalError( + $message, + 'running PHPStan Pro worker', + $trace, + $traceAsString, + false, + )], ]]); - throw $e; }); } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index addfb472fe..3cc415b3f4 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -3,27 +3,19 @@ namespace PHPStan\Command; use Clue\React\NDJson\Encoder; -use PHPStan\AnalysedCodeException; use PHPStan\Analyser\AnalyserResult; +use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\Ignore\IgnoredErrorHelperResult; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\ResultCache\ResultCacheManager; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; -use PHPStan\Analyser\RuleErrorTransformer; -use PHPStan\Analyser\ScopeContext; -use PHPStan\Analyser\ScopeFactory; -use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use PHPStan\BetterReflection\Reflection\Exception\CircularReference; -use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\Collectors\CollectedData; use PHPStan\DependencyInjection\Container; use PHPStan\File\PathNotFoundException; -use PHPStan\Node\CollectedDataNode; use PHPStan\Parallel\ParallelAnalyser; use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; -use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\ShouldNotHappenException; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; @@ -36,7 +28,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use function array_diff; +use function array_key_exists; use function count; +use function filemtime; use function in_array; use function is_array; use function is_bool; @@ -45,6 +39,7 @@ use function memory_get_peak_usage; use function React\Promise\resolve; use function sprintf; +use function usort; use const JSON_INVALID_UTF8_IGNORE; class FixerWorkerCommand extends Command @@ -130,7 +125,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $loop = new StreamSelectLoop(); $tcpConnector = new TcpConnector($loop); - $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->done(function (ConnectionInterface $connection) use ($container, $inceptionResult, $configuration, $input, $ignoredErrorHelperResult, $loop): void { + $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->then(function (ConnectionInterface $connection) use ($container, $inceptionResult, $configuration, $input, $ignoredErrorHelperResult, $loop): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable @@ -141,6 +136,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create(); $projectConfigArray = $inceptionResult->getProjectConfigArray(); + /** @var AnalyserResultFinalizer $analyserResultFinalizer */ + $analyserResultFinalizer = $container->getByType(AnalyserResultFinalizer::class); + try { [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); } catch (PathNotFoundException | InceptionNotSuccessfulException) { @@ -187,13 +185,47 @@ protected function execute(InputInterface $input, OutputInterface $output): int ], ]); + $filesToAnalyse = $resultCache->getFilesToAnalyse(); + usort($filesToAnalyse, static function (string $a, string $b): int { + $aTime = @filemtime($a); + if ($aTime === false) { + return 1; + } + + $bTime = @filemtime($b); + if ($bTime === false) { + return -1; + } + + // files are sorted from the oldest + // because ParallelAnalyser reverses the scheduler jobs to do the smallest + // jobs first + return $aTime <=> $bTime; + }); + $this->runAnalyser( $loop, $container, - $resultCache->getFilesToAnalyse(), + $filesToAnalyse, $configuration, $input, function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ($out, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles): void { + $internalErrors = []; + foreach ($errors as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } + + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); + } + + if (count($internalErrors) > 0) { + $out->write(['action' => 'analysisCrash', 'data' => [ + 'internalErrors' => $internalErrors, + ]]); + return; + } + [$errors, $ignoredErrors] = $this->filterErrors($errors, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); foreach ($locallyIgnoredErrors as $locallyIgnoredError) { $ignoredErrors[] = [$locallyIgnoredError, null]; @@ -207,33 +239,55 @@ function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ], ]); }, - )->then(function (AnalyserResult $intermediateAnalyserResult) use ($resultCacheManager, $resultCache, $inceptionResult, $container, $isOnlyFiles, $ignoredErrorHelperResult, $inceptionFiles, $out): void { - $result = $resultCacheManager->process( + )->then(function (AnalyserResult $intermediateAnalyserResult) use ($analyserResultFinalizer, $resultCacheManager, $resultCache, $inceptionResult, $isOnlyFiles, $ignoredErrorHelperResult, $inceptionFiles, $out): void { + $analyserResult = $resultCacheManager->process( $intermediateAnalyserResult, $resultCache, $inceptionResult->getErrorOutput(), false, true, )->getAnalyserResult(); + $finalizerResult = $analyserResultFinalizer->finalize($analyserResult, $isOnlyFiles, false); + + $internalErrors = []; + foreach ($finalizerResult->getAnalyserResult()->getInternalErrors() as $internalError) { + $internalErrors[] = new InternalError( + $internalError->getTraceAsString() !== null ? sprintf('Internal error: %s', $internalError->getMessage()) : $internalError->getMessage(), + $internalError->getContextDescription(), + $internalError->getTrace(), + $internalError->getTraceAsString(), + $internalError->shouldReportBug(), + ); + } - $hasInternalErrors = count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(); - - $collectorErrors = []; - $intermediateErrors = $result->getErrors(); - if (!$hasInternalErrors) { - foreach ($this->getCollectedDataErrors($container, $result->getCollectedData(), $isOnlyFiles) as $error) { - $collectorErrors[] = $error; - $intermediateErrors[] = $error; + foreach ($finalizerResult->getAnalyserResult()->getUnorderedErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; } - } else { + + $internalErrors[] = $this->transformErrorIntoInternalError($fileSpecificError); + } + + $hasInternalErrors = count($internalErrors) > 0 || $finalizerResult->getAnalyserResult()->hasReachedInternalErrorsCountLimit(); + + if ($hasInternalErrors) { $out->write(['action' => 'analysisCrash', 'data' => [ - 'errors' => count($result->getInternalErrors()) > 0 ? $result->getInternalErrors() : [ - 'Internal error occurred', + 'internalErrors' => count($internalErrors) > 0 ? $internalErrors : [ + new InternalError( + 'Internal error occurred', + 'running analyser in PHPStan Pro worker', + [], + null, + false, + ), ], ]]); } - [$collectorErrors, $ignoredCollectorErrors] = $this->filterErrors($collectorErrors, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, $hasInternalErrors); + [$collectorErrors, $ignoredCollectorErrors] = $this->filterErrors($finalizerResult->getCollectorErrors(), $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, $hasInternalErrors); + foreach ($finalizerResult->getLocallyIgnoredCollectorErrors() as $locallyIgnoredCollectorError) { + $ignoredCollectorErrors[] = [$locallyIgnoredCollectorError, null]; + } $out->write([ 'action' => 'analysisStream', 'result' => [ @@ -244,20 +298,17 @@ function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ]); $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process( - $intermediateErrors, + $finalizerResult->getErrors(), $isOnlyFiles, $inceptionFiles, $hasInternalErrors, ); - $intermediateErrors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); - $ignoreNotFileErrors = $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(); - $ignoreFileErrors = []; - foreach ($intermediateErrors as $error) { + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { if ($error->getIdentifier() === null) { continue; } - if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched'], true)) { + if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier'], true)) { continue; } $ignoreFileErrors[] = $error; @@ -267,7 +318,7 @@ function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use 'action' => 'analysisEnd', 'result' => [ 'ignoreFileErrors' => $ignoreFileErrors, - 'ignoreNotFileErrors' => $ignoreNotFileErrors, + 'ignoreNotFileErrors' => $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(), ], ]); }); @@ -277,6 +328,26 @@ function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use return 0; } + private function transformErrorIntoInternalError(Error $error): InternalError + { + $message = $error->getMessage(); + $metadata = $error->getMetadata(); + if ( + $error->getIdentifier() === 'phpstan.internal' + && array_key_exists(InternalError::STACK_TRACE_AS_STRING_METADATA_KEY, $metadata) + ) { + $message = sprintf('Internal error: %s', $message); + } + + return new InternalError( + $message, + sprintf('analysing file %s', $error->getTraitFilePath() ?? $error->getFilePath()), + $metadata[InternalError::STACK_TRACE_METADATA_KEY] ?? [], + $metadata[InternalError::STACK_TRACE_AS_STRING_METADATA_KEY] ?? null, + true, + ); + } + /** * @param string[] $inceptionFiles * @param array $errors @@ -303,45 +374,10 @@ private function filterErrors(array $errors, IgnoredErrorHelperResult $ignoredEr ]; } - /** - * @param CollectedData[] $collectedData - * @return Error[] - */ - private function getCollectedDataErrors(Container $container, array $collectedData, bool $onlyFiles): array - { - $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($collectedData, $onlyFiles); - $file = 'N/A'; - $scope = $container->getByType(ScopeFactory::class)->create(ScopeContext::create($file)); - $ruleRegistry = $container->getByType(RuleRegistry::class); - $ruleErrorTransformer = $container->getByType(RuleErrorTransformer::class); - - $errors = []; - foreach ($ruleRegistry->getRules($nodeType) as $rule) { - try { - $ruleErrors = $rule->processNode($node, $scope); - } catch (AnalysedCodeException $e) { - $errors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); - continue; - } catch (IdentifierNotFound $e) { - $errors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); - continue; - } catch (UnableToCompileNode | CircularReference $e) { - $errors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); - continue; - } - - foreach ($ruleErrors as $ruleError) { - $errors[] = $ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); - } - } - - return $errors; - } - /** * @param string[] $files * @param callable(list, list, string[]): void $onFileAnalysisHandler + * @return PromiseInterface */ private function runAnalyser(LoopInterface $loop, Container $container, array $files, ?string $configuration, InputInterface $input, callable $onFileAnalysisHandler): PromiseInterface { @@ -349,7 +385,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 */ diff --git a/src/Command/InceptionResult.php b/src/Command/InceptionResult.php index 308935fb9d..14a1d5202d 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -5,8 +5,12 @@ use PHPStan\DependencyInjection\Container; use PHPStan\File\PathNotFoundException; use PHPStan\Internal\BytesHelper; +use function floor; +use function implode; use function max; use function memory_get_peak_usage; +use function microtime; +use function round; use function sprintf; class InceptionResult @@ -84,8 +88,15 @@ public function getGenerateBaselineFile(): ?string return $this->generateBaselineFile; } - public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes): int + public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes, float $analysisStartTime): int { + if ($this->getErrorOutput()->isVerbose()) { + $this->getErrorOutput()->writeLineFormatted(sprintf( + 'Elapsed time: %s', + $this->formatDuration((int) round(microtime(true) - $analysisStartTime)), + )); + } + if ($peakMemoryUsageBytes !== null && $this->getErrorOutput()->isVerbose()) { $this->getErrorOutput()->writeLineFormatted(sprintf( 'Used memory: %s', @@ -96,4 +107,21 @@ public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes): int return $exitCode; } + private function formatDuration(int $seconds): string + { + $minutes = (int) floor($seconds / 60); + $remainingSeconds = $seconds % 60; + + $result = []; + if ($minutes > 0) { + $result[] = $minutes . ' minute' . ($minutes > 1 ? 's' : ''); + } + + if ($remainingSeconds > 0) { + $result[] = $remainingSeconds . ' second' . ($remainingSeconds > 1 ? 's' : ''); + } + + return implode(' ', $result); + } + } diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index c9d648bed6..9646518839 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -5,6 +5,7 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\DependencyInjection\Container; @@ -23,6 +24,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; use function array_fill_keys; +use function array_merge; use function defined; use function is_array; use function is_bool; @@ -125,7 +127,7 @@ 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))->done(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): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable @@ -161,8 +163,25 @@ private function runWorker( $out->write([ 'action' => 'result', 'result' => [ - 'errors' => [$error->getMessage()], + 'errors' => [], + 'internalErrors' => [ + new InternalError( + $error->getMessage(), + 'communicating with main process in parallel worker', + InternalError::prepareTrace($error), + $error->getTraceAsString(), + true, + ), + ], + 'filteredPhpErrors' => [], + 'allPhpErrors' => [], + 'locallyIgnoredErrors' => [], + 'linesToIgnore' => [], + 'unmatchedLineIgnores' => [], + 'collectedData' => [], + 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => [], + 'exportedNodes' => [], 'files' => [], 'internalErrorsCount' => 1, ], @@ -173,7 +192,7 @@ private function runWorker( $fileAnalyser = $container->getByType(FileAnalyser::class); $ruleRegistry = $container->getByType(RuleRegistry::class); $collectorRegistry = $container->getByType(CollectorRegistry::class); - $in->on('data', function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles, $output): void { + $in->on('data', static function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles): void { $action = $json['action']; if ($action !== 'analyse') { return; @@ -182,7 +201,12 @@ private function runWorker( $internalErrorsCount = 0; $files = $json['files']; $errors = []; + $internalErrors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; $collectedData = []; $dependencies = []; $exportedNodes = []; @@ -190,6 +214,10 @@ private function runWorker( try { $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); $fileErrors = $fileAnalyserResult->getErrors(); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $dependencies[$file] = $fileAnalyserResult->getDependencies(); $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); foreach ($fileErrors as $fileError) { @@ -202,20 +230,14 @@ private function runWorker( $collectedData[] = $data; } } catch (Throwable $t) { - $this->errorCount++; $internalErrorsCount++; - $internalErrorMessage = sprintf('Internal error: %s while analysing file %s', $t->getMessage(), $file); - - $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml'; - if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { - $trace = sprintf('## %s(%d)%s', $t->getFile(), $t->getLine(), "\n"); - $trace .= $t->getTraceAsString(); - $internalErrorMessage .= sprintf('%sPost the following stack trace to %s: %s%s', "\n\n", $bugReportUrl, "\n", $trace); - } else { - $internalErrorMessage .= sprintf('%sRun PHPStan with -v option and post the stack trace to:%s%s', "\n", "\n", $bugReportUrl); - } - - $errors[] = $internalErrorMessage; + $internalErrors[] = new InternalError( + $t->getMessage(), + sprintf('analysing file %s', $file), + InternalError::prepareTrace($t), + $t->getTraceAsString(), + true, + ); } } @@ -223,7 +245,12 @@ private function runWorker( 'action' => 'result', 'result' => [ 'errors' => $errors, + 'internalErrors' => $internalErrors, + 'filteredPhpErrors' => $filteredPhpErrors, + 'allPhpErrors' => $allPhpErrors, 'locallyIgnoredErrors' => $locallyIgnoredErrors, + 'linesToIgnore' => $linesToIgnore, + 'unmatchedLineIgnores' => $unmatchedLineIgnores, 'collectedData' => $collectedData, 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => $dependencies, diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 3f37bc295f..0688b5d19e 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -14,9 +14,7 @@ use PHPStan\File\FileHelper; use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\InClassMethodNode; -use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; -use PHPStan\Node\InTraitNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParameterReflectionWithPhpDocs; @@ -46,22 +44,6 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies { $dependenciesReflections = []; - if ($node instanceof InClassNode || $node instanceof InTraitNode) { - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - null, - $docComment->getText(), - ); - foreach ($phpDoc->getTypeAliasImportTags() as $importTag) { - $this->addClassToDependencies($importTag->getImportedFrom(), $dependenciesReflections); - } - } - } - if ($node instanceof Node\Stmt\Class_) { if ($node->namespacedName !== null) { $this->addClassToDependencies($node->namespacedName->toString(), $dependenciesReflections); @@ -156,10 +138,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($functionReflection->getVariants() as $functionVariant) { foreach ($functionVariant->getParameters() as $parameter) { - if ($parameter->getOutType() === null) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { continue; } - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } @@ -190,10 +177,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies if (!$parameter instanceof ParameterReflectionWithPhpDocs) { continue; } - if ($parameter->getOutType() === null) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { continue; } - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } @@ -223,10 +215,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); foreach ($methodReflection->getVariants() as $methodVariant) { foreach ($methodVariant->getParameters() as $parameter) { - if ($parameter->getOutType() === null) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { continue; } - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } @@ -290,10 +287,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); foreach ($methodReflection->getVariants() as $methodVariant) { foreach ($methodVariant->getParameters() as $parameter) { - if ($parameter->getOutType() === null) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { continue; } - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } @@ -306,10 +308,15 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); foreach ($methodReflection->getVariants() as $methodVariant) { foreach ($methodVariant->getParameters() as $parameter) { - if ($parameter->getOutType() === null) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { continue; } - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } @@ -595,6 +602,13 @@ private function addClassToDependencies(string $className, array &$dependenciesR } } + $phpDoc = $classReflection->getResolvedPhpDoc(); + if ($phpDoc !== null) { + foreach ($phpDoc->getTypeAliasImportTags() as $importTag) { + $dependenciesReflections[] = $this->reflectionProvider->getClass($importTag->getImportedFrom()); + } + } + $classReflection = $classReflection->getParentClass(); } while ($classReflection !== null); } @@ -622,11 +636,15 @@ private function extractFromParametersAcceptor( $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - if ($parameter->getOutType() === null) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { continue; } - - foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } diff --git a/src/DependencyInjection/ConditionalTagsExtension.php b/src/DependencyInjection/ConditionalTagsExtension.php index 358e673e10..e11f095fda 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -9,6 +9,9 @@ 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; @@ -49,6 +52,13 @@ public function getConfigSchema(): Nette\Schema\Schema 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)); } diff --git a/src/DependencyInjection/InvalidExcludePathsException.php b/src/DependencyInjection/InvalidExcludePathsException.php new file mode 100644 index 0000000000..d230b21108 --- /dev/null +++ b/src/DependencyInjection/InvalidExcludePathsException.php @@ -0,0 +1,27 @@ +errors)); + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } + +} diff --git a/src/DependencyInjection/Neon/OptionalPath.php b/src/DependencyInjection/Neon/OptionalPath.php new file mode 100644 index 0000000000..7794f3b872 --- /dev/null +++ b/src/DependencyInjection/Neon/OptionalPath.php @@ -0,0 +1,12 @@ +process([$val->value], $fileKeyToPass, $file); - $val = new Statement($tmp[0], $this->process($val->attributes, $fileKeyToPass, $file)); + if ( + in_array($keyToResolve, [ + '[parameters][excludePaths][]', + '[parameters][excludePaths][analyse][]', + '[parameters][excludePaths][analyseAndScan][]', + ], true) + && count($val->attributes) === 1 + && $val->attributes[0] === '?' + && is_string($val->value) + && !str_contains($val->value, '%') + && !str_starts_with($val->value, '*') + ) { + $fileHelper = $this->createFileHelperByFile($file); + $val = new OptionalPath($fileHelper->normalizePath($fileHelper->absolutizePath($val->value))); + } else { + $tmp = $this->process([$val->value], $fileKeyToPass, $file); + $val = new Statement($tmp[0], $this->process($val->attributes, $fileKeyToPass, $file)); + } } } - $keyToResolve = $fileKey; - if (is_int($key)) { - $keyToResolve .= '[]'; - } else { - $keyToResolve .= '[' . $key . ']'; - } - if (in_array($keyToResolve, [ '[parameters][paths][]', '[parameters][excludes_analyse][]', diff --git a/src/DependencyInjection/ProjectConfigHelper.php b/src/DependencyInjection/ProjectConfigHelper.php new file mode 100644 index 0000000000..0ac6ef97a2 --- /dev/null +++ b/src/DependencyInjection/ProjectConfigHelper.php @@ -0,0 +1,66 @@ + $projectConfig + * @return list + */ + public static function getServiceClassNames(array $projectConfig): array + { + $services = array_merge( + $projectConfig['services'] ?? [], + $projectConfig['rules'] ?? [], + ); + $classes = []; + foreach ($services as $service) { + $classes = array_merge($classes, self::getClassesFromConfigDefinition($service)); + if (!is_array($service)) { + continue; + } + + foreach (['class', 'factory', 'implement'] as $key) { + if (!isset($service[$key])) { + continue; + } + + $classes = array_merge($classes, self::getClassesFromConfigDefinition($service[$key])); + } + } + + return array_values(array_unique($classes)); + } + + /** + * @param mixed $definition + * @return string[] + */ + private static function getClassesFromConfigDefinition($definition): array + { + if (is_string($definition)) { + return [$definition]; + } + + if ($definition instanceof Statement) { + $entity = $definition->entity; + if (is_string($entity)) { + return [$entity]; + } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { + return [$entity[0]]; + } + } + + return []; + } + +} diff --git a/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php new file mode 100644 index 0000000000..9b1db71fc5 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php @@ -0,0 +1,33 @@ +container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getMethodParameterClosureTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getStaticMethodParameterClosureTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php new file mode 100644 index 0000000000..3cf13bf0ba --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterOutTypeExtensionProvider.php @@ -0,0 +1,33 @@ +container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getMethodParameterOutTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getStaticMethodParameterOutTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php new file mode 100644 index 0000000000..817bd6acf1 --- /dev/null +++ b/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php @@ -0,0 +1,21 @@ +getContainerBuilder(); + $excludePaths = $builder->parameters['excludePaths']; + if ($excludePaths === null) { + return; + } + + $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)) { + continue; + } + if (is_file($path)) { + continue; + } + } + if (FileExcluder::isFnmatchPattern($path)) { + continue; + } + + $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']; + } + + foreach ($newExcludePaths as $key => $p) { + $newExcludePaths[$key] = array_map( + static fn ($path) => $path instanceof OptionalPath ? $path->path : $path, + $p, + ); + } + + $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 5432fc0eec..fea4461b45 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\NameScope; use PHPStan\Command\IgnoredRegexValidator; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\File\FileExcluder; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; use PHPStan\PhpDoc\TypeNodeResolver; @@ -34,6 +35,8 @@ use function count; use function implode; use function is_array; +use function is_dir; +use function is_file; use function sprintf; use const PHP_VERSION_ID; @@ -55,8 +58,10 @@ public function loadConfiguration(): void return; } + $noImplicitWildcard = $builder->parameters['featureToggles']['noImplicitWildcard']; + /** @throws void */ - $parser = Llk::load(new Read('hoa://Library/Regex/Grammar.pp')); + $parser = Llk::load(new Read(__DIR__ . '/../../resources/RegexGrammar.pp')); $reflectionProvider = new DummyReflectionProvider(); $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); @@ -131,6 +136,40 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry } } + $reportUnmatched = (bool) $builder->parameters['reportUnmatchedIgnoredErrors']; + + if ($noImplicitWildcard && $reportUnmatched) { + foreach ($ignoreErrors as $ignoreError) { + if (!is_array($ignoreError)) { + continue; + } + + if (isset($ignoreError['path'])) { + $ignorePaths = [$ignoreError['path']]; + } elseif (isset($ignoreError['paths'])) { + $ignorePaths = $ignoreError['paths']; + } else { + continue; + } + + foreach ($ignorePaths as $ignorePath) { + if (FileExcluder::isAbsolutePath($ignorePath)) { + if (is_dir($ignorePath)) { + continue; + } + if (is_file($ignorePath)) { + continue; + } + } + if (FileExcluder::isFnmatchPattern($ignorePath)) { + continue; + } + + $errors[] = sprintf('Path %s is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath); + } + } + } + if (count($errors) === 0) { return; } diff --git a/src/Diagnose/DiagnoseExtension.php b/src/Diagnose/DiagnoseExtension.php new file mode 100644 index 0000000000..084134495a --- /dev/null +++ b/src/Diagnose/DiagnoseExtension.php @@ -0,0 +1,31 @@ +writeLineFormatted(sprintf( + 'PHP runtime version: %s', + $phpRuntimeVersion->getVersionString(), + )); + $output->writeLineFormatted(sprintf( + 'PHP version for analysis: %s (from %s)', + $this->phpVersion->getVersionString(), + $this->phpVersion->getSourceLabel(), + )); + $output->writeLineFormatted(''); + + $output->writeLineFormatted(sprintf( + 'PHPStan version: %s', + ComposerHelper::getPhpStanVersion(), + )); + $output->writeLineFormatted('PHPStan running from:'); + $pharRunning = Phar::running(false); + if ($pharRunning !== '') { + $output->writeLineFormatted(dirname($pharRunning)); + } else { + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $output->writeLineFormatted($_SERVER['argv'][0]); + } else { + $output->writeLineFormatted('Unknown'); + } + } + $output->writeLineFormatted(''); + + $configFilesFromExtensionInstaller = []; + if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { + $output->writeLineFormatted('Extension installer:'); + if (count(GeneratedConfig::EXTENSIONS) === 0) { + $output->writeLineFormatted('No extensions installed'); + } + + $generatedConfigReflection = new ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); + $generatedConfigDirectory = dirname($generatedConfigReflection->getFileName()); + foreach (GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { + $output->writeLineFormatted(sprintf('%s: %s', $name, $extensionConfig['version'] ?? 'Unknown version')); + foreach ($extensionConfig['extra']['includes'] ?? [] as $includedFile) { + $includedFilePath = null; + if (isset($extensionConfig['relative_install_path'])) { + $includedFilePath = sprintf('%s/%s/%s', $generatedConfigDirectory, $extensionConfig['relative_install_path'], $includedFile); + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { + $includedFilePath = null; + } + } + + if ($includedFilePath === null) { + $includedFilePath = sprintf('%s/%s', $extensionConfig['install_path'], $includedFile); + } + + $configFilesFromExtensionInstaller[] = $this->fileHelper->normalizePath($includedFilePath, '/'); + } + } + } else { + $output->writeLineFormatted('Extension installer: Not installed'); + } + $output->writeLineFormatted(''); + + $thirdPartyIncludedConfigs = []; + foreach ($this->allConfigFiles as $configFile) { + $configFile = $this->fileHelper->normalizePath($configFile, '/'); + if (in_array($configFile, $configFilesFromExtensionInstaller, true)) { + continue; + } + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $composerConfig = ComposerHelper::getComposerConfig($composerAutoloaderProjectPath); + if ($composerConfig === null) { + continue; + } + $vendorDir = $this->fileHelper->normalizePath(ComposerHelper::getVendorDirFromComposerConfig($composerAutoloaderProjectPath, $composerConfig), '/'); + if (!str_starts_with($configFile, $vendorDir)) { + continue; + } + + $installedPath = $vendorDir . '/composer/installed.php'; + if (!is_file($installedPath)) { + continue; + } + + $installed = require $installedPath; + + $trimmed = substr($configFile, strlen($vendorDir) + 1); + $parts = explode('/', $trimmed); + $package = implode('/', array_slice($parts, 0, 2)); + $configPath = implode('/', array_slice($parts, 2)); + if (!array_key_exists($package, $installed['versions'])) { + continue; + } + + $packageVersion = $installed['versions'][$package]['pretty_version'] ?? null; + if ($packageVersion === null) { + continue; + } + + $thirdPartyIncludedConfigs[] = [$package, $packageVersion, $configPath]; + } + } + + if (count($thirdPartyIncludedConfigs) > 0) { + $output->writeLineFormatted('Included configs from Composer packages:'); + foreach ($thirdPartyIncludedConfigs as [$package, $packageVersion, $configPath]) { + $output->writeLineFormatted(sprintf('%s (%s): %s', $package, $configPath, $packageVersion)); + } + $output->writeLineFormatted(''); + } + + $composerAutoloaderProjectPathsCount = count($this->composerAutoloaderProjectPaths); + $output->writeLineFormatted(sprintf( + 'Discovered Composer project %s:', + $composerAutoloaderProjectPathsCount === 1 ? 'root' : 'roots', + )); + if ($composerAutoloaderProjectPathsCount === 0) { + $output->writeLineFormatted('None'); + } + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $output->writeLineFormatted($composerAutoloaderProjectPath); + } + $output->writeLineFormatted(''); + } + +} diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php index 2ea5271d5d..568f7f1bbb 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -4,9 +4,12 @@ use function fnmatch; use function in_array; +use function is_dir; +use function is_file; use function preg_match; use function str_starts_with; use function strlen; +use function substr; use const DIRECTORY_SEPARATOR; use const FNM_CASEFOLD; use const FNM_NOESCAPE; @@ -15,12 +18,26 @@ class FileExcluder { /** - * Directories to exclude from analysing + * Paths to exclude from analysing * * @var string[] */ private array $literalAnalyseExcludes = []; + /** + * Directories to exclude from analysing + * + * @var string[] + */ + private array $literalAnalyseDirectoryExcludes = []; + + /** + * Files to exclude from analysing + * + * @var string[] + */ + private array $literalAnalyseFilesExcludes = []; + /** * fnmatch() patterns to use for excluding files and directories from analysing * @var string[] @@ -35,6 +52,7 @@ class FileExcluder public function __construct( FileHelper $fileHelper, array $analyseExcludes, + private bool $noImplicitWildcard, ) { foreach ($analyseExcludes as $exclude) { @@ -47,10 +65,22 @@ public function __construct( $normalized .= DIRECTORY_SEPARATOR; } - if ($this->isFnmatchPattern($normalized)) { + if (self::isFnmatchPattern($normalized)) { $this->fnmatchAnalyseExcludes[] = $normalized; } else { - $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized); + if ($this->noImplicitWildcard) { + if (is_file($normalized)) { + $this->literalAnalyseFilesExcludes[] = $normalized; + } elseif (is_dir($normalized)) { + if (!$trailingDirSeparator) { + $normalized .= DIRECTORY_SEPARATOR; + } + + $this->literalAnalyseDirectoryExcludes[] = $normalized; + } + } else { + $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized); + } } } @@ -69,6 +99,18 @@ public function isExcludedFromAnalysing(string $file): bool return true; } } + if ($this->noImplicitWildcard) { + foreach ($this->literalAnalyseDirectoryExcludes as $exclude) { + if (str_starts_with($file, $exclude)) { + return true; + } + } + foreach ($this->literalAnalyseFilesExcludes as $exclude) { + if ($file === $exclude) { + return true; + } + } + } foreach ($this->fnmatchAnalyseExcludes as $exclude) { if (fnmatch($exclude, $file, $this->fnmatchFlags)) { return true; @@ -78,7 +120,20 @@ public function isExcludedFromAnalysing(string $file): bool return false; } - private function isFnmatchPattern(string $path): bool + public static function isAbsolutePath(string $path): bool + { + if (DIRECTORY_SEPARATOR === '/') { + if (str_starts_with($path, '/')) { + return true; + } + } elseif (substr($path, 1, 1) === ':') { + return true; + } + + return false; + } + + public static function isFnmatchPattern(string $path): bool { return preg_match('~[*?[\]]~', $path) > 0; } diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index 33f4878cb4..06bca961f3 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -9,6 +9,7 @@ use function ltrim; use function preg_match; use function rtrim; +use function str_ends_with; use function str_replace; use function str_starts_with; use function strlen; @@ -36,14 +37,13 @@ public function getWorkingDirectory(): string public function absolutizePath(string $path): string { if (DIRECTORY_SEPARATOR === '/') { - if (substr($path, 0, 1) === '/') { - return $path; - } - } else { - if (substr($path, 1, 1) === ':') { + if (str_starts_with($path, '/')) { return $path; } + } elseif (substr($path, 1, 1) === ':') { + return $path; } + if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) { return $path; } @@ -87,12 +87,10 @@ public function normalizePath(string $originalPath, string $directorySeparator = continue; } if ($pathPart === '..') { - /** @var string $removedPart */ $removedPart = array_pop($normalizedPathParts); - if ($scheme === 'phar' && substr($removedPart, -5) === '.phar') { + if ($scheme === 'phar' && $removedPart !== null && str_ends_with($removedPart, '.phar')) { $scheme = null; } - } else { $normalizedPathParts[] = $pathPart; } diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index 3654c10c08..dc2d44e505 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -40,7 +40,7 @@ public function __construct( $pathBeginning = null; $pathToTrimArray = null; $trimBeginning = static function (string $path): array { - if (substr($path, 0, 1) === '/') { + if (str_starts_with($path, '/')) { return [ '/', substr($path, 1), diff --git a/src/File/SystemAgnosticSimpleRelativePathHelper.php b/src/File/SystemAgnosticSimpleRelativePathHelper.php new file mode 100644 index 0000000000..d040956fb2 --- /dev/null +++ b/src/File/SystemAgnosticSimpleRelativePathHelper.php @@ -0,0 +1,26 @@ +fileHelper->getWorkingDirectory(); + if ($cwd !== '' && str_starts_with($filename, $cwd)) { + return substr($filename, strlen($cwd) + 1); + } + + return $filename; + } + +} diff --git a/src/Internal/ComposerHelper.php b/src/Internal/ComposerHelper.php index 322d7f1048..e1995bc34d 100644 --- a/src/Internal/ComposerHelper.php +++ b/src/Internal/ComposerHelper.php @@ -17,6 +17,8 @@ final class ComposerHelper { + public const UNKNOWN_VERSION = 'Unknown version'; + private static ?string $phpstanVersion = null; /** @return array|null */ @@ -75,7 +77,7 @@ public static function getPhpStanVersion(): string $installed = require __DIR__ . '/../../vendor/composer/installed.php'; $rootPackage = $installed['root'] ?? null; if ($rootPackage === null) { - return self::$phpstanVersion = 'Unknown version'; + return self::$phpstanVersion = self::UNKNOWN_VERSION; } if (preg_match('/[^v\d.]/', $rootPackage['pretty_version']) === 0) { diff --git a/src/Node/AnonymousClassNode.php b/src/Node/AnonymousClassNode.php new file mode 100644 index 0000000000..b5d8333496 --- /dev/null +++ b/src/Node/AnonymousClassNode.php @@ -0,0 +1,32 @@ +getSubNodeNames() as $subNodeName) { + $subNodes[$subNodeName] = $node->$subNodeName; + } + + return new AnonymousClassNode( + $node->name, + $subNodes, + $node->getAttributes(), + ); + } + + public function isAnonymous(): bool + { + return true; + } + +} diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 5a3a5930e0..8d9659a68b 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -126,12 +126,10 @@ public function getUninitializedProperties( } $originalProperties[$property->getName()] = $property; $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); - if (!$is->yes()) { + if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { + $propertyReflection = $classReflection->getNativeProperty($property->getName()); + foreach ($extensions as $extension) { - if (!$classReflection->hasNativeProperty($property->getName())) { - continue; - } - $propertyReflection = $classReflection->getNativeProperty($property->getName()); if (!$extension->isInitialized($propertyReflection, $property->getName())) { continue; } @@ -154,15 +152,15 @@ public function getUninitializedProperties( return [$uninitializedProperties, [], []]; } - $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors); - $prematureAccess = []; - $additionalAssigns = []; - $initializedInConstructor = []; if ($classReflection->hasConstructor()) { $initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties)); } + $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor); + $prematureAccess = []; + $additionalAssigns = []; + foreach ($this->getPropertyUsages() as $usage) { $fetch = $usage->getFetch(); if (!$fetch instanceof PropertyFetch) { @@ -313,6 +311,8 @@ private function collectUninitializedProperties(array $constructors, array $unin * @param string[] $methods * @param array $initialInitializedProperties * @param array> $initializedProperties + * @param array $initializedInConstructorProperties + * * @return array> */ private function getMethodsCalledFromConstructor( @@ -320,10 +320,12 @@ private function getMethodsCalledFromConstructor( array $initialInitializedProperties, array $initializedProperties, array $methods, + array $initializedInConstructorProperties, ): array { $originalMap = $initializedProperties; $originalMethods = $methods; + foreach ($this->methodCalls as $methodCall) { $methodCallNode = $methodCall->getNode(); if ($methodCallNode instanceof Array_) { @@ -355,6 +357,12 @@ private function getMethodsCalledFromConstructor( continue; } + if ($inMethod->getName() !== '__construct') { + foreach ($initializedInConstructorProperties as $propertyName => $propertyNode) { + $initializedProperties[$inMethod->getName()][$propertyName] = TrinaryLogic::createYes(); + } + } + $methodName = $methodCallNode->name->toString(); if (array_key_exists($methodName, $initializedProperties)) { foreach ($this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties) as $propertyName => $isInitialized) { @@ -377,7 +385,7 @@ private function getMethodsCalledFromConstructor( return $initializedProperties; } - return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods); + return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods, $initializedInConstructorProperties); } /** diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index 904186224f..6b8b2438c3 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -18,6 +18,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\TypeUtils; +use ReflectionProperty; use function count; use function in_array; use function strtolower; @@ -157,6 +158,9 @@ private function gatherNodes(Node $node, Scope $scope): void } if ($node instanceof MethodCall || $node instanceof StaticCall) { $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope); + if ($node instanceof StaticCall && $node->name instanceof Identifier && $node->name->toLowerString() === '__construct') { + $this->tryToApplyPropertyWritesFromAncestorConstructor($node, $scope); + } return; } if ($node instanceof MethodCallableNode || $node instanceof StaticMethodCallableNode) { @@ -252,4 +256,28 @@ private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): voi } } + private function tryToApplyPropertyWritesFromAncestorConstructor(StaticCall $ancestorConstructorCall, Scope $scope): void + { + if (!$ancestorConstructorCall->class instanceof Node\Name) { + return; + } + + $calledOnType = $scope->resolveTypeByName($ancestorConstructorCall->class); + if ($calledOnType->getClassReflection() === null || TypeUtils::findThisType($calledOnType) === null) { + return; + } + + $classReflection = $calledOnType->getClassReflection()->getNativeReflection(); + foreach ($classReflection->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) { + if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + $this->propertyUsages[] = new PropertyWrite( + new PropertyFetch(new Expr\Variable('this'), new Identifier($property->getName()), $ancestorConstructorCall->getAttributes()), + $scope, + false, + ); + } + } + } diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 07eb1b9f95..0a20615ca8 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; use function count; @@ -20,6 +21,7 @@ class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatemen * @param list $returnStatements * @param list $yieldStatements * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( Closure $closureExpr, @@ -27,6 +29,7 @@ public function __construct( private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, ) { parent::__construct($closureExpr->getAttributes()); @@ -53,6 +56,11 @@ public function getExecutionEnds(): array return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getYieldStatements(): array { return $this->yieldStatements; diff --git a/src/Node/ExecutionEndNode.php b/src/Node/ExecutionEndNode.php index 225036a5ba..c0a0f52dd8 100644 --- a/src/Node/ExecutionEndNode.php +++ b/src/Node/ExecutionEndNode.php @@ -11,7 +11,7 @@ class ExecutionEndNode extends NodeAbstract implements VirtualNode { public function __construct( - private Node $node, + private Node\Stmt $node, private StatementResult $statementResult, private bool $hasNativeReturnTypehint, ) @@ -19,7 +19,7 @@ public function __construct( parent::__construct($node->getAttributes()); } - public function getNode(): Node + public function getNode(): Node\Stmt { return $this->node; } diff --git a/src/Node/Expr/ExistingArrayDimFetch.php b/src/Node/Expr/ExistingArrayDimFetch.php new file mode 100644 index 0000000000..80412b4fa3 --- /dev/null +++ b/src/Node/Expr/ExistingArrayDimFetch.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_ExistingArrayDimFetch'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/ParameterVariableOriginalValueExpr.php b/src/Node/Expr/ParameterVariableOriginalValueExpr.php new file mode 100644 index 0000000000..277d51c040 --- /dev/null +++ b/src/Node/Expr/ParameterVariableOriginalValueExpr.php @@ -0,0 +1,34 @@ +variableName; + } + + public function getType(): string + { + return 'PHPStan_Node_ParameterVariableOriginalValueExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php new file mode 100644 index 0000000000..52eb9a4b37 --- /dev/null +++ b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php @@ -0,0 +1,44 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getValue(): Expr + { + return $this->value; + } + + public function getType(): string + { + return 'PHPStan_Node_SetExistingOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/UnsetOffsetExpr.php b/src/Node/Expr/UnsetOffsetExpr.php new file mode 100644 index 0000000000..55c81eef92 --- /dev/null +++ b/src/Node/Expr/UnsetOffsetExpr.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_UnsetOffsetExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index 0b8f4b4494..86df35e24c 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -4,8 +4,10 @@ use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Function_; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; use PHPStan\Reflection\FunctionReflection; use function count; @@ -18,6 +20,7 @@ class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStateme * @param list $returnStatements * @param list $yieldStatements * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( private Function_ $function, @@ -25,6 +28,7 @@ public function __construct( private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, private FunctionReflection $functionReflection, ) { @@ -46,6 +50,11 @@ public function getExecutionEnds(): array return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->function->byRef; @@ -84,4 +93,12 @@ public function getFunctionReflection(): FunctionReflection return $this->functionReflection; } + /** + * @return Stmt[] + */ + public function getStatements(): array + { + return $this->function->getStmts(); + } + } diff --git a/src/Node/InvalidateExprNode.php b/src/Node/InvalidateExprNode.php new file mode 100644 index 0000000000..fa8ab6b061 --- /dev/null +++ b/src/Node/InvalidateExprNode.php @@ -0,0 +1,35 @@ +getAttributes()); + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): string + { + return 'PHPStan_Node_InvalidateExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index fe7aa691fb..3151d12ae3 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -4,11 +4,13 @@ use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use function count; /** @api */ @@ -21,6 +23,7 @@ class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatement * @param list $returnStatements * @param list $yieldStatements * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( ClassMethod $method, @@ -28,8 +31,9 @@ public function __construct( private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, private ClassReflection $classReflection, - private MethodReflection $methodReflection, + private ExtendedMethodReflection $methodReflection, ) { parent::__construct($method->getAttributes()); @@ -51,6 +55,11 @@ public function getExecutionEnds(): array return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->classMethod->byRef; @@ -76,11 +85,24 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } - public function getMethodReflection(): MethodReflection + public function getMethodReflection(): ExtendedMethodReflection { return $this->methodReflection; } + /** + * @return Stmt[] + */ + public function getStatements(): array + { + $stmts = $this->classMethod->getStmts(); + if ($stmts === null) { + return []; + } + + return $stmts; + } + public function isGenerator(): bool { return count($this->yieldStatements) > 0; diff --git a/src/Node/NoopExpressionNode.php b/src/Node/NoopExpressionNode.php new file mode 100644 index 0000000000..38e9222a8c --- /dev/null +++ b/src/Node/NoopExpressionNode.php @@ -0,0 +1,39 @@ +originalExpr->getAttributes()); + } + + public function getOriginalExpr(): Expr + { + return $this->originalExpr; + } + + public function hasAssign(): bool + { + return $this->hasAssign; + } + + public function getType(): string + { + return 'PHPStan_Node_NoopExpressionNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 5812d16927..953174fc70 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -4,13 +4,17 @@ use PhpParser\PrettyPrinter\Standard; use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\IssetExpr; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -33,6 +37,11 @@ protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $ return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); } + protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); @@ -43,6 +52,11 @@ protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $ return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr())); } + protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore + { + return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch())); @@ -53,6 +67,11 @@ protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $ return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue())); } + protected function pPHPStan_Node_SetExistingOffsetValueTypeExpr(SetExistingOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanSetExistingOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()), $this->p($expr->getValue())); + } + protected function pPHPStan_Node_AlwaysRememberedExpr(AlwaysRememberedExpr $expr): string // phpcs:ignore { return sprintf('__phpstanRembered(%s)', $this->p($expr->getExpr())); @@ -63,6 +82,11 @@ protected function pPHPStan_Node_PropertyInitializationExpr(PropertyInitializati return sprintf('__phpstanPropertyInitialization(%s)', $expr->getPropertyName()); } + protected function pPHPStan_Node_ParameterVariableOriginalValueExpr(ParameterVariableOriginalValueExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanParameterVariableOriginalValue(%s)', $expr->getVariableName()); + } + protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ignore { return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr())); diff --git a/src/Node/ReturnStatementsNode.php b/src/Node/ReturnStatementsNode.php index 8ba7687219..34c28ef538 100644 --- a/src/Node/ReturnStatementsNode.php +++ b/src/Node/ReturnStatementsNode.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; /** @api */ @@ -22,6 +23,11 @@ public function getStatementResult(): StatementResult; */ public function getExecutionEnds(): array; + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array; + public function returnsByRef(): bool; public function hasNativeReturnTypehint(): bool; diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php new file mode 100644 index 0000000000..2857e853ba --- /dev/null +++ b/src/Node/VariableAssignNode.php @@ -0,0 +1,48 @@ +getAttributes()); + } + + public function getVariable(): Expr\Variable + { + return $this->variable; + } + + public function getAssignedExpr(): Expr + { + return $this->assignedExpr; + } + + public function isAssignOp(): bool + { + return $this->assignOp; + } + + public function getType(): string + { + return 'PHPStan_Node_VariableAssignNodeNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 682331f887..65f8776c19 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -8,6 +8,7 @@ use Nette\Utils\Random; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; +use PHPStan\Analyser\InternalError; use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; use PHPStan\Process\ProcessHelper; @@ -25,7 +26,6 @@ use function count; use function defined; use function ini_get; -use function is_string; use function max; use function memory_get_usage; use function parse_url; @@ -54,6 +54,7 @@ public function __construct( /** * @param Closure(int ): void|null $postFileCallback * @param (callable(list, list, string[]): void)|null $onFileAnalysisHandler + * @return PromiseInterface */ public function analyse( LoopInterface $loop, @@ -70,7 +71,11 @@ public function analyse( $numberOfProcesses = $schedule->getNumberOfProcesses(); $someChildEnded = false; $errors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; $peakMemoryUsages = []; $internalErrors = []; $internalErrorsCount = 0; @@ -79,18 +84,29 @@ public function analyse( $reachedInternalErrorsCountLimit = false; $exportedNodes = []; + /** @var Deferred $deferred */ $deferred = new Deferred(); $server = new TcpServer('127.0.0.1:0', $loop); - $this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$locallyIgnoredErrors, &$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, &$exportedNodes, &$peakMemoryUsages): void { if (count($jobs) > 0 && $internalErrorsCount === 0) { - $internalErrors[] = 'Some parallel worker jobs have not finished.'; + $internalErrors[] = new InternalError( + 'Some parallel worker jobs have not finished.', + 'running parallel worker', + [], + null, + true, + ); $internalErrorsCount++; } $deferred->resolve(new AnalyserResult( $errors, + $filteredPhpErrors, + $allPhpErrors, $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, $internalErrors, $collectedData, $internalErrorsCount === 0 ? $dependencies : null, @@ -129,7 +145,13 @@ public function analyse( $serverPort = parse_url(http://github.com/$serverAddress,%20PHP_URL_PORT); $handleError = function (Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { - $internalErrors[] = sprintf('Internal error: ' . $error->getMessage()); + $internalErrors[] = new InternalError( + $error->getMessage(), + 'communicating with parallel worker', + InternalError::prepareTrace($error), + $error->getTraceAsString(), + !$error instanceof ProcessTimedOutException, + ); $internalErrorsCount++; $reachedInternalErrorsCountLimit = true; $this->processPool->quitAll(); @@ -155,16 +177,22 @@ public function analyse( $commandOptions, $input, ), $loop, $this->processTimeout); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$locallyIgnoredErrors, &$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, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler): void { $fileErrors = []; foreach ($json['errors'] as $jsonError) { - if (is_string($jsonError)) { - $internalErrors[] = sprintf('Internal error: %s', $jsonError); - continue; - } - $fileErrors[] = Error::decode($jsonError); } + foreach ($json['internalErrors'] as $internalJsonError) { + $internalErrors[] = InternalError::decode($internalJsonError); + } + + foreach ($json['filteredPhpErrors'] as $filteredPhpError) { + $filteredPhpErrors[] = Error::decode($filteredPhpError); + } + + foreach ($json['allPhpErrors'] as $allPhpError) { + $allPhpErrors[] = Error::decode($allPhpError); + } $locallyIgnoredFileErrors = []; foreach ($json['locallyIgnoredErrors'] as $locallyIgnoredJsonError) { @@ -195,6 +223,20 @@ public function analyse( $dependencies[$file] = $fileDependencies; } + foreach ($json['linesToIgnore'] as $file => $fileLinesToIgnore) { + if (count($fileLinesToIgnore) === 0) { + continue; + } + $linesToIgnore[$file] = $fileLinesToIgnore; + } + + foreach ($json['unmatchedLineIgnores'] as $file => $fileUnmatchedLineIgnores) { + if (count($fileUnmatchedLineIgnores) === 0) { + continue; + } + $unmatchedLineIgnores[$file] = $fileUnmatchedLineIgnores; + } + /** * @var string $file * @var array $fileExportedNodes @@ -248,23 +290,23 @@ public function analyse( $memoryLimitMessage = 'PHPStan process crashed because it reached configured PHP memory limit'; if (str_contains($output, $memoryLimitMessage)) { foreach ($internalErrors as $internalError) { - if (!str_contains($internalError, $memoryLimitMessage)) { + if (!str_contains($internalError->getMessage(), $memoryLimitMessage)) { continue; } return; } - $internalErrors[] = sprintf(sprintf( - "Child process error: %s: %s\n%s\n", + $internalErrors[] = new InternalError(sprintf( + "Child process error: %s: %s\n%s\n", $memoryLimitMessage, ini_get('memory_limit'), 'Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.', - )); + ), 'running parallel worker', [], null, false); $internalErrorsCount++; return; } - $internalErrors[] = sprintf('Child process error (exit code %d): %s', $exitCode, $output); + $internalErrors[] = new InternalError(sprintf('Child process error (exit code %d): %s', $exitCode, $output), 'running parallel worker', [], null, true); $internalErrorsCount++; }); $this->processPool->attachProcess($processIdentifier, $process); diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index 34fc0dcc5c..d95609b6ef 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -2,7 +2,6 @@ namespace PHPStan\Parallel; -use Exception; use PHPStan\ShouldNotHappenException; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; @@ -112,7 +111,7 @@ public function request(array $data): void $this->in->write($data); $this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void { $onError = $this->onError; - $onError(new Exception(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); + $onError(new ProcessTimedOutException(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); }); } diff --git a/src/Parallel/ProcessTimedOutException.php b/src/Parallel/ProcessTimedOutException.php new file mode 100644 index 0000000000..77e42107f7 --- /dev/null +++ b/src/Parallel/ProcessTimedOutException.php @@ -0,0 +1,10 @@ +maximumNumberOfProcesses), $jobs); + $usedNumberOfProcesses = min($numberOfProcesses, $this->maximumNumberOfProcesses); + $this->storedData = [$cpuCores, count($files), count($jobs), $usedNumberOfProcesses]; + + return new Schedule($usedNumberOfProcesses, $jobs); + } + + public function print(Output $output): void + { + if ($this->storedData === null) { + return; + } + + [$cpuCores, $filesCount, $jobsCount, $usedNumberOfProcesses] = $this->storedData; + + $output->writeLineFormatted('Parallel processing scheduler:'); + $output->writeLineFormatted(sprintf( + '# of detected CPU %s: %s%d', + $cpuCores === 1 ? 'core' : 'cores', + $cpuCores === 1 ? '' : ' ', + $cpuCores, + )); + $output->writeLineFormatted(sprintf('# of analysed files: %d', $filesCount)); + $output->writeLineFormatted(sprintf('# of jobs: %d', $jobsCount)); + $output->writeLineFormatted(sprintf('# of spawned processes: %d', $usedNumberOfProcesses)); + $output->writeLineFormatted(''); } } diff --git a/src/Parser/AnonymousClassVisitor.php b/src/Parser/AnonymousClassVisitor.php new file mode 100644 index 0000000000..33ca16c567 --- /dev/null +++ b/src/Parser/AnonymousClassVisitor.php @@ -0,0 +1,52 @@ +> */ + private array $nodesPerLine = []; + + public function beforeTraverse(array $nodes): ?array + { + $this->nodesPerLine = []; + return null; + } + + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt\Class_ || !$node->isAnonymous()) { + return null; + } + + $node = AnonymousClassNode::createFromClassNode($node); + $node->setAttribute('anonymousClass', true); // We keep this for backward compatibility + $this->nodesPerLine[$node->getStartLine()][] = $node; + + return $node; + } + + public function afterTraverse(array $nodes): ?array + { + foreach ($this->nodesPerLine as $nodesOnLine) { + if (count($nodesOnLine) === 1) { + continue; + } + for ($i = 0; $i < count($nodesOnLine); $i++) { + $nodesOnLine[$i]->setAttribute(self::ATTRIBUTE_LINE_INDEX, $i + 1); + } + } + + $this->nodesPerLine = []; + return null; + } + +} diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php new file mode 100644 index 0000000000..de341a57d9 --- /dev/null +++ b/src/Parser/ClosureBindArgVisitor.php @@ -0,0 +1,33 @@ +class instanceof Node\Name + && $node->class->toLowerString() === 'closure' + && $node->name instanceof Identifier + && $node->name->toLowerString() === 'bind' + && !$node->isFirstClassCallable() + ) { + $args = $node->getArgs(); + if (count($args) > 1) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + return null; + } + +} diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php new file mode 100644 index 0000000000..f3582ba617 --- /dev/null +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -0,0 +1,30 @@ +name instanceof Identifier + && $node->name->toLowerString() === 'bindto' + && !$node->isFirstClassCallable() + ) { + $args = $node->getArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, $node->var); + } + } + return null; + } + +} diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index dbae41eacc..1013be73d4 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -22,7 +22,7 @@ public function __construct( private ?string $parsedFile, ) { - parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getMessage(), $errors))); + parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getRawMessage(), $errors))); if (count($errors) > 0) { $this->attributes = $errors[0]->getAttributes(); } else { diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index 436416bcf9..def6786f5e 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -4,7 +4,14 @@ use PHPStan\File\FileHelper; use function array_fill_keys; +use function array_slice; +use function count; +use function explode; +use function implode; +use function is_link; +use function realpath; use function str_contains; +use const DIRECTORY_SEPARATOR; class PathRoutingParser implements Parser { @@ -41,6 +48,24 @@ public function parseFile(string $file): array $file = $this->fileHelper->normalizePath($file); if (!isset($this->analysedFiles[$file])) { + // check symlinked file that still might be in analysedFiles + $pathParts = explode(DIRECTORY_SEPARATOR, $file); + for ($i = count($pathParts); $i > 1; $i--) { + $joinedPartOfPath = implode(DIRECTORY_SEPARATOR, array_slice($pathParts, 0, $i)); + if (!@is_link($joinedPartOfPath)) { + continue; + } + + $realFilePath = realpath($file); + if ($realFilePath !== false) { + $normalizedRealFilePath = $this->fileHelper->normalizePath($realFilePath); + if (isset($this->analysedFiles[$normalizedRealFilePath])) { + return $this->currentPhpVersionRichParser->parseFile($file); + } + } + break; + } + return $this->currentPhpVersionSimpleParser->parseFile($file); } diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index b21729086d..11281ca7fd 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -13,8 +13,7 @@ use PHPStan\File\FileReader; use PHPStan\ShouldNotHappenException; use function array_filter; -use function array_pop; -use function array_values; +use function array_map; use function count; use function implode; use function in_array; @@ -278,51 +277,66 @@ private function getLinesToIgnoreForTokenByIgnoreComment( private function parseIdentifiers(string $text, int $ignorePos): array { $text = substr($text, $ignorePos + strlen('@phpstan-ignore')); - $tokens = $this->ignoreLexer->tokenize($text); - $tokens = array_values(array_filter($tokens, static fn (array $token) => !in_array($token[IgnoreLexer::TYPE_OFFSET], [IgnoreLexer::TOKEN_WHITESPACE, IgnoreLexer::TOKEN_EOL], true))); + $originalTokens = $this->ignoreLexer->tokenize($text); + $tokens = []; + + foreach ($originalTokens as $originalToken) { + if ($originalToken[IgnoreLexer::TYPE_OFFSET] === IgnoreLexer::TOKEN_WHITESPACE) { + continue; + } + $tokens[] = $originalToken; + } + $c = count($tokens); $identifiers = []; - $depth = 0; - $parenthesisStack = []; + $openParenthesisCount = 0; + $expected = [IgnoreLexer::TOKEN_IDENTIFIER]; + for ($i = 0; $i < $c; $i++) { + $lastTokenTypeLabel = isset($tokenType) ? $this->ignoreLexer->getLabel($tokenType) : '@phpstan-ignore'; [IgnoreLexer::VALUE_OFFSET => $content, IgnoreLexer::TYPE_OFFSET => $tokenType, IgnoreLexer::LINE_OFFSET => $tokenLine] = $tokens[$i]; - if ($tokenType === IgnoreLexer::TOKEN_IDENTIFIER && $depth === 0) { - $identifiers[] = $content; - if (isset($tokens[$i + 1])) { - if ($tokens[$i + 1][IgnoreLexer::TYPE_OFFSET] === IgnoreLexer::TOKEN_COMMA) { - $i++; - } - } - continue; - } - if ($i === 0) { - throw new IgnoreParseException('First token is not an identifier', $tokenLine); + + if ($expected !== null && !in_array($tokenType, $expected, true)) { + $tokenTypeLabel = $this->ignoreLexer->getLabel($tokenType); + $otherTokenContent = $tokenType === IgnoreLexer::TOKEN_OTHER ? sprintf(" '%s'", $content) : ''; + $expectedLabels = implode(' or ', array_map(fn ($token) => $this->ignoreLexer->getLabel($token), $expected)); + + throw new IgnoreParseException(sprintf('Unexpected %s%s after %s, expected %s', $tokenTypeLabel, $otherTokenContent, $lastTokenTypeLabel, $expectedLabels), $tokenLine); } - if ($tokenType === IgnoreLexer::TOKEN_COMMA) { - throw new IgnoreParseException('Unexpected comma (,)', $tokenLine); + + if ($tokenType === IgnoreLexer::TOKEN_OPEN_PARENTHESIS) { + $openParenthesisCount++; + $expected = null; + continue; } + if ($tokenType === IgnoreLexer::TOKEN_CLOSE_PARENTHESIS) { - if ($depth < 1) { - throw new IgnoreParseException('Closing parenthesis ")" before opening parenthesis "("', $tokenLine); + $openParenthesisCount--; + if ($openParenthesisCount === 0) { + $expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END]; } + continue; + } - $depth--; - array_pop($parenthesisStack); - if ($depth === 0) { - break; - } + if ($openParenthesisCount > 0) { + continue; // waiting for comment end } - if ($tokenType !== IgnoreLexer::TOKEN_OPEN_PARENTHESIS) { + + if ($tokenType === IgnoreLexer::TOKEN_IDENTIFIER) { + $identifiers[] = $content; + $expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END, IgnoreLexer::TOKEN_OPEN_PARENTHESIS]; continue; } - $depth++; - $parenthesisStack[] = $tokenLine; + if ($tokenType === IgnoreLexer::TOKEN_COMMA) { + $expected = [IgnoreLexer::TOKEN_IDENTIFIER]; + continue; + } } - if (count($parenthesisStack) > 0) { - throw new IgnoreParseException('Unclosed opening parenthesis "(" without closing parenthesis ")"', $parenthesisStack[count($parenthesisStack) - 1]); + if ($openParenthesisCount > 0) { + throw new IgnoreParseException('Unexpected end, unclosed opening parenthesis', $tokenLine ?? 1); } if (count($identifiers) === 0) { diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 2ca9954a25..8aa5e2435b 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -8,8 +8,30 @@ class PhpVersion { - public function __construct(private int $versionId) + public const SOURCE_RUNTIME = 1; + public const SOURCE_CONFIG = 2; + public const SOURCE_COMPOSER_PLATFORM_PHP = 3; + public const SOURCE_UNKNOWN = 4; + + /** + * @param self::SOURCE_* $source + */ + public function __construct(private int $versionId, private int $source = self::SOURCE_UNKNOWN) + { + } + + public function getSourceLabel(): string { + switch ($this->source) { + case self::SOURCE_RUNTIME: + return 'runtime'; + case self::SOURCE_CONFIG: + return 'config'; + case self::SOURCE_COMPOSER_PLATFORM_PHP: + return 'config.platform.php in composer.json'; + } + + return 'unknown'; } public function getVersionId(): int @@ -56,6 +78,16 @@ public function deprecatesRequiredParameterAfterOptional(): bool return $this->versionId >= 80000; } + public function deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull(): bool + { + return $this->versionId >= 80100; + } + + public function deprecatesRequiredParameterAfterOptionalUnionOrMixed(): bool + { + return $this->versionId >= 80300; + } + public function supportsLessOverridenParametersWithVariadic(): bool { return $this->versionId >= 80000; @@ -277,4 +309,30 @@ public function supportsNeverReturnTypeInArrowFunction(): bool return $this->versionId >= 80200; } + public function supportsPregUnmatchedAsNull(): bool + { + // while PREG_UNMATCHED_AS_NULL is defined in php-src since 7.2.x it starts working as expected with 7.4.x + // https://3v4l.org/v3HE4 + return $this->versionId >= 70400; + } + + public function supportsPregCaptureOnlyNamedGroups(): bool + { + // https://php.watch/versions/8.2/preg-n-no-capture-modifier + return $this->versionId >= 80200; + } + + public function hasDateTimeExceptions(): bool + { + return $this->versionId >= 80300; + } + + public function isCurloptUrlCheckingFileSchemeWithOpenBasedir(): bool + { + // Before PHP 8.0, when setting CURLOPT_URL, an unparsable URL or a file:// scheme would fail if open_basedir is used + // https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158 + // https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L128-L130 + return $this->versionId < 80000; + } + } diff --git a/src/Php/PhpVersionFactory.php b/src/Php/PhpVersionFactory.php index 2ed8fe6781..02c0c8b749 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -20,18 +20,20 @@ public function __construct( public function create(): PhpVersion { $versionId = $this->versionId; - if ($versionId === null && $this->composerPhpVersion !== null) { + if ($versionId !== null) { + $source = PhpVersion::SOURCE_CONFIG; + } elseif ($this->composerPhpVersion !== null) { $parts = explode('.', $this->composerPhpVersion); $tmp = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); $tmp = max($tmp, 70100); $versionId = min($tmp, 80399); - } - - if ($versionId === null) { + $source = PhpVersion::SOURCE_COMPOSER_PLATFORM_PHP; + } else { $versionId = PHP_VERSION_ID; + $source = PhpVersion::SOURCE_RUNTIME; } - return new PhpVersion($versionId); + return new PhpVersion($versionId, $source); } } diff --git a/src/PhpDoc/DefaultStubFilesProvider.php b/src/PhpDoc/DefaultStubFilesProvider.php index da52332a1f..15919f8430 100644 --- a/src/PhpDoc/DefaultStubFilesProvider.php +++ b/src/PhpDoc/DefaultStubFilesProvider.php @@ -20,11 +20,12 @@ class DefaultStubFilesProvider implements StubFilesProvider /** * @param string[] $stubFiles + * @param string[] $composerAutoloaderProjectPaths */ public function __construct( private Container $container, private array $stubFiles, - private string $currentWorkingDirectory, + private array $composerAutoloaderProjectPaths, ) { } @@ -52,19 +53,22 @@ public function getProjectStubFiles(): array return $this->cachedProjectFiles; } - $composerConfig = ComposerHelper::getComposerConfig($this->currentWorkingDirectory); + $filteredStubFiles = $this->getStubFiles(); + foreach ($this->composerAutoloaderProjectPaths as $composerAutoloaderProjectPath) { + $composerConfig = ComposerHelper::getComposerConfig($composerAutoloaderProjectPath); + if ($composerConfig === null) { + continue; + } - if ($composerConfig === null) { - return $this->getStubFiles(); + $vendorDir = ComposerHelper::getVendorDirFromComposerConfig($composerAutoloaderProjectPath, $composerConfig); + $vendorDir = strtr($vendorDir, '\\', '/'); + $filteredStubFiles = array_filter( + $filteredStubFiles, + static fn (string $file): bool => !str_contains(strtr($file, '\\', '/'), $vendorDir) + ); } - $vendorDir = ComposerHelper::getVendorDirFromComposerConfig($this->currentWorkingDirectory, $composerConfig); - $vendorDir = strtr($vendorDir, '\\', '/'); - - return $this->cachedProjectFiles = array_values(array_filter( - $this->getStubFiles(), - static fn (string $file): bool => !str_contains(strtr($file, '\\', '/'), $vendorDir) - )); + return $this->cachedProjectFiles = array_values($filteredStubFiles); } } diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index a0b34db3e0..0e909dd96a 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -119,7 +119,7 @@ private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $t $classReflection = $phpDocBlock->getClassReflection(); if ($functionName !== null && $classReflection->getNativeReflection()->hasMethod($functionName)) { $methodReflection = $classReflection->getNativeReflection()->getMethod($functionName); - $stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); + $stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); if ($stub !== null) { return $stub; } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 57eaa65279..c11c26d7b0 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -11,6 +11,7 @@ use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MethodTagParameter; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; @@ -30,6 +31,9 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\Reflection\PassedByReference; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -62,7 +66,7 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar { $resolved = []; $resolvedByTag = []; - foreach (['@var', '@psalm-var', '@phpstan-var'] as $tagName) { + foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) { $tagResolved = []; foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -157,9 +161,29 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; + $originalNameScope = $nameScope; - foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) { + foreach (['@method', '@phan-method', '@psalm-method', '@phpstan-method'] as $tagName) { foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) { + $nameScope = $originalNameScope; + $templateTags = []; + + if (count($tagValue->templateTypes) > 0 && $nameScope->getClassName() !== null) { + foreach ($tagValue->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant(), + ); + } + + $templateTypeScope = TemplateTypeScope::createWithMethod($nameScope->getClassName(), $tagValue->methodName); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } + $parameters = []; foreach ($tagValue->parameters as $parameterNode) { $parameterName = substr($parameterNode->parameterName, 1); @@ -191,6 +215,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): : new MixedType(), $tagValue->isStatic, $parameters, + $templateTags, ); } } @@ -205,7 +230,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) { $resolved = []; - foreach (['@extends', '@template-extends', '@phpstan-extends'] as $tagName) { + foreach (['@extends', '@phan-extends', '@phan-inherits', '@template-extends', '@phpstan-extends'] as $tagName) { foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) { $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( $this->typeNodeResolver->resolve($tagValue->type, $nameScope), @@ -262,8 +287,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope $prefixPriority = [ '' => 0, - 'psalm' => 1, - 'phpstan' => 2, + 'phan' => 1, + 'psalm' => 2, + 'phpstan' => 3, ]; foreach ($phpDocNode->getTags() as $phpDocTagNode) { @@ -273,7 +299,7 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } $tagName = $phpDocTagNode->name; - if (in_array($tagName, ['@template', '@psalm-template', '@phpstan-template'], true)) { + if (in_array($tagName, ['@template', '@phan-template', '@psalm-template', '@phpstan-template'], true)) { $variance = TemplateTypeVariance::createInvariant(); } elseif (in_array($tagName, ['@template-covariant', '@psalm-template-covariant', '@phpstan-template-covariant'], true)) { $variance = TemplateTypeVariance::createCovariant(); @@ -283,7 +309,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope continue; } - if (str_starts_with($tagName, '@psalm-')) { + if (str_starts_with($tagName, '@phan-')) { + $prefix = 'phan'; + } elseif (str_starts_with($tagName, '@psalm-')) { $prefix = 'psalm'; } elseif (str_starts_with($tagName, '@phpstan-')) { $prefix = 'phpstan'; @@ -316,7 +344,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): { $resolved = []; - foreach (['@param', '@psalm-param', '@phpstan-param'] as $tagName) { + foreach (['@param', '@phan-param', '@psalm-param', '@phpstan-param'] as $tagName) { foreach ($phpDocNode->getParamTagValues($tagName) as $tagValue) { $parameterName = substr($tagValue->parameterName, 1); $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -362,11 +390,49 @@ public function resolveParamOutTags(PhpDocNode $phpDocNode, NameScope $nameScope return $resolved; } + /** + * @return array + */ + public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): array + { + $parameters = []; + foreach (['@param-immediately-invoked-callable', '@phpstan-param-immediately-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamImmediatelyInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = true; + } + } + foreach (['@param-later-invoked-callable', '@phpstan-param-later-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamLaterInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = false; + } + } + + return $parameters; + } + + /** + * @return array + */ + public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $closureThisTypes = []; + foreach (['@param-closure-this', '@phpstan-param-closure-this'] as $tagName) { + foreach ($phpDocNode->getParamClosureThisTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $closureThisTypes[$parameterName] = new ParamClosureThisTag($this->typeNodeResolver->resolve($tagValue->type, $nameScope)); + } + } + + return $closureThisTypes; + } + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; - foreach (['@return', '@psalm-return', '@phpstan-return'] as $tagName) { + foreach (['@return', '@phan-return', '@phan-real-return', '@psalm-return', '@phpstan-return'] as $tagName) { foreach ($phpDocNode->getReturnTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); if ($this->shouldSkipType($tagName, $type)) { @@ -454,7 +520,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop { $resolved = []; - foreach (['@psalm-type', '@phpstan-type'] as $tagName) { + foreach (['@phan-type', '@psalm-type', '@phpstan-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { $alias = $typeAliasTagValue->alias; $typeNode = $typeAliasTagValue->type; @@ -489,7 +555,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na */ public function resolveAssertTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { - foreach (['@phpstan', '@psalm'] as $prefix) { + foreach (['@phpstan', '@psalm', '@phan'] as $prefix) { $resolved = array_merge( $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert', AssertTag::NULL), $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-true', AssertTag::IF_TRUE), @@ -515,19 +581,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); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, 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); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, 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); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, true); } return $resolved; @@ -590,7 +656,7 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool public function resolveIsPure(PhpDocNode $phpDocNode): bool { foreach ($phpDocNode->getTags() as $phpDocTagNode) { - if (in_array($phpDocTagNode->name, ['@pure', '@psalm-pure', '@phpstan-pure'], true)) { + if (in_array($phpDocTagNode->name, ['@pure', '@phan-pure', '@phan-side-effect-free', '@psalm-pure', '@phpstan-pure'], true)) { return true; } } @@ -611,7 +677,7 @@ public function resolveIsImpure(PhpDocNode $phpDocNode): bool public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool { - foreach (['@readonly', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { + foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { $tags = $phpDocNode->getTagsByName($tagName); if (count($tags) > 0) { @@ -624,7 +690,7 @@ public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool public function resolveIsImmutable(PhpDocNode $phpDocNode): bool { - foreach (['@immutable', '@psalm-immutable', '@phpstan-immutable'] as $tagName) { + foreach (['@immutable', '@phan-immutable', '@psalm-immutable', '@phpstan-immutable'] as $tagName) { $tags = $phpDocNode->getTagsByName($tagName); if (count($tags) > 0) { diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 1dc8b5d13e..4783a8f710 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -9,6 +9,7 @@ use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; @@ -89,6 +90,12 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramOutTags = false; + /** @var array|false */ + private array|false $paramsImmediatelyInvokedCallable = false; + + /** @var array|false */ + private array|false $paramClosureThisTags = false; + private ReturnTag|false|null $returnTag = false; private ThrowsTag|false|null $throwsTag = false; @@ -154,7 +161,7 @@ public static function create( ReflectionProvider $reflectionProvider, ): self { - // new property also needs to be added to createEmpty() and merge() + // new property also needs to be added to withNameScope(), createEmpty() and merge() $self = new self(); $self->phpDocNode = $phpDocNode; $self->phpDocNodes = [$phpDocNode]; @@ -169,6 +176,22 @@ public static function create( return $self; } + public function withNameScope(NameScope $nameScope): self + { + $self = new self(); + $self->phpDocNode = $this->phpDocNode; + $self->phpDocNodes = $this->phpDocNodes; + $self->phpDocString = $this->phpDocString; + $self->filename = $this->filename; + $self->nameScope = $nameScope; + $self->templateTypeMap = $this->templateTypeMap; + $self->templateTags = $this->templateTags; + $self->phpDocNodeResolver = $this->phpDocNodeResolver; + $self->reflectionProvider = $this->reflectionProvider; + + return $self; + } + public static function createEmpty(): self { // new property also needs to be added to merge() @@ -186,6 +209,8 @@ public static function createEmpty(): self $self->usesTags = []; $self->paramTags = []; $self->paramOutTags = []; + $self->paramsImmediatelyInvokedCallable = []; + $self->paramClosureThisTags = []; $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; @@ -248,6 +273,8 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->usesTags = $this->getUsesTags(); $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); + $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks); + $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks); $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); $result->mixinTags = $this->getMixinTags(); @@ -262,7 +289,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); - $result->isPure = $this->isPure(); + $result->isPure = self::mergePureTags($this->isPure(), $parents); $result->isReadOnly = $this->isReadOnly(); $result->isImmutable = $this->isImmutable(); $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); @@ -281,38 +308,59 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $this; } - $paramTags = $this->getParamTags(); + $mapParameterCb = static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $parameterNameMapping)) { + $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]); + } + } + + return $traverse($type); + }; $newParamTags = []; - foreach ($paramTags as $key => $paramTag) { + foreach ($this->getParamTags() as $key => $paramTag) { if (!array_key_exists($key, $parameterNameMapping)) { continue; } - $newParamTags[$parameterNameMapping[$key]] = $paramTag; + $transformedType = TypeTraverser::map($paramTag->getType(), $mapParameterCb); + $newParamTags[$parameterNameMapping[$key]] = $paramTag->withType($transformedType); } - $paramOutTags = $this->getParamOutTags(); - $newParamOutTags = []; - foreach ($paramOutTags as $key => $paramOutTag) { + foreach ($this->getParamOutTags() as $key => $paramOutTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $transformedType = TypeTraverser::map($paramOutTag->getType(), $mapParameterCb); + $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag->withType($transformedType); + } + + $newParamsImmediatelyInvokedCallable = []; + foreach ($this->getParamsImmediatelyInvokedCallable() as $key => $immediatelyInvokedCallable) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $newParamsImmediatelyInvokedCallable[$parameterNameMapping[$key]] = $immediatelyInvokedCallable; + } + + $paramClosureThisTags = $this->getParamClosureThisTags(); + $newParamClosureThisTags = []; + foreach ($paramClosureThisTags as $key => $paramClosureThisTag) { if (!array_key_exists($key, $parameterNameMapping)) { continue; } - $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag; + + $transformedType = TypeTraverser::map($paramClosureThisTag->getType(), $mapParameterCb); + $newParamClosureThisTags[$parameterNameMapping[$key]] = $paramClosureThisTag->withType($transformedType); } $returnTag = $this->getReturnTag(); if ($returnTag !== null) { - $transformedType = TypeTraverser::map($returnTag->getType(), static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { - if ($type instanceof ConditionalTypeForParameter) { - $parameterName = substr($type->getParameterName(), 1); - if (array_key_exists($parameterName, $parameterNameMapping)) { - $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]); - } - } - - return $traverse($type); - }); + $transformedType = TypeTraverser::map($returnTag->getType(), $mapParameterCb); $returnTag = $returnTag->withType($transformedType); } @@ -345,6 +393,8 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->usesTags = $this->usesTags; $self->paramTags = $newParamTags; $self->paramOutTags = $newParamOutTags; + $self->paramsImmediatelyInvokedCallable = $newParamsImmediatelyInvokedCallable; + $self->paramClosureThisTags = $newParamClosureThisTags; $self->returnTag = $returnTag; $self->throwsTag = $this->throwsTag; $self->mixinTags = $this->mixinTags; @@ -517,6 +567,33 @@ public function getParamOutTags(): array return $this->paramOutTags; } + /** + * @return array + */ + public function getParamsImmediatelyInvokedCallable(): array + { + if ($this->paramsImmediatelyInvokedCallable === false) { + $this->paramsImmediatelyInvokedCallable = $this->phpDocNodeResolver->resolveParamImmediatelyInvokedCallable($this->phpDocNode); + } + + return $this->paramsImmediatelyInvokedCallable; + } + + /** + * @return array + */ + public function getParamClosureThisTags(): array + { + if ($this->paramClosureThisTags === false) { + $this->paramClosureThisTags = $this->phpDocNodeResolver->resolveParamClosureThisTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->paramClosureThisTags; + } + public function getReturnTag(): ?ReturnTag { if (is_bool($this->returnTag)) { @@ -802,7 +879,6 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare } /** - * @param ResolvedPhpDocBlock $parent * @return array|null */ private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array @@ -831,7 +907,6 @@ private static function mergeParamTags(array $paramTags, array $parents, array $ /** * @param array $paramTags - * @param ResolvedPhpDocBlock $parent * @return array */ private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array @@ -843,7 +918,11 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, continue; } - $paramTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock, TemplateTypeVariance::createContravariant()); + $paramTags[$name] = self::resolveTemplateTypeInTag( + $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $phpDocBlock, + TemplateTypeVariance::createContravariant(), + ); } return $paramTags; @@ -931,8 +1010,12 @@ private static function mergeAssertTags(array $assertTags, array $parents, array $phpDocBlock = $parentPhpDocBlocks[$i]; return array_map( - static fn (AssertTag $assertTag) => $assertTag->withParameter( - $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag( + $assertTag->withParameter( + $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + )->toImplicit(), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), ), $result, ); @@ -1021,7 +1104,6 @@ private static function mergeParamOutTags(array $paramOutTags, array $parents, a /** * @param array $paramOutTags - * @param ResolvedPhpDocBlock $parent * @return array */ private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array @@ -1033,12 +1115,111 @@ private static function mergeOneParentParamOutTags(array $paramOutTags, self $pa continue; } - $paramOutTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock, TemplateTypeVariance::createCovariant()); + $paramOutTags[$name] = self::resolveTemplateTypeInTag( + $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), + ); } return $paramOutTags; } + /** + * @param array $paramsImmediatelyInvokedCallable + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamsImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsImmediatelyInvokedCallable = self::mergeOneParentParamImmediatelyInvokedCallable($paramsImmediatelyInvokedCallable, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsImmediatelyInvokedCallable; + } + + /** + * @param array $paramsImmediatelyInvokedCallable + * @return array + */ + private static function mergeOneParentParamImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentImmediatelyInvokedCallable = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsImmediatelyInvokedCallable()); + + foreach ($parentImmediatelyInvokedCallable as $name => $parentIsImmediatelyInvokedCallable) { + if (array_key_exists($name, $paramsImmediatelyInvokedCallable)) { + continue; + } + + $paramsImmediatelyInvokedCallable[$name] = $parentIsImmediatelyInvokedCallable; + } + + return $paramsImmediatelyInvokedCallable; + } + + /** + * @param array $paramsClosureThisTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamClosureThisTags(array $paramsClosureThisTags, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsClosureThisTags = self::mergeOneParentParamClosureThisTag($paramsClosureThisTags, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsClosureThisTags; + } + + /** + * @param array $paramsClosureThisTags + * @return array + */ + private static function mergeOneParentParamClosureThisTag(array $paramsClosureThisTags, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentClosureThisTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamClosureThisTags()); + + foreach ($parentClosureThisTags as $name => $parentParamClosureThisTag) { + if (array_key_exists($name, $paramsClosureThisTags)) { + continue; + } + + $paramsClosureThisTags[$name] = self::resolveTemplateTypeInTag( + $parentParamClosureThisTag->withType( + $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureThisTag->getType()), + ), + $phpDocBlock, + TemplateTypeVariance::createContravariant(), + ); + } + + return $paramsClosureThisTags; + } + + /** + * @param array $parents + */ + private static function mergePureTags(?bool $isPure, array $parents): ?bool + { + if ($isPure !== null) { + return $isPure; + } + + foreach ($parents as $parent) { + $parentIsPure = $parent->isPure(); + if ($parentIsPure === null) { + continue; + } + + return $parentIsPure; + } + + return null; + } + /** * @template T of TypedTag * @param T $tag diff --git a/src/PhpDoc/SocketSelectStubFilesExtension.php b/src/PhpDoc/SocketSelectStubFilesExtension.php new file mode 100644 index 0000000000..1113a10f89 --- /dev/null +++ b/src/PhpDoc/SocketSelectStubFilesExtension.php @@ -0,0 +1,23 @@ +phpVersion->getVersionId() >= 80000) { + return [__DIR__ . '/../../stubs/socket_select_php8.stub']; + } + + return [__DIR__ . '/../../stubs/socket_select.stub']; + } + +} diff --git a/src/PhpDoc/StubPhpDocProvider.php b/src/PhpDoc/StubPhpDocProvider.php index b42350184a..d00085e985 100644 --- a/src/PhpDoc/StubPhpDocProvider.php +++ b/src/PhpDoc/StubPhpDocProvider.php @@ -146,7 +146,12 @@ public function findClassConstantPhpDoc(string $className, string $constantName) /** * @param array $positionalParameterNames */ - public function findMethodPhpDoc(string $className, string $methodName, array $positionalParameterNames): ?ResolvedPhpDocBlock + public function findMethodPhpDoc( + string $className, + string $implementingClassName, + string $methodName, + array $positionalParameterNames, + ): ?ResolvedPhpDocBlock { if (!$this->isKnownClass($className)) { return null; @@ -170,6 +175,12 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p throw new ShouldNotHappenException(); } + if ($className !== $implementingClassName && $resolvedPhpDoc->getNullableNameScope() !== null) { + $resolvedPhpDoc = $resolvedPhpDoc->withNameScope( + $resolvedPhpDoc->getNullableNameScope()->withClassName($implementingClassName), + ); + } + $methodParameterNames = $this->knownMethodsParameterNames[$className][$methodName]; $parameterNameMapping = []; foreach ($positionalParameterNames as $i => $parameterName) { diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 6eb52f2e56..d0628f435c 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -4,6 +4,7 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\InternalError; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Broker\Broker; use PHPStan\Collectors\Registry as CollectorRegistry; @@ -16,7 +17,6 @@ use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Classes\DuplicateClassDeclarationRule; use PHPStan\Rules\Classes\DuplicateDeclarationRule; use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule; @@ -26,6 +26,7 @@ use PHPStan\Rules\Classes\LocalTypeAliasesCheck; use PHPStan\Rules\Classes\LocalTypeAliasesRule; use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Functions\DuplicateFunctionDeclarationRule; @@ -52,6 +53,7 @@ use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule; use PHPStan\Rules\Methods\OverridingMethodRule; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper; use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule; use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule; use PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule; @@ -90,7 +92,7 @@ public function validate(array $stubFiles, bool $debug): array $originalBroker = Broker::getInstance(); $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - $originalPhpVerison = PhpVersionStaticAccessor::getInstance(); + $originalPhpVersion = PhpVersionStaticAccessor::getInstance(); $container = $this->derivativeContainerFactory->create([ __DIR__ . '/../../conf/config.stubValidator.neon', ]); @@ -128,13 +130,18 @@ static function (): void { } $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); - $errors[] = (new Error($internalErrorMessage, $stubFile, null, $e))->withIdentifier('phpstan.internal'); + $errors[] = (new Error($internalErrorMessage, $stubFile, null, $e)) + ->withIdentifier('phpstan.internal') + ->withMetadata([ + InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), + InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), + ]); } } Broker::registerInstance($originalBroker); ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); - PhpVersionStaticAccessor::registerInstance($originalPhpVerison); + PhpVersionStaticAccessor::registerInstance($originalPhpVersion); ObjectType::resetCaches(); return $errors; @@ -148,7 +155,7 @@ private function getRuleRegistry(Container $container): RuleRegistry $templateTypeCheck = $container->getByType(TemplateTypeCheck::class); $varianceCheck = $container->getByType(VarianceCheck::class); $reflectionProvider = $container->getByType(ReflectionProvider::class); - $classCaseSensitivityCheck = $container->getByType(ClassCaseSensitivityCheck::class); + $classNameCheck = $container->getByType(ClassNameCheck::class); $functionDefinitionCheck = $container->getByType(FunctionDefinitionCheck::class); $missingTypehintCheck = $container->getByType(MissingTypehintCheck::class); $unresolvableTypeHelper = $container->getByType(UnresolvableTypeHelper::class); @@ -156,16 +163,17 @@ private function getRuleRegistry(Container $container): RuleRegistry $phpVersion = $container->getByType(PhpVersion::class); $localTypeAliasesCheck = $container->getByType(LocalTypeAliasesCheck::class); $phpClassReflectionExtension = $container->getByType(PhpClassReflectionExtension::class); + $genericCallableRuleHelper = $container->getByType(GenericCallableRuleHelper::class); $rules = [ // level 0 - new ExistingClassesInClassImplementsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassesInInterfaceExtendsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassInClassExtendsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassInTraitUseRule($classCaseSensitivityCheck, $reflectionProvider), + new ExistingClassesInClassImplementsRule($classNameCheck, $reflectionProvider), + new ExistingClassesInInterfaceExtendsRule($classNameCheck, $reflectionProvider), + new ExistingClassInClassExtendsRule($classNameCheck, $reflectionProvider), + new ExistingClassInTraitUseRule($classNameCheck, $reflectionProvider), new ExistingClassesInTypehintsRule($functionDefinitionCheck), new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck), - new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, $unresolvableTypeHelper, $phpVersion, true, false), + 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 DuplicateDeclarationRule(), new LocalTypeAliasesRule($localTypeAliasesCheck), @@ -181,12 +189,8 @@ private function getRuleRegistry(Container $container): RuleRegistry new MethodTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new MethodSignatureVarianceRule($varianceCheck), new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck), - new IncompatiblePhpDocTypeRule( - $fileTypeMapper, - $genericObjectTypeCheck, - $unresolvableTypeHelper, - ), - new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), + new IncompatiblePhpDocTypeRule($fileTypeMapper, $genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), + new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), $container->getByType(PhpDocParser::class), @@ -196,9 +200,9 @@ private function getRuleRegistry(Container $container): RuleRegistry new InvalidThrowsPhpDocValueRule($fileTypeMapper), // level 6 - new MissingFunctionParameterTypehintRule($missingTypehintCheck), + new MissingFunctionParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), new MissingFunctionReturnTypehintRule($missingTypehintCheck), - new MissingMethodParameterTypehintRule($missingTypehintCheck), + new MissingMethodParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), ]; diff --git a/src/PhpDoc/Tag/AssertTag.php b/src/PhpDoc/Tag/AssertTag.php index 4e436873a2..301b5b4876 100644 --- a/src/PhpDoc/Tag/AssertTag.php +++ b/src/PhpDoc/Tag/AssertTag.php @@ -18,7 +18,7 @@ final class AssertTag implements TypedTag /** * @param self::NULL|self::IF_TRUE|self::IF_FALSE $if */ - public function __construct(private string $if, private Type $type, private AssertTagParameter $parameter, private bool $negated, private bool $equality) + public function __construct(private string $if, private Type $type, private AssertTagParameter $parameter, private bool $negated, private bool $equality, private bool $isExplicit) { } @@ -60,14 +60,14 @@ public function isEquality(): bool */ public function withType(Type $type): TypedTag { - $tag = new self($this->if, $type, $this->parameter, $this->negated, $this->equality); + $tag = new self($this->if, $type, $this->parameter, $this->negated, $this->equality, $this->isExplicit); $tag->originalType = $this->getOriginalType(); return $tag; } public function withParameter(AssertTagParameter $parameter): self { - $tag = new self($this->if, $this->type, $parameter, $this->negated, $this->equality); + $tag = new self($this->if, $this->type, $parameter, $this->negated, $this->equality, $this->isExplicit); $tag->originalType = $this->getOriginalType(); return $tag; } @@ -78,9 +78,19 @@ public function negate(): self throw new ShouldNotHappenException(); } - $tag = new self($this->if, $this->type, $this->parameter, !$this->negated, $this->equality); + $tag = new self($this->if, $this->type, $this->parameter, !$this->negated, $this->equality, $this->isExplicit); $tag->originalType = $this->getOriginalType(); return $tag; } + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self + { + return new self($this->if, $this->type, $this->parameter, $this->negated, $this->equality, false); + } + } diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index e640418ea8..9f46c124d2 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -10,11 +10,13 @@ class MethodTag /** * @param array $parameters + * @param array $templateTags */ public function __construct( private Type $returnType, private bool $isStatic, private array $parameters, + private array $templateTags = [], ) { } @@ -37,4 +39,12 @@ public function getParameters(): array return $this->parameters; } + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + } diff --git a/src/PhpDoc/Tag/ParamClosureThisTag.php b/src/PhpDoc/Tag/ParamClosureThisTag.php new file mode 100644 index 0000000000..1dacb4c41d --- /dev/null +++ b/src/PhpDoc/Tag/ParamClosureThisTag.php @@ -0,0 +1,30 @@ +type; + } + + /** + * @return self + */ + public function withType(Type $type): TypedTag + { + return new self($type); + } + +} diff --git a/src/PhpDoc/Tag/ParamTag.php b/src/PhpDoc/Tag/ParamTag.php index 651064cec1..8515df30b9 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -8,7 +8,10 @@ class ParamTag implements TypedTag { - public function __construct(private Type $type, private bool $isVariadic) + public function __construct( + private Type $type, + private bool $isVariadic, + ) { } diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index 4a2180c052..78707555c8 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -9,10 +9,16 @@ class TemplateTag { + /** + * @param non-empty-string $name + */ public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance) { } + /** + * @return non-empty-string + */ public function getName(): string { return $this->name; diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index dfe513aaf4..6a8a409e56 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\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -35,12 +36,14 @@ use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -65,6 +68,10 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Helper\GetTemplateTypeType; use PHPStan\Type\IntegerRangeType; @@ -73,6 +80,7 @@ use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NewObjectType; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; @@ -346,9 +354,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new IterableType(new MixedType(), new MixedType()); case 'callable': - case 'pure-callable': return new CallableType(); + case 'pure-callable': + return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()); + + case 'pure-closure': + return ClosureType::createPure(); + case 'resource': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -743,12 +756,22 @@ static function (string $variance): TemplateTypeVariance { return TypeCombinator::union(...$result); } + return new ErrorType(); + } elseif ($mainTypeName === 'new') { + if (count($genericTypes) === 1) { + $type = new NewObjectType($genericTypes[0]); + return $type->isResolvable() ? $type->resolve() : $type; + } + return new ErrorType(); } $mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope); $mainTypeObjectClassNames = $mainType->getObjectClassNames(); if (count($mainTypeObjectClassNames) > 1) { + if ($mainType instanceof TemplateType) { + return new ErrorType(); + } throw new ShouldNotHappenException(); } $mainTypeClassName = $mainTypeObjectClassNames[0] ?? null; @@ -869,7 +892,32 @@ static function (string $variance): TemplateTypeVariance { private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type { + $templateTags = []; + + if (count($typeNode->templateTypes ?? []) > 0) { + foreach ($typeNode->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant(), + ); + } + $templateTypeScope = TemplateTypeScope::createWithAnonymousFunction(); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $templateTags, + )); + + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } else { + $templateTypeMap = TemplateTypeMap::createEmpty(); + } + $mainType = $this->resolve($typeNode->identifier, $nameScope); + $isVariadic = false; $parameters = array_map( function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection { @@ -878,6 +926,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi if (str_starts_with($parameterName, '$')) { $parameterName = substr($parameterName, 1); } + return new NativeParameterReflection( $parameterName, $parameterNode->isOptional || $parameterNode->isVariadic, @@ -889,16 +938,35 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi }, $typeNode->parameters, ); + $returnType = $this->resolve($typeNode->returnType, $nameScope); if ($mainType instanceof CallableType) { - return new CallableType($parameters, $returnType, $isVariadic); + $pure = $mainType->isPure(); + if ($pure->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure); } elseif ( $mainType instanceof ObjectType && $mainType->getClassName() === Closure::class ) { - return new ClosureType($parameters, $returnType, $isVariadic); + return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], [ + new SimpleImpurePoint( + 'functionCall', + 'call to a Closure', + false, + ), + ]); + } elseif ($mainType instanceof ClosureType) { + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints()); + if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return $closure; } return new ErrorType(); diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php index 91362ace74..55e3cbcf2e 100644 --- a/src/Process/ProcessPromise.php +++ b/src/Process/ProcessPromise.php @@ -5,9 +5,8 @@ use PHPStan\ShouldNotHappenException; use React\ChildProcess\Process; use React\EventLoop\LoopInterface; -use React\Promise\CancellablePromiseInterface; use React\Promise\Deferred; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; use function fclose; use function rewind; use function stream_get_contents; @@ -16,6 +15,7 @@ class ProcessPromise { + /** @var Deferred */ private Deferred $deferred; private ?Process $process = null; @@ -33,9 +33,9 @@ public function getName(): string } /** - * @return ExtendedPromiseInterface&CancellablePromiseInterface + * @return PromiseInterface */ - public function run(): CancellablePromiseInterface + public function run(): PromiseInterface { $tmpStdOutResource = tmpfile(); if ($tmpStdOutResource === false) { @@ -72,6 +72,9 @@ public function run(): CancellablePromiseInterface } if ($exitCode === 0) { + if ($stdOut === false) { + $stdOut = ''; + } $this->deferred->resolve($stdOut); return; } @@ -79,7 +82,6 @@ public function run(): CancellablePromiseInterface $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); }); - /** @var ExtendedPromiseInterface&CancellablePromiseInterface */ return $this->deferred->promise(); } diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 6a50a0ce9c..cfd5b74e03 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -10,6 +10,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; class AnnotationMethodReflection implements ExtendedMethodReflection @@ -29,6 +30,7 @@ public function __construct( private bool $isStatic, private bool $isVariadic, private ?Type $throwType, + private TemplateTypeMap $templateTypeMap, ) { } @@ -68,7 +70,7 @@ public function getVariants(): array if ($this->variants === null) { $this->variants = [ new FunctionVariantWithPhpDocs( - TemplateTypeMap::createEmpty(), + $this->templateTypeMap, null, $this->parameters, $this->isVariadic, @@ -122,6 +124,10 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createYes(); } + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->returnType)->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -150,4 +156,13 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index 555a77e255..fac7e5a9bb 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -44,6 +45,16 @@ public function getOutType(): ?Type return null; } + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClosureThisType(): ?Type + { + return null; + } + public function passedByReference(): PassedByReference { return $this->passedByReference; diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index e1cee2da24..2860988c91 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -2,12 +2,18 @@ namespace PHPStan\Reflection\Annotations; +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; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Type; +use function array_map; use function count; class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension @@ -57,6 +63,13 @@ private function findClassReflectionWithMethod( ); } + $templateTypeScope = TemplateTypeScope::createWithClass($classReflection->getName()); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $methodTags[$methodName]->getTemplateTags(), + )); + $isStatic = $methodTags[$methodName]->isStatic(); $nativeCallMethodName = $isStatic ? '__callStatic' : '__call'; @@ -75,6 +88,7 @@ private function findClassReflectionWithMethod( $classReflection->hasNativeMethod($nativeCallMethodName) ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() : null, + $templateTypeMap, ); } diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index d7bf76a56d..cd18c1bfe3 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\BetterReflection; use Closure; +use Nette\Utils\Strings; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; @@ -23,11 +24,12 @@ use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; use PHPStan\PhpDoc\Tag\ParamOutTag; -use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassNameHelper; use PHPStan\Reflection\ClassReflection; @@ -43,6 +45,7 @@ use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -199,7 +202,6 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $scopeFile, ); $classNode->name = new Node\Identifier($className); - $classNode->setAttribute('anonymousClass', true); if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]; @@ -212,6 +214,14 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ null, ); + /** @var int|null $classLineIndex */ + $classLineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); + if ($classLineIndex === null) { + $displayName = sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()); + } else { + $displayName = sprintf('class@anonymous/%s:%s:%d', $filename, $classNode->getStartLine(), $classLineIndex); + } + self::$anonymousClasses[$className] = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), $this->initializerExprTypeResolver, @@ -225,7 +235,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), - sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()), + $displayName, new ReflectionClass($reflectionClass), $scopeFile, null, @@ -269,7 +279,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection { $reflectionFunction = new ReflectionFunction($this->reflector->reflectFunction($functionName)); $templateTypeMap = TemplateTypeMap::createEmpty(); - $phpDocParameterTags = []; + $phpDocParameterTypes = []; $phpDocReturnTag = null; $phpDocThrowsTag = null; $deprecatedTag = null; @@ -280,6 +290,8 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $asserts = Assertions::createEmpty(); $phpDocComment = null; $phpDocParameterOutTags = []; + $phpDocParameterImmediatelyInvokedCallable = []; + $phpDocParameterClosureThisTypeTags = []; $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { @@ -289,7 +301,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTags = $resolvedPhpDoc->getParamTags(); + $phpDocParameterTypes = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamTags()); $phpDocReturnTag = $resolvedPhpDoc->getReturnTag(); $phpDocThrowsTag = $resolvedPhpDoc->getThrowsTag(); $deprecatedTag = $resolvedPhpDoc->getDeprecatedTag(); @@ -302,12 +314,14 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocComment = $resolvedPhpDoc->getPhpDocString(); } $phpDocParameterOutTags = $resolvedPhpDoc->getParamOutTags(); + $phpDocParameterImmediatelyInvokedCallable = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); + $phpDocParameterClosureThisTypeTags = $resolvedPhpDoc->getParamClosureThisTags(); } return $this->functionReflectionFactory->create( $reflectionFunction, $templateTypeMap, - array_map(static fn (ParamTag $paramTag): Type => $paramTag->getType(), $phpDocParameterTags), + $phpDocParameterTypes, $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, @@ -319,6 +333,8 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $asserts, $phpDocComment, array_map(static fn (ParamOutTag $paramOutTag): Type => $paramOutTag->getType(), $phpDocParameterOutTags), + $phpDocParameterImmediatelyInvokedCallable, + array_map(static fn (ParamClosureThisTag $tag): Type => $tag->getType(), $phpDocParameterClosureThisTypeTags), ); } @@ -360,11 +376,39 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn $constantReflection = $this->reflector->reflectConstant($constantName); $fileName = $constantReflection->getFileName(); $constantValueType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpression(), InitializerExprContext::fromGlobalConstant($constantReflection)); + $docComment = $constantReflection->getDocComment(); + + $isDeprecated = TrinaryLogic::createNo(); + $deprecatedDescription = null; + if ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, null, $docComment); + $isDeprecated = TrinaryLogic::createFromBoolean($resolvedPhpDoc->isDeprecated()); + + if ($resolvedPhpDoc->isDeprecated() && $resolvedPhpDoc->getDeprecatedTag() !== null) { + $deprecatedMessage = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); + + $matches = Strings::match($deprecatedMessage ?? '', '#^(\d+)\.(\d+)(?:\.(\d+))?$#'); + if ($matches !== null) { + $major = $matches[1]; + $minor = $matches[2]; + $patch = $matches[3] ?? 0; + $versionId = sprintf('%d%02d%02d', $major, $minor, $patch); + + $isDeprecated = TrinaryLogic::createFromBoolean($this->phpVersion->getVersionId() >= $versionId); + } else { + // filter raw version number messages like in + // https://github.com/JetBrains/phpstorm-stubs/blob/9608c953230b08f07b703ecfe459cc58d5421437/filter/filter.php#L478 + $deprecatedDescription = $deprecatedMessage; + } + } + } return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( $constantName, $constantValueType, $fileName, + $isDeprecated, + $deprecatedDescription, ); } diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index ff9068d835..3d123ccfe2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -112,6 +112,7 @@ public function create(string $projectInstallationPath): ?SourceLocator foreach ($classMapPaths as $classMapPath) { if (is_dir($classMapPath)) { $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapPath); + continue; } if (!is_file($classMapPath)) { continue; diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php index d9d45cd3e5..6e76066fee 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -233,7 +233,7 @@ private function skipComment(): void private function skipToNewline(): void { while ($this->index < $this->len) { - if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { + if (in_array($this->contents[$this->index], ["\r", "\n"], true)) { return; } $this->index += 1; @@ -285,6 +285,7 @@ private function peek(string $char): bool /** * @param string[]|null $match + * @param-out string[] $match */ private function match(string $regex, ?array &$match = null, ?int $offset = null): bool { diff --git a/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php new file mode 100644 index 0000000000..04da7cd605 --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/ReflectionSourceStubberFactory.php @@ -0,0 +1,21 @@ +printer, $this->phpVersion->getVersionId()); + } + +} diff --git a/src/Reflection/CallableFunctionVariantWithPhpDocs.php b/src/Reflection/CallableFunctionVariantWithPhpDocs.php new file mode 100644 index 0000000000..f9a8c002c6 --- /dev/null +++ b/src/Reflection/CallableFunctionVariantWithPhpDocs.php @@ -0,0 +1,77 @@ + $parameters + * @param SimpleThrowPoint[] $throwPoints + * @param SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables + */ + public function __construct( + TemplateTypeMap $templateTypeMap, + ?TemplateTypeMap $resolvedTemplateTypeMap, + array $parameters, + bool $isVariadic, + Type $returnType, + Type $phpDocReturnType, + Type $nativeReturnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap, + private array $throwPoints, + private TrinaryLogic $isPure, + private array $impurePoints, + private array $invalidateExpressions, + private array $usedVariables, + ) + { + parent::__construct( + $templateTypeMap, + $resolvedTemplateTypeMap, + $parameters, + $isVariadic, + $returnType, + $phpDocReturnType, + $nativeReturnType, + $callSiteVarianceMap, + ); + } + + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + +} diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php new file mode 100644 index 0000000000..62371a0e85 --- /dev/null +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -0,0 +1,37 @@ + new self($function, $variant), $variants); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getResolvedTemplateTypeMap(); + } + + /** + * @return array + */ + public function getParameters(): array + { + return $this->variant->getParameters(); + } + + public function isVariadic(): bool + { + return $this->variant->isVariadic(); + } + + public function getReturnType(): Type + { + return $this->variant->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->variant->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->variant->getNativeReturnType(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->variant->getCallSiteVarianceMap(); + } + + public function getThrowPoints(): array + { + if ($this->throwPoints !== null) { + return $this->throwPoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->throwPoints = $this->variant->getThrowPoints(); + } + + $returnType = $this->variant->getReturnType(); + $throwType = $this->function->getThrowType(); + if ($throwType === null) { + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + $throwPoints = []; + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnType)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + return $this->throwPoints = $throwPoints; + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + if ($this->impurePoints !== null) { + return $this->impurePoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->impurePoints = $this->variant->getImpurePoints(); + } + + $impurePoint = SimpleImpurePoint::createFromVariant($this->function, $this->variant); + if ($impurePoint === null) { + return $this->impurePoints = []; + } + + return $this->impurePoints = [$impurePoint]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + +} diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php new file mode 100644 index 0000000000..91b807b3ad --- /dev/null +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -0,0 +1,72 @@ +hasSideEffects()->no()) { + $certain = $function->isPure()->no(); + if ($variant !== null) { + $certain = $certain || $variant->getReturnType()->isVoid()->yes(); + } + + if ($function instanceof FunctionReflection) { + return new SimpleImpurePoint( + 'functionCall', + sprintf('call to function %s()', $function->getName()), + $certain, + ); + } + + return new SimpleImpurePoint( + 'methodCall', + sprintf('call to method %s::%s()', $function->getDeclaringClass()->getDisplayName(), $function->getName()), + $certain, + ); + } + + return null; + } + + /** + * @return ImpurePointIdentifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isCertain(): bool + { + return $this->certain; + } + +} diff --git a/src/Reflection/Callables/SimpleThrowPoint.php b/src/Reflection/Callables/SimpleThrowPoint.php new file mode 100644 index 0000000000..6a32d4eacd --- /dev/null +++ b/src/Reflection/Callables/SimpleThrowPoint.php @@ -0,0 +1,45 @@ +type; + } + + public function isExplicit(): bool + { + return $this->explicit; + } + + public function canContainAnyThrowable(): bool + { + return $this->canContainAnyThrowable; + } + +} diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index 75ed3e0359..fd69ce8129 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -10,6 +10,7 @@ use PHPStan\Type\TypehintHelper; use const NAN; +/** @api */ class ClassConstantReflection implements ConstantReflection { diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 42bd8333e0..7c9a569dcc 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -79,9 +79,12 @@ class ClassReflection /** @var PropertyReflection[] */ private array $properties = []; - /** @var ConstantReflection[] */ + /** @var ClassConstantReflection[] */ private array $constants = []; + /** @var EnumCaseReflection[]|null */ + private ?array $enumCases = null; + /** @var int[]|null */ private ?array $classHierarchyDistances = null; @@ -99,6 +102,8 @@ class ClassReflection private ?bool $hasConsistentConstructor = null; + private ?bool $acceptsNamedArguments = null; + private ?TemplateTypeMap $templateTypeMap = null; private ?TemplateTypeMap $activeTemplateTypeMap = null; @@ -688,7 +693,7 @@ public function isTrait(): bool */ public function isEnum(): bool { - return $this->reflection->isEnum() && $this->reflection instanceof ReflectionEnum; + return $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum(); } /** @@ -752,18 +757,23 @@ public function getEnumCases(): array throw new ShouldNotHappenException(); } + if ($this->enumCases !== null) { + return $this->enumCases; + } + $cases = []; + $initializerExprContext = InitializerExprContext::fromClassReflection($this); foreach ($this->reflection->getCases() as $case) { $valueType = null; if ($case instanceof ReflectionEnumBackedCase) { - $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this)); + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext); } /** @var string $caseName */ $caseName = $case->getName(); $cases[$caseName] = new EnumCaseReflection($this, $caseName, $valueType); } - return $cases; + return $this->enumCases = $cases; } public function getEnumCase(string $name): EnumCaseReflection @@ -776,6 +786,10 @@ public function getEnumCase(string $name): EnumCaseReflection throw new ShouldNotHappenException(); } + if ($this->enumCases !== null && array_key_exists($name, $this->enumCases)) { + return $this->enumCases[$name]; + } + $case = $this->reflection->getCase($name); $valueType = null; if ($case instanceof ReflectionEnumBackedCase) { @@ -998,10 +1012,10 @@ public function getTraits(bool $recursive = false): array public function getParentClassesNames(): array { $parentNames = []; - $currentClassReflection = $this; - while ($currentClassReflection->getParentClass() !== null) { - $parentNames[] = $currentClassReflection->getParentClass()->getName(); - $currentClassReflection = $currentClassReflection->getParentClass(); + $parentClass = $this->getParentClass(); + while ($parentClass !== null) { + $parentNames[] = $parentClass->getName(); + $parentClass = $parentClass->getParentClass(); } return $parentNames; @@ -1021,7 +1035,7 @@ public function hasConstant(string $name): bool return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); } - public function getConstant(string $name): ConstantReflection + public function getConstant(string $name): ClassConstantReflection { if (!isset($this->constants[$name])) { $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); @@ -1231,6 +1245,16 @@ public function hasConsistentConstructor(): bool return $this->hasConsistentConstructor; } + public function acceptsNamedArguments(): bool + { + if ($this->acceptsNamedArguments === null) { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + $this->acceptsNamedArguments = $resolvedPhpDoc === null || $resolvedPhpDoc->acceptsNamedArguments(); + } + + return $this->acceptsNamedArguments; + } + public function isFinalByKeyword(): bool { if ($this->isAnonymous()) { diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index 341ae68cc4..7bc10879f4 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -13,6 +13,8 @@ public function __construct( private string $name, private Type $valueType, private ?string $fileName, + private TrinaryLogic $isDeprecated, + private ?string $deprecatedDescription, ) { } @@ -34,12 +36,12 @@ public function getFileName(): ?string public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::createNo(); + return $this->isDeprecated; } public function getDeprecatedDescription(): ?string { - return null; + return $this->deprecatedDescription; } public function isInternal(): TrinaryLogic diff --git a/src/Reflection/ConstantNameHelper.php b/src/Reflection/ConstantNameHelper.php index 237c29c166..cd4b526a77 100644 --- a/src/Reflection/ConstantNameHelper.php +++ b/src/Reflection/ConstantNameHelper.php @@ -19,7 +19,7 @@ public static function normalize(string $name): string return $name; } - $nameParts = array_filter(explode('\\', $name)); + $nameParts = array_filter(explode('\\', $name), static fn ($part) => $part !== ''); return strtolower(implode('\\', array_slice($nameParts, 0, -1))) . '\\' . end($nameParts); } diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index ff82f0662a..4fc7daca3d 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -127,4 +127,9 @@ public function isAbstract(): TrinaryLogic return $abstract; } + public function isPure(): TrinaryLogic + { + return $this->reflection->isPure(); + } + } diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index f6a67026cb..b3cdde365f 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -131,4 +131,9 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index f454126ec2..806c24c815 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -123,4 +123,9 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 47d6d4d61a..0ff0f8f2de 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -42,4 +42,13 @@ public function isFinalByKeyword(): TrinaryLogic; public function isAbstract(): TrinaryLogic|bool; + /** + * This indicates whether the method has phpstan-pure + * or phpstan-impure annotation above it. + * + * In most cases asking hasSideEffects() is much more practical + * as it also accounts for void return type (method being always impure). + */ + public function isPure(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 271aec1e86..91afcbaadb 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -43,4 +43,13 @@ public function getDocComment(): ?string; public function returnsByReference(): TrinaryLogic; + /** + * This indicates whether the function has phpstan-pure + * or phpstan-impure annotation above it. + * + * In most cases asking hasSideEffects() is much more practical + * as it also accounts for void return type (method being always impure). + */ + public function isPure(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 28382fd315..67684abdab 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -11,8 +11,10 @@ interface FunctionReflectionFactory { /** - * @param Type[] $phpDocParameterTypes - * @param Type[] $phpDocParameterOutTypes + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes */ public function create( ReflectionFunction $reflection, @@ -29,6 +31,8 @@ public function create( Assertions $asserts, ?string $phpDocComment, array $phpDocParameterOutTypes, + array $phpDocParameterImmediatelyInvokedCallable, + array $phpDocParameterClosureThisTypes, ): PhpFunctionReflection; } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 46154a33b5..edbb692931 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -2,7 +2,9 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\TrinaryLogic; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -83,6 +85,8 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $typeMap->getTypes(), )); + $originalParametersAcceptor = $parametersAcceptor; + if (!$parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { $parametersAcceptor = new FunctionVariantWithPhpDocs( $parametersAcceptor->getTemplateTypeMap(), @@ -97,6 +101,8 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc new MixedType(), $parameter->getType(), null, + TrinaryLogic::createMaybe(), + null, ), $parametersAcceptor->getParameters()), $parametersAcceptor->isVariadic(), $parametersAcceptor->getReturnType(), @@ -106,12 +112,24 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc ); } - return new ResolvedFunctionVariant( + $result = new ResolvedFunctionVariantWithOriginal( $parametersAcceptor, $resolvedTemplateTypeMap, $parametersAcceptor->getCallSiteVarianceMap(), $passedArgs, ); + if ($originalParametersAcceptor instanceof CallableParametersAcceptor) { + return new ResolvedFunctionVariantWithCallable( + $result, + $originalParametersAcceptor->getThrowPoints(), + $originalParametersAcceptor->isPure(), + $originalParametersAcceptor->getImpurePoints(), + $originalParametersAcceptor->getInvalidateExpressions(), + $originalParametersAcceptor->getUsedVariables(), + ); + } + + return $result; } } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 3d732648c5..5b8504a7d6 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -2,12 +2,15 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class InaccessibleMethod implements ParametersAcceptor +class InaccessibleMethod implements CallableParametersAcceptor { public function __construct(private MethodReflection $methodReflection) @@ -52,4 +55,35 @@ public function getReturnType(): Type return new MixedType(); } + public function getThrowPoints(): array + { + return []; + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + return [ + new SimpleImpurePoint( + 'methodCall', + 'call to unknown method', + false, + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + } diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index c71a75c7d0..c6e2f890a8 100644 --- a/src/Reflection/InitializerExprContext.php +++ b/src/Reflection/InitializerExprContext.php @@ -8,6 +8,7 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\ReflectionConstant; +use PHPStan\ShouldNotHappenException; use function array_slice; use function count; use function explode; @@ -18,6 +19,9 @@ class InitializerExprContext implements NamespaceAnswerer { + /** + * @param non-empty-string|null $namespace + */ private function __construct( private ?string $file, private ?string $namespace, @@ -43,11 +47,18 @@ public static function fromScope(Scope $scope): self ); } + /** + * @return non-empty-string|null + */ private static function parseNamespace(string $name): ?string { $parts = explode('\\', $name); if (count($parts) > 1) { - return implode('\\', array_slice($parts, 0, -1)); + $ns = implode('\\', array_slice($parts, 0, -1)); + if ($ns === '') { + throw new ShouldNotHappenException('Namespace cannot be empty.'); + } + return $ns; } return null; diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 4826510945..3615ac70b4 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use Nette\Utils\Strings; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp; @@ -28,6 +29,7 @@ use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -75,9 +77,11 @@ use function dirname; use function floor; use function in_array; +use function intval; use function is_finite; use function is_float; use function is_int; +use function is_numeric; use function max; use function min; use function sprintf; @@ -475,6 +479,32 @@ public function resolveConcatType(Type $left, Type $right): Type $accessoryTypes[] = new AccessoryLiteralStringType(); } + $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); + if ($leftNumericStringNonEmpty->isNumericString()->yes()) { + $allRightConstantsZeroOrMore = false; + foreach ($rightConstantStrings as $rightConstantString) { + if ($rightConstantString->getValue() === '') { + continue; + } + + if ( + !is_numeric($rightConstantString->getValue()) + || Strings::match($rightConstantString->getValue(), '#^[0-9]+$#') === null + ) { + $allRightConstantsZeroOrMore = false; + break; + } + + $allRightConstantsZeroOrMore = true; + } + + $zeroOrMoreInteger = IntegerRangeType::fromInterval(0, null); + $nonNegativeRight = $allRightConstantsZeroOrMore || $zeroOrMoreInteger->isSuperTypeOf($right)->yes(); + if ($nonNegativeRight) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + } + if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); return new IntersectionType($accessoryTypes); @@ -746,17 +776,13 @@ public function getSpaceshipType(Expr $left, Expr $right, callable $getTypeCallb $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); - if ($leftTypesCount > 0 && $rightTypesCount > 0) { + if ($leftTypesCount > 0 && $rightTypesCount > 0 && $leftTypesCount * $rightTypesCount <= self::CALCULATE_SCALARS_LIMIT) { $resultTypes = []; - $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; foreach ($leftTypes as $leftType) { foreach ($rightTypes as $rightType) { $leftValue = $leftType->getValue(); $rightValue = $rightType->getValue(); $resultType = $this->getTypeFromValue($leftValue <=> $rightValue); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); - } $resultTypes[] = $resultType; } } @@ -794,7 +820,7 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): throw new ShouldNotHappenException(); } - if ($rightNumberType->getValue() === 0 || $rightNumberType->getValue() === 0.0) { + if (in_array($rightNumberType->getValue(), [0, 0.0], true)) { return new ErrorType(); } @@ -830,6 +856,10 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): return $this->getNeverType($leftType, $rightType); } + if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) { + return new ErrorType(); + } + $leftTypes = $leftType->getConstantScalarTypes(); $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); @@ -1065,9 +1095,17 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): new BooleanType(), ]); - if ($plusable->isSuperTypeOf($leftType)->yes() && $plusable->isSuperTypeOf($rightType)->yes()) { + $plusableSuperTypeOfLeft = $plusable->isSuperTypeOf($leftType)->yes(); + $plusableSuperTypeOfRight = $plusable->isSuperTypeOf($rightType)->yes(); + if ($plusableSuperTypeOfLeft && $plusableSuperTypeOfRight) { return TypeCombinator::union($leftType, $rightType); } + if ($plusableSuperTypeOfLeft && $rightType instanceof MixedType) { + return $leftType; + } + if ($plusableSuperTypeOfRight && $leftType instanceof MixedType) { + return $rightType; + } } return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType); @@ -1230,7 +1268,7 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb return new ErrorType(); } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() << $rightNumberType->getValue()); + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) << intval($rightNumberType->getValue())); if ($generalize) { $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); } @@ -1248,7 +1286,7 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb return new ErrorType(); } - return new IntegerType(); + return $this->resolveCommonMath(new Expr\BinaryOp\ShiftLeft($left, $right), $leftType, $rightType); } /** @@ -1287,7 +1325,7 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall return new ErrorType(); } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() >> $rightNumberType->getValue()); + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) >> intval($rightNumberType->getValue())); if ($generalize) { $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); } @@ -1305,11 +1343,15 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall return new ErrorType(); } - return new IntegerType(); + return $this->resolveCommonMath(new Expr\BinaryOp\ShiftRight($left, $right), $leftType, $rightType); } public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanType { + if ($leftType instanceof NeverType || $rightType instanceof NeverType) { + return new ConstantBooleanType(false); + } + if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { return new ConstantBooleanType($leftType->getValue() === $rightType->getValue()); } @@ -1320,8 +1362,7 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanTy return new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])); } - $isSuperset = $leftType->isSuperTypeOf($rightType); - if ($isSuperset->no()) { + if ($leftType->isSuperTypeOf($rightType)->no() && $rightType->isSuperTypeOf($leftType)->no()) { return new ConstantBooleanType(false); } @@ -1337,9 +1378,6 @@ public function resolveEqualType(Type $leftType, Type $rightType): BooleanType if ( ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) || ($rightType->isEnum()->yes() && $leftType->isTrue()->no()) - || ($leftType->isString()->yes() && $rightType->isString()->yes()) - || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) - || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) ) { return $this->resolveIdenticalType($leftType, $rightType); } @@ -1444,7 +1482,7 @@ private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type } /** - * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div $expr + * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $expr */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { @@ -1511,6 +1549,9 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri $leftNumberType->isFloat()->yes() || $rightNumberType->isFloat()->yes() ) { + if ($expr instanceof Expr\BinaryOp\ShiftLeft || $expr instanceof Expr\BinaryOp\ShiftRight) { + return new IntegerType(); + } return new FloatType(); } @@ -1535,7 +1576,7 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri /** * @param ConstantIntegerType|IntegerRangeType $range - * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus $node + * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $node */ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): Type { @@ -1658,7 +1699,7 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T if (!is_finite($max)) { $max = null; } - } else { + } elseif ($node instanceof Expr\BinaryOp\Div) { if ($operand instanceof ConstantIntegerType) { $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null; $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null; @@ -1731,31 +1772,45 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T [$min, $max] = [$max, $min]; } - if ($operand instanceof IntegerRangeType - || ($rangeMin === null || $rangeMax === null) - || is_float($min) - || is_float($max) - ) { - if (is_float($min)) { - $min = (int) ceil($min); - } - if (is_float($max)) { - $max = (int) floor($max); - } + if (is_float($min)) { + $min = (int) ceil($min); + } + if (is_float($max)) { + $max = (int) floor($max); + } - // invert maximas on division with negative constants - if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) - || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) - && ($min === null || $max === null)) { - [$min, $max] = [$max, $min]; - } + // invert maximas on division with negative constants + if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) + || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) + && ($min === null || $max === null)) { + [$min, $max] = [$max, $min]; + } - if ($min === null && $max === null) { - return new BenevolentUnionType([new IntegerType(), new FloatType()]); - } + if ($min === null && $max === null) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } - return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()); + return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()); + } elseif ($node instanceof Expr\BinaryOp\ShiftLeft) { + if (!$operand instanceof ConstantIntegerType) { + return new IntegerType(); } + if ($operand->getValue() < 0) { + return new ErrorType(); + } + $min = $rangeMin !== null ? intval($rangeMin) << $operand->getValue() : null; + $max = $rangeMax !== null ? intval($rangeMax) << $operand->getValue() : null; + } elseif ($node instanceof Expr\BinaryOp\ShiftRight) { + if (!$operand instanceof ConstantIntegerType) { + return new IntegerType(); + } + if ($operand->getValue() < 0) { + return new ErrorType(); + } + $min = $rangeMin !== null ? intval($rangeMin) >> $operand->getValue() : null; + $max = $rangeMax !== null ? intval($rangeMax) >> $operand->getValue() : null; + } else { + throw new ShouldNotHappenException(); } if (is_float($min)) { @@ -1795,7 +1850,7 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con } if (in_array(strtolower($constantClass), $namesToResolve, true)) { $resolvedName = $this->resolveName($class, $classReflection); - if ($resolvedName === 'parent' && strtolower($constantName) === 'class') { + if (strtolower($resolvedName) === 'parent' && strtolower($constantName) === 'class') { return new ClassStringType(); } $constantClassType = $this->resolveTypeByName($class, $classReflection); @@ -1862,6 +1917,13 @@ function (Type $type, callable $traverse): Type { ); } + if ($constantClassType->isClassStringType()->yes()) { + if ($constantClassType->isConstantScalarValue()->yes()) { + $isObject = false; + } + $constantClassType = $constantClassType->getClassStringObjectType(); + } + $types = []; foreach ($constantClassType->getObjectClassNames() as $referencedClass) { if (!$this->getReflectionProvider()->hasClass($referencedClass)) { @@ -1910,8 +1972,7 @@ function (Type $type, callable $traverse): Type { $constantReflection = $constantClassReflection->getConstant($constantName); if ( - $constantReflection instanceof ClassConstantReflection - && !$constantClassReflection->isFinal() + !$constantClassReflection->isFinal() && !$constantReflection->hasPhpDocType() && !$constantReflection->hasNativeType() ) { @@ -1919,19 +1980,13 @@ function (Type $type, callable $traverse): Type { return new MixedType(); } - if ( - !$constantReflection instanceof ClassConstantReflection - || !$constantClassReflection->isFinal() - ) { + if (!$constantClassReflection->isFinal()) { $constantType = $constantReflection->getValueType(); } else { $constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass())); } - $nativeType = null; - if ($constantReflection instanceof ClassConstantReflection) { - $nativeType = $constantReflection->getNativeType(); - } + $nativeType = $constantReflection->getNativeType(); $constantType = $this->constantResolver->resolveClassConstantType( $constantClassReflection->getName(), $constantName, @@ -2035,12 +2090,14 @@ private function resolveName(Name $name, ?ClassReflection $classReflection): str { $originalClass = (string) $name; if ($classReflection !== null) { - if (in_array(strtolower($originalClass), [ + $lowerClass = strtolower($originalClass); + + if (in_array($lowerClass, [ 'self', 'static', ], true)) { return $classReflection->getName(); - } elseif ($originalClass === 'parent') { + } elseif ($lowerClass === 'parent') { if ($classReflection->getParentClass() !== null) { return $classReflection->getParentClass()->getName(); } diff --git a/src/Reflection/NamespaceAnswerer.php b/src/Reflection/NamespaceAnswerer.php index a0c4e74d5d..4e908a6d8e 100644 --- a/src/Reflection/NamespaceAnswerer.php +++ b/src/Reflection/NamespaceAnswerer.php @@ -6,6 +6,9 @@ interface NamespaceAnswerer { + /** + * @return non-empty-string|null + */ public function getNamespace(): ?string; } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 4484573314..ff52194908 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -92,6 +92,15 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 868e4927cb..3112fefa48 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -72,6 +72,10 @@ public function getPrototype(): ClassMemberReflection $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); } + if (!$prototypeDeclaringClass->hasNativeMethod($prototypeMethod->getName())) { + return $this; + } + $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); @@ -153,6 +157,15 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { diff --git a/src/Reflection/Native/NativeParameterReflection.php b/src/Reflection/Native/NativeParameterReflection.php index 219f48069d..7f92bfdb5e 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class NativeParameterReflection implements ParameterReflection { @@ -50,6 +51,18 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function union(self $other): self + { + return new self( + $this->name, + $this->optional && $other->optional, + TypeCombinator::union($this->type, $other->type), + $this->passedByReference->combine($other->passedByReference), + $this->variadic && $other->variadic, + $this->optional && $other->optional ? $this->defaultValue : null, + ); + } + /** * @param mixed[] $properties */ diff --git a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php index 14f79af016..3a81fbb4da 100644 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; class NativeParameterWithPhpDocsReflection implements ParameterReflectionWithPhpDocs @@ -19,6 +20,8 @@ public function __construct( private bool $variadic, private ?Type $defaultValue, private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { } @@ -68,6 +71,16 @@ public function getOutType(): ?Type return $this->outType; } + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + /** * @param mixed[] $properties */ @@ -83,6 +96,8 @@ public static function __set_state(array $properties): self $properties['variadic'], $properties['defaultValue'], $properties['outType'], + $properties['immediatelyInvokedCallable'], + $properties['closureThisType'], ); } diff --git a/src/Reflection/ParameterReflectionWithPhpDocs.php b/src/Reflection/ParameterReflectionWithPhpDocs.php index 38373441bf..943338a493 100644 --- a/src/Reflection/ParameterReflectionWithPhpDocs.php +++ b/src/Reflection/ParameterReflectionWithPhpDocs.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; /** @api */ @@ -14,4 +15,8 @@ public function getNativeType(): Type; public function getOutType(): ?Type; + public function isImmediatelyInvokedCallable(): TrinaryLogic; + + public function getClosureThisType(): ?Type; + } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 9e647e7030..33f536fe49 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -2,12 +2,19 @@ namespace PHPStan\Reflection; +use Closure; use PhpParser\Node; +use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Parser\ArrayFilterArgVisitor; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\ArrayWalkArgVisitor; +use PHPStan\Parser\ClosureBindArgVisitor; +use PHPStan\Parser\ClosureBindToVarVisitor; use PHPStan\Parser\CurlSetOptArgVisitor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; @@ -25,18 +32,22 @@ use PHPStan\Type\LateResolvableType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; +use function array_key_exists; use function array_key_last; use function array_map; +use function array_merge; use function array_slice; use function constant; use function count; use function defined; +use function is_string; use function sprintf; use const ARRAY_FILTER_USE_BOTH; use const ARRAY_FILTER_USE_KEY; @@ -92,7 +103,20 @@ public static function selectFromArgs( $parameters = $acceptor->getParameters(); $callbackParameters = []; foreach ($arrayMapArgs as $arg) { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null); + $argType = $scope->getType($arg->value); + if ($arg->unpack) { + $constantArrays = $argType->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + $valueTypes = $constantArray->getValueTypes(); + foreach ($valueTypes as $valueType) { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), false, PassedByReference::createNo(), false, null); + } + } + } + } else { + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null); + } } $parameters[0] = new NativeParameterReflection( $parameters[0]->getName(), @@ -176,7 +200,7 @@ public static function selectFromArgs( $arrayFilterParameters ?? [ new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ], - new MixedType(), + new BooleanType(), false, ), new NullType(), @@ -227,6 +251,97 @@ public static function selectFromArgs( ), ]; } + + if (isset($args[0])) { + $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); + if ( + $closureBindToVar !== null + && $closureBindToVar instanceof Node\Expr\Variable + && is_string($closureBindToVar->name) + ) { + $varType = $scope->getType($closureBindToVar); + if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $inFunctionVariant = self::selectSingle($inFunction->getVariants()); + $closureThisParameters = []; + foreach ($inFunctionVariant->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureBindToVar->name, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureBindToVar->name))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[0] = new NativeParameterReflection( + $parameters[0]->getName(), + $parameters[0]->isOptional(), + $closureThisParameters[$closureBindToVar->name], + $parameters[0]->passedByReference(), + $parameters[0]->isVariadic(), + $parameters[0]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } + + if ( + $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null + && $args[0]->value instanceof Node\Expr\Variable + && is_string($args[0]->value->name) + ) { + $closureVarName = $args[0]->value->name; + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $inFunctionVariant = self::selectSingle($inFunction->getVariants()); + $closureThisParameters = []; + foreach ($inFunctionVariant->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureVarName, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureVarName))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + $closureThisParameters[$closureVarName], + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } } if (count($parametersAcceptors) === 1) { @@ -236,16 +351,44 @@ public static function selectFromArgs( } } + $reorderedArgs = $args; + $parameters = null; + $singleParametersAcceptor = null; + if (count($parametersAcceptors) === 1) { + $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); + $singleParametersAcceptor = $parametersAcceptors[0]; + } + $hasName = false; - foreach ($args as $i => $arg) { - $type = $scope->getType($arg->value); - if ($arg->name !== null) { - $index = $arg->name->toString(); + foreach ($reorderedArgs ?? $args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $parameter = null; + if ($singleParametersAcceptor !== null) { + $parameters = $singleParametersAcceptor->getParameters(); + if (isset($parameters[$i])) { + $parameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { + $parameter = $parameters[count($parameters) - 1]; + } + } + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + + $type = $scope->getType($originalArg->value); + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + + if ($originalArg->name !== null) { + $index = $originalArg->name->toString(); $hasName = true; } else { $index = $i; } - if ($arg->unpack) { + if ($originalArg->unpack) { $unpack = true; $types[$index] = $type->getIterableValueType(); } else { @@ -267,6 +410,22 @@ private static function hasAcceptorTemplateOrLateResolvableType(ParametersAccept } foreach ($acceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getOutType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getOutType()) + ) { + return true; + } + + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getClosureThisType()) + ) { + return true; + } + if (!self::hasTemplateOrLateResolvableType($parameter->getType())) { continue; } @@ -438,6 +597,12 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $returnTypes = []; $phpDocReturnTypes = []; $nativeReturnTypes = []; + $callableOccurred = false; + $throwPoints = []; + $isPure = TrinaryLogic::createNo(); + $impurePoints = []; + $invalidateExpressions = []; + $usedVariables = []; foreach ($acceptors as $acceptor) { $returnTypes[] = $acceptor->getReturnType(); @@ -446,6 +611,14 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $phpDocReturnTypes[] = $acceptor->getPhpDocReturnType(); $nativeReturnTypes[] = $acceptor->getNativeReturnType(); } + if ($acceptor instanceof CallableParametersAcceptor) { + $callableOccurred = true; + $throwPoints = array_merge($throwPoints, $acceptor->getThrowPoints()); + $isPure = $isPure->or($acceptor->isPure()); + $impurePoints = array_merge($impurePoints, $acceptor->getImpurePoints()); + $invalidateExpressions = array_merge($invalidateExpressions, $acceptor->getInvalidateExpressions()); + $usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables()); + } $isVariadic = $isVariadic || $acceptor->isVariadic(); foreach ($acceptor->getParameters() as $i => $parameter) { @@ -460,6 +633,8 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $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, ); continue; } @@ -477,6 +652,8 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $nativeType = $parameters[$i]->getNativeType(); $phpDocType = $parameters[$i]->getPhpDocType(); $outType = $parameters[$i]->getOutType(); + $immediatelyInvokedCallable = $parameters[$i]->isImmediatelyInvokedCallable(); + $closureThisType = $parameters[$i]->getClosureThisType(); if ($parameter instanceof ParameterReflectionWithPhpDocs) { $nativeType = TypeCombinator::union($nativeType, $parameter->getNativeType()); $phpDocType = TypeCombinator::union($phpDocType, $parameter->getPhpDocType()); @@ -486,10 +663,20 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit } else { $outType = null; } + + if ($parameter->getClosureThisType() !== null && $closureThisType !== null) { + $closureThisType = TypeCombinator::union($closureThisType, $parameter->getClosureThisType()); + } else { + $closureThisType = null; + } + + $immediatelyInvokedCallable = $parameter->isImmediatelyInvokedCallable()->or($immediatelyInvokedCallable); } else { $nativeType = new MixedType(); $phpDocType = $type; $outType = null; + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + $closureThisType = null; } $parameters[$i] = new DummyParameterWithPhpDocs( @@ -502,6 +689,8 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $nativeType, $phpDocType, $outType, + $immediatelyInvokedCallable, + $closureThisType, ); if ($isVariadic) { @@ -515,6 +704,24 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $phpDocReturnType = $phpDocReturnTypes === [] ? null : TypeCombinator::union(...$phpDocReturnTypes); $nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes); + if ($callableOccurred) { + return new CallableFunctionVariantWithPhpDocs( + TemplateTypeMap::createEmpty(), + null, + $parameters, + $isVariadic, + $returnType, + $phpDocReturnType ?? $returnType, + $nativeReturnType ?? new MixedType(), + null, + $throwPoints, + $isPure, + $impurePoints, + $invalidateExpressions, + $usedVariables, + ); + } + return new FunctionVariantWithPhpDocs( TemplateTypeMap::createEmpty(), null, @@ -532,6 +739,24 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAc return $acceptor; } + if ($acceptor instanceof CallableParametersAcceptor) { + return new CallableFunctionVariantWithPhpDocs( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => self::wrapParameter($parameter), $acceptor->getParameters()), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + $acceptor->getThrowPoints(), + $acceptor->isPure(), + $acceptor->getImpurePoints(), + $acceptor->getInvalidateExpressions(), + $acceptor->getUsedVariables(), + ); + } + return new FunctionVariantWithPhpDocs( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), @@ -556,6 +781,8 @@ private static function wrapParameter(ParameterReflection $parameter): Parameter new MixedType(), $parameter->getType(), null, + TrinaryLogic::createMaybe(), + null, ); } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index e51c1e75ec..7e2b402bf1 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -93,6 +93,8 @@ public function getVariants(): array new MixedType(), $parameter->getType(), null, + TrinaryLogic::createMaybe(), + null, ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), @@ -168,4 +170,9 @@ public function isAbstract(): TrinaryLogic return $abstract; } + public function isPure(): TrinaryLogic + { + return $this->nativeMethodReflection->isPure(); + } + } diff --git a/src/Reflection/Php/DummyParameterWithPhpDocs.php b/src/Reflection/Php/DummyParameterWithPhpDocs.php index e1b4df10df..262b601bea 100644 --- a/src/Reflection/Php/DummyParameterWithPhpDocs.php +++ b/src/Reflection/Php/DummyParameterWithPhpDocs.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; class DummyParameterWithPhpDocs extends DummyParameter implements ParameterReflectionWithPhpDocs @@ -19,6 +20,8 @@ public function __construct( private Type $nativeType, private Type $phpDocType, private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -39,4 +42,14 @@ public function getOutType(): ?Type return $this->outType; } + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 246901ae13..0665eee656 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -135,4 +135,9 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 321ee288ee..f3ff0aa2ab 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -497,13 +497,18 @@ private function createMethod( $stubPhpDocPair = null; $stubPhpParameterOutTypes = []; $phpDocParameterOutTypes = []; + $immediatelyInvokedCallableParameters = []; + $closureThisParameters = []; + $stubImmediatelyInvokedCallableParameters = []; + $stubClosureThisParameters = []; if (count($methodSignatures) === 1) { - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); if ($stubPhpDocPair !== null) { [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); $callSiteVarianceMap = $stubDeclaringClass->getCallSiteVarianceMap(); $returnTag = $stubPhpDoc->getReturnTag(); + $stubImmediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $stubPhpDoc->getParamsImmediatelyInvokedCallable()); if ($returnTag !== null) { $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( $returnTag->getType(), @@ -513,6 +518,7 @@ private function createMethod( ); } + $stubClosureThisParameters = array_map(static fn ($tag) => $tag->getType(), $stubPhpDoc->getParamClosureThisTags()); foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( $paramTag->getType(), @@ -567,6 +573,8 @@ private function createMethod( if ($returnTag !== null && count($methodSignatures) === 1) { $phpDocReturnType = $returnTag->getType(); } + $immediatelyInvokedCallableParameters = array_map(static fn ($immediate) => TrinaryLogic::createFromBoolean($immediate), $phpDocBlock->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $phpDocBlock->getParamClosureThisTags()); foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { $phpDocParameterTypes[$name] = $paramTag->getType(); } @@ -595,7 +603,7 @@ private function createMethod( } } } - $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes, $signatureType !== 'named'); + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes, $stubImmediatelyInvokedCallableParameters, $immediatelyInvokedCallableParameters, $stubClosureThisParameters, $closureThisParameters, $signatureType !== 'named'); } } @@ -629,7 +637,7 @@ private function createMethod( public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, BuiltinMethodReflection $methodReflection, ?string $declaringTraitName): PhpMethodReflection { $resolvedPhpDoc = null; - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); $phpDocBlockClassReflection = $fileDeclaringClass; if ($methodReflection->getReflection() !== null) { @@ -639,6 +647,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla 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(), @@ -719,6 +728,8 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { @@ -781,6 +792,8 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $selfOutType, $phpDocComment, $phpDocParameterOutTypes, + $immediatelyInvokedCallableParameters, + $closureThisParameters, ); } @@ -791,6 +804,10 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla * @param array $phpDocParameterNameMapping * @param array $stubPhpDocParameterOutTypes * @param array $phpDocParameterOutTypes + * @param array $stubImmediatelyInvokedCallableParameters + * @param array $immediatelyInvokedCallableParameters + * @param array $stubClosureThisParameters + * @param array $closureThisParameters */ private function createNativeMethodVariant( FunctionSignature $methodSignature, @@ -802,6 +819,10 @@ private function createNativeMethodVariant( array $phpDocParameterNameMapping, array $stubPhpDocParameterOutTypes, array $phpDocParameterOutTypes, + array $stubImmediatelyInvokedCallableParameters, + array $immediatelyInvokedCallableParameters, + array $stubClosureThisParameters, + array $closureThisParameters, bool $usePhpDocParameterNames, ): FunctionVariantWithPhpDocs { @@ -826,6 +847,21 @@ private function createNativeMethodVariant( $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; } + if (isset($stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()])) { + $immediatelyInvoked = $stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()]; + } elseif (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { + $immediatelyInvoked = $immediatelyInvokedCallableParameters[$phpDocParameterName]; + } else { + $immediatelyInvoked = TrinaryLogic::createMaybe(); + } + + $closureThisType = null; + if (isset($stubClosureThisParameters[$parameterSignature->getName()])) { + $closureThisType = $stubClosureThisParameters[$parameterSignature->getName()]; + } elseif (isset($closureThisParameters[$phpDocParameterName])) { + $closureThisType = $closureThisParameters[$phpDocParameterName]; + } + $parameters[] = new NativeParameterWithPhpDocsReflection( $usePhpDocParameterNames ? $phpDocParameterName @@ -838,6 +874,8 @@ private function createNativeMethodVariant( $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), $parameterSignature->getDefaultValue(), $parameterOutType ?? $parameterSignature->getOutType(), + $immediatelyInvoked, + $closureThisType, ); } @@ -950,7 +988,7 @@ private function inferAndCachePropertyTypes( $classScope = $classScope->enterNamespace($namespace); } $classScope = $classScope->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -967,6 +1005,8 @@ private function inferAndCachePropertyTypes( $selfOutType, $phpDocComment, $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $propertyTypes = []; @@ -1087,10 +1127,15 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection * @param array $positionalParameterNames * @return array{ResolvedPhpDocBlock, ClassReflection}|null */ - private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringClass, string $methodName, array $positionalParameterNames): ?array + private function findMethodPhpDocIncludingAncestors( + ClassReflection $declaringClass, + ClassReflection $implementingClass, + string $methodName, + array $positionalParameterNames, + ): ?array { $declaringClassName = $declaringClass->getName(); - $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($declaringClassName, $methodName, $positionalParameterNames); + $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($declaringClassName, $implementingClass->getName(), $methodName, $positionalParameterNames); if ($resolved !== null) { return [$resolved, $declaringClass]; } @@ -1107,7 +1152,7 @@ private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringCl continue; } - $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($ancestor->getName(), $methodName, $positionalParameterNames); + $resolved = $this->stubPhpDocProvider->findMethodPhpDoc($ancestor->getName(), $ancestor->getName(), $methodName, $positionalParameterNames); if ($resolved === null) { continue; } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 05b4c39ac3..8e2429016c 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -41,6 +41,8 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function __construct( FunctionLike $functionLike, @@ -56,11 +58,13 @@ public function __construct( private bool $isDeprecated, private bool $isInternal, private bool $isFinal, - private ?bool $isPure, + protected ?bool $isPure, private bool $acceptsNamedArguments, private Assertions $assertions, private ?string $phpDocComment, private array $parameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, ) { $this->functionLike = $functionLike; @@ -133,6 +137,19 @@ private function getParameters(): array if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { throw new ShouldNotHappenException(); } + + if (isset($this->immediatelyInvokedCallableParameters[$parameter->var->name])) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->immediatelyInvokedCallableParameters[$parameter->var->name]); + } else { + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + } + + if (isset($this->phpDocClosureThisTypeParameters[$parameter->var->name])) { + $closureThisType = $this->phpDocClosureThisTypeParameters[$parameter->var->name]; + } else { + $closureThisType = null; + } + $parameters[] = new PhpParameterFromParserNodeReflection( $parameter->var->name, $isOptional, @@ -144,6 +161,8 @@ private function getParameters(): array $this->realParameterDefaultValues[$parameter->var->name] ?? null, $parameter->variadic, $this->parameterOutTypes[$parameter->var->name] ?? null, + $immediatelyInvokedCallable, + $closureThisType, ); } @@ -161,7 +180,7 @@ private function isVariadic(): bool return false; } - private function getReturnType(): Type + protected function getReturnType(): Type { return TypehintHelper::decideType($this->realReturnType, $this->phpDocReturnType); } @@ -281,4 +300,13 @@ public function returnsByReference(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->functionLike->returnsByRef()); } + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 17ae2baed7..d437c86f5e 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -24,6 +24,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use function array_key_exists; use function array_map; use function filemtime; use function is_file; @@ -37,8 +38,10 @@ class PhpFunctionReflection implements FunctionReflection private ?array $variants = null; /** - * @param Type[] $phpDocParameterTypes - * @param Type[] $phpDocParameterOutTypes + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -59,6 +62,8 @@ public function __construct( private Assertions $asserts, private ?string $phpDocComment, private array $phpDocParameterOutTypes, + private array $phpDocParameterImmediatelyInvokedCallable, + private array $phpDocParameterClosureThisTypes, ) { } @@ -113,13 +118,22 @@ public function getNamedArgumentsVariants(): ?array */ private function getParameters(): array { - return array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( - $this->initializerExprTypeResolver, - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - null, - $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, - ), $this->reflection->getParameters()); + return array_map(function (ReflectionParameter $reflection): PhpParameterReflection { + if (array_key_exists($reflection->getName(), $this->phpDocParameterImmediatelyInvokedCallable)) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->phpDocParameterImmediatelyInvokedCallable[$reflection->getName()]); + } else { + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + } + return new PhpParameterReflection( + $this->initializerExprTypeResolver, + $reflection, + $this->phpDocParameterTypes[$reflection->getName()] ?? null, + null, + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, + $immediatelyInvokedCallable, + $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, + ); + }, $this->reflection->getParameters()); } private function isVariadic(): bool @@ -254,6 +268,15 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index f04d2bac38..7e858b714e 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -33,6 +33,8 @@ class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflect * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function __construct( private ClassReflection $declaringClass, @@ -55,6 +57,8 @@ public function __construct( private ?Type $selfOutType, ?string $phpDocComment, array $parameterOutTypes, + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, ) { $name = strtolower($classMethod->name->name); @@ -109,6 +113,8 @@ public function __construct( $assertions, $phpDocComment, $parameterOutTypes, + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); } @@ -168,4 +174,19 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->getClassMethod()->isAbstract()); } + public function hasSideEffects(): TrinaryLogic + { + if ( + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() + ) { + return TrinaryLogic::createYes(); + } + if ($this->isPure !== null) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index b374e9e6f7..0fba6c34ce 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -30,6 +30,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; @@ -61,6 +62,8 @@ class PhpMethodReflection implements ExtendedMethodReflection /** * @param Type[] $phpDocParameterTypes * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -84,6 +87,8 @@ public function __construct( private ?Type $selfOutType, private ?string $phpDocComment, private array $phpDocParameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, ) { } @@ -110,6 +115,10 @@ public function getPrototype(): ClassMemberReflection $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); } + if (!$prototypeDeclaringClass->hasNativeMethod($prototypeMethod->getName())) { + return $this; + } + $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); @@ -216,6 +225,8 @@ private function getParameters(): array $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()); } @@ -430,6 +441,10 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createFromBoolean(!$this->isPure); } + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -453,4 +468,13 @@ public function returnsByReference(): TrinaryLogic return $this->reflection->returnsByReference(); } + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index a73c617df0..8da0dcb5c6 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -13,6 +14,8 @@ interface PhpMethodReflectionFactory /** * @param Type[] $phpDocParameterTypes * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function create( ClassReflection $declaringClass, @@ -31,6 +34,8 @@ public function create( ?Type $selfOutType, ?string $phpDocComment, array $phpDocParameterOutTypes, + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index ad6410ccd1..9dd5a25958 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -23,6 +24,8 @@ public function __construct( private ?Type $defaultValue, private bool $variadic, private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { } @@ -85,4 +88,14 @@ public function getOutType(): ?Type return $this->outType; } + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index be16f41bcb..0e6ce5ae77 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -25,6 +26,8 @@ public function __construct( private ?Type $phpDocType, private ?string $declaringClassName, private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { } @@ -119,4 +122,14 @@ public function getOutType(): ?Type return $this->outType; } + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + } diff --git a/src/Reflection/ReflectionProvider.php b/src/Reflection/ReflectionProvider.php index 1248766326..e268800f5a 100644 --- a/src/Reflection/ReflectionProvider.php +++ b/src/Reflection/ReflectionProvider.php @@ -9,6 +9,7 @@ interface ReflectionProvider { + /** @phpstan-assert-if-true =class-string $className */ public function hasClass(string $className): bool; public function getClass(string $className): ClassReflection; diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index eb631bbaf3..a6139853fb 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -2,266 +2,15 @@ namespace PHPStan\Reflection; -use PHPStan\DependencyInjection\BleedingEdgeToggle; -use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; -use PHPStan\Type\ConditionalTypeForParameter; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\GenericObjectType; -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\NonAcceptingNeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use function array_key_exists; -use function array_map; -class ResolvedFunctionVariant implements ParametersAcceptorWithPhpDocs +interface ResolvedFunctionVariant extends ParametersAcceptorWithPhpDocs { - /** @var ParameterReflectionWithPhpDocs[]|null */ - private ?array $parameters = null; + public function getOriginalParametersAcceptor(): ParametersAcceptor; - private ?Type $returnTypeWithUnresolvableTemplateTypes = null; + public function getReturnTypeWithUnresolvableTemplateTypes(): Type; - private ?Type $phpDocReturnTypeWithUnresolvableTemplateTypes = null; - - private ?Type $returnType = null; - - private ?Type $phpDocReturnType = null; - - /** - * @param array $passedArgs - */ - public function __construct( - private ParametersAcceptorWithPhpDocs $parametersAcceptor, - private TemplateTypeMap $resolvedTemplateTypeMap, - private TemplateTypeVarianceMap $callSiteVarianceMap, - private array $passedArgs, - ) - { - } - - public function getOriginalParametersAcceptor(): ParametersAcceptor - { - return $this->parametersAcceptor; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getCallSiteVarianceMap(): TemplateTypeVarianceMap - { - return $this->callSiteVarianceMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map( - function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs { - $paramType = TypeUtils::resolveLateResolvableTypes( - TemplateTypeHelper::resolveTemplateTypes( - $this->resolveConditionalTypesForParameter($param->getType()), - $this->resolvedTemplateTypeMap, - $this->callSiteVarianceMap, - TemplateTypeVariance::createContravariant(), - ), - false, - ); - - return new DummyParameterWithPhpDocs( - $param->getName(), - $paramType, - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue(), - $param->getNativeType(), - $param->getPhpDocType(), - $param->getOutType(), - ); - }, - $this->parametersAcceptor->getParameters(), - ); - - $this->parameters = $parameters; - } - - return $parameters; - } - - public function isVariadic(): bool - { - return $this->parametersAcceptor->isVariadic(); - } - - public function getReturnTypeWithUnresolvableTemplateTypes(): Type - { - return $this->returnTypeWithUnresolvableTemplateTypes ??= - $this->resolveConditionalTypesForParameter( - $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType(), TemplateTypeVariance::createCovariant()), - ); - } - - public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type - { - return $this->phpDocReturnTypeWithUnresolvableTemplateTypes ??= - $this->resolveConditionalTypesForParameter( - $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getPhpDocReturnType(), TemplateTypeVariance::createCovariant()), - ); - } - - public function getReturnType(): Type - { - $type = $this->returnType; - - if ($type === null) { - $type = TypeUtils::resolveLateResolvableTypes( - TemplateTypeHelper::resolveTemplateTypes( - $this->getReturnTypeWithUnresolvableTemplateTypes(), - $this->resolvedTemplateTypeMap, - $this->callSiteVarianceMap, - TemplateTypeVariance::createCovariant(), - ), - false, - ); - - $this->returnType = $type; - } - - return $type; - } - - public function getPhpDocReturnType(): Type - { - $type = $this->phpDocReturnType; - - if ($type === null) { - $type = TypeUtils::resolveLateResolvableTypes( - TemplateTypeHelper::resolveTemplateTypes( - $this->getPhpDocReturnTypeWithUnresolvableTemplateTypes(), - $this->resolvedTemplateTypeMap, - $this->callSiteVarianceMap, - TemplateTypeVariance::createCovariant(), - ), - false, - ); - - $this->phpDocReturnType = $type; - } - - return $type; - } - - public function getNativeReturnType(): Type - { - return $this->parametersAcceptor->getNativeReturnType(); - } - - private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance $positionVariance): Type - { - $references = $type->getReferencedTemplateTypes($positionVariance); - - $objectCb = function (Type $type, callable $traverse) use ($references): Type { - if ($type instanceof TemplateType && !$type->isArgument()) { - $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); - if ($newType === null || $newType instanceof ErrorType) { - return $traverse($type); - } - - $newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); - $variance = TemplateTypeVariance::createInvariant(); - foreach ($references as $reference) { - // this uses identity to distinguish between different occurrences of the same template type - // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details - if ($reference->getType() === $type) { - $variance = $reference->getPositionVariance(); - break; - } - } - - $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); - if ($callSiteVariance === null || $callSiteVariance->invariant()) { - return $newType; - } - - if (!$callSiteVariance->covariant() && $variance->covariant()) { - return $traverse($type->getBound()); - } - - if (!$callSiteVariance->contravariant() && $variance->contravariant()) { - return new NonAcceptingNeverType(); - } - - return $newType; - } - - return $traverse($type); - }; - - return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type { - if (BleedingEdgeToggle::isBleedingEdge() && $type instanceof GenericObjectType) { - return TypeTraverser::map($type, $objectCb); - } - - if ($type instanceof TemplateType && !$type->isArgument()) { - $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); - if ($newType === null || $newType instanceof ErrorType) { - return $traverse($type); - } - - $variance = TemplateTypeVariance::createInvariant(); - foreach ($references as $reference) { - // this uses identity to distinguish between different occurrences of the same template type - // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details - if ($reference->getType() === $type) { - $variance = $reference->getPositionVariance(); - break; - } - } - - $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); - if ($callSiteVariance === null || $callSiteVariance->invariant()) { - return $newType; - } - - if (!$callSiteVariance->covariant() && $variance->covariant()) { - return $traverse($type->getBound()); - } - - if (!$callSiteVariance->contravariant() && $variance->contravariant()) { - return new NonAcceptingNeverType(); - } - - return $newType; - } - - return $traverse($type); - }); - } - - private function resolveConditionalTypesForParameter(Type $type): Type - { - return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { - if ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) { - $type = $type->toConditional($this->passedArgs[$type->getParameterName()]); - } - - return $traverse($type); - }); - } + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type; } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php new file mode 100644 index 0000000000..4714738167 --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -0,0 +1,114 @@ +parametersAcceptor->getOriginalParametersAcceptor(); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getResolvedTemplateTypeMap(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->parametersAcceptor->getCallSiteVarianceMap(); + } + + public function getParameters(): array + { + return $this->parametersAcceptor->getParameters(); + } + + public function isVariadic(): bool + { + return $this->parametersAcceptor->isVariadic(); + } + + public function getReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(); + } + + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->parametersAcceptor->getPhpDocReturnTypeWithUnresolvableTemplateTypes(); + } + + public function getReturnType(): Type + { + return $this->parametersAcceptor->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->parametersAcceptor->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->parametersAcceptor->getNativeReturnType(); + } + + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + +} diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php new file mode 100644 index 0000000000..6d72ef75bc --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -0,0 +1,299 @@ + $passedArgs + */ + public function __construct( + private ParametersAcceptorWithPhpDocs $parametersAcceptor, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + private array $passedArgs, + ) + { + } + + public function getOriginalParametersAcceptor(): ParametersAcceptor + { + return $this->parametersAcceptor; + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->resolvedTemplateTypeMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + + public function getParameters(): array + { + $parameters = $this->parameters; + + if ($parameters === null) { + $parameters = array_map( + function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs { + $paramType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($param->getType()), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ), + false, + ); + + $paramOutType = $param->getOutType(); + if ($paramOutType !== null) { + $paramOutType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($paramOutType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + + $closureThisType = $param->getClosureThisType(); + if ($closureThisType !== null) { + $closureThisType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($closureThisType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + + return new DummyParameterWithPhpDocs( + $param->getName(), + $paramType, + $param->isOptional(), + $param->passedByReference(), + $param->isVariadic(), + $param->getDefaultValue(), + $param->getNativeType(), + $param->getPhpDocType(), + $paramOutType, + $param->isImmediatelyInvokedCallable(), + $closureThisType, + ); + }, + $this->parametersAcceptor->getParameters(), + ); + + $this->parameters = $parameters; + } + + return $parameters; + } + + public function isVariadic(): bool + { + return $this->parametersAcceptor->isVariadic(); + } + + public function getReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->returnTypeWithUnresolvableTemplateTypes ??= + $this->resolveConditionalTypesForParameter( + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType(), TemplateTypeVariance::createCovariant()), + ); + } + + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->phpDocReturnTypeWithUnresolvableTemplateTypes ??= + $this->resolveConditionalTypesForParameter( + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getPhpDocReturnType(), TemplateTypeVariance::createCovariant()), + ); + } + + public function getReturnType(): Type + { + $type = $this->returnType; + + if ($type === null) { + $type = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->getReturnTypeWithUnresolvableTemplateTypes(), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + + $this->returnType = $type; + } + + return $type; + } + + public function getPhpDocReturnType(): Type + { + $type = $this->phpDocReturnType; + + if ($type === null) { + $type = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->getPhpDocReturnTypeWithUnresolvableTemplateTypes(), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + + $this->phpDocReturnType = $type; + } + + return $type; + } + + public function getNativeReturnType(): Type + { + return $this->parametersAcceptor->getNativeReturnType(); + } + + private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance $positionVariance): Type + { + $references = $type->getReferencedTemplateTypes($positionVariance); + + $objectCb = function (Type $type, callable $traverse) use ($references): Type { + if ( + $type instanceof TemplateType + && !$type->isArgument() + && $type->getScope()->getFunctionName() !== null + ) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + $newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + }; + + return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type { + if (BleedingEdgeToggle::isBleedingEdge() && $type instanceof GenericObjectType) { + return TypeTraverser::map($type, $objectCb); + } + + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + }); + } + + private function resolveConditionalTypesForParameter(Type $type): Type + { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) { + $type = $type->toConditional($this->passedArgs[$type->getParameterName()]); + } + + return $traverse($type); + }); + } + +} diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 870941e2cf..31433330b8 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -75,7 +75,7 @@ private function resolveVariants(array $variants): array { $result = []; foreach ($variants as $variant) { - $result[] = new ResolvedFunctionVariant( + $result[] = new ResolvedFunctionVariantWithOriginal( $variant, $this->resolvedTemplateTypeMap, $this->callSiteVarianceMap, @@ -155,6 +155,11 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return $this->reflection->isPure(); + } + public function getAsserts(): Assertions { return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 9b8c21910b..66ca12c487 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -184,71 +184,54 @@ public function getSignatureMap(): array $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); if ($this->stricterFunctionMap) { - $stricterFunctionMap = require __DIR__ . '/../../../resources/functionMap_bleedingEdge.php'; - if (!is_array($stricterFunctionMap)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = $this->computeSignatureMap($signatureMap, $stricterFunctionMap); - - if ($this->phpVersion->getVersionId() >= 80000) { - $php80StricterFunctionMapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta_bleedingEdge.php'; - if (!is_array($php80StricterFunctionMapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = $this->computeSignatureMap($signatureMap, $php80StricterFunctionMapDelta); - } + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_bleedingEdge.php'); } if ($this->phpVersion->getVersionId() >= 70400) { - $php74MapDelta = require __DIR__ . '/../../../resources/functionMap_php74delta.php'; - if (!is_array($php74MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = $this->computeSignatureMap($signatureMap, $php74MapDelta); + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php74delta.php'); } if ($this->phpVersion->getVersionId() >= 80000) { - $php80MapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta.php'; - if (!is_array($php80MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php80delta.php'); - $signatureMap = $this->computeSignatureMap($signatureMap, $php80MapDelta); + if ($this->stricterFunctionMap) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php80delta_bleedingEdge.php'); + } } if ($this->phpVersion->getVersionId() >= 80100) { - $php81MapDelta = require __DIR__ . '/../../../resources/functionMap_php81delta.php'; - if (!is_array($php81MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = $this->computeSignatureMap($signatureMap, $php81MapDelta); + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php81delta.php'); } if ($this->phpVersion->getVersionId() >= 80200) { - $php82MapDelta = require __DIR__ . '/../../../resources/functionMap_php82delta.php'; - if (!is_array($php82MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } - - $signatureMap = $this->computeSignatureMap($signatureMap, $php82MapDelta); + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php82delta.php'); } if ($this->phpVersion->getVersionId() >= 80300) { - $php83MapDelta = require __DIR__ . '/../../../resources/functionMap_php83delta.php'; - if (!is_array($php83MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php83delta.php'); + } - $signatureMap = $this->computeSignatureMap($signatureMap, $php83MapDelta); + if ($this->phpVersion->getVersionId() >= 80400) { + $signatureMap = $this->computeSignatureMapFile($signatureMap, __DIR__ . '/../../../resources/functionMap_php84delta.php'); } return self::$signatureMaps[$cacheKey] = $signatureMap; } + /** + * @param array $signatureMap + * @return array + */ + private function computeSignatureMapFile(array $signatureMap, string $file): array + { + $signatureMapDelta = include $file; + if (!is_array($signatureMapDelta)) { + throw new ShouldNotHappenException(sprintf('Signature map file "%s" could not be loaded.', $file)); + } + + return $this->computeSignatureMap($signatureMap, $signatureMapDelta); + } + /** * @param array $signatureMap * @param array> $delta diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 734bf96f7d..7c850a582e 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -96,10 +96,17 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $type = $parameterSignature->getType(); $phpDocType = null; + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + $closureThisType = null; if ($phpDoc !== null) { - $phpDocParam = $phpDoc->getParamTags()[$parameterSignature->getName()] ?? null; - if ($phpDocParam !== null) { - $phpDocType = $phpDocParam->getType(); + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamTags())) { + $phpDocType = $phpDoc->getParamTags()[$parameterSignature->getName()]->getType(); + } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamsImmediatelyInvokedCallable())) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($phpDoc->getParamsImmediatelyInvokedCallable()[$parameterSignature->getName()]); + } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamClosureThisTags())) { + $closureThisType = $phpDoc->getParamClosureThisTags()[$parameterSignature->getName()]->getType(); } } @@ -113,6 +120,8 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $parameterSignature->isVariadic(), $parameterSignature->getDefaultValue(), $phpDoc !== null ? NativeFunctionReflectionProvider::getParamOutTypeFromPhpDoc($parameterSignature->getName(), $phpDoc) : null, + $immediatelyInvokedCallable, + $closureThisType, ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 746b0582a0..3f663f7909 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -2,17 +2,21 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function sprintf; /** @api */ -class TrivialParametersAcceptor implements ParametersAcceptorWithPhpDocs +class TrivialParametersAcceptor implements ParametersAcceptorWithPhpDocs, CallableParametersAcceptor { /** @api */ - public function __construct() + public function __construct(private string $callableName = 'callable') { } @@ -56,4 +60,35 @@ public function getNativeReturnType(): Type return new MixedType(); } + public function getThrowPoints(): array + { + return []; + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + return [ + new SimpleImpurePoint( + 'functionCall', + sprintf('call to a %s', $this->callableName), + false, + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + } diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 61d7a2f00b..a6f23841f3 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -94,8 +94,10 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->isVariadic(), $parameter->getDefaultValue(), $parameter->getNativeType(), - $parameter->getPhpDocType(), - $parameter->getOutType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, ), $acceptor->getParameters(), ), diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index a76cf064aa..bf4421d1f7 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -89,8 +89,10 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->isVariadic(), $parameter->getDefaultValue(), $parameter->getNativeType(), - $parameter->getPhpDocType(), - $parameter->getOutType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, ), $acceptor->getParameters(), ), diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index bf8dd27853..fb9a8ecdd3 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -166,6 +166,11 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects()); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 61298dbf0c..65ca5bd674 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -154,6 +154,11 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects()); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 352edc5870..c565601261 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -73,6 +73,8 @@ public function getVariants(): array new MixedType(), $parameter->getType(), null, + TrinaryLogic::createMaybe(), + null, ), $variant->getParameters()), $variant->isVariadic(), $variant->getReturnType(), @@ -125,6 +127,11 @@ public function hasSideEffects(): TrinaryLogic return $this->method->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getAsserts(): Assertions { return Assertions::createEmpty(); diff --git a/src/Rules/Api/BcUncoveredInterface.php b/src/Rules/Api/BcUncoveredInterface.php index 795f5f8f7d..9a78dc5cc7 100644 --- a/src/Rules/Api/BcUncoveredInterface.php +++ b/src/Rules/Api/BcUncoveredInterface.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Api; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParameterReflectionWithPhpDocs; @@ -28,6 +29,7 @@ final class BcUncoveredInterface ExtendedMethodReflection::class, ParametersAcceptorWithPhpDocs::class, ParameterReflectionWithPhpDocs::class, + CallableParametersAcceptor::class, FileRuleError::class, IdentifierRuleError::class, LineRuleError::class, diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index 606ef7aa20..a1eaf7e131 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -8,10 +8,12 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ConstantScalarType; use function array_keys; use function count; use function implode; +use function max; use function sprintf; use function var_export; @@ -38,25 +40,55 @@ public function processNode(Node $node, Scope $scope): array $duplicateKeys = []; $printedValues = []; $valueLines = []; + + /** + * @var int|false|null $autoGeneratedIndex + * - An int value represent the biggest integer used as array key. + * When no key is provided this value + 1 will be used. + * - Null is used as initializer instead of 0 to avoid issue with negative keys. + * - False means a non-scalar value was encountered and we cannot be sure of the next keys. + */ + $autoGeneratedIndex = null; foreach ($node->getItemNodes() as $itemNode) { $item = $itemNode->getArrayItem(); if ($item === null) { - continue; - } - if ($item->key === null) { + $autoGeneratedIndex = false; continue; } $key = $item->key; - $keyType = $itemNode->getScope()->getType($key); - if ( - !$keyType instanceof ConstantScalarType - ) { + if ($key === null) { + if ($autoGeneratedIndex === false) { + continue; + } + + if ($autoGeneratedIndex === null) { + $autoGeneratedIndex = 0; + $keyType = new ConstantIntegerType(0); + } else { + $keyType = new ConstantIntegerType(++$autoGeneratedIndex); + } + } else { + $keyType = $itemNode->getScope()->getType($key); + + $arrayKeyValue = $keyType->toArrayKey(); + if ($arrayKeyValue instanceof ConstantIntegerType) { + $autoGeneratedIndex = $autoGeneratedIndex === null + ? $arrayKeyValue->getValue() + : max($autoGeneratedIndex, $arrayKeyValue->getValue()); + } + } + + if (!$keyType instanceof ConstantScalarType) { + $autoGeneratedIndex = false; continue; } - $printedValue = $this->exprPrinter->printExpr($key); $value = $keyType->getValue(); + $printedValue = $key !== null + ? $this->exprPrinter->printExpr($key) + : $value; + $printedValues[$value][] = $printedValue; if (!isset($valueLines[$value])) { diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php index ede68cc01a..6ef401c577 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -23,6 +23,8 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private bool $reportMaybes, private bool $bleedingEdge, + private bool $reportPossiblyNonexistentGeneralArrayOffset, + private bool $reportPossiblyNonexistentConstantArrayOffset, ) { } @@ -69,6 +71,23 @@ public function check( $flattenedTypes = TypeUtils::flattenTypes($type); } foreach ($flattenedTypes as $innerType) { + if ( + $this->reportPossiblyNonexistentGeneralArrayOffset + && $innerType->isArray()->yes() + && !$innerType->isConstantArray()->yes() + && !$innerType->hasOffsetValueType($dimType)->yes() + ) { + $report = true; + break; + } + if ( + $this->reportPossiblyNonexistentConstantArrayOffset + && $innerType->isConstantArray()->yes() + && !$innerType->hasOffsetValueType($dimType)->yes() + ) { + $report = true; + break; + } if ($dimType instanceof UnionType) { if ($innerType->hasOffsetValueType($dimType)->no()) { $report = true; @@ -85,7 +104,7 @@ public function check( } if ($report) { - if ($this->bleedingEdge) { + 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') diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index cf8f3f43c1..547e2f4fe4 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -64,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->isUndefinedExpressionAllowed($node) && !$isOffsetAccessible->no()) { + if ($scope->isUndefinedExpressionAllowed($node) && $isOffsetAccessibleType->isOffsetAccessLegal()->yes()) { return []; } diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 151e2581ed..916c6e1c7d 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -20,7 +20,7 @@ class AttributesCheck public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $functionCallParametersCheck, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $deprecationRulesInstalled, ) { @@ -67,7 +67,7 @@ public function check( ->build(); } - foreach ($this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { + foreach ($this->classCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { $errors[] = $caseSensitivityError; } diff --git a/src/Rules/ClassForbiddenNameCheck.php b/src/Rules/ClassForbiddenNameCheck.php new file mode 100644 index 0000000000..c86fede587 --- /dev/null +++ b/src/Rules/ClassForbiddenNameCheck.php @@ -0,0 +1,93 @@ + '_PHPStan_', + 'Rector' => 'RectorPrefix', + 'PHP-Scoper' => '_PhpScoper', + 'PHPUnit' => 'PHPUnitPHAR', + 'Box' => '_HumbugBox', + ]; + + public function __construct(private Container $container) + { + } + + /** + * @param ClassNameNodePair[] $pairs + * @return list + */ + public function checkClassNames(array $pairs): array + { + $extensions = $this->container->getServicesByTag(ForbiddenClassNameExtension::EXTENSION_TAG); + + $classPrefixes = array_merge( + self::INTERNAL_CLASS_PREFIXES, + ...array_map( + static fn (ForbiddenClassNameExtension $extension): array => $extension->getClassPrefixes(), + $extensions, + ), + ); + + $errors = []; + foreach ($pairs as $pair) { + $className = $pair->getClassName(); + + $projectName = null; + $withoutPrefixClassName = null; + foreach ($classPrefixes as $project => $prefix) { + if (!str_starts_with($className, $prefix)) { + continue; + } + + $projectName = $project; + $withoutPrefixClassName = substr($className, strlen($prefix)); + + if (strpos($withoutPrefixClassName, '\\') === false) { + continue; + } + + $withoutPrefixClassName = substr($withoutPrefixClassName, strpos($withoutPrefixClassName, '\\')); + } + + if ($projectName === null) { + continue; + } + + $error = RuleErrorBuilder::message(sprintf( + 'Referencing prefixed %s class: %s.', + $projectName, + $className, + )) + ->line($pair->getNode()->getLine()) + ->identifier('class.prefixed') + ->nonIgnorable(); + + if ($withoutPrefixClassName !== null) { + $error->tip(sprintf( + 'This is most likely unintentional. Did you mean to type %s?', + $withoutPrefixClassName, + )); + } + + $errors[] = $error->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/ClassNameCheck.php b/src/Rules/ClassNameCheck.php new file mode 100644 index 0000000000..c168b74ce7 --- /dev/null +++ b/src/Rules/ClassNameCheck.php @@ -0,0 +1,35 @@ + + */ + public function checkClassNames(array $pairs, bool $checkClassCaseSensitivity = true): array + { + $errors = []; + + if ($checkClassCaseSensitivity) { + foreach ($this->classCaseSensitivityCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + } + foreach ($this->classForbiddenNameCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 2217ba992d..f8425ff295 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -9,7 +9,7 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -34,7 +34,7 @@ class ClassConstantRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private PhpVersion $phpVersion, ) { @@ -111,7 +111,7 @@ public function processNode(Node $node, Scope $scope): array ]; } - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); $classType = $scope->resolveTypeByName($class); } diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index df11f73247..b0a6aae26d 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -18,7 +18,7 @@ class ExistingClassInClassExtendsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -35,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $extendedClassName = (string) $node->extends; - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($extendedClassName, $node->extends)]); + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($extendedClassName, $node->extends)]); $currentClassName = null; if (isset($node->namespacedName)) { $currentClassName = (string) $node->namespacedName; @@ -99,6 +99,30 @@ public function processNode(Node $node, Scope $scope): array ->identifier('class.extendsFinalByPhpDoc') ->build(); } + + if ($reflection->isClass()) { + if ($node->isReadonly()) { + if (!$reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends non-readonly class %s.', + $currentClassName !== null ? sprintf('Readonly class %s', $currentClassName) : 'Anonymous readonly class', + $reflection->getDisplayName(), + )) + ->identifier('class.readOnly') + ->nonIgnorable() + ->build(); + } + } elseif ($reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends readonly class %s.', + $currentClassName !== null ? sprintf('Non-readonly class %s', $currentClassName) : 'Anonymous non-readonly class', + $reflection->getDisplayName(), + )) + ->identifier('class.nonReadOnly') + ->nonIgnorable() + ->build(); + } + } } return $messages; diff --git a/src/Rules/Classes/ExistingClassInInstanceOfRule.php b/src/Rules/Classes/ExistingClassInInstanceOfRule.php index a3f130db95..7eef8ba523 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\Instanceof_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -24,7 +24,7 @@ class ExistingClassInInstanceOfRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, ) { @@ -76,13 +76,16 @@ public function processNode(Node $node, Scope $scope): array ->discoveringSymbolsTip() ->build(), ]; - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $class)]), - ); } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + [new ClassNameNodePair($name, $class)], + $this->checkClassCaseSensitivity, + ), + ); + $classReflection = $this->reflectionProvider->getClass($name); if ($classReflection->isTrait()) { diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index d3cbd76660..e47d9cf5d6 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -20,7 +20,7 @@ class ExistingClassInTraitUseRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -33,7 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), ); diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index c7d5238b02..5aeb3fde46 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +19,7 @@ class ExistingClassesInClassImplementsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -32,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), ); diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php index f596681c0c..413f32fcd8 100644 --- a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +19,7 @@ class ExistingClassesInEnumImplementsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -32,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), ); diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index 7968563bdd..e25a2ac974 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +19,7 @@ class ExistingClassesInInterfaceExtendsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -32,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->extends), ); diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index 69eb96f832..c84092f810 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$instanceofType->getValue()) { diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 51143e7c0c..7e31e5adcd 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -9,7 +9,7 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\IdentifierRuleError; @@ -32,7 +32,7 @@ class InstantiationRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, ) { } @@ -128,7 +128,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ ]; } - $messages = $this->classCaseSensitivityCheck->checkClassNames([ + $messages = $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $node->class), ]); @@ -222,7 +222,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ } /** - * @param Node\Expr\New_ $node $node + * @param Node\Expr\New_ $node * @return array */ private function getClassNames(Node $node, Scope $scope): array diff --git a/src/Rules/Classes/MixinRule.php b/src/Rules/Classes/MixinRule.php index 29f8bdd162..ce397c3e21 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; @@ -26,7 +26,7 @@ class MixinRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private MissingTypehintCheck $missingTypehintCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, @@ -79,7 +79,6 @@ public function processNode(Node $node, Scope $scope): array $innerName, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } @@ -94,12 +93,12 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class)) ->identifier('mixin.trait') ->build(); - } elseif ($this->checkClassCaseSensitivity) { + } else { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $node), - ]), + ], $this->checkClassCaseSensitivity), ); } } diff --git a/src/Rules/Classes/RequireExtendsRule.php b/src/Rules/Classes/RequireExtendsRule.php index a2f59e7b51..34a35ab11f 100644 --- a/src/Rules/Classes/RequireExtendsRule.php +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -50,7 +50,9 @@ public function processNode(Node $node, Scope $scope): array $type->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(), ), - )->build(); + ) + ->identifier('class.missingExtends') + ->build(); } } @@ -73,7 +75,9 @@ public function processNode(Node $node, Scope $scope): array $type->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(), ), - )->build(); + ) + ->identifier('class.missingExtends') + ->build(); } } diff --git a/src/Rules/Classes/RequireImplementsRule.php b/src/Rules/Classes/RequireImplementsRule.php index 551cdd3ed2..32c9361d65 100644 --- a/src/Rules/Classes/RequireImplementsRule.php +++ b/src/Rules/Classes/RequireImplementsRule.php @@ -46,7 +46,9 @@ public function processNode(Node $node, Scope $scope): array $type->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(), ), - )->build(); + ) + ->identifier('class.missingImplements') + ->build(); } } diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 37024cf299..44c574fb68 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -42,9 +42,8 @@ public function processNode( $nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '&&'; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $originalNode): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -54,7 +53,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); @@ -79,7 +78,7 @@ public function processNode( $originalNode->right, ); if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -92,7 +91,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); @@ -114,7 +113,7 @@ public function processNode( if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -124,7 +123,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index 95c993f693..d41cb743ca 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -46,7 +46,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index bcc25056c4..831829355c 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -42,9 +42,8 @@ public function processNode( $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -54,7 +53,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); @@ -79,7 +78,7 @@ public function processNode( $originalNode->right, ); if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -92,7 +91,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); @@ -114,7 +113,7 @@ public function processNode( if (count($messages) === 0 && !$scope->isInFirstLevelStatement()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -124,7 +123,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php index 0b84c5fc85..5222647d17 100644 --- a/src/Rules/Comparison/ConstantLooseComparisonRule.php +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -51,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$nodeType->getValue()) { diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 967011118f..6074f8d20c 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 11af13a5aa..d1bf93a6db 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -46,7 +46,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->cond->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index 23cba9a839..8bf57ebcd4 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -44,7 +44,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 914f78640e..55423dbf50 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 9463072340..828cfa1b82 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -88,7 +88,7 @@ public function findSpecifiedType( return null; } elseif ($functionName === 'array_search') { return null; - } elseif ($functionName === 'in_array' && $argsCount >= 3) { + } elseif ($functionName === 'in_array' && $argsCount >= 2) { $haystackArg = $node->getArgs()[1]->value; $haystackType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($haystackArg) : $scope->getNativeType($haystackArg)); if ($haystackType instanceof MixedType) { @@ -101,6 +101,21 @@ public function findSpecifiedType( $needleArg = $node->getArgs()[0]->value; $needleType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($needleArg) : $scope->getNativeType($needleArg)); + + $isStrictComparison = false; + if ($argsCount >= 3) { + $strictNodeType = $scope->getType($node->getArgs()[2]->value); + $isStrictComparison = $strictNodeType->isTrue()->yes(); + } + + $isStrictComparison = $isStrictComparison + || $needleType->isEnum()->yes() + || $haystackType->getIterableValueType()->isEnum()->yes(); + + if (!$isStrictComparison) { + return null; + } + $valueType = $haystackType->getIterableValueType(); $constantNeedleTypesCount = count($needleType->getFiniteTypes()); $constantHaystackTypesCount = count($valueType->getFiniteTypes()); diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index f5e6ae4ec5..9fc4e4067d 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index ae497b945e..5d35d1be1a 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php index d807b4c041..250b55bd6e 100644 --- a/src/Rules/Comparison/LogicalXorConstantConditionRule.php +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -34,9 +34,8 @@ public function processNode(Node $node, Scope $scope): array { $errors = []; $leftType = $this->helper->getBooleanType($scope, $node->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $node): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -46,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); @@ -66,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array $rightType = $this->helper->getBooleanType($scope, $node->right); if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $tipText): RuleErrorBuilder { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -79,7 +78,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index 5ab373b77a..081cdef1e0 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -56,7 +56,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; switch (get_class($node)) { diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 1b40ed5080..66f447db3b 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$nodeType->getValue()) { diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index 01f35d64d6..9513924e6b 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -44,7 +44,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Comparison/UnreachableIfBranchesRule.php b/src/Rules/Comparison/UnreachableIfBranchesRule.php index 120fb0b69d..4918a320d6 100644 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ b/src/Rules/Comparison/UnreachableIfBranchesRule.php @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; foreach ($node->elseifs as $elseif) { diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php index aa3975df1c..32ef874c5f 100644 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message('Else branch is unreachable because ternary operator condition is always true.')) diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index f53fa8ce3e..01ae378030 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -44,7 +44,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index 26aa3047a7..42a885b6ad 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -71,7 +71,7 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 024480345d..8a9aee1355 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -51,7 +51,10 @@ public function processNode(Node $node, Scope $scope): array private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - $constantType = $constantReflection->getValueType(); + $constantType = $constantReflection->getPhpDocType(); + if ($constantType === null) { + return []; + } $errors = []; foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($constantType) as $iterableType) { @@ -75,7 +78,6 @@ private function processSingleConstant(ClassReflection $classReflection, string $name, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php index 4d3735c293..555cbfc0dd 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -58,10 +58,6 @@ private function processSingleConstant(ClassReflection $classReflection, string } $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { - return []; - } - $errors = []; if ($prototype->isFinal()) { $errors[] = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php index 4a98b41673..37b72e1c72 100644 --- a/src/Rules/Constants/ValueAssignedToClassConstantRule.php +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -58,10 +57,6 @@ public function processNode(Node $node, Scope $scope): array private function processSingleConstant(ClassReflection $classReflection, string $constantName, Type $valueExprType, ?Type $nativeType): array { $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { - return []; - } - $phpDocType = $constantReflection->getPhpDocType(); if ($phpDocType === null) { if ($nativeType === null) { diff --git a/src/Rules/DeadCode/BetterNoopRule.php b/src/Rules/DeadCode/BetterNoopRule.php new file mode 100644 index 0000000000..22ce47032f --- /dev/null +++ b/src/Rules/DeadCode/BetterNoopRule.php @@ -0,0 +1,138 @@ + + */ +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 new file mode 100644 index 0000000000..ab14e74d36 --- /dev/null +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classesWithConstructors = []; + foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as [$class]) { + $classesWithConstructors[strtolower($class)] = $class; + } + + $errors = []; + foreach ($node->get(PossiblyPureNewCollector::class) as $filePath => $data) { + foreach ($data as [$class, $line]) { + $lowerClass = strtolower($class); + if (!array_key_exists($lowerClass, $classesWithConstructors)) { + continue; + } + + $originalClassName = $classesWithConstructors[$lowerClass]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to new %s() on a separate line has no effect.', + $originalClassName, + ))->file($filePath) + ->line($line) + ->identifier('new.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..1635b12050 --- /dev/null +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $functions = []; + foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as [$functionName]) { + $functions[strtolower($functionName)] = $functionName; + } + + $errors = []; + foreach ($node->get(PossiblyPureFuncCallCollector::class) as $filePath => $data) { + foreach ($data as [$func, $line]) { + $lowerFunc = strtolower($func); + if (!array_key_exists($lowerFunc, $functions)) { + continue; + } + + $originalFunctionName = $functions[$lowerFunc]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line has no effect.', + $originalFunctionName, + ))->file($filePath) + ->line($line) + ->identifier('function.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..5c4d597cd9 --- /dev/null +++ b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,71 @@ + + */ +class CallToMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + $methods[$className] = []; + } + $methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureMethodCallCollector::class) as $filePath => $data) { + foreach ($data as [$classNames, $method, $line]) { + $originalMethodName = null; + foreach ($classNames as $className) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + continue 2; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$className])) { + continue 2; + } + + $originalMethodName = $methods[$className][$lowerMethod]; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to method %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('method.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..c64b29f8bf --- /dev/null +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,68 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + $methods[$lowerClassName] = []; + } + $methods[$lowerClassName][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureStaticCallCollector::class) as $filePath => $data) { + foreach ($data as [$className, $method, $line]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + continue; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$lowerClassName])) { + continue; + } + + $originalMethodName = $methods[$lowerClassName][$lowerMethod]; + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('staticMethod.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..dae162500a --- /dev/null +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -0,0 +1,59 @@ + + */ +class ConstructorWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (strtolower($method->getName()) !== '__construct') { + return null; + } + + if (!$method->isPure()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + return $method->getDeclaringClass()->getName(); + } + +} diff --git a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..528e5c76e6 --- /dev/null +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -0,0 +1,57 @@ + + */ +class FunctionWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $function = $node->getFunctionReflection(); + if (!$function->isPure()->maybe()) { + return null; + } + if (!$function->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($function->getAsserts()->getAll()) !== 0) { + return null; + } + + return $function->getName(); + } + +} diff --git a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..b6bdb3ca34 --- /dev/null +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -0,0 +1,65 @@ + + */ +class MethodWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (!$method->isPure()->maybe()) { + return null; + } + if (!$method->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + $declaringClass = $method->getDeclaringClass(); + if ( + $declaringClass->hasConstructor() + && $declaringClass->getConstructor()->getName() === $method->getName() + ) { + return null; + } + + return [$method->getDeclaringClass()->getName(), $method->getName(), $method->getDeclaringClass()->getDisplayName()]; + } + +} diff --git a/src/Rules/DeadCode/NoopRule.php b/src/Rules/DeadCode/NoopRule.php index 8105463cc0..34855261ef 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -10,12 +10,13 @@ use function sprintf; /** + * @deprecated Replaced by PHPStan\Rules\DeadCode\BetterNoopRule * @implements Rule */ class NoopRule implements Rule { - public function __construct(private ExprPrinter $exprPrinter, private bool $logicalXor) + public function __construct(private ExprPrinter $exprPrinter, private bool $better) { } @@ -26,6 +27,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($this->better) { + // disabled in bleeding edge + return []; + } $originalExpr = $node->expr; $expr = $originalExpr; if ( @@ -36,70 +41,7 @@ public function processNode(Node $node, Scope $scope): array ) { $expr = $expr->expr; } - if ($this->logicalXor) { - 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) { - if (!$this->isNoopExpr($expr->right)) { - return []; - } - - $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 ($expr instanceof Node\Expr\BinaryOp\BooleanAnd || $expr instanceof Node\Expr\BinaryOp\BooleanOr) { - if (!$this->isNoopExpr($expr->right)) { - return []; - } - $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) { - $if = $expr->if; - if ($if === null) { - $if = $expr->cond; - } - - if (!$this->isNoopExpr($if) || !$this->isNoopExpr($expr->else)) { - return []; - } - - return [ - RuleErrorBuilder::message('Unused result of ternary operator.') - ->line($expr->getStartLine()) - ->identifier('ternary.resultUnused') - ->build(), - ]; - } - } if (!$this->isNoopExpr($expr)) { return []; } diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php new file mode 100644 index 0000000000..952da73ba2 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -0,0 +1,50 @@ + + */ +class PossiblyPureFuncCallCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\FuncCall) { + return null; + } + if (!$node->expr->name instanceof Node\Name) { + return null; + } + + if (!$this->reflectionProvider->hasFunction($node->expr->name, $scope)) { + return null; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->expr->name, $scope); + if (!$functionReflection->isPure()->maybe()) { + return null; + } + if (!$functionReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$functionReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php new file mode 100644 index 0000000000..256d2bf781 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php @@ -0,0 +1,74 @@ +, string, int}> + */ +class PossiblyPureMethodCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\MethodCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->getType($node->expr->var); + if (!$calledOnType->hasMethod($methodName)->yes()) { + return null; + } + + $classNames = []; + $methodReflection = null; + foreach ($calledOnType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->hasMethod($methodName)) { + return null; + } + + $methodReflection = $classReflection->getMethod($methodName, $scope); + if ( + !$methodReflection->isPrivate() + && !$methodReflection->isFinal()->yes() + && !$methodReflection->getDeclaringClass()->isFinal() + ) { + if (!$classReflection->isFinal()) { + return null; + } + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + $classNames[] = $methodReflection->getDeclaringClass()->getName(); + } + + if ($methodReflection === null) { + return null; + } + + return [$classNames, $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureNewCollector.php b/src/Rules/DeadCode/PossiblyPureNewCollector.php new file mode 100644 index 0000000000..e2fabe49ca --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureNewCollector.php @@ -0,0 +1,60 @@ + + */ +class PossiblyPureNewCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\New_) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $className = $node->expr->class->toString(); + + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstructor()) { + return null; + } + + $constructor = $classReflection->getConstructor(); + if (strtolower($constructor->getName()) !== '__construct') { + return null; + } + + if (!$constructor->isPure()->maybe()) { + return null; + } + + return [$constructor->getDeclaringClass()->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php new file mode 100644 index 0000000000..d934b57a6d --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -0,0 +1,55 @@ + + */ +class PossiblyPureStaticCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\StaticCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->resolveTypeByName($node->expr->class); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + + if ($methodReflection === null) { + return null; + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$methodReflection->getDeclaringClass()->getName(), $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 9036f24931..ccb160f60f 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -33,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $node->getClassReflection(); - $classType = new ObjectType($classReflection->getName()); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $constants = []; foreach ($node->getConstants() as $constant) { diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index 9cce91220e..aa868808d4 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -7,6 +7,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassMethodsNode; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantStringType; @@ -22,6 +23,10 @@ class UnusedPrivateMethodRule implements Rule { + public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) + { + } + public function getNodeType(): string { return ClassMethodsNode::class; @@ -33,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $node->getClassReflection(); - $classType = new ObjectType($classReflection->getName()); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $constructor = null; if ($classReflection->hasConstructor()) { $constructor = $classReflection->getConstructor(); @@ -54,6 +59,14 @@ public function processNode(Node $node, Scope $scope): array if (strtolower($methodName) === '__clone') { continue; } + + $methodReflection = $classReflection->getNativeMethod($methodName); + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysUsed($methodReflection)) { + continue 2; + } + } + $methods[strtolower($methodName)] = $method; } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index 5f4ce6eef1..6af054659d 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -47,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $node->getClassReflection(); - $classType = new ObjectType($classReflection->getName()); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $properties = []; foreach ($node->getProperties() as $property) { if (!$property->isPrivate()) { diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index 620a75def1..1231a0d5e2 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Stmt\Catch_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -22,7 +22,7 @@ class CaughtExceptionExistenceRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, ) { @@ -58,13 +58,12 @@ public function processNode(Node $node, Scope $scope): array ->build(); } - if (!$this->checkClassCaseSensitivity) { - continue; - } - $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]), + $this->classCheck->checkClassNames( + [new ClassNameNodePair($className, $class)], + $this->checkClassCaseSensitivity, + ), ); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 4d4a29c713..4c34fb4f43 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -4,9 +4,11 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,6 +28,7 @@ use function array_fill; use function array_key_exists; use function count; +use function implode; use function is_string; use function max; use function sprintf; @@ -77,7 +80,7 @@ public function check( $functionParametersMaxCount = -1; } - /** @var array $arguments */ + /** @var array $arguments */ $arguments = []; /** @var array $args */ $args = $funcCall->getArgs(); @@ -170,7 +173,7 @@ public function check( $arguments[] = [ $arg->value, - $type, + null, false, $argumentName, $arg->getStartLine(), @@ -275,22 +278,31 @@ public function check( continue; } + if ($argumentValueType === null) { + if ($scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + $argumentValueType = $scope->getType($argumentValue); + + if ($scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + } + if ($this->checkArgumentTypes) { $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); - if (!$parameter->passedByReference()->createsNewVariable()) { + if ( + !$parameter->passedByReference()->createsNewVariable() + || (!$isBuiltin && $this->checkUnresolvableParameterTypes) // bleeding edge only + ) { $accepts = $this->ruleLevelHelper->acceptsWithReason($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); if (!$accepts->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[6], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), $parameterType->describe($verbosityLevel), $argumentValueType->describe($verbosityLevel), )) @@ -307,16 +319,28 @@ public function check( && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) ) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[13], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->identifier('argument.unresolvableType')->line($argumentLine)->build(); } + + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && ($argumentValue instanceof Expr\Closure || $argumentValue instanceof Expr\ArrowFunction) + && $argumentValue->static + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $messages[6], + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + 'bindable closure', + 'static closure', + )) + ->identifier('argument.staticClosure') + ->line($argumentLine) + ->build(); + } } if ( @@ -327,10 +351,9 @@ public function check( } if ($this->nullsafeCheck->containsNullSafe($argumentValue)) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), )) ->identifier('argument.byRef') ->line($argumentLine) @@ -357,10 +380,9 @@ public function check( $propertyDescription = sprintf('readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( 'Parameter %s is passed by reference so it does not accept %s.', - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), $propertyDescription, ))->identifier('argument.byRef')->line($argumentLine)->build(); } @@ -373,10 +395,9 @@ public function check( continue; } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->identifier('argument.byRef')->line($argumentLine)->build(); } @@ -455,8 +476,8 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty } /** - * @param array $arguments - * @return array{list, array} + * @param array $arguments + * @return array{list, array} */ private function processArguments( ParametersAcceptor $parametersAcceptor, @@ -576,4 +597,19 @@ private function processArguments( return [$errors, $newArguments]; } + private function describeParameter(ParameterReflection $parameter, ?int $position): string + { + $parts = []; + if ($position !== null) { + $parts[] = '#' . $position; + } + + $name = $parameter->getName(); + if ($name !== '') { + $parts[] = ($parameter->isVariadic() ? '...$' : '$') . $name; + } + + return implode(' ', $parts); + } + } diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 1aee6ba688..d2bdef50ba 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -6,6 +6,10 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Identifier; +use PhpParser\Node\IntersectionType; +use PhpParser\Node\Name; +use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; @@ -29,10 +33,12 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +use function array_filter; use function array_keys; use function array_map; use function array_merge; use function count; +use function in_array; use function is_string; use function sprintf; @@ -41,7 +47,7 @@ class FunctionDefinitionCheck public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, private bool $checkClassCaseSensitivity, @@ -138,9 +144,9 @@ public function checkAnonymousFunction( foreach ($type->getReferencedClasses() as $class) { if (!$this->reflectionProvider->hasClass($class)) { $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class)) - ->line($param->type->getStartLine()) - ->identifier('class.notFound') - ->build(); + ->line($param->type->getStartLine()) + ->identifier('class.notFound') + ->build(); continue; } @@ -153,15 +159,11 @@ public function checkAnonymousFunction( continue; } - if (!$this->checkClassCaseSensitivity) { - continue; - } - $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $param->type), - ]), + ], $this->checkClassCaseSensitivity), ); } } @@ -181,7 +183,7 @@ public function checkAnonymousFunction( ) { $errors[] = RuleErrorBuilder::message($unionTypesMessage) ->line($returnTypeNode->getStartLine()) - ->identifier('reeturn.unionTypeNotSupported') + ->identifier('return.unionTypeNotSupported') ->nonIgnorable() ->build(); } @@ -201,7 +203,7 @@ public function checkAnonymousFunction( foreach ($returnType->getReferencedClasses() as $returnTypeClass) { if (!$this->reflectionProvider->hasClass($returnTypeClass)) { $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass)) - ->line($returnTypeNode->getStartLine()) + ->line($returnTypeNode->getLine()) ->identifier('class.notFound') ->build(); continue; @@ -215,15 +217,11 @@ public function checkAnonymousFunction( continue; } - if (!$this->checkClassCaseSensitivity) { - continue; - } - $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($returnTypeClass, $returnTypeNode), - ]), + ], $this->checkClassCaseSensitivity), ); } @@ -364,12 +362,13 @@ private function checkParametersAcceptor( ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses)), - ); - } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses), + $this->checkClassCaseSensitivity, + ), + ); if (!($parameter->getType() instanceof NonexistentParentClassType)) { continue; } @@ -411,12 +410,13 @@ private function checkParametersAcceptor( ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses)), - ); - } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses), + $this->checkClassCaseSensitivity, + ), + ); if ($parametersAcceptor->getReturnType() instanceof NonexistentParentClassType) { $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly()))) ->line($returnTypeNode->getStartLine()) @@ -469,6 +469,7 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr /** @var string|null $optionalParameter */ $optionalParameter = null; $errors = []; + $targetPhpVersion = null; foreach ($parameterNodes as $parameterNode) { if (!$parameterNode->var instanceof Variable) { throw new ShouldNotHappenException(); @@ -478,10 +479,17 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr } $parameterName = $parameterNode->var->name; if ($optionalParameter !== null && $parameterNode->default === null && !$parameterNode->variadic) { - $errors[] = RuleErrorBuilder::message(sprintf('Deprecated in PHP 8.0: Required parameter $%s follows optional parameter $%s.', $parameterName, $optionalParameter)) - ->line($parameterNode->getStartLine()) + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Deprecated in PHP %s: Required parameter $%s follows optional parameter $%s.', + $targetPhpVersion ?? '8.0', + $parameterName, + $optionalParameter, + ), + )->line($parameterNode->getStartLine()) ->identifier('parameter.requiredAfterOptional') ->build(); + $targetPhpVersion = null; continue; } if ($parameterNode->default === null) { @@ -500,7 +508,35 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr $constantName = $defaultValue->name->toLowerString(); if ($constantName === 'null') { - continue; + if (!$this->phpVersion->deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull()) { + continue; + } + + $parameterNodeType = $parameterNode->type; + + if ($parameterNodeType instanceof NullableType) { + $targetPhpVersion = '8.1'; + } + + if ($this->phpVersion->deprecatesRequiredParameterAfterOptionalUnionOrMixed()) { + $types = []; + + if ($parameterNodeType instanceof UnionType) { + $types = $parameterNodeType->types; + } elseif ($parameterNodeType instanceof Identifier) { + $types = [$parameterNodeType]; + } + + $nullOrMixed = array_filter($types, static fn (Identifier|Name|IntersectionType $type): bool => $type instanceof Identifier && (in_array($type->name, ['null', 'mixed'], true))); + + if (0 < count($nullOrMixed)) { + $targetPhpVersion = '8.3'; + } + } + + if ($targetPhpVersion === null) { + continue; + } } $optionalParameter = $parameterName; diff --git a/src/Rules/Functions/ArrayFilterRule.php b/src/Rules/Functions/ArrayFilterRule.php index d40cdbae12..177d1d218d 100644 --- a/src/Rules/Functions/ArrayFilterRule.php +++ b/src/Rules/Functions/ArrayFilterRule.php @@ -4,7 +4,9 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,7 +14,6 @@ use PHPStan\Type\VerbosityLevel; use function count; use function sprintf; -use function strtolower; /** * @implements Rule @@ -38,13 +39,28 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_filter') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); - if ($functionName === null || strtolower($functionName) !== 'array_filter') { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedFuncCall === null) { return []; } - $args = $node->getArgs(); + $args = $normalizedFuncCall->getArgs(); if (count($args) !== 1) { return []; } @@ -57,11 +73,18 @@ public function processNode(Node $node, Scope $scope): array if ($arrayType->isIterableAtLeastOnce()->no()) { $message = 'Parameter #1 $array (%s) to function array_filter is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->identifier('arrayFilter.empty')->build(), + $errorBuilder->build(), ]; } @@ -70,21 +93,41 @@ public function processNode(Node $node, Scope $scope): array if ($isSuperType->no()) { $message = 'Parameter #1 $array (%s) to function array_filter does not contain falsy values, the array will always stay the same.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.same'); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if (!$isNativeSuperType->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->identifier('arrayFilter.same')->build(), + $errorBuilder->build(), ]; } if ($isSuperType->yes()) { $message = 'Parameter #1 $array (%s) to function array_filter contains falsy values only, the result will always be an empty array.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.alwaysEmpty'); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if (!$isNativeSuperType->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->identifier('arrayFilter.alwaysEmpty')->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Functions/ArrayValuesRule.php b/src/Rules/Functions/ArrayValuesRule.php new file mode 100644 index 0000000000..bb62442ec8 --- /dev/null +++ b/src/Rules/Functions/ArrayValuesRule.php @@ -0,0 +1,118 @@ + + */ +class ArrayValuesRule implements Rule +{ + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + private readonly bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (AccessoryArrayListType::isListTypeEnabled() === false) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_values') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); + if (count($args) === 0) { + return []; + } + + if ($this->treatPhpDocTypesAsCertain) { + $arrayType = $scope->getType($args[0]->value); + } else { + $arrayType = $scope->getNativeType($args[0]->value); + } + + if ($arrayType->isIterableAtLeastOnce()->no()) { + $message = 'Parameter #1 $array (%s) to function array_values is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + if ($arrayType->isList()->yes()) { + $message = 'Parameter #1 $array (%s) of array_values is already a list, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.list'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isList()->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 545bcd1a27..685fa41de0 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -20,10 +20,18 @@ class CallToFunctionStatementWithoutSideEffectsRule implements Rule { private const SIDE_EFFECT_FLIP_PARAMETERS = [ - // functionName => [name, pos, testName, defaultHasSideEffect] - 'file_get_contents' => ['context', 2, 'isNotNull', false], - 'print_r' => ['return', 1, 'isTruthy', true], - 'var_export' => ['return', 1, 'isTruthy', true], + // functionName => [name, pos, testName] + 'print_r' => ['return', 1, 'isTruthy'], + 'var_export' => ['return', 1, 'isTruthy'], + 'highlight_string' => ['return', 1, 'isTruthy'], + + ]; + + public const PHPSTAN_TESTING_FUNCTIONS = [ + 'PHPStan\\dumpType', + 'PHPStan\\Testing\\assertType', + 'PHPStan\\Testing\\assertNativeType', + 'PHPStan\\Testing\\assertVariableCertainty', ]; public function __construct(private ReflectionProvider $reflectionProvider) @@ -54,12 +62,7 @@ public function processNode(Node $node, Scope $scope): array $functionName = $function->getName(); $functionHasSideEffects = !$function->hasSideEffects()->no(); - if (in_array($functionName, [ - 'PHPStan\\dumpType', - 'PHPStan\\Testing\\assertType', - 'PHPStan\\Testing\\assertNativeType', - 'PHPStan\\Testing\\assertVariableCertainty', - ], true)) { + if (in_array($functionName, self::PHPSTAN_TESTING_FUNCTIONS, true)) { return []; } @@ -68,7 +71,6 @@ public function processNode(Node $node, Scope $scope): array $flipParameterName, $flipParameterPosition, $testName, - $defaultHasSideEffect, ] = self::SIDE_EFFECT_FLIP_PARAMETERS[$functionName]; $sideEffectFlipped = false; @@ -102,7 +104,7 @@ public function processNode(Node $node, Scope $scope): array } } - if ($sideEffectFlipped xor $defaultHasSideEffect) { + if (!$sideEffectFlipped) { return []; } diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php index a9615bedeb..b890a8ff66 100644 --- a/src/Rules/Functions/ImplodeFunctionRule.php +++ b/src/Rules/Functions/ImplodeFunctionRule.php @@ -17,6 +17,7 @@ use function sprintf; /** + * @deprecated Replaced by PHPStan\Rules\Functions\ImplodeParameterCastableToStringRuleTest * @implements Rule */ class ImplodeFunctionRule implements Rule @@ -25,6 +26,7 @@ class ImplodeFunctionRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + private bool $disabled, ) { } @@ -36,6 +38,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($this->disabled) { + return []; + } + if (!($node->name instanceof Node\Name)) { return []; } diff --git a/src/Rules/Functions/ImplodeParameterCastableToStringRule.php b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php new file mode 100644 index 0000000000..df5136a808 --- /dev/null +++ b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php @@ -0,0 +1,117 @@ + + */ +class ImplodeParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + if (!in_array($functionName, ['implode', 'join'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + $errorMessage = 'Parameter %s of function %s expects array, %s given.'; + if (count($normalizedArgs) === 1) { + $argsToCheck = [0 => $normalizedArgs[0]]; + } elseif (count($normalizedArgs) === 2) { + $argsToCheck = [1 => $normalizedArgs[1]]; + } else { + return []; + } + + $origNamedArgs = []; + foreach ($origArgs as $arg) { + if ($arg->unpack || $arg->name === null) { + continue; + } + + $origNamedArgs[$arg->name->toString()] = $arg; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + // implode has weird variants, so $array has to be fixed. It's especially weird with named arguments. + if (array_key_exists('array', $origNamedArgs)) { + $argName = '$array'; + } elseif (array_key_exists('separator', $origNamedArgs) && count($origArgs) === 1) { + $argName = '$separator'; + } else { + $argName = sprintf('#%d $array', $argIdx + 1); + } + + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + static fn (Type $t) => $t->toString(), + $functionName, + $argName, + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php index 5c924f370b..d722abffe1 100644 --- a/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php +++ b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php @@ -18,18 +18,6 @@ class InvalidLexicalVariablesInClosureUseRule implements Rule { - private const SUPERGLOBAL_NAMES = [ - '_COOKIE', - '_ENV', - '_FILES', - '_GET', - '_POST', - '_REQUEST', - '_SERVER', - '_SESSION', - 'GLOBALS', - ]; - public function getNodeType(): string { return Node\Expr\Closure::class; @@ -54,7 +42,7 @@ static function (Node\Param $param) { return $param->var->name; }, $node->getParams(), - )); + ), static fn ($name) => $name !== false); foreach ($node->uses as $use) { if (!is_string($use->var->name)) { @@ -72,7 +60,7 @@ static function (Node\Param $param) { continue; } - if (in_array($var, self::SUPERGLOBAL_NAMES, true)) { + if (in_array($var, Scope::SUPERGLOBAL_VARIABLES, true)) { $errors[] = RuleErrorBuilder::message(sprintf('Cannot use superglobal variable $%s as lexical variable.', $var)) ->line($use->getStartLine()) ->identifier('closure.useSuperGlobal') diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 1cafd36892..73782fcf53 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -6,13 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function implode; use function sprintf; @@ -25,6 +25,7 @@ final class MissingFunctionParameterTypehintRule implements Rule public function __construct( private MissingTypehintCheck $missingTypehintCheck, + private bool $paramOut, ) { } @@ -40,7 +41,24 @@ public function processNode(Node $node, Scope $scope): array $messages = []; foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkFunctionParameter($functionReflection, $parameterReflection) as $parameterMessage) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if ($parameterReflection->getClosureThisType() !== null) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-closure-this PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureThisType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + + if (!$this->paramOut) { + continue; + } + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -51,16 +69,14 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function checkFunctionParameter(FunctionReflection $functionReflection, ParameterReflection $parameterReflection): array + private function checkFunctionParameter(FunctionReflection $functionReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no type specified.', + 'Function %s() has %s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, ))->identifier('missingType.parameter')->build(), ]; } @@ -69,9 +85,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no value type specified in iterable type %s.', + 'Function %s() has %s with no value type specified in iterable type %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, )) ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) @@ -81,22 +97,21 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with generic %s but does not specify its types: %s', + 'Function %s() has %s with generic %s but does not specify its types: %s', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no signature specified for %s.', + 'Function %s() has %s with no signature specified for %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), ))->identifier('missingType.callable')->build(); } diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index 9f2607f813..1546a63219 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -61,7 +61,6 @@ public function processNode(Node $node, Scope $scope): array $name, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } diff --git a/src/Rules/Functions/ParameterCastableToStringRule.php b/src/Rules/Functions/ParameterCastableToStringRule.php new file mode 100644 index 0000000000..b6b26da76e --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToStringRule.php @@ -0,0 +1,118 @@ + + */ +class ParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + $checkAllArgsFunctions = ['array_intersect', 'array_intersect_assoc', 'array_diff', 'array_diff_assoc']; + $checkFirstArgFunctions = [ + 'array_combine', + 'natcasesort', + 'natsort', + 'array_count_values', + 'array_fill_keys', + ]; + + if ( + !in_array($functionName, $checkAllArgsFunctions, true) + && !in_array($functionName, $checkFirstArgFunctions, true) + ) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; + $functionParameters = $parametersAcceptor->getParameters(); + + if (in_array($functionName, $checkAllArgsFunctions, true)) { + $argsToCheck = $origArgs; + } elseif (in_array($functionName, $checkFirstArgFunctions, true)) { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + if (!array_key_exists(0, $normalizedArgs)) { + return []; + } + $argsToCheck = [0 => $normalizedArgs[0]]; + } else { + return []; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + static fn (Type $t) => $t->toString(), + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $arg, + $argIdx, + $functionParameters[$argIdx] ?? null, + ), + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/PrintfArrayParametersRule.php b/src/Rules/Functions/PrintfArrayParametersRule.php new file mode 100644 index 0000000000..d06cdc8f4a --- /dev/null +++ b/src/Rules/Functions/PrintfArrayParametersRule.php @@ -0,0 +1,188 @@ + + */ +class PrintfArrayParametersRule implements Rule +{ + + public function __construct( + private PrintfHelper $printfHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $name = $functionReflection->getName(); + if (!in_array($name, ['vprintf', 'vsprintf'], true)) { + return []; + } + + $args = $node->getArgs(); + $argsCount = count($args); + if ($argsCount < 1) { + return []; // caught by CallToFunctionParametersRule + } + + $formatArgType = $scope->getType($args[0]->value); + $placeHoldersCounts = []; + foreach ($formatArgType->getConstantStrings() as $formatString) { + $format = $formatString->getValue(); + + $placeHoldersCounts[] = $this->printfHelper->getPrintfPlaceholdersCount($format); + } + + if ($placeHoldersCounts === []) { + return []; + } + + $minCount = min($placeHoldersCounts); + $maxCount = max($placeHoldersCounts); + if ($minCount === $maxCount) { + $placeHoldersCount = new ConstantIntegerType($minCount); + } else { + $placeHoldersCount = IntegerRangeType::fromInterval($minCount, $maxCount); + + if (!$placeHoldersCount instanceof IntegerRangeType && !$placeHoldersCount instanceof ConstantIntegerType) { + return []; + } + } + + $formatArgsCounts = []; + if (isset($args[1])) { + $formatArgsType = $scope->getType($args[1]->value); + + $constantArrays = $formatArgsType->getConstantArrays(); + if ($constantArrays === []) { + $formatArgsCounts[] = new IntegerType(); + } + foreach ($constantArrays as $constantArray) { + $formatArgsCounts[] = $constantArray->getArraySize(); + } + } + + if ($formatArgsCounts === []) { + $formatArgsCount = new ConstantIntegerType(0); + } else { + $formatArgsCount = TypeCombinator::union(...$formatArgsCounts); + + if (!$formatArgsCount instanceof IntegerRangeType && !$formatArgsCount instanceof ConstantIntegerType) { + return []; + } + } + + if (!$this->placeholdersMatchesArgsCount($placeHoldersCount, $formatArgsCount)) { + + if ($placeHoldersCount instanceof IntegerRangeType) { + $placeholders = $this->getIntegerRangeAsString($placeHoldersCount); + $singlePlaceholder = false; + } else { + $placeholders = $placeHoldersCount->getValue(); + $singlePlaceholder = $placeholders === 1; + } + + if ($formatArgsCount instanceof IntegerRangeType) { + $values = $this->getIntegerRangeAsString($formatArgsCount); + $singleValue = false; + } else { + $values = $formatArgsCount->getValue(); + $singleValue = $values === 1; + } + + return [ + RuleErrorBuilder::message(sprintf( + sprintf( + '%s, %s.', + $singlePlaceholder ? 'Call to %s contains %d placeholder' : 'Call to %s contains %s placeholders', + $singleValue ? '%d value given' : '%s values given', + ), + $name, + $placeholders, + $values, + ))->identifier(sprintf('argument.%s', $name))->build(), + ]; + } + + return []; + } + + private function placeholdersMatchesArgsCount(ConstantIntegerType|IntegerRangeType $placeHoldersCount, ConstantIntegerType|IntegerRangeType $formatArgsCount): bool + { + if ($placeHoldersCount instanceof ConstantIntegerType) { + if ($formatArgsCount instanceof ConstantIntegerType) { + return $placeHoldersCount->getValue() === $formatArgsCount->getValue(); + } + + // Zero placeholders + array + if ($placeHoldersCount->getValue() === 0) { + return true; + } + + return false; + } + + if ( + $formatArgsCount instanceof IntegerRangeType + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($placeHoldersCount)->yes() + ) { + if ($formatArgsCount->getMin() !== null && $formatArgsCount->getMax() !== null) { + // constant array + return $placeHoldersCount->isSuperTypeOf($formatArgsCount)->yes(); + } + + // general array + return IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($formatArgsCount)->yes(); + } + + return false; + } + + private function getIntegerRangeAsString(IntegerRangeType $range): string + { + if ($range->getMin() !== null && $range->getMax() !== null) { + return $range->getMin() . '-' . $range->getMax(); + } elseif ($range->getMin() !== null) { + return $range->getMin() . ' or more'; + } elseif ($range->getMax() !== null) { + return $range->getMax() . ' or less'; + } + + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Functions/PrintfHelper.php b/src/Rules/Functions/PrintfHelper.php new file mode 100644 index 0000000000..a5d4571f76 --- /dev/null +++ b/src/Rules/Functions/PrintfHelper.php @@ -0,0 +1,75 @@ +getPlaceholdersCount('(?:[bs%s]|l?[cdeEgfFGouxX])', $format); + } + + public function getScanfPlaceholdersCount(string $format): int + { + return $this->getPlaceholdersCount('(?:[cdDeEfinosuxX%s]|\[[^\]]+\])', $format); + } + + private function getPlaceholdersCount(string $specifiersPattern, string $format): int + { + $addSpecifier = ''; + if ($this->phpVersion->supportsHhPrintfSpecifier()) { + $addSpecifier .= 'hH'; + } + + $specifiers = sprintf($specifiersPattern, $addSpecifier); + + $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?\*)?-?\d*(?:\.(?:\d+|(?\*))?)?' . $specifiers . '~'; + + $matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER); + + if (count($matches) === 0) { + return 0; + } + + $placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0); + + if (count($placeholders) === 0) { + return 0; + } + + $maxPositionedNumber = 0; + $maxOrdinaryNumber = 0; + foreach ($placeholders as $placeholder) { + if (isset($placeholder['width']) && $placeholder['width'] !== '') { + $maxOrdinaryNumber++; + } + + if (isset($placeholder['precision']) && $placeholder['precision'] !== '') { + $maxOrdinaryNumber++; + } + + if (isset($placeholder['position']) && $placeholder['position'] !== '') { + $maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber); + } else { + $maxOrdinaryNumber++; + } + } + + return max($maxPositionedNumber, $maxOrdinaryNumber); + } + +} diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index 8f5f9f2bd5..a1d8e52f35 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -2,22 +2,16 @@ namespace PHPStan\Rules\Functions; -use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use function array_filter; use function array_key_exists; use function count; use function in_array; -use function max; use function sprintf; -use function strlen; -use function strtolower; -use const PREG_SET_ORDER; /** * @implements Rule @@ -25,7 +19,23 @@ class PrintfParametersRule implements Rule { - public function __construct(private PhpVersion $phpVersion) + private const FORMAT_ARGUMENT_POSITIONS = [ + 'printf' => 0, + 'sprintf' => 0, + 'sscanf' => 1, + 'fscanf' => 1, + ]; + private const MINIMUM_NUMBER_OF_ARGUMENTS = [ + 'printf' => 1, + 'sprintf' => 1, + 'sscanf' => 3, + 'fscanf' => 3, + ]; + + public function __construct( + private PrintfHelper $printfHelper, + private ReflectionProvider $reflectionProvider, + ) { } @@ -40,25 +50,17 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionsArgumentPositions = [ - 'printf' => 0, - 'sprintf' => 0, - 'sscanf' => 1, - 'fscanf' => 1, - ]; - $minimumNumberOfArguments = [ - 'printf' => 1, - 'sprintf' => 1, - 'sscanf' => 3, - 'fscanf' => 3, - ]; - - $name = strtolower((string) $node->name); - if (!array_key_exists($name, $functionsArgumentPositions)) { + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { return []; } - $formatArgumentPosition = $functionsArgumentPositions[$name]; + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $name = $functionReflection->getName(); + if (!array_key_exists($name, self::FORMAT_ARGUMENT_POSITIONS)) { + return []; + } + + $formatArgumentPosition = self::FORMAT_ARGUMENT_POSITIONS[$name]; $args = $node->getArgs(); foreach ($args as $arg) { @@ -67,38 +69,44 @@ public function processNode(Node $node, Scope $scope): array } } $argsCount = count($args); - if ($argsCount < $minimumNumberOfArguments[$name]) { + if ($argsCount < self::MINIMUM_NUMBER_OF_ARGUMENTS[$name]) { return []; // caught by CallToFunctionParametersRule } $formatArgType = $scope->getType($args[$formatArgumentPosition]->value); - $placeHoldersCount = null; + $maxPlaceHoldersCount = null; foreach ($formatArgType->getConstantStrings() as $formatString) { $format = $formatString->getValue(); - $tempPlaceHoldersCount = $this->getPlaceholdersCount($name, $format); - if ($placeHoldersCount === null) { - $placeHoldersCount = $tempPlaceHoldersCount; - } elseif ($tempPlaceHoldersCount > $placeHoldersCount) { - $placeHoldersCount = $tempPlaceHoldersCount; + + if (in_array($name, ['sprintf', 'printf'], true)) { + $tempPlaceHoldersCount = $this->printfHelper->getPrintfPlaceholdersCount($format); + } else { + $tempPlaceHoldersCount = $this->printfHelper->getScanfPlaceholdersCount($format); + } + + if ($maxPlaceHoldersCount === null) { + $maxPlaceHoldersCount = $tempPlaceHoldersCount; + } elseif ($tempPlaceHoldersCount > $maxPlaceHoldersCount) { + $maxPlaceHoldersCount = $tempPlaceHoldersCount; } } - if ($placeHoldersCount === null) { + if ($maxPlaceHoldersCount === null) { return []; } $argsCount -= $formatArgumentPosition; - if ($argsCount !== $placeHoldersCount + 1) { + if ($argsCount !== $maxPlaceHoldersCount + 1) { return [ RuleErrorBuilder::message(sprintf( sprintf( '%s, %s.', - $placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders', + $maxPlaceHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders', $argsCount - 1 === 1 ? '%d value given' : '%d values given', ), $name, - $placeHoldersCount, + $maxPlaceHoldersCount, $argsCount - 1, ))->identifier(sprintf('argument.%s', $name))->build(), ]; @@ -107,49 +115,4 @@ public function processNode(Node $node, Scope $scope): array return []; } - private function getPlaceholdersCount(string $functionName, string $format): int - { - $specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '(?:[bs%s]|l?[cdeEgfFGouxX])' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])'; - $addSpecifier = ''; - if ($this->phpVersion->supportsHhPrintfSpecifier()) { - $addSpecifier .= 'hH'; - } - - $specifiers = sprintf($specifiers, $addSpecifier); - - $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?\*)?-?\d*(?:\.(?:\d+|(?\*))?)?' . $specifiers . '~'; - - $matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER); - - if (count($matches) === 0) { - return 0; - } - - $placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0); - - if (count($placeholders) === 0) { - return 0; - } - - $maxPositionedNumber = 0; - $maxOrdinaryNumber = 0; - foreach ($placeholders as $placeholder) { - if (isset($placeholder['width']) && $placeholder['width'] !== '') { - $maxOrdinaryNumber++; - } - - if (isset($placeholder['precision']) && $placeholder['precision'] !== '') { - $maxOrdinaryNumber++; - } - - if (isset($placeholder['position']) && $placeholder['position'] !== '') { - $maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber); - } else { - $maxOrdinaryNumber++; - } - } - - return max($maxPositionedNumber, $maxOrdinaryNumber); - } - } diff --git a/src/Rules/Functions/SortParameterCastableToStringRule.php b/src/Rules/Functions/SortParameterCastableToStringRule.php new file mode 100644 index 0000000000..dc1d4b63cf --- /dev/null +++ b/src/Rules/Functions/SortParameterCastableToStringRule.php @@ -0,0 +1,150 @@ + + */ +class SortParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + + if (!in_array($functionName, ['array_unique', 'sort', 'rsort', 'asort', 'arsort'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $functionParameters = $parametersAcceptor->getParameters(); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + if (!array_key_exists(0, $normalizedArgs)) { + return []; + } + + $argsToCheck = [0 => $normalizedArgs[0]]; + $flags = null; + if (array_key_exists(1, $normalizedArgs)) { + $flags = $scope->getType($normalizedArgs[1]->value); + } elseif (array_key_exists(1, $functionParameters)) { + $flags = $functionParameters[1]->getDefaultValue(); + } + + if ($flags === null || $flags->equals(new ConstantIntegerType(SORT_REGULAR))) { + return []; + } + + $constantIntFlags = TypeUtils::getConstantIntegers($flags); + $mustBeCastableToString = $mustBeCastableToFloat = $constantIntFlags === []; + + foreach ($constantIntFlags as $flag) { + if ($flag->getValue() === SORT_NUMERIC) { + $mustBeCastableToFloat = true; + } elseif (in_array($flag->getValue() & (~SORT_FLAG_CASE), [SORT_STRING, SORT_LOCALE_STRING, SORT_NATURAL], true)) { + $mustBeCastableToString = true; + } + } + + if ($mustBeCastableToString && !$mustBeCastableToFloat) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; + $castFn = static fn (Type $t) => $t->toString(); + } elseif ($mustBeCastableToString) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string and float, %s given.'; + $castFn = static function (Type $t): Type { + $float = $t->toFloat(); + + return $float instanceof ErrorType + ? $float + : $t->toString(); + }; + } elseif ($mustBeCastableToFloat) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to float, %s given.'; + $castFn = static fn (Type $t) => $t->toFloat(); + } else { + return []; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + $castFn, + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $arg, + $argIdx, + $functionParameters[$argIdx] ?? null, + ), + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/UselessFunctionReturnValueRule.php b/src/Rules/Functions/UselessFunctionReturnValueRule.php new file mode 100644 index 0000000000..102f9fec2f --- /dev/null +++ b/src/Rules/Functions/UselessFunctionReturnValueRule.php @@ -0,0 +1,89 @@ + + */ +class UselessFunctionReturnValueRule implements Rule +{ + + private const USELESS_FUNCTIONS = [ + 'var_export' => 'null', + 'print_r' => 'true', + 'highlight_string' => 'true', + ]; + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $funcCall, Scope $scope): array + { + if (!($funcCall->name instanceof Node\Name) || $scope->isInFirstLevelStatement()) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($funcCall->name, $scope); + + if (!array_key_exists($functionReflection->getName(), self::USELESS_FUNCTIONS)) { + return []; + } + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $funcCall->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $reorderedFuncCall = ArgumentsNormalizer::reorderFuncArguments( + $parametersAcceptor, + $funcCall, + ); + + if ($reorderedFuncCall === null) { + return []; + } + $reorderedArgs = $reorderedFuncCall->getArgs(); + + if (count($reorderedArgs) === 1 || (count($reorderedArgs) >= 2 && $scope->getType($reorderedArgs[1]->value)->isFalse()->yes())) { + return [RuleErrorBuilder::message( + sprintf( + 'Return value of function %s() is always %s and the result is printed instead of being returned. Pass in true as parameter #%d $%s to return the output instead.', + $functionReflection->getName(), + self::USELESS_FUNCTIONS[$functionReflection->getName()], + 2, + $parametersAcceptor->getParameters()[1]->getName(), + ), + ) + ->identifier('function.uselessReturnValue') + ->line($funcCall->getStartLine()) + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index 14ebfb0c71..f78cbaf01c 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -6,7 +6,6 @@ use PhpParser\Node\Name; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; -use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -151,7 +150,6 @@ public function check( $unusedName, implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeRule.php b/src/Rules/Generics/MethodTagTemplateTypeRule.php new file mode 100644 index 0000000000..6b60d6557a --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeRule.php @@ -0,0 +1,86 @@ + + */ +class MethodTagTemplateTypeRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $classReflection = $node->getClassReflection(); + $className = $classReflection->getDisplayName(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + null, + $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/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index ff739cb457..234c4e8a71 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -7,7 +7,7 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -41,7 +41,7 @@ class TemplateTypeCheck public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private TypeAliasResolver $typeAliasResolver, private bool $checkClassCaseSensitivity, @@ -100,12 +100,9 @@ public function check( ))->identifier('generics.traitBound')->build(); } - if ($this->checkClassCaseSensitivity) { - $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); - $messages = array_merge($messages, $this->classCaseSensitivityCheck->checkClassNames($classNameNodePairs)); - } + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); + $messages = array_merge($messages, $this->classCheck->checkClassNames($classNameNodePairs, $this->checkClassCaseSensitivity)); - $boundType = $templateTag->getBound(); $boundTypeClass = get_class($boundType); if ( $boundTypeClass !== MixedType::class diff --git a/src/Rules/Methods/AlwaysUsedMethodExtension.php b/src/Rules/Methods/AlwaysUsedMethodExtension.php new file mode 100644 index 0000000000..cfccf5b972 --- /dev/null +++ b/src/Rules/Methods/AlwaysUsedMethodExtension.php @@ -0,0 +1,27 @@ +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 []; } diff --git a/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php new file mode 100644 index 0000000000..b51cf7763a --- /dev/null +++ b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,20 @@ +extensions; + } + +} diff --git a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php index 92c7f49aac..92aa955a0e 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -43,7 +43,8 @@ public function processNode(Node $node, Scope $scope): array } $defaultValueType = $scope->getType($param->default); - $parameterType = $parameters->getParameters()[$paramI]->getType(); + $parameter = $parameters->getParameters()[$paramI]; + $parameterType = $parameter->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); $accepts = $parameterType->acceptsWithReason($defaultValueType, true); diff --git a/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php new file mode 100644 index 0000000000..cbd397ee94 --- /dev/null +++ b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,22 @@ +extensions ??= $this->container->getServicesByTag(static::EXTENSION_TAG); + } + +} diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php index bbf66a2f68..57d0c165e6 100644 --- a/src/Rules/Methods/MethodCallCheck.php +++ b/src/Rules/Methods/MethodCallCheck.php @@ -12,6 +12,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; @@ -50,13 +51,18 @@ public function check( if ($type instanceof ErrorType) { return [$typeResult->getUnknownClassErrors(), null]; } + + $typeForDescribe = $type; + if ($type instanceof StaticType) { + $typeForDescribe = $type->getStaticObjectType(); + } if (!$type->canCallMethods()->yes() || $type->isClassStringType()->yes()) { return [ [ RuleErrorBuilder::message(sprintf( 'Cannot call method %s() on %s.', $methodName, - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), ))->identifier('method.nonObject')->build(), ], null, @@ -105,7 +111,7 @@ public function check( [ RuleErrorBuilder::message(sprintf( 'Call to an undefined method %s::%s().', - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $methodName, ))->identifier('method.notFound')->build(), ], diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 7b2ffe290e..5ef4db93d1 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -6,13 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function implode; use function sprintf; @@ -23,7 +23,10 @@ final class MissingMethodParameterTypehintRule implements Rule { - public function __construct(private MissingTypehintCheck $missingTypehintCheck) + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + private bool $paramOut, + ) { } @@ -38,7 +41,24 @@ public function processNode(Node $node, Scope $scope): array $messages = []; foreach (ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkMethodParameter($methodReflection, $parameterReflection) as $parameterMessage) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if ($parameterReflection->getClosureThisType() !== null) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-closure-this PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureThisType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + + if (!$this->paramOut) { + continue; + } + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -49,17 +69,15 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function checkMethodParameter(MethodReflection $methodReflection, ParameterReflection $parameterReflection): array + private function checkMethodParameter(MethodReflection $methodReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no type specified.', + 'Method %s::%s() has %s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, ))->identifier('missingType.parameter')->build(), ]; } @@ -68,10 +86,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no value type specified in iterable type %s.', + 'Method %s::%s() has %s with no value type specified in iterable type %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, )) ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) @@ -81,24 +99,23 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with generic %s but does not specify its types: %s', + 'Method %s::%s() has %s with generic %s but does not specify its types: %s', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no signature specified for %s.', + 'Method %s::%s() has %s with no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), ))->identifier('missingType.callable')->build(); } diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index 64e0826908..e0e6685ae9 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -73,7 +73,6 @@ public function processNode(Node $node, Scope $scope): array $name, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index ca933009b5..008f4e7d08 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -23,13 +22,8 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { - return []; - } - - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if (!$calledOnType->isNull()->no()) { return []; } diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index e970886bc0..d50f8dfd32 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -13,7 +13,7 @@ use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -22,8 +22,8 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\StaticType; use PHPStan\Type\StringType; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; @@ -38,7 +38,7 @@ class StaticMethodCallCheck public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, private bool $reportMagicMethods, ) @@ -131,7 +131,7 @@ public function check( ]; } - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $errors = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); $classType = $scope->resolveTypeByName($class); } @@ -169,7 +169,7 @@ public function check( } $typeForDescribe = $classType; - if ($classType instanceof ThisType) { + if ($classType instanceof StaticType) { $typeForDescribe = $classType->getStaticObjectType(); } $classType = TypeCombinator::remove($classType, new StringType()); diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 489ed14699..ae407a2a30 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -9,6 +9,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\ConditionalType; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\GenericObjectType; @@ -31,8 +32,6 @@ class MissingTypehintCheck public const MISSING_ITERABLE_VALUE_TYPE_TIP = 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'; - public const TURN_OFF_NON_GENERIC_CHECK_TIP = 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.'; - private const ITERABLE_GENERIC_CLASS_NAMES = [ Traversable::class, Iterator::class, @@ -162,8 +161,10 @@ public function getCallablesWithMissingSignature(Type $type): array $result = []; TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { if ( - ($type instanceof CallableType && $type->isCommonCallable()) || - ($type instanceof ObjectType && $type->getClassName() === Closure::class)) { + ($type instanceof CallableType && $type->isCommonCallable()) + || ($type instanceof ClosureType && $type->isCommonCallable()) + || ($type instanceof ObjectType && $type->getClassName() === Closure::class) + ) { $result[] = $type; } return $traverse($type); diff --git a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php index a05be7c5cc..b961e235fc 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Stmt\Use_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -24,7 +24,7 @@ class ExistingNamesInGroupUseRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, ) { @@ -116,7 +116,7 @@ private function checkFunction(Node\Name $name): ?IdentifierRuleError private function checkClass(Node\Name $name): ?IdentifierRuleError { - $errors = $this->classCaseSensitivityCheck->checkClassNames([ + $errors = $this->classCheck->checkClassNames([ new ClassNameNodePair((string) $name, $name), ]); if (count($errors) === 0) { diff --git a/src/Rules/Namespaces/ExistingNamesInUseRule.php b/src/Rules/Namespaces/ExistingNamesInUseRule.php index 104ad6d21e..92415f012c 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -23,7 +23,7 @@ class ExistingNamesInUseRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, ) { @@ -122,7 +122,7 @@ private function checkFunctions(array $uses): array */ private function checkClasses(array $uses): array { - return $this->classCaseSensitivityCheck->checkClassNames( + return $this->classCheck->checkClassNames( array_map(static fn (Node\Stmt\UseUse $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), ); } diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 4fb2167030..2c42f7be1e 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -26,6 +26,7 @@ class InvalidBinaryOperationRule implements Rule public function __construct( private ExprPrinter $exprPrinter, private RuleLevelHelper $ruleLevelHelper, + private bool $bleedingEdge, ) { } @@ -44,81 +45,83 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($node) instanceof ErrorType) { - $leftName = '__PHPSTAN__LEFT__'; - $rightName = '__PHPSTAN__RIGHT__'; - $leftVariable = new Node\Expr\Variable($leftName); - $rightVariable = new Node\Expr\Variable($rightName); - if ($node instanceof Node\Expr\AssignOp) { - $identifier = 'assignOp'; - $newNode = clone $node; - $newNode->setAttribute('phpstan_cache_printer', null); - $left = $node->var; - $right = $node->expr; - $newNode->var = $leftVariable; - $newNode->expr = $rightVariable; - } else { - $identifier = 'binaryOp'; - $newNode = clone $node; - $newNode->setAttribute('phpstan_cache_printer', null); - $left = $node->left; - $right = $node->right; - $newNode->left = $leftVariable; - $newNode->right = $rightVariable; - } - - if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) { - $callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType; - } else { - $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; - } - - $leftType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $left, - '', - $callback, - )->getType(); - if ($leftType instanceof ErrorType) { - return []; - } - - $rightType = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $right, - '', - $callback, - )->getType(); - if ($rightType instanceof ErrorType) { - return []; - } - - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - $scope = $scope - ->assignVariable($leftName, $leftType, $leftType) - ->assignVariable($rightName, $rightType, $rightType); - - if (!$scope->getType($newNode) instanceof ErrorType) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Binary operation "%s" between %s and %s results in an error.', - substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)), - $scope->getType($left)->describe(VerbosityLevel::value()), - $scope->getType($right)->describe(VerbosityLevel::value()), - )) - ->line($left->getStartLine()) - ->identifier(sprintf('%s.invalid', $identifier)) - ->build(), - ]; + if (!$scope->getType($node) instanceof ErrorType && !$this->bleedingEdge) { + return []; + } + + $leftName = '__PHPSTAN__LEFT__'; + $rightName = '__PHPSTAN__RIGHT__'; + $leftVariable = new Node\Expr\Variable($leftName); + $rightVariable = new Node\Expr\Variable($rightName); + if ($node instanceof Node\Expr\AssignOp) { + $identifier = 'assignOp'; + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $left = $node->var; + $right = $node->expr; + $newNode->var = $leftVariable; + $newNode->expr = $rightVariable; + } else { + $identifier = 'binaryOp'; + $newNode = clone $node; + $newNode->setAttribute('phpstan_cache_printer', null); + $left = $node->left; + $right = $node->right; + $newNode->left = $leftVariable; + $newNode->right = $rightVariable; + } + + if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) { + $callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType; + } elseif ($node instanceof Node\Expr\AssignOp\Plus || $node instanceof Node\Expr\BinaryOp\Plus) { + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType || $type->isArray()->yes(); + } else { + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; + } + + $leftType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $left, + '', + $callback, + )->getType(); + if ($leftType instanceof ErrorType) { + return []; + } + + $rightType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $right, + '', + $callback, + )->getType(); + if ($rightType instanceof ErrorType) { + return []; + } + + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $scope = $scope + ->assignVariable($leftName, $leftType, $leftType) + ->assignVariable($rightName, $rightType, $rightType); + + if (!$scope->getType($newNode) instanceof ErrorType) { + return []; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Binary operation "%s" between %s and %s results in an error.', + substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)), + $scope->getType($left)->describe(VerbosityLevel::value()), + $scope->getType($right)->describe(VerbosityLevel::value()), + )) + ->line($left->getStartLine()) + ->identifier(sprintf('%s.invalid', $identifier)) + ->build(), + ]; } } diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index b3e4a29263..9cd551cbf3 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -6,8 +6,17 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\BooleanType; use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function get_class; use function sprintf; @@ -18,7 +27,11 @@ class InvalidIncDecOperationRule implements Rule { - public function __construct(private bool $checkThisOnly) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $bleedingEdge, + private bool $checkThisOnly, + ) { } @@ -74,7 +87,11 @@ public function processNode(Node $node, Scope $scope): array ]; } - if (!$this->checkThisOnly) { + if (!$this->bleedingEdge) { + if ($this->checkThisOnly) { + return []; + } + $varType = $scope->getType($node->var); if (!$varType->toString() instanceof ErrorType) { return []; @@ -82,20 +99,30 @@ public function processNode(Node $node, Scope $scope): array 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(); - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot use %s on %s.', - $operatorString, - $varType->describe(VerbosityLevel::value()), - )) - ->line($node->var->getStartLine()) - ->identifier(sprintf('%s.type', $nodeType)) - ->build(), - ]; + if ($varType instanceof ErrorType || $allowedTypes->isSuperTypeOf($varType)->yes()) { + return []; + } } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot use %s on %s.', + $operatorString, + $varType->describe(VerbosityLevel::value()), + )) + ->line($node->var->getStartLine()) + ->identifier(sprintf('%s.type', $nodeType)) + ->build(), + ]; } } diff --git a/src/Rules/Operators/InvalidUnaryOperationRule.php b/src/Rules/Operators/InvalidUnaryOperationRule.php index 000b0529f2..099c1d6507 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -3,10 +3,14 @@ namespace PHPStan\Rules\Operators; use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -16,6 +20,13 @@ class InvalidUnaryOperationRule implements Rule { + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $bleedingEdge, + ) + { + } + public function getNodeType(): string { return Node\Expr::class; @@ -31,28 +42,58 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($node) instanceof ErrorType) { + if ($this->bleedingEdge) { + $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\UnaryPlus) { - $operator = '+'; - } elseif ($node instanceof Node\Expr\UnaryMinus) { - $operator = '-'; + if ($node instanceof Node\Expr\BitwiseNot) { + $callback = static fn (Type $type): bool => $type->isString()->yes() || $type->isInteger()->yes() || $type->isFloat()->yes(); } else { - $operator = '~'; + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; } - return [ - RuleErrorBuilder::message(sprintf( - 'Unary operation "%s" on %s results in an error.', - $operator, - $scope->getType($node->expr)->describe(VerbosityLevel::value()), - )) - ->line($node->expr->getStartLine()) - ->identifier('unaryOp.invalid') - ->build(), - ]; + + $exprType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + '', + $callback, + )->getType(); + if ($exprType instanceof ErrorType) { + return []; + } + + 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) { + return []; } - return []; + if ($node instanceof Node\Expr\UnaryPlus) { + $operator = '+'; + } elseif ($node instanceof Node\Expr\UnaryMinus) { + $operator = '-'; + } else { + $operator = '~'; + } + return [ + RuleErrorBuilder::message(sprintf( + 'Unary operation "%s" on %s results in an error.', + $operator, + $scope->getType($node->expr)->describe(VerbosityLevel::value()), + )) + ->line($node->expr->getStartLine()) + ->identifier('unaryOp.invalid') + ->build(), + ]; } } diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php new file mode 100644 index 0000000000..e34f6fba22 --- /dev/null +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -0,0 +1,70 @@ +unpack) { + return null; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $parameter->value, + '', + static fn (Type $type): bool => !$castFn($type->getIterableValueType()) instanceof ErrorType, + ); + + if ($typeResult->getType() instanceof ErrorType + || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType) { + return null; + } + + return RuleErrorBuilder::message( + sprintf($errorMessageTemplate, $parameterName, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), + )->identifier('argument.type')->build(); + } + + public function getParameterName(Arg $parameter, int $parameterIdx, ?ParameterReflection $parameterReflection): string + { + if ($parameterReflection === null) { + return sprintf('#%d', $parameterIdx + 1); + } + + $paramName = $parameterReflection->getName(); + $origParameter = $parameter->getAttributes()[ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE] ?? null; + + if (!$origParameter instanceof Arg) { + $origParameter = $parameter; + } + + return $origParameter->name !== null + ? sprintf('$%s', $paramName) + : sprintf('#%d $%s', $parameterIdx + 1, $paramName); + } + +} diff --git a/src/Rules/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php index 6026b18a31..e7d3f8e73b 100644 --- a/src/Rules/PhpDoc/AssertRuleHelper.php +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -51,6 +51,10 @@ public function check(ExtendedMethodReflection|FunctionReflection $reflection, P continue; } + if (!$assert->isExplicit()) { + continue; + } + $assertedExpr = $assert->getParameter()->getExpr(new TypeExpr($parametersByName[$parameterName])); $assertedExprType = $this->initializerExprTypeResolver->getType($assertedExpr, $context); if ($assertedExprType instanceof ErrorType) { diff --git a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php index e970ba377d..7ad14e65b6 100644 --- a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ConditionalType; @@ -23,7 +23,7 @@ class ConditionalReturnTypeRuleHelper /** * @return list */ - public function check(ParametersAcceptor $acceptor): array + public function check(ParametersAcceptorWithPhpDocs $acceptor): array { $conditionalTypes = []; $parametersByName = []; @@ -36,6 +36,26 @@ public function check(ParametersAcceptor $acceptor): array return $traverse($type); }); + if ($parameter->getOutType() !== null) { + TypeTraverser::map($parameter->getOutType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + + if ($parameter->getClosureThisType() !== null) { + TypeTraverser::map($parameter->getClosureThisType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + $parametersByName[$parameter->getName()] = $parameter; } diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php new file mode 100644 index 0000000000..1fdb7a17c0 --- /dev/null +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -0,0 +1,117 @@ + $functionTemplateTags + * + * @return list + */ + public function check( + Node $node, + Scope $scope, + string $location, + Type $callableType, + ?string $functionName, + array $functionTemplateTags, + ?ClassReflection $classReflection, + ): array + { + $errors = []; + + TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) { + if (!($type instanceof CallableType || $type instanceof ClosureType)) { + return $traverse($type); + } + + $typeDescription = $type->describe(VerbosityLevel::precise()); + + $errors = $this->templateTypeCheck->check( + $scope, + $node, + TemplateTypeScope::createWithAnonymousFunction(), + $type->getTemplateTags(), + sprintf('PHPDoc tag %s template of %s cannot have existing class %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), + ); + + $templateTags = $type->getTemplateTags(); + + $classDescription = null; + if ($classReflection !== null) { + $classDescription = $classReflection->getDisplayName(); + } + + if ($functionName !== null) { + $functionDescription = sprintf('function %s', $functionName); + if ($classReflection !== null) { + $functionDescription = sprintf('method %s::%s', $classDescription, $functionName); + } + + foreach (array_keys($functionTemplateTags) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for %s.', + $location, + $name, + $typeDescription, + $name, + $functionDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + if ($classReflection !== null) { + foreach (array_keys($classReflection->getTemplateTags()) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for class %s.', + $location, + $name, + $typeDescription, + $name, + $classDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + return $traverse($type); + }); + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index a85b7e02ab..3b89aea5c3 100644 --- a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\IdentifierRuleError; @@ -62,10 +61,6 @@ public function processNode(Node $node, Scope $scope): array private function processSingleConstant(ClassReflection $classReflection, ?Type $nativeType, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { - return []; - } - $phpDocType = $constantReflection->getPhpDocType(); if ($phpDocType === null) { return []; diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index 39b16ef750..e37db8ae89 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -31,6 +31,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -137,6 +138,16 @@ public function processNode(Node $node, Scope $scope): array ), )); + $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( @@ -215,6 +226,16 @@ public function processNode(Node $node, Scope $scope): array $errors[] = $errorBuilder->build(); } + + $errors = array_merge($errors, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@return', + $phpDocReturnType, + $functionName, + $resolvedPhpDoc->getTemplateTags(), + $scope->isInClass() ? $scope->getClassReflection() : null, + )); } } diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index cbb82ee889..4713a0cd08 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -24,6 +24,7 @@ class IncompatiblePropertyPhpDocTypeRule implements Rule public function __construct( private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -93,6 +94,18 @@ public function processNode(Node $node, Scope $scope): array $className = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + if ($node->isPromoted() === false) { + $messages = array_merge($messages, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@var', + $phpDocType, + null, + [], + $classReflection, + )); + } + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index c57b26b18f..5d4f65420c 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -55,6 +55,9 @@ class InvalidPHPStanDocTagRule implements Rule '@phpstan-readonly-allow-private-mutation', '@phpstan-require-extends', '@phpstan-require-implements', + '@phpstan-param-immediately-invoked-callable', + '@phpstan-param-later-invoked-callable', + '@phpstan-param-closure-this', ]; public function __construct( @@ -97,7 +100,6 @@ public function processNode(Node $node, Scope $scope): array } } - // todo $docComment = $node->getDocComment(); if ($docComment === null) { return []; diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index 5b5ee77fb9..6734c54e4c 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -64,7 +64,6 @@ public function processNode(Node $node, Scope $scope): array } } - // todo $docComment = $node->getDocComment(); if ($docComment === null) { return []; @@ -76,7 +75,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { - if (str_starts_with($phpDocTag->name, '@psalm-')) { + if (str_starts_with($phpDocTag->name, '@phan-') || str_starts_with($phpDocTag->name, '@psalm-')) { continue; } diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index 8c8ff51e5e..029f558bb8 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; @@ -29,7 +29,7 @@ class InvalidPhpDocVarTagTypeRule implements Rule public function __construct( private FileTypeMapper $fileTypeMapper, private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private MissingTypehintCheck $missingTypehintCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, @@ -118,7 +118,6 @@ public function processNode(Node $node, Scope $scope): array $innerName, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } @@ -148,13 +147,12 @@ public function processNode(Node $node, Scope $scope): array ->build(); } - if (!$this->checkClassCaseSensitivity) { - continue; - } - $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + $this->checkClassCaseSensitivity, + ), ); } diff --git a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php index 2a13cb56e1..1efb15ffaa 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -8,6 +8,9 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use Throwable; use function sprintf; @@ -60,8 +63,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $isThrowsSuperType = (new ObjectType(Throwable::class))->isSuperTypeOf($phpDocThrowsType); - if ($isThrowsSuperType->yes()) { + if ($this->isThrowsValid($phpDocThrowsType)) { return []; } @@ -73,4 +75,32 @@ public function processNode(Node $node, Scope $scope): array ]; } + private function isThrowsValid(Type $phpDocThrowsType): bool + { + $throwType = new ObjectType(Throwable::class); + if ($phpDocThrowsType instanceof UnionType) { + foreach ($phpDocThrowsType->getTypes() as $innerType) { + if (!$this->isThrowsValid($innerType)) { + return false; + } + } + + return true; + } + + $toIntersectWith = []; + foreach ($phpDocThrowsType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->isInterface()) { + continue; + } + foreach ($classReflection->getRequireExtendsTags() as $requireExtendsTag) { + $toIntersectWith[] = $requireExtendsTag->getType(); + } + } + + return $throwType->isSuperTypeOf( + TypeCombinator::intersect($phpDocThrowsType, ...$toIntersectWith), + )->yes(); + } + } diff --git a/src/Rules/PhpDoc/RequireExtendsCheck.php b/src/Rules/PhpDoc/RequireExtendsCheck.php index 8dea37c68c..14f6d43647 100644 --- a/src/Rules/PhpDoc/RequireExtendsCheck.php +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -4,21 +4,22 @@ use PhpParser\Node; use PHPStan\PhpDoc\Tag\RequireExtendsTag; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function array_merge; use function count; use function sprintf; +use function strtolower; final class RequireExtendsCheck { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, ) { @@ -26,20 +27,24 @@ public function __construct( /** * @param array $extendsTags - * @return RuleError[] + * @return list */ public function checkExtendsTags(Node $node, array $extendsTags): array { $errors = []; if (count($extendsTags) > 1) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends can only be used once.'))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends can only be used once.')) + ->identifier('requireExtends.duplicate') + ->build(); } foreach ($extendsTags as $extendsTag) { $type = $extendsTag->getType(); if (!$type instanceof ObjectType) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireExtends.nonObject') + ->build(); continue; } @@ -47,20 +52,27 @@ public function checkExtendsTags(Node $node, array $extendsTags): array $referencedClassReflection = $type->getClassReflection(); if ($referencedClassReflection === null) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains unknown class %s.', $class))->discoveringSymbolsTip()->build(); + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains unknown class %s.', $class)) + ->discoveringSymbolsTip() + ->identifier('class.notFound') + ->build(); continue; } if (!$referencedClassReflection->isClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain non-class type %s.', $class))->build(); + $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))->build(); - } elseif ($this->checkClassCaseSensitivity) { + $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->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $node), - ]), + ], $this->checkClassCaseSensitivity), ); } } diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php index c4a463f093..27d8b3d6a3 100644 --- a/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; +use function sprintf; /** * @implements Rule @@ -37,7 +38,9 @@ public function processNode(Node $node, Scope $scope): array if (!$classReflection->isInterface()) { return [ - RuleErrorBuilder::message('PHPDoc tag @phpstan-require-extends is only valid on trait or interface.')->build(), + RuleErrorBuilder::message('PHPDoc tag @phpstan-require-extends is only valid on trait or interface.') + ->identifier(sprintf('requireExtends.on%s', $classReflection->getClassTypeDescription())) + ->build(), ]; } diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php index d4325e65c1..03e9422e2a 100644 --- a/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function count; +use function sprintf; /** * @implements Rule @@ -30,7 +31,9 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('PHPDoc tag @phpstan-require-implements is only valid on trait.')->build(), + RuleErrorBuilder::message('PHPDoc tag @phpstan-require-implements is only valid on trait.') + ->identifier(sprintf('requireImplements.on%s', $classReflection->getClassTypeDescription())) + ->build(), ]; } diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php index 27eb76f7bd..2c18b2e3ef 100644 --- a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -13,6 +13,7 @@ use PHPStan\Type\VerbosityLevel; use function array_merge; use function sprintf; +use function strtolower; /** * @implements Rule @@ -22,7 +23,7 @@ class RequireImplementsDefinitionTraitRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, ) { @@ -49,25 +50,32 @@ public function processNode(Node $node, Scope $scope): array foreach ($implementsTags as $implementsTag) { $type = $implementsTag->getType(); if (!$type instanceof ObjectType) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))->build(); + $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()->build(); + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains unknown class %s.', $class)) + ->discoveringSymbolsTip() + ->identifier('class.notFound') + ->build(); continue; } if (!$referencedClassReflection->isInterface()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements cannot contain non-interface type %s.', $class))->build(); - } elseif ($this->checkClassCaseSensitivity) { + $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->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $node), - ]), + ], $this->checkClassCaseSensitivity), ); } } diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index 90fe8cb7b4..d01b17396b 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -24,7 +24,7 @@ class VarTagTypeRuleHelper { - public function __construct(private bool $checkTypeAgainstPhpDocType) + public function __construct(private bool $checkTypeAgainstPhpDocType, private bool $strictWideningCheck) { } @@ -156,6 +156,10 @@ private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $v private function checkType(Type $type, Type $varTagType, int $depth = 0): bool { + if ($this->strictWideningCheck) { + return !$type->isSuperTypeOf($varTagType)->yes(); + } + if ($type->isConstantArray()->yes()) { if ($type->isIterableAtLeastOnce()->no()) { $type = new ArrayType(new MixedType(), new MixedType()); diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index d3dfbdde08..0a8d6cdbcf 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -15,6 +15,7 @@ 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; @@ -78,12 +79,17 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string 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, - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), ))->identifier('property.nonObject')->build(), ]; } @@ -138,7 +144,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( 'Access to an undefined property %s::$%s.', - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $name, ))->identifier('property.notFound'); if ($typeResult->getTip() !== null) { diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 2f58060b5a..9d929c39a2 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -9,7 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -40,7 +40,7 @@ class AccessStaticPropertiesRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, ) { } @@ -137,7 +137,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ]; } - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); $classType = $scope->resolveTypeByName($node->class); } diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index ad0d4badc7..8fd67c1e5f 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -39,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection = $classReflection->getNativeProperty($node->getName()); $propertyType = $propertyReflection->getWritableType(); if ($propertyReflection->getNativeType() instanceof MixedType) { - if ($default instanceof Node\Expr\ConstFetch && (string) $default->name === 'null') { + if ($default instanceof Node\Expr\ConstFetch && $default->name->toLowerString() === 'null') { return []; } } diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index e87973d297..2b138d5bbd 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -7,7 +7,7 @@ use PHPStan\Node\ClassPropertyNode; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -24,7 +24,7 @@ class ExistingClassesInPropertiesRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, private bool $checkClassCaseSensitivity, @@ -72,12 +72,13 @@ public function processNode(Node $node, Scope $scope): array ))->identifier('class.notFound')->discoveringSymbolsTip()->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), - ); - } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + $this->checkClassCaseSensitivity, + ), + ); if ( $this->phpVersion->supportsPureIntersectionTypes() diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index f103d9752a..c429a30fdd 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -70,7 +70,6 @@ public function processNode(Node $node, Scope $scope): array $name, implode(', ', $genericTypeNames), )) - ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) ->identifier('missingType.generics') ->build(); } diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php index 5f32b4cbbc..1195f47022 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -27,13 +26,8 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { - return []; - } - - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if (!$calledOnType->isNull()->no()) { return []; } diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php new file mode 100644 index 0000000000..4e294dc0fe --- /dev/null +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -0,0 +1,150 @@ + + */ + public function check( + string $functionDescription, + string $identifier, + FunctionReflection|ExtendedMethodReflection $functionReflection, + array $parameters, + Type $returnType, + array $impurePoints, + array $throwPoints, + array $statements, + ): 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) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as pure but parameter $%s is passed by reference.', + $functionDescription, + $parameter->getName(), + ))->identifier(sprintf('pure%s.parameterByRef', $identifier))->build(); + } + + if ($returnType->isVoid()->yes() && !$isConstructor) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as pure but returns void.', + $functionDescription, + ))->identifier(sprintf('pure%s.void', $identifier))->build(); + } + + foreach ($impurePoints as $impurePoint) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s in pure %s.', + $impurePoint->isCertain() ? 'Impure' : 'Possibly impure', + $impurePoint->getDescription(), + lcfirst($functionDescription), + )) + ->line($impurePoint->getNode()->getStartLine()) + ->identifier(sprintf( + '%s.%s', + $impurePoint->isCertain() ? 'impure' : 'possiblyImpure', + $impurePoint->getIdentifier(), + )) + ->build(); + } + } elseif ($isPure->no()) { + if ( + count($throwPoints) === 0 + && count($impurePoints) === 0 + && count($functionReflection->getAsserts()->getAll()) === 0 + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as impure but does not have any side effects.', + $functionDescription, + ))->identifier(sprintf('impure%s.pure', $identifier))->build(); + } + } elseif ($returnType->isVoid()->yes()) { + if ( + count($throwPoints) === 0 + && count($impurePoints) === 0 + && !$isConstructor + && (!$functionReflection instanceof ExtendedMethodReflection || $functionReflection->isPrivate()) + && count($functionReflection->getAsserts()->getAll()) === 0 + ) { + $hasByRef = false; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + $hasByRef = true; + break; + } + + $statements = array_filter($statements, static function (Stmt $stmt): bool { + if ($stmt instanceof Stmt\Nop) { + return false; + } + + if (!$stmt instanceof Stmt\Expression) { + return true; + } + if (!$stmt->expr instanceof FuncCall) { + return true; + } + if (!$stmt->expr->name instanceof Name) { + return true; + } + + return !in_array($stmt->expr->name->toString(), CallToFunctionStatementWithoutSideEffectsRule::PHPSTAN_TESTING_FUNCTIONS, true); + }); + + if (!$hasByRef && count($statements) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s returns void but does not have any side effects.', + $functionDescription, + ))->identifier('void.pure')->build(); + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Pure/PureFunctionRule.php b/src/Rules/Pure/PureFunctionRule.php new file mode 100644 index 0000000000..838e9bc67b --- /dev/null +++ b/src/Rules/Pure/PureFunctionRule.php @@ -0,0 +1,44 @@ + + */ +class PureFunctionRule implements Rule +{ + + public function __construct(private FunctionPurityCheck $check) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $function = $node->getFunctionReflection(); + $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + + return $this->check->check( + sprintf('Function %s()', $function->getName()), + 'Function', + $function, + $variant->getParameters(), + $variant->getReturnType(), + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + $node->getStatements(), + ); + } + +} diff --git a/src/Rules/Pure/PureMethodRule.php b/src/Rules/Pure/PureMethodRule.php new file mode 100644 index 0000000000..a023720118 --- /dev/null +++ b/src/Rules/Pure/PureMethodRule.php @@ -0,0 +1,44 @@ + + */ +class PureMethodRule implements Rule +{ + + public function __construct(private FunctionPurityCheck $check) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + + return $this->check->check( + sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + 'Method', + $method, + $variant->getParameters(), + $variant->getReturnType(), + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + $node->getStatements(), + ); + } + +} diff --git a/src/Rules/Regexp/RegularExpressionPatternRule.php b/src/Rules/Regexp/RegularExpressionPatternRule.php index df4f74b67d..fde058e62a 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Regex\RegexExpressionHelper; use function in_array; use function sprintf; use function str_starts_with; @@ -20,6 +21,12 @@ class RegularExpressionPatternRule implements Rule { + public function __construct( + private RegexExpressionHelper $regexExpressionHelper, + ) + { + } + public function getNodeType(): string { return FuncCall::class; @@ -63,46 +70,47 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array $patternStrings = []; - foreach ($patternType->getConstantStrings() as $constantStringType) { - if ( - !in_array($functionName, [ - 'preg_match', - 'preg_match_all', - 'preg_split', - 'preg_grep', - 'preg_replace', - 'preg_replace_callback', - 'preg_filter', - ], true) - ) { - continue; + if ( + in_array($functionName, [ + 'preg_match', + 'preg_match_all', + 'preg_split', + 'preg_grep', + 'preg_replace', + 'preg_replace_callback', + 'preg_filter', + ], true) + ) { + if ($patternNode instanceof Node\Expr\BinaryOp\Concat) { + $patternType = $this->regexExpressionHelper->resolvePatternConcat($patternNode, $scope); + } + foreach ($patternType->getConstantStrings() as $constantStringType) { + $patternStrings[] = $constantStringType->getValue(); } - - $patternStrings[] = $constantStringType->getValue(); } - foreach ($patternType->getConstantArrays() as $constantArrayType) { - if ( - in_array($functionName, [ - 'preg_replace', - 'preg_replace_callback', - 'preg_filter', - ], true) - ) { + if ( + in_array($functionName, [ + 'preg_replace', + 'preg_replace_callback', + 'preg_filter', + ], true) + ) { + foreach ($patternType->getConstantArrays() as $constantArrayType) { foreach ($constantArrayType->getValueTypes() as $arrayKeyType) { foreach ($arrayKeyType->getConstantStrings() as $constantString) { $patternStrings[] = $constantString->getValue(); } } } + } - if ($functionName !== 'preg_replace_callback_array') { - continue; - } - - foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { - foreach ($arrayKeyType->getConstantStrings() as $constantString) { - $patternStrings[] = $constantString->getValue(); + if ($functionName === 'preg_replace_callback_array') { + foreach ($patternType->getConstantArrays() as $constantArrayType) { + foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); + } } } } diff --git a/src/Rules/Regexp/RegularExpressionQuotingRule.php b/src/Rules/Regexp/RegularExpressionQuotingRule.php new file mode 100644 index 0000000000..e6a5839fdb --- /dev/null +++ b/src/Rules/Regexp/RegularExpressionQuotingRule.php @@ -0,0 +1,242 @@ + + */ +class RegularExpressionQuotingRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private RegexExpressionHelper $regexExpressionHelper, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ( + !in_array($functionReflection->getName(), [ + 'preg_match', + 'preg_match_all', + 'preg_filter', + 'preg_grep', + 'preg_replace', + 'preg_replace_callback', + 'preg_split', + ], true) + ) { + return []; + } + + $normalizedArgs = $this->getNormalizedArgs($node, $scope, $functionReflection); + if ($normalizedArgs === null) { + return []; + } + if (!isset($normalizedArgs[0])) { + return []; + } + if (!$normalizedArgs[0]->value instanceof Concat) { + return []; + } + + $patternDelimiters = $this->regexExpressionHelper->getPatternDelimiters($normalizedArgs[0]->value, $scope); + return $this->validateQuoteDelimiters($normalizedArgs[0]->value, $scope, $patternDelimiters); + } + + /** + * @param string[] $patternDelimiters + * + * @return list + */ + private function validateQuoteDelimiters(Concat $concat, Scope $scope, array $patternDelimiters): array + { + if ($patternDelimiters === []) { + return []; + } + + $errors = []; + if ( + $concat->left instanceof FuncCall + && $concat->left->name instanceof Name + && $concat->left->name->toLowerString() === 'preg_quote' + ) { + $pregError = $this->validatePregQuote($concat->left, $scope, $patternDelimiters); + if ($pregError !== null) { + $errors[] = $pregError; + } + } elseif ($concat->left instanceof Concat) { + $errors = array_merge($errors, $this->validateQuoteDelimiters($concat->left, $scope, $patternDelimiters)); + } + + if ( + $concat->right instanceof FuncCall + && $concat->right->name instanceof Name + && $concat->right->name->toLowerString() === 'preg_quote' + ) { + $pregError = $this->validatePregQuote($concat->right, $scope, $patternDelimiters); + if ($pregError !== null) { + $errors[] = $pregError; + } + } elseif ($concat->right instanceof Concat) { + $errors = array_merge($errors, $this->validateQuoteDelimiters($concat->right, $scope, $patternDelimiters)); + } + + return $errors; + } + + /** + * @param string[] $patternDelimiters + */ + private function validatePregQuote(FuncCall $pregQuote, Scope $scope, array $patternDelimiters): ?IdentifierRuleError + { + if (!$pregQuote->name instanceof Node\Name) { + return null; + } + + if (!$this->reflectionProvider->hasFunction($pregQuote->name, $scope)) { + return null; + } + $functionReflection = $this->reflectionProvider->getFunction($pregQuote->name, $scope); + + $args = $this->getNormalizedArgs($pregQuote, $scope, $functionReflection); + if ($args === null) { + return null; + } + + $patternDelimiters = $this->removeDefaultEscapedDelimiters($patternDelimiters); + if ($patternDelimiters === []) { + return null; + } + + if (count($args) === 1) { + if (count($patternDelimiters) === 1) { + return RuleErrorBuilder::message(sprintf('Call to preg_quote() is missing delimiter %s to be effective.', $patternDelimiters[0])) + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + + return RuleErrorBuilder::message('Call to preg_quote() is missing delimiter parameter to be effective.') + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + + if (count($args) >= 2) { + + foreach ($scope->getType($args[1]->value)->getConstantStrings() as $quoteDelimiterType) { + $quoteDelimiter = $quoteDelimiterType->getValue(); + + $quoteDelimiters = $this->removeDefaultEscapedDelimiters([$quoteDelimiter]); + if ($quoteDelimiters === []) { + continue; + } + + if (count($quoteDelimiters) !== 1) { + throw new ShouldNotHappenException(); + } + $quoteDelimiter = $quoteDelimiters[0]; + + if (!in_array($quoteDelimiter, $patternDelimiters, true)) { + if (count($patternDelimiters) === 1) { + return RuleErrorBuilder::message(sprintf('Call to preg_quote() uses invalid delimiter %s while pattern uses %s.', $quoteDelimiter, $patternDelimiters[0])) + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + + return RuleErrorBuilder::message(sprintf('Call to preg_quote() uses invalid delimiter %s.', $quoteDelimiter)) + ->line($pregQuote->getStartLine()) + ->identifier('argument.invalidPregQuote') + ->build(); + } + } + } + + return null; + } + + /** + * @param string[] $delimiters + * + * @return list + */ + private function removeDefaultEscapedDelimiters(array $delimiters): array + { + return array_values(array_filter($delimiters, fn (string $delimiter): bool => !$this->isDefaultEscaped($delimiter))); + } + + private function isDefaultEscaped(string $delimiter): bool + { + if (strlen($delimiter) !== 1) { + return false; + } + + return in_array( + $delimiter, + // these delimiters are escaped, no matter what preg_quote() 2nd arg looks like + ['.', '\\', '+', '*', '?', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', '-', '#'], + true, + ); + } + + /** + * @return Node\Arg[]|null + */ + private function getNormalizedArgs(FuncCall $functionCall, Scope $scope, FunctionReflection $functionReflection): ?array + { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $functionCall); + if ($normalizedFuncCall === null) { + return null; + } + + return $normalizedFuncCall->getArgs(); + } + +} diff --git a/src/Rules/RuleErrorBuilder.php b/src/Rules/RuleErrorBuilder.php index 35c4d4c253..a838200800 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -201,6 +201,15 @@ public function acceptsReasonsTip(array $reasons): self return $this; } + /** + * @phpstan-this-out self + * @return self + */ + public function treatPhpDocTypesAsCertainTip(): self + { + return $this->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + /** * Sets an error identifier. * @@ -212,7 +221,7 @@ public function acceptsReasonsTip(array $reasons): self public function identifier(string $identifier): self { if (!Error::validateIdentifier($identifier)) { - throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $identifier)); + throw new ShouldNotHappenException(sprintf('Invalid identifier: %s, error identifiers must match /%s/', $identifier, Error::PATTERN_IDENTIFIER)); } $this->properties['identifier'] = $identifier; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 341a7b0bbd..c5a08ba5ab 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -10,6 +10,7 @@ use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateMixedType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -63,7 +64,13 @@ private function transformCommonType(Type $type): Type return TypeTraverser::map($type, function (Type $type, callable $traverse) { if ($type instanceof TemplateMixedType) { - return $type->toStrictMixedType(); + if (!$this->newRuleLevelHelper) { + return $type->toStrictMixedType(); + } + + if ($this->checkExplicitMixed) { + return $type->toStrictMixedType(); + } } if ( $type instanceof MixedType @@ -88,17 +95,25 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { if ($acceptedType instanceof CallableType) { if ($acceptedType->isCommonCallable()) { - return new CallableType(null, null, $acceptedType->isVariadic()); + return $acceptedType; } return new CallableType( $acceptedType->getParameters(), $traverse($this->transformCommonType($acceptedType->getReturnType())), $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getTemplateTags(), + $acceptedType->isPure(), ); } if ($acceptedType instanceof ClosureType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } + return new ClosureType( $acceptedType->getParameters(), $traverse($this->transformCommonType($acceptedType->getReturnType())), @@ -106,6 +121,9 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType->getTemplateTypeMap(), $acceptedType->getResolvedTemplateTypeMap(), $acceptedType->getCallSiteVarianceMap(), + $acceptedType->getTemplateTags(), + $acceptedType->getThrowPoints(), + $acceptedType->getImpurePoints(), ); } @@ -297,56 +315,93 @@ public function findTypeToCheck( return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); + + return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); + } + + /** @param callable(Type $type): bool $unionTypeCriteriaCallback */ + private function findTypeToCheckImplementation( + Scope $scope, + Expr $var, + Type $type, + string $unknownClassErrorPattern, + callable $unionTypeCriteriaCallback, + bool $isTopLevel = false, + ): FoundTypeResult + { if (!$this->checkNullables && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } - if ( - $this->checkExplicitMixed - && $type instanceof MixedType - && !$type instanceof TemplateMixedType - && $type->isExplicitMixed() - ) { - return new FoundTypeResult(new StrictMixedType(), [], [], null); - } + 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->checkImplicitMixed + && $type instanceof MixedType + && !$type instanceof TemplateMixedType + && !$type->isExplicitMixed() + ) { + return new FoundTypeResult(new StrictMixedType(), [], [], null); + } } if ($type instanceof MixedType || $type instanceof NeverType) { return new FoundTypeResult(new ErrorType(), [], [], null); } - if ($type instanceof StaticType) { - $type = $type->getStaticObjectType(); + if (!$this->newRuleLevelHelper) { + if ($isTopLevel && $type instanceof StaticType) { + $type = $type->getStaticObjectType(); + } } $errors = []; - $directClassNames = $type->getObjectClassNames(); $hasClassExistsClass = false; - foreach ($directClassNames as $referencedClass) { - if ($this->reflectionProvider->hasClass($referencedClass)) { - $classReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$classReflection->isTrait()) { + $directClassNames = []; + + if ($isTopLevel) { + $directClassNames = $type->getObjectClassNames(); + foreach ($directClassNames as $referencedClass) { + if ($this->reflectionProvider->hasClass($referencedClass)) { + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { + continue; + } + } + + if ($scope->isInClassExists($referencedClass)) { + $hasClassExistsClass = true; continue; } - } - if ($scope->isInClassExists($referencedClass)) { - $hasClassExistsClass = true; - continue; + $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) + ->line($var->getStartLine()) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(); } - - $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) - ->line($var->getStartLine()) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->build(); } if (count($errors) > 0 || $hasClassExistsClass) { @@ -357,28 +412,84 @@ public function findTypeToCheck( return new FoundTypeResult(new ErrorType(), [], [], null); } - if ( - ( - !$this->checkUnionTypes - && $type instanceof UnionType - && !$type instanceof BenevolentUnionType - ) || ( - !$this->checkBenevolentUnionTypes - && $type instanceof BenevolentUnionType - ) - ) { - $newTypes = []; + if ($this->newRuleLevelHelper) { + if ($type instanceof UnionType) { + $shouldFilterUnion = ( + !$this->checkUnionTypes + && !$type instanceof BenevolentUnionType + ) || ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ); - foreach ($type->getTypes() as $innerType) { - if (!$unionTypeCriteriaCallback($innerType)) { - continue; + $newTypes = []; + + foreach ($type->getTypes() as $innerType) { + if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { + continue; + } + + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType, + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); } - $newTypes[] = $innerType; + 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 (count($newTypes) > 0) { - return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); + if ($type instanceof IntersectionType) { + $newTypes = []; + + foreach ($type->getTypes() as $innerType) { + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType, + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); + } + + return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); + } + } 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); + } } } diff --git a/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php new file mode 100644 index 0000000000..8056f39420 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php @@ -0,0 +1,41 @@ + + */ +class TooWideFunctionParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $node->getFunctionReflection(); + + return $this->check->check( + $node->getExecutionEnds(), + $node->getReturnStatements(), + ParametersAcceptorSelector::selectSingle($inFunction->getVariants())->getParameters(), + sprintf('Function %s()', $inFunction->getName()), + ); + } + +} diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index db2d633fa5..9451787f4d 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -9,6 +9,7 @@ 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 function count; @@ -30,6 +31,7 @@ public function processNode(Node $node, Scope $scope): array $function = $node->getFunctionReflection(); $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); if (!$functionReturnType instanceof UnionType) { return []; } diff --git a/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php new file mode 100644 index 0000000000..e715ce7d91 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php @@ -0,0 +1,41 @@ + + */ +class TooWideMethodParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inMethod = $node->getMethodReflection(); + + return $this->check->check( + $node->getExecutionEnds(), + $node->getReturnStatements(), + ParametersAcceptorSelector::selectSingle($inMethod->getVariants())->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 2125bbbc15..5e62501f72 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -10,6 +10,7 @@ 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 function count; @@ -56,6 +57,7 @@ public function processNode(Node $node, Scope $scope): array } $methodReturnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $methodReturnType = TypeUtils::resolveLateResolvableTypes($methodReturnType); if (!$methodReturnType instanceof UnionType) { return []; } diff --git a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php new file mode 100644 index 0000000000..097812f918 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php @@ -0,0 +1,118 @@ + $executionEnds + * @param list $returnStatements + * @param ParameterReflectionWithPhpDocs[] $parameters + * @return list + */ + public function check( + array $executionEnds, + array $returnStatements, + array $parameters, + string $functionDescription, + ): array + { + $finalScope = null; + foreach ($executionEnds as $executionEnd) { + $endScope = $executionEnd->getStatementResult()->getScope(); + if ($finalScope === null) { + $finalScope = $endScope; + continue; + } + + $finalScope = $finalScope->mergeWith($endScope); + } + + foreach ($returnStatements as $statement) { + if ($finalScope === null) { + $finalScope = $statement->getScope(); + continue; + } + + $finalScope = $finalScope->mergeWith($statement->getScope()); + } + + if ($finalScope === null) { + return []; + } + + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($finalScope, $functionDescription, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + string $functionDescription, + ParameterReflectionWithPhpDocs $parameter, + ): array + { + $isParamOutType = true; + $outType = $parameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $parameter->getType(); + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + if (!$outType instanceof UnionType) { + return []; + } + + $variableExpr = new Variable($parameter->getName()); + $variableType = $scope->getType($variableExpr); + + $messages = []; + foreach ($outType->getTypes() as $type) { + if (!$type->isSuperTypeOf($variableType)->no()) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf( + '%s never assigns %s to &$%s so it can be removed from the %s.', + $functionDescription, + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + $parameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + ))->identifier(sprintf('%s.unusedType', $isParamOutType ? 'paramOut' : 'parameterByRef')); + if (!$isParamOutType) { + $errorBuilder->tip('You can narrow the parameter out type with @param-out PHPDoc tag.'); + } + + $messages[] = $errorBuilder->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 46f5d174be..8da2427d31 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -62,7 +62,7 @@ private function getUsedVariables(Scope $scope, $node): array if ($node instanceof Node) { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if ($functionName === 'func_get_args') { + if ($functionName === 'func_get_args' || $functionName === 'get_defined_vars') { return $scope->getDefinedVariables(); } } diff --git a/src/Rules/Variables/ParameterOutAssignedTypeRule.php b/src/Rules/Variables/ParameterOutAssignedTypeRule.php new file mode 100644 index 0000000000..9c24597525 --- /dev/null +++ b/src/Rules/Variables/ParameterOutAssignedTypeRule.php @@ -0,0 +1,122 @@ + + */ +class ParameterOutAssignedTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return VariableAssignNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $variable = $node->getVariable(); + if (!is_string($variable->name)) { + return []; + } + + $variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants()); + $parameters = $variant->getParameters(); + $foundParameter = null; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + if ($parameter->getName() !== $variable->name) { + continue; + } + + $foundParameter = $parameter; + break; + } + + if ($foundParameter === null) { + return []; + } + + $isParamOutType = true; + $outType = $foundParameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $foundParameter->getType(); + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->getAssignedExpr(), + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($node->getAssignedExpr()); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s %s of %s expects %s, %s given.', + $foundParameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('%s.type', $isParamOutType ? 'paramOut' : 'parameterByRef')); + + if (!$isParamOutType) { + $errorBuilder->tip('You can change the parameter out type with @param-out PHPDoc tag.'); + } + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php new file mode 100644 index 0000000000..648781ccc5 --- /dev/null +++ b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php @@ -0,0 +1,137 @@ + + */ +class ParameterOutExecutionEndTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return ExecutionEndNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $endNode = $node->getNode(); + if ($endNode instanceof Node\Stmt\Expression) { + $endNodeExpr = $endNode->expr; + $endNodeExprType = $scope->getType($endNodeExpr); + if ($endNodeExprType instanceof NeverType && $endNodeExprType->isExplicit()) { + return []; + } + } + if ($endNode instanceof Node\Stmt\Throw_) { + return []; + } + + $variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants()); + $parameters = $variant->getParameters(); + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($scope, $inFunction, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + FunctionReflection|ExtendedMethodReflection $inFunction, + ParameterReflectionWithPhpDocs $parameter, + ): array + { + $outType = $parameter->getOutType(); + if ($outType === null) { + return []; + } + + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($parameter->getName()))->no()) { + return []; + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $variableExpr = new Node\Expr\Variable($parameter->getName()); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $variableExpr, + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($variableExpr); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s @param-out type of %s expects %s, %s given.', + $parameter->getName(), + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('paramOut.type')); + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Testing/ErrorFormatterTestCase.php b/src/Testing/ErrorFormatterTestCase.php index 04f5d316ad..1009a53a0c 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -16,6 +16,9 @@ use function explode; use function fopen; use function implode; +use function in_array; +use function is_int; +use function range; use function rewind; use function rtrim; use function stream_get_contents; @@ -27,6 +30,8 @@ abstract class ErrorFormatterTestCase extends PHPStanTestCase private const KIND_DECORATED = 'decorated'; private const KIND_PLAIN = 'plain'; + private const KIND_VERBOSE = '+verbose'; + private const KIND_NOT_VERBOSE = '+not-verbose'; /** @var array */ private array $outputStream = []; @@ -34,25 +39,30 @@ abstract class ErrorFormatterTestCase extends PHPStanTestCase /** @var array */ private array $output = []; - private function getOutputStream(bool $decorated = false): StreamOutput + private function getOutputStream(bool $decorated = false, bool $verbose = false): StreamOutput { $kind = $decorated ? self::KIND_DECORATED : self::KIND_PLAIN; + $kind .= $verbose ? self::KIND_VERBOSE : self::KIND_NOT_VERBOSE; + if (!isset($this->outputStream[$kind])) { $resource = fopen('php://memory', 'w', false); if ($resource === false) { throw new ShouldNotHappenException(); } - $this->outputStream[$kind] = new StreamOutput($resource, StreamOutput::VERBOSITY_NORMAL, $decorated); + $verbosity = $verbose ? StreamOutput::VERBOSITY_VERBOSE : StreamOutput::VERBOSITY_NORMAL; + $this->outputStream[$kind] = new StreamOutput($resource, $verbosity, $decorated); } return $this->outputStream[$kind]; } - protected function getOutput(bool $decorated = false): Output + protected function getOutput(bool $decorated = false, bool $verbose = false): Output { $kind = $decorated ? self::KIND_DECORATED : self::KIND_PLAIN; + $kind .= $verbose ? self::KIND_VERBOSE : self::KIND_NOT_VERBOSE; + if (!isset($this->output[$kind])) { - $outputStream = $this->getOutputStream($decorated); + $outputStream = $this->getOutputStream($decorated, $verbose); $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $outputStream); $this->output[$kind] = new SymfonyOutput($outputStream, new SymfonyStyle($errorConsoleStyle)); } @@ -60,11 +70,11 @@ protected function getOutput(bool $decorated = false): Output return $this->output[$kind]; } - protected function getOutputContent(bool $decorated = false): string + protected function getOutputContent(bool $decorated = false, bool $verbose = false): string { - rewind($this->getOutputStream($decorated)->getStream()); + rewind($this->getOutputStream($decorated, $verbose)->getStream()); - $contents = stream_get_contents($this->getOutputStream($decorated)->getStream()); + $contents = stream_get_contents($this->getOutputStream($decorated, $verbose)->getStream()); if ($contents === false) { throw new ShouldNotHappenException(); } @@ -72,23 +82,36 @@ protected function getOutputContent(bool $decorated = false): string return $this->rtrimMultiline($contents); } - protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): AnalysisResult + /** + * @param array{int, int}|int $numFileErrors + */ + protected function getAnalysisResult(array|int $numFileErrors, int $numGenericErrors): AnalysisResult { - if ($numFileErrors > 5 || $numFileErrors < 0 || $numGenericErrors > 2 || $numGenericErrors < 0) { + if (is_int($numFileErrors)) { + $offsetFileErrors = 0; + } else { + [$offsetFileErrors, $numFileErrors] = $numFileErrors; + } + + if (!in_array($numFileErrors, range(0, 6), true) || + !in_array($offsetFileErrors, range(0, 6), true) || + !in_array($numGenericErrors, range(0, 2), true) + ) { throw new ShouldNotHappenException(); } $fileErrors = array_slice([ new Error('Foo', self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 4), - new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), + new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', 5, true, null, null, 'a tip'), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 2), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', null), - ], 0, $numFileErrors); + new Error('Foobar\\Buz', self::DIRECTORY_PATH . '/foo.php', 5, true, null, null, 'a tip', null, null, 'foobar.buz'), + ], $offsetFileErrors, $numFileErrors); $genericErrors = array_slice([ 'first generic error', - 'second generic error', + 'second generic', ], 0, $numGenericErrors); return new AnalysisResult( @@ -102,6 +125,7 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): true, 0, false, + [], ); } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index e4c548d4d4..3b4330a297 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -4,18 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Analyser; +use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\InternalError; +use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\RuleErrorTransformer; -use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Collectors\Collector; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; -use PHPStan\Node\CollectedDataNode; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; @@ -67,12 +70,9 @@ protected function getTypeSpecifier(): TypeSpecifier return self::getContainer()->getService('typeSpecifier'); } - private function getAnalyser(): Analyser + private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser { if ($this->analyser === null) { - $ruleRegistry = new DirectRuleRegistry([ - $this->getRule(), - ]); $collectorRegistry = new CollectorRegistry($this->getCollectors()); $reflectionProvider = $this->createReflectionProvider(); @@ -84,6 +84,7 @@ private function getAnalyser(): Analyser self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), $this->getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), @@ -94,6 +95,7 @@ private function getAnalyser(): Analyser $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), @@ -103,6 +105,8 @@ private function getAnalyser(): Analyser self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], + self::getContainer()->getParameter('featureToggles')['paramOutType'], + self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'], ); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), @@ -110,7 +114,7 @@ private function getAnalyser(): Analyser $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), new RuleErrorTransformer(), - true, + new LocalIgnoresProcessor(), ); $this->analyser = new Analyser( $fileAnalyser, @@ -165,37 +169,36 @@ static function (Error $error) use ($strictlyTypedSprintf): string { */ public function gatherAnalyserErrors(array $files): array { + $ruleRegistry = new DirectRuleRegistry([ + $this->getRule(), + ]); $files = array_map([$this->getFileHelper(), 'normalizePath'], $files); - $analyserResult = $this->getAnalyser()->analyse( + $analyserResult = $this->getAnalyser($ruleRegistry)->analyse( $files, null, null, true, ); if (count($analyserResult->getInternalErrors()) > 0) { - $this->fail(implode("\n", $analyserResult->getInternalErrors())); + $this->fail(implode("\n", array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()))); } - $actualErrors = $analyserResult->getUnorderedErrors(); - $ruleErrorTransformer = new RuleErrorTransformer(); - if (count($analyserResult->getCollectedData()) > 0) { - $ruleRegistry = new DirectRuleRegistry([ - $this->getRule(), - ]); - - $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($analyserResult->getCollectedData(), false); - $scopeFactory = $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()); - $scope = $scopeFactory->create(ScopeContext::create('irrelevant')); - foreach ($ruleRegistry->getRules($nodeType) as $rule) { - $ruleErrors = $rule->processNode($node, $scope); - foreach ($ruleErrors as $ruleError) { - $actualErrors[] = $ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); - } - } + if ($this->shouldFailOnPhpErrors() && count($analyserResult->getAllPhpErrors()) > 0) { + $this->fail(implode("\n", array_map( + static fn (Error $error): string => sprintf('%s on %s:%d', $error->getMessage(), $error->getFile(), $error->getLine()), + $analyserResult->getAllPhpErrors(), + ))); } - return $actualErrors; + $finalizer = new AnalyserResultFinalizer( + $ruleRegistry, + new RuleErrorTransformer(), + $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()), + new LocalIgnoresProcessor(), + true, + ); + + return $finalizer->finalize($analyserResult, false, true)->getAnalyserResult()->getUnorderedErrors(); } protected function shouldPolluteScopeWithLoopInitialAssignments(): bool @@ -208,6 +211,11 @@ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool return true; } + protected function shouldFailOnPhpErrors(): bool + { + return true; + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index edfd4eabb9..c9c32db584 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -9,7 +9,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; +use PHPStan\File\SystemAgnosticSimpleRelativePathHelper; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; @@ -19,15 +22,25 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use Symfony\Component\Finder\Finder; use function array_map; use function array_merge; use function count; +use function fclose; +use function fgets; +use function fopen; use function in_array; +use function is_dir; use function is_string; +use function preg_match; use function sprintf; use function stripos; +use function strpos; use function strtolower; +use function version_compare; +use const PHP_VERSION; /** @api */ abstract class TypeInferenceTestCase extends PHPStanTestCase @@ -51,6 +64,7 @@ public static function processFile( self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), self::getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), @@ -61,15 +75,18 @@ public static function processFile( $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), - true, - true, + self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'), + self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'), static::getEarlyTerminatingMethodCalls(), static::getEarlyTerminatingFunctionCalls(), self::getContainer()->getParameter('universalObjectCratesClasses'), - true, + self::getContainer()->getParameter('exceptions')['implicitThrows'], self::getContainer()->getParameter('treatPhpDocTypesAsCertain'), self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], + self::getContainer()->getParameter('featureToggles')['paramOutType'], + self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'], ); $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], static::getAdditionalAnalysedFiles()))); @@ -94,11 +111,18 @@ public function assertFileAsserts( ): void { if ($assertType === 'type') { - $expectedType = $args[0]; - $this->assertInstanceOf(ConstantScalarType::class, $expectedType); - $expected = $expectedType->getValue(); - $actualType = $args[1]; - $actual = $actualType->describe(VerbosityLevel::precise()); + if ($args[0] instanceof Type) { + // backward compatibility + $expectedType = $args[0]; + $this->assertInstanceOf(ConstantScalarType::class, $expectedType); + $expected = $expectedType->getValue(); + $actualType = $args[1]; + $actual = $actualType->describe(VerbosityLevel::precise()); + } else { + $expected = $args[0]; + $actual = $args[1]; + } + $this->assertSame( $expected, $actual, @@ -121,8 +145,14 @@ public function assertFileAsserts( */ public static function gatherAssertTypes(string $file): array { + $fileHelper = self::getContainer()->getByType(FileHelper::class); + + $relativePathHelper = new SystemAgnosticSimpleRelativePathHelper($fileHelper); + + $file = $fileHelper->normalizePath($file); + $asserts = []; - self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, $file): void { + self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, $file, $relativePathHelper): void { if (!$node instanceof Node\Expr\FuncCall) { return; } @@ -135,18 +165,36 @@ public static function gatherAssertTypes(string $file): array $functionName = $nameNode->toString(); if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) { self::fail(sprintf( - 'Missing use statement for %s() on line %d.', + 'Missing use statement for %s() in %s on line %d.', $functionName, + $relativePathHelper->getRelativePath($file), $node->getStartLine(), )); } elseif ($functionName === 'PHPStan\\Testing\\assertType') { $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getLine(), + )); + } $actualType = $scope->getType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getStartLine()]; + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf( + 'Expected type must be a literal string, %s given in %s on line %d.', + $expectedType->describe(VerbosityLevel::precise()), + $relativePathHelper->getRelativePath($file), + $node->getLine(), + )); + } + $actualType = $scope->getNativeType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getStartLine()]; + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') { $certainty = $node->getArgs()[0]->value; if (!$certainty instanceof StaticCall) { @@ -197,17 +245,19 @@ public static function gatherAssertTypes(string $file): array } self::fail(sprintf( - 'Function %s imported with wrong namespace %s called on line %d.', + 'Function %s imported with wrong namespace %s called in %s on line %d.', $correctFunction, $functionName, + $relativePathHelper->getRelativePath($file), $node->getStartLine(), )); } if (count($node->getArgs()) !== 2) { self::fail(sprintf( - 'ERROR: Wrong %s() call on line %d.', + 'ERROR: Wrong %s() call in %s on line %d.', $functionName, + $relativePathHelper->getRelativePath($file), $node->getStartLine(), )); } @@ -222,6 +272,64 @@ public static function gatherAssertTypes(string $file): array return $asserts; } + /** + * @api + * @return array + */ + public static function gatherAssertTypesFromDirectory(string $directory): array + { + if (!is_dir($directory)) { + self::fail(sprintf('Directory %s does not exist.', $directory)); + } + + $finder = new Finder(); + $finder->followLinks(); + $asserts = []; + 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; + } + } + + return $asserts; + } + + /** + * From https://github.com/php-parallel-lint/PHP-Parallel-Lint/blob/0c2706086ac36dce31967cb36062ff8915fe03f7/bin/skip-linting.php + * + * Copyright (c) 2012, Jakub Onderka + */ + private static function isFileLintSkipped(string $file): bool + { + $f = @fopen($file, 'r'); + if ($f !== false) { + $firstLine = fgets($f); + if ($firstLine === false) { + return false; + } + + // ignore shebang line + if (strpos($firstLine, '#!') === 0) { + $firstLine = fgets($f); + if ($firstLine === false) { + return false; + } + } + + @fclose($f); + + if (preg_match('~ $reasons */ public function __construct( @@ -43,9 +44,12 @@ public static function createYes(): self return new self(TrinaryLogic::createYes(), []); } - public static function createNo(): self + /** + * @param list $reasons + */ + public static function createNo(array $reasons = []): self { - return new self(TrinaryLogic::createNo(), []); + return new self(TrinaryLogic::createNo(), $reasons); } public static function createMaybe(): self diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index bad40674cb..874b177716 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -142,6 +142,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->getIterableKeyType()->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); @@ -161,6 +166,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return $this; + } + + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { if ($this->hasOffsetValueType($offsetType)->no()) { @@ -394,6 +408,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return TypeCombinator::union( diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 13e98c7bd3..da1a00a16a 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -132,6 +132,11 @@ 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()); @@ -147,6 +152,15 @@ public function getOffsetValueType(Type $offsetType): Type } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + if ($valueType->isLiteralString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { return $this; } @@ -161,6 +175,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 504e1b91b8..8e98af9d78 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -134,6 +134,11 @@ 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()); @@ -157,6 +162,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -167,6 +177,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 9da8d9fc86..460137066c 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -15,7 +15,6 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; @@ -135,6 +134,11 @@ 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()); @@ -150,6 +154,15 @@ public function getOffsetValueType(Type $offsetType): Type } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + if ($valueType->isNonFalsyString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { return $this; } @@ -164,12 +177,14 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { - return new UnionType([ - IntegerRangeType::fromInterval(null, -1), - IntegerRangeType::fromInterval(1, null), - ]); + return new IntegerType(); } public function toFloat(): Type diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 29fb36ce45..292d00f7a0 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -137,6 +137,11 @@ 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()); @@ -156,6 +161,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -169,6 +179,11 @@ public function toNumber(): Type ]); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 070f853329..98503a13a8 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -120,6 +120,11 @@ public function describe(VerbosityLevel $level): string return sprintf('hasMethod(%s)', $this->methodName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasMethod(string $methodName): TrinaryLogic { if ($this->getCanonicalMethodName() === strtolower($methodName)) { diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 829b46c775..cdb258d613 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -138,6 +138,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { @@ -157,6 +162,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { @@ -318,6 +328,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 2dd2984b1e..a87f7879ab 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -146,6 +146,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { @@ -181,6 +186,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new self($offsetType, $valueType); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self($this->offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { @@ -364,6 +374,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 53170faac3..f508447135 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -118,6 +118,11 @@ public function describe(VerbosityLevel $level): string return sprintf('hasProperty(%s)', $this->propertyName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index a379f3d842..8ef7ecbb88 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -139,6 +139,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -154,6 +159,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -379,6 +389,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ConstantIntegerType(1); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index c3058eed18..c6c9c5b6d7 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -135,6 +135,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -150,6 +155,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -375,6 +385,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ConstantIntegerType(1); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index cec0f88390..6040f0ee06 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -7,7 +7,6 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -132,7 +131,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { return $this->getItemType()->isSuperTypeOf($type->getItemType()) - ->and($this->keyType->isSuperTypeOf($type->keyType)); + ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType())); } if ($type instanceof CompoundType) { @@ -394,10 +393,18 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $offsetType = $offsetType->toArrayKey(); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return TrinaryLogic::createNo(); } @@ -407,7 +414,9 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { $offsetType = $offsetType->toArrayKey(); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return new ErrorType(); } @@ -477,6 +486,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni ); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self( + $this->keyType, + TypeCombinator::union($this->itemType, $valueType), + ); + } + public function unsetOffset(Type $offsetType): Type { $offsetType = $offsetType->toArrayKey(); @@ -524,9 +541,7 @@ public function intersectKeyArray(Type $otherArraysType): Type } if ($isKeySuperType->yes()) { - return $otherArraysType->isIterableAtLeastOnce()->yes() - ? TypeCombinator::intersect($this, new NonEmptyArrayType()) - : $this; + return $this; } return new self($otherArraysType->getIterableKeyType(), $this->getIterableValueType()); @@ -557,9 +572,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe()->and($this->itemType->isString()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { @@ -574,6 +586,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -624,7 +641,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } if ($receivedType->isArray()->yes()) { - $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType()); return $keyTypeMap->union($itemTypeMap); @@ -638,7 +655,7 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); return array_merge( - $this->getKeyType()->getReferencedTemplateTypes($variance), + $this->getIterableKeyType()->getReferencedTemplateTypes($variance), $this->getItemType()->getReferencedTemplateTypes($variance), ); } diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index 27987a03b3..dc1245ad45 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -43,11 +43,18 @@ protected function unionTypes(callable $getType): Type return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$resultTypes)); } - protected function pickFromTypes(callable $getValues): array + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array { $values = []; foreach ($this->getTypes() as $type) { $innerValues = $getValues($type); + if ($innerValues === [] && $criteria($type)) { + return []; + } + foreach ($innerValues as $innerType) { $values[] = $innerType; } diff --git a/src/Type/BitwiseFlagHelper.php b/src/Type/BitwiseFlagHelper.php index 9b7175cd3f..7d5e4c3db9 100644 --- a/src/Type/BitwiseFlagHelper.php +++ b/src/Type/BitwiseFlagHelper.php @@ -18,6 +18,9 @@ public function __construct(private ReflectionProvider $reflectionProvider) { } + /** + * @param non-empty-string $constName + */ public function bitwiseOrContainsConstant(Expr $expr, Scope $scope, string $constName): TrinaryLogic { if ($expr instanceof ConstFetch) { diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index c0ac5b5920..72be662c95 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -56,6 +56,11 @@ public function toNumber(): Type return $this->toInteger(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return TypeCombinator::union( @@ -96,6 +101,11 @@ public function toArrayKey(): Type return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 698e5b7250..e51cf45631 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -4,11 +4,16 @@ use PHPStan\Analyser\OutOfClassScope; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; @@ -35,7 +40,7 @@ use function count; /** @api */ -class CallableType implements CompoundType, ParametersAcceptor +class CallableType implements CompoundType, CallableParametersAcceptor { use MaybeArrayTypeTrait; @@ -54,19 +59,46 @@ class CallableType implements CompoundType, ParametersAcceptor private bool $isCommonCallable; + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + + private TrinaryLogic $isPure; + /** * @api * @param array|null $parameters + * @param array $templateTags */ public function __construct( ?array $parameters = null, ?Type $returnType = null, private bool $variadic = true, + ?TemplateTypeMap $templateTypeMap = null, + ?TemplateTypeMap $resolvedTemplateTypeMap = null, + private array $templateTags = [], + ?TrinaryLogic $isPure = null, ) { $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); $this->isCommonCallable = $parameters === null && $returnType === null; + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->isPure = $isPure ?? TrinaryLogic::createMaybe(); + } + + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; } /** @@ -123,7 +155,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult { $isCallable = new AcceptsResult($type->isCallable(), []); - if ($isCallable->no() || $this->isCommonCallable) { + if ($isCallable->no()) { return $isCallable; } @@ -132,6 +164,19 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Accep $scope = new OutOfClassScope(); } + if ($this->isCommonCallable) { + if ($this->isPure()->yes()) { + $typePure = TrinaryLogic::createYes(); + foreach ($type->getCallableParametersAcceptors($scope) as $variant) { + $typePure = $typePure->and($variant->isPure()); + } + + return $isCallable->and(new AcceptsResult($typePure, [])); + } + + return $isCallable; + } + $variantsResult = null; foreach ($type->getCallableParametersAcceptors($scope) as $variant) { $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny); @@ -171,7 +216,11 @@ public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): public function equals(Type $type): bool { - return $type instanceof self; + if (!$type instanceof self) { + return false; + } + + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); } public function describe(VerbosityLevel $level): string @@ -191,6 +240,10 @@ function (): string { ), $this->parameters), $this->returnType, $this->variadic, + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, ); return $printer->print($selfWithoutParameterNames->toPhpDocNode()); @@ -203,19 +256,54 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; } + public function getThrowPoints(): array + { + return [ + SimpleThrowPoint::createImplicit(), + ]; + } + + public function getImpurePoints(): array + { + $pure = $this->isPure(); + if ($pure->yes()) { + return []; + } + + return [ + new SimpleImpurePoint( + 'functionCall', + 'call to a callable', + $pure->no(), + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + public function toNumber(): Type { return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -241,14 +329,19 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->templateTypeMap; } public function getResolvedTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->resolvedTemplateTypeMap; } public function getCallSiteVarianceMap(): TemplateTypeVarianceMap @@ -356,6 +449,10 @@ public function traverse(callable $cb): Type $parameters, $cb($this->getReturnType()), $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, ); } @@ -402,6 +499,10 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $parameters, $cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()), $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, ); } @@ -538,7 +639,7 @@ public function getFiniteTypes(): array public function toPhpDocNode(): TypeNode { if ($this->isCommonCallable) { - return new IdentifierTypeNode('callable'); + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable'); } $parameters = []; @@ -552,10 +653,20 @@ public function toPhpDocNode(): TypeNode ); } + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + return new CallableTypeNode( - new IdentifierTypeNode('callable'), + new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'), $parameters, $this->returnType->toPhpDocNode(), + $templateTags, ); } @@ -568,6 +679,10 @@ public static function __set_state(array $properties): Type (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 e90675d011..eb58de74bb 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\TrinaryLogic; use function array_key_exists; use function array_merge; @@ -13,8 +13,8 @@ class CallableTypeHelper { public static function isParametersAcceptorSuperTypeOf( - ParametersAcceptor $ours, - ParametersAcceptor $theirs, + CallableParametersAcceptor $ours, + CallableParametersAcceptor $theirs, bool $treatMixedAsAny, ): AcceptsResult { @@ -25,10 +25,12 @@ public static function isParametersAcceptorSuperTypeOf( foreach ($theirParameters as $theirParameter) { $lastParameter = $theirParameter; } + $theirParameterCount = count($theirParameters); + $ourParameterCount = count($ourParameters); if ( $lastParameter !== null && $lastParameter->isVariadic() - && count($theirParameters) < count($ourParameters) + && $theirParameterCount < $ourParameterCount ) { foreach ($ourParameters as $i => $ourParameter) { if (array_key_exists($i, $theirParameters)) { @@ -38,7 +40,7 @@ public static function isParametersAcceptorSuperTypeOf( } } - $result = null; + $result = AcceptsResult::createYes(); foreach ($theirParameters as $i => $theirParameter) { $parameterDescription = $theirParameter->getName() === '' ? sprintf('#%d', $i + 1) : sprintf('#%d $%s', $i + 1, $theirParameter->getName()); if (!isset($ourParameters[$i])) { @@ -52,11 +54,7 @@ public static function isParametersAcceptorSuperTypeOf( $parameterDescription, ), ]); - if ($result === null) { - $result = $accepts; - } else { - $result = $result->and($accepts); - } + $result = $result->and($accepts); continue; } @@ -70,11 +68,7 @@ public static function isParametersAcceptorSuperTypeOf( $parameterDescription, ), ]); - if ($result === null) { - $result = $accepts; - } else { - $result = $result->and($accepts); - } + $result = $result->and($accepts); } if ($treatMixedAsAny) { @@ -95,11 +89,11 @@ public static function isParametersAcceptorSuperTypeOf( ])); } - if ($result === null) { - $result = $isSuperType; - } else { - $result = $result->and($isSuperType); - } + $result = $result->and($isSuperType); + } + + if (!$treatMixedAsAny && $theirParameterCount < $ourParameterCount) { + $result = $result->and(AcceptsResult::createMaybe()); } $theirReturnType = $theirs->getReturnType(); @@ -109,13 +103,14 @@ public static function isParametersAcceptorSuperTypeOf( $isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []); } - if ($result === null) { - $result = $isReturnTypeSuperType; - } else { - $result = $result->and($isReturnTypeSuperType); + $pure = $ours->isPure(); + if ($pure->yes()) { + $result = $result->and(new AcceptsResult($theirs->isPure(), [])); + } elseif ($pure->no()) { + $result = $result->and(new AcceptsResult($theirs->isPure()->negate(), [])); } - return $result; + return $result->and($isReturnTypeSuperType); } } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index ff872ed7ae..93b9989102 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -4,12 +4,18 @@ use Closure; use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Node\InvalidateExprNode; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; @@ -43,7 +49,7 @@ use function count; /** @api */ -class ClosureType implements TypeWithClassName, ParametersAcceptor +class ClosureType implements TypeWithClassName, CallableParametersAcceptor { use NonArrayTypeTrait; @@ -54,6 +60,13 @@ class ClosureType implements TypeWithClassName, ParametersAcceptor use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; + /** @var array */ + private array $parameters; + + private Type $returnType; + + private bool $isCommonCallable; + private ObjectType $objectType; private TemplateTypeMap $templateTypeMap; @@ -62,23 +75,72 @@ class ClosureType implements TypeWithClassName, ParametersAcceptor private TemplateTypeVarianceMap $callSiteVarianceMap; + /** @var SimpleImpurePoint[] */ + private array $impurePoints; + /** * @api - * @param array $parameters + * @param array|null $parameters + * @param array $templateTags + * @param SimpleThrowPoint[] $throwPoints + * @param ?SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables */ public function __construct( - private array $parameters, - private Type $returnType, - private bool $variadic, + ?array $parameters = null, + ?Type $returnType = null, + private bool $variadic = true, ?TemplateTypeMap $templateTypeMap = null, ?TemplateTypeMap $resolvedTemplateTypeMap = null, ?TemplateTypeVarianceMap $callSiteVarianceMap = null, + private array $templateTags = [], + private array $throwPoints = [], + ?array $impurePoints = null, + private array $invalidateExpressions = [], + private array $usedVariables = [], ) { + $this->parameters = $parameters ?? []; + $this->returnType = $returnType ?? new MixedType(); + $this->isCommonCallable = $parameters === null && $returnType === null; $this->objectType = new ObjectType(Closure::class); $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); + $this->impurePoints = $impurePoints ?? [new SimpleImpurePoint('functionCall', 'call to an unknown Closure', false)]; + } + + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + + public static function createPure(): self + { + return new self(null, null, true, null, null, null, [], [], []); + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); } public function getClassName(): string @@ -169,7 +231,7 @@ public function equals(Type $type): bool return false; } - return $this->returnType->equals($type->returnType); + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); } public function describe(VerbosityLevel $level): string @@ -177,6 +239,10 @@ public function describe(VerbosityLevel $level): string return $level->handle( static fn (): string => 'Closure', function (): string { + if ($this->isCommonCallable) { + return $this->isPure()->yes() ? 'pure-Closure' : 'Closure'; + } + $printer = new Printer(); $selfWithoutParameterNames = new self( array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( @@ -192,6 +258,11 @@ function (): string { $this->templateTypeMap, $this->resolvedTemplateTypeMap, $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, ); return $printer->print($selfWithoutParameterNames->toPhpDocNode()); @@ -199,6 +270,11 @@ function (): string { ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isObject(): TrinaryLogic { return $this->objectType->isObject(); @@ -301,14 +377,36 @@ public function getEnumCases(): array return []; } - /** - * @return ParametersAcceptor[] - */ + public function isCommonCallable(): bool + { + return $this->isCommonCallable; + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; } + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -324,6 +422,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); @@ -433,6 +536,10 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para public function traverse(callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + return new self( array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); @@ -450,11 +557,20 @@ public function traverse(callable $cb): Type $this->templateTypeMap, $this->resolvedTemplateTypeMap, $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, ); } public function traverseSimultaneously(Type $right, callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + if (!$right instanceof self) { return $this; } @@ -487,6 +603,14 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $parameters, $cb($this->getReturnType(), $right->getReturnType()), $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, ); } @@ -607,6 +731,10 @@ public function getFiniteTypes(): array public function toPhpDocNode(): TypeNode { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-Closure' : 'Closure'); + } + $parameters = []; foreach ($this->parameters as $parameter) { $parameters[] = new CallableTypeParameterNode( @@ -618,10 +746,20 @@ public function toPhpDocNode(): TypeNode ); } + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + return new CallableTypeNode( new IdentifierTypeNode('Closure'), $parameters, $this->returnType->toPhpDocNode(), + $templateTags, ); } @@ -637,6 +775,11 @@ public static function __set_state(array $properties): Type $properties['templateTypeMap'], $properties['resolvedTemplateTypeMap'], $properties['callSiteVarianceMap'], + $properties['templateTags'], + $properties['throwPoints'], + $properties['impurePoints'], + $properties['invalidateExpressions'], + $properties['usedVariables'], ); } diff --git a/src/Type/ConditionalType.php b/src/Type/ConditionalType.php index 8543e38859..aa5d8af0a6 100644 --- a/src/Type/ConditionalType.php +++ b/src/Type/ConditionalType.php @@ -18,6 +18,14 @@ final class ConditionalType implements CompoundType, LateResolvableType use LateResolvableTypeTrait; use NonGeneralizableTypeTrait; + private ?Type $normalizedIf = null; + + private ?Type $normalizedElse = null; + + private ?Type $subjectWithTargetIntersectedType = null; + + private ?Type $subjectWithTargetRemovedType = null; + public function __construct( private Type $subject, private Type $target, @@ -113,37 +121,33 @@ protected function getResult(): Type { $isSuperType = $this->target->isSuperTypeOf($this->subject); - $intersectedType = TypeCombinator::intersect($this->subject, $this->target); - $removedType = TypeCombinator::remove($this->subject, $this->target); - - $yesType = fn () => TypeTraverser::map( - !$this->negated ? $this->if : $this->else, - fn (Type $type, callable $traverse) => $type === $this->subject ? (!$this->negated ? $intersectedType : $removedType) : $traverse($type), - ); - $noType = fn () => TypeTraverser::map( - !$this->negated ? $this->else : $this->if, - fn (Type $type, callable $traverse) => $type === $this->subject ? (!$this->negated ? $removedType : $intersectedType) : $traverse($type), - ); - if ($isSuperType->yes()) { - return $yesType(); + return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse(); } if ($isSuperType->no()) { - return $noType(); + return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf(); } - return TypeCombinator::union($yesType(), $noType()); + return TypeCombinator::union( + $this->getNormalizedIf(), + $this->getNormalizedElse(), + ); } public function traverse(callable $cb): Type { $subject = $cb($this->subject); $target = $cb($this->target); - $if = $cb($this->if); - $else = $cb($this->else); - - if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) { + $if = $cb($this->getNormalizedIf()); + $else = $cb($this->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { return $this; } @@ -158,10 +162,15 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $subject = $cb($this->subject, $right->subject); $target = $cb($this->target, $right->target); - $if = $cb($this->if, $right->if); - $else = $cb($this->else, $right->else); - - if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) { + $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf()); + $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { return $this; } @@ -193,4 +202,34 @@ public static function __set_state(array $properties): Type ); } + private function getNormalizedIf(): Type + { + return $this->normalizedIf ??= TypeTraverser::map( + $this->if, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType()) + : $traverse($type), + ); + } + + private function getNormalizedElse(): Type + { + return $this->normalizedElse ??= TypeTraverser::map( + $this->else, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType()) + : $traverse($type), + ); + } + + private function getSubjectWithTargetIntersectedType(): Type + { + return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target); + } + + private function getSubjectWithTargetRemovedType(): Type + { + return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target); + } + } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index dc0fead5e5..3853794348 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -14,10 +14,10 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; @@ -33,14 +33,12 @@ use PHPStan\Type\ConstantType; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; @@ -199,7 +197,7 @@ public function getAllArrays(): array $keys = array_merge($requiredKeys, $combination); sort($keys); - if ($this->isList->yes() && array_keys($keys) !== array_values($keys)) { + if ($this->isList->yes() && array_keys($keys) !== $keys) { continue; } @@ -297,7 +295,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof MixedType && !$type instanceof TemplateMixedType) { + if ($type instanceof CompoundType && !$type instanceof IntersectionType) { return $type->isAcceptedWithReasonBy($this, $strictTypes); } @@ -365,14 +363,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { if (count($this->keyTypes) === 0) { - if (count($type->keyTypes) > 0) { - if (count($type->optionalKeys) > 0) { - return TrinaryLogic::createMaybe(); - } - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createYes(); + return $type->isIterableAtLeastOnce()->negate(); } $results = []; @@ -405,10 +396,12 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $result; } - return $result->and( - $this->getKeyType()->isSuperTypeOf($type->getKeyType()), - $this->getItemType()->isSuperTypeOf($type->getItemType()), - ); + $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType()); + if ($isKeySuperType->no()) { + return TrinaryLogic::createNo(); + } + + return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType())); } if ($type instanceof CompoundType) { @@ -470,9 +463,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes()->and(...$results); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { $typeAndMethodNames = $this->findTypeAndMethodNames(); @@ -495,29 +485,52 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) continue; } - array_push($acceptors, ...$method->getVariants()); + array_push($acceptors, ...FunctionCallableVariant::createFromVariants($method, $method->getVariants())); } return $acceptors; } - /** @deprecated Use findTypeAndMethodNames() instead */ - public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod + /** + * @return array{Type, Type}|array{} + */ + private function getClassOrObjectAndMethods(): array { if (count($this->keyTypes) !== 2) { - return null; + return []; } - if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return null; + $classOrObject = null; + $method = null; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->isSuperTypeOf(new ConstantIntegerType(0))->yes()) { + $classOrObject = $this->valueTypes[$i]; + continue; + } + + if (!$keyType->isSuperTypeOf(new ConstantIntegerType(1))->yes()) { + continue; + } + + $method = $this->valueTypes[$i]; } - if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { - return null; + if ($classOrObject === null || $method === null) { + return []; } - [$classOrObject, $method] = $this->valueTypes; + 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(); } @@ -542,19 +555,12 @@ public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod /** @return ConstantArrayTypeAndMethod[] */ public function findTypeAndMethodNames(): array { - if (count($this->keyTypes) !== 2) { + $callableArray = $this->getClassOrObjectAndMethods(); + if ($callableArray === []) { return []; } - if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return []; - } - - if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { - return []; - } - - [$classOrObject, $methods] = $this->valueTypes; + [$classOrObject, $methods] = $callableArray; if (count($methods->getConstantStrings()) === 0) { return [ConstantArrayTypeAndMethod::createUnknown()]; } @@ -596,13 +602,32 @@ public function findTypeAndMethodNames(): array public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $offsetType = $offsetType->toArrayKey(); + if ($offsetType instanceof UnionType) { + $results = []; + foreach ($offsetType->getTypes() as $innerType) { + $results[] = $this->hasOffsetValueType($innerType); + } + + return TrinaryLogic::extremeIdentity(...$results); + } + if ($offsetType instanceof IntegerRangeType) { + $finiteTypes = $offsetType->getFiniteTypes(); + if ($finiteTypes !== []) { + $results = []; + foreach ($finiteTypes as $innerType) { + $results[] = $this->hasOffsetValueType($innerType); + } + + return TrinaryLogic::extremeIdentity(...$results); + } + } $result = TrinaryLogic::createNo(); foreach ($this->keyTypes as $i => $keyType) { if ( $keyType instanceof ConstantIntegerType - && $offsetType instanceof StringType - && !$offsetType instanceof ConstantStringType + && !$offsetType->isString()->no() + && $offsetType->isConstantScalarValue()->no() ) { return TrinaryLogic::createMaybe(); } @@ -666,6 +691,21 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $builder->getArray(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + $offsetType = $offsetType->toArrayKey(); + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + foreach ($this->keyTypes as $keyType) { + if ($offsetType->isSuperTypeOf($keyType)->no()) { + continue; + } + + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); + } + public function unsetOffset(Type $offsetType): Type { $offsetType = $offsetType->toArrayKey(); @@ -843,7 +883,7 @@ public function shuffleArray(): Type return $valuesArray; } - $generalizedArray = new ArrayType($valuesArray->getKeyType(), $valuesArray->getItemType()); + $generalizedArray = new ArrayType($valuesArray->getIterableKeyType(), $valuesArray->getItemType()); if ($isIterableAtLeastOnce->yes()) { $generalizedArray = TypeCombinator::intersect($generalizedArray, new NonEmptyArrayType()); @@ -1197,7 +1237,7 @@ public function generalize(GeneralizePrecision $precision): Type } $arrayType = new ArrayType( - $this->getKeyType()->generalize($precision), + $this->getIterableKeyType()->generalize($precision), $this->getItemType()->generalize($precision), ); @@ -1249,7 +1289,7 @@ public function generalizeToArray(): Type return $this; } - $arrayType = new ArrayType($this->getKeyType(), $this->getItemType()); + $arrayType = new ArrayType($this->getIterableKeyType(), $this->getItemType()); if ($isIterableAtLeastOnce->yes()) { $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); @@ -1525,7 +1565,7 @@ public function isKeysSupersetOf(self $otherArray): bool public function mergeWith(self $otherArray): self { - // only call this after verifying isKeysSupersetOf + // only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue $valueTypes = $this->valueTypes; $optionalKeys = $this->optionalKeys; foreach ($this->keyTypes as $i => $keyType) { diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 88b7822f6a..db5328d0d2 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -82,6 +82,11 @@ public function toNumber(): Type return new ConstantIntegerType((int) $this->value); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType((string) $this->value); diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 229bf6696f..3aeb1e2e18 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -5,8 +5,6 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; 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\FloatType; use PHPStan\Type\GeneralizePrecision; @@ -18,8 +16,8 @@ use function ini_get; use function ini_set; use function is_finite; +use function is_nan; use function str_contains; -use const PHP_FLOAT_EPSILON; /** @api */ class ConstantFloatType extends FloatType implements ConstantScalarType @@ -42,7 +40,7 @@ public function getValue(): float public function equals(Type $type): bool { - return $type instanceof self && abs($this->value - $type->value) < PHP_FLOAT_EPSILON; + return $type instanceof self && ($this->value === $type->value || is_nan($this->value) && is_nan($type->value)); } private function castFloatToString(float $value): string @@ -69,31 +67,6 @@ public function describe(VerbosityLevel $level): string ); } - public function isSuperTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof self) { - if (!$this->equals($type)) { - if (abs($this->value - $type->value) < PHP_FLOAT_EPSILON) { - return TrinaryLogic::createMaybe(); - } - - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } - - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); - } - - return TrinaryLogic::createNo(); - } - public function toString(): Type { return new ConstantStringType((string) $this->value); @@ -104,6 +77,11 @@ public function toInteger(): Type return new ConstantIntegerType((int) $this->value); } + public function toAbsoluteNumber(): Type + { + return new self(abs($this->value)); + } + public function toArrayKey(): Type { return new ConstantIntegerType((int) $this->value); diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index ae3d0511a8..9226acacc2 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -15,6 +15,7 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function abs; use function sprintf; /** @api */ @@ -76,6 +77,11 @@ public function toFloat(): Type return new ConstantFloatType($this->value); } + public function toAbsoluteNumber(): Type + { + return new self(abs($this->value)); + } + public function toString(): Type { return new ConstantStringType((string) $this->value); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 1b957df361..fbdbaf2f97 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -10,10 +10,10 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\InaccessibleMethod; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; @@ -22,6 +22,7 @@ use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -236,17 +237,19 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + if ($this->value === '') { + return []; + } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' $functionName = new Name($this->value); if ($reflectionProvider->hasFunction($functionName, null)) { - return $reflectionProvider->getFunction($functionName, null)->getVariants(); + $function = $reflectionProvider->getFunction($functionName, null); + return FunctionCallableVariant::createFromVariants($function, $function->getVariants()); } // 'MyClass::myStaticFunction' @@ -263,7 +266,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new InaccessibleMethod($method)]; } - return $method->getVariants(); + return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } if (!$classReflection->getNativeReflection()->isFinal()) { @@ -289,6 +292,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new ConstantIntegerType((int) $this->value); @@ -386,6 +394,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return parent::setOffsetValueType($offsetType, $valueType); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return parent::setOffsetValueType($offsetType, $valueType); + } + public function append(self $otherString): self { return new self($this->getValue() . $otherString->getValue()); @@ -402,19 +415,22 @@ public function generalize(GeneralizePrecision $precision): Type } if ($this->getValue() !== '' && $precision->isMoreSpecific()) { + $accessories = [ + new StringType(), + new AccessoryLiteralStringType(), + ]; + + if (is_numeric($this->getValue())) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($this->getValue() !== '0') { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - new AccessoryLiteralStringType(), - ]); + $accessories[] = new AccessoryNonFalsyStringType(); + } else { + $accessories[] = new AccessoryNonEmptyStringType(); } - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - new AccessoryLiteralStringType(), - ]); + return new IntersectionType($accessories); } if ($precision->isMoreSpecific()) { diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index 4589f8e4c2..42d8f4afc2 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -17,6 +17,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\ObjectType; +use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -53,8 +54,8 @@ public function equals(Type $type): bool return false; } - return $this->getClassName() === $type->getClassName() - && $this->enumCaseName === $type->enumCaseName; + return $this->enumCaseName === $type->enumCaseName && + $this->getClassName() === $type->getClassName(); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic @@ -71,8 +72,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { return TrinaryLogic::createFromBoolean( - $this->getClassName() === $type->getClassName() - && $this->enumCaseName === $type->enumCaseName, + $this->enumCaseName === $type->enumCaseName && $this->getClassName() === $type->getClassName(), ); } @@ -80,6 +80,16 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } + if ( + $type instanceof SubtractableType + && $type->getSubtractedType() !== null + ) { + $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); + if ($isSuperType->yes()) { + return TrinaryLogic::createNo(); + } + } + $parent = new parent($this->getClassName(), $this->getSubtractedType(), $this->getClassReflection()); return $parent->isSuperTypeOf($type)->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/ExponentiateHelper.php b/src/Type/ExponentiateHelper.php index 4e62ed9a5d..10da4dd484 100644 --- a/src/Type/ExponentiateHelper.php +++ b/src/Type/ExponentiateHelper.php @@ -6,6 +6,9 @@ use PHPStan\Type\Constant\ConstantIntegerType; use function is_float; use function is_int; +use function is_numeric; +use function is_string; +use function pow; final class ExponentiateHelper { @@ -83,10 +86,16 @@ private static function exponentiateConstantScalar(ConstantScalarType $base, Typ $min = null; $max = null; if ($exponent->getMin() !== null) { - $min = $base->getValue() ** $exponent->getMin(); + $min = self::pow($base->getValue(), $exponent->getMin()); + if ($min === null) { + return new ErrorType(); + } } if ($exponent->getMax() !== null) { - $max = $base->getValue() ** $exponent->getMax(); + $max = self::pow($base->getValue(), $exponent->getMax()); + if ($max === null) { + return new ErrorType(); + } } if (!is_float($min) && !is_float($max)) { @@ -95,7 +104,11 @@ private static function exponentiateConstantScalar(ConstantScalarType $base, Typ } if ($exponent instanceof ConstantScalarType) { - $result = $base->getValue() ** $exponent->getValue(); + $result = self::pow($base->getValue(), $exponent->getValue()); + if ($result === null) { + return new ErrorType(); + } + if (is_int($result)) { return new ConstantIntegerType($result); } @@ -105,4 +118,15 @@ private static function exponentiateConstantScalar(ConstantScalarType $base, Typ return null; } + private static function pow(mixed $base, mixed $exp): float|int|null + { + if (is_string($base) && !is_numeric($base)) { + return null; + } + if (is_string($exp) && !is_numeric($exp)) { + return null; + } + return pow($base, $exp); + } + } diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 7886d40e7e..3e0c73a93e 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -260,7 +260,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); - } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) { $className = $node->name->name; } else { if ($traitFound) { @@ -451,7 +451,7 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun } $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); - } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + } elseif ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) { $className = $node->name->name; } else { if ($traitFound) { @@ -482,6 +482,10 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun $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_) { if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { $phpDocNode = $phpDocNodeMap[$nameScopeKey]; diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index c82a50e4e7..2111d4d912 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -110,6 +110,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toFloat(): Type { return $this; @@ -144,6 +149,11 @@ public function toArrayKey(): Type return new IntegerType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/FunctionParameterClosureTypeExtension.php b/src/Type/FunctionParameterClosureTypeExtension.php new file mode 100644 index 0000000000..5a68d5517c --- /dev/null +++ b/src/Type/FunctionParameterClosureTypeExtension.php @@ -0,0 +1,32 @@ + */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php index 7962d50d0e..66ce5db4ec 100644 --- a/src/Type/Generic/TemplateBooleanType.php +++ b/src/Type/Generic/TemplateBooleanType.php @@ -13,6 +13,9 @@ final class TemplateBooleanType extends BooleanType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index 8bb8aa696d..b291dab577 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -13,6 +13,9 @@ final class TemplateConstantArrayType extends ConstantArrayType implements Templ use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php index 4aa00d7805..e411af4edc 100644 --- a/src/Type/Generic/TemplateConstantIntegerType.php +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -13,6 +13,9 @@ final class TemplateConstantIntegerType extends ConstantIntegerType implements T use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php index 85c871e277..bcb3b0cf94 100644 --- a/src/Type/Generic/TemplateConstantStringType.php +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -13,6 +13,9 @@ final class TemplateConstantStringType extends ConstantStringType implements Tem use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index f7e2210e86..32332bb3ef 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -13,6 +13,9 @@ final class TemplateFloatType extends FloatType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index 485781aaca..3810841ec9 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -13,6 +13,9 @@ final class TemplateGenericObjectType extends GenericObjectType implements Templ /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 62222f6b8e..64c631980d 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -13,6 +13,9 @@ final class TemplateIntegerType extends IntegerType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateIntersectionType.php b/src/Type/Generic/TemplateIntersectionType.php index 805739874a..87f1ca18a7 100644 --- a/src/Type/Generic/TemplateIntersectionType.php +++ b/src/Type/Generic/TemplateIntersectionType.php @@ -11,6 +11,9 @@ final class TemplateIntersectionType extends IntersectionType implements Templat /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateKeyOfType.php b/src/Type/Generic/TemplateKeyOfType.php index 003de6f431..7312ea2ef4 100644 --- a/src/Type/Generic/TemplateKeyOfType.php +++ b/src/Type/Generic/TemplateKeyOfType.php @@ -14,6 +14,9 @@ final class TemplateKeyOfType extends KeyOfType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 11af221db4..3363818673 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -15,6 +15,9 @@ final class TemplateMixedType extends MixedType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php index 7fe78cbdbd..5b1f187c6d 100644 --- a/src/Type/Generic/TemplateObjectShapeType.php +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -13,6 +13,9 @@ final class TemplateObjectShapeType extends ObjectShapeType implements TemplateT use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index ef2c76ef7f..a67aa723dd 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -13,6 +13,9 @@ final class TemplateObjectType extends ObjectType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 1f9b0fe0ce..3d3cb9e8ca 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -13,6 +13,9 @@ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements T /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index 8949142920..6ae56cc228 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -15,6 +15,9 @@ final class TemplateStrictMixedType extends StrictMixedType implements TemplateT /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 17cec7dc55..084612c641 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -13,6 +13,9 @@ final class TemplateStringType extends StringType implements TemplateType use TemplateTypeTrait; use UndecidedComparisonCompoundTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index 01287c374a..7661078ca1 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -11,6 +11,7 @@ interface TemplateType extends CompoundType { + /** @return non-empty-string */ public function getName(): string; public function getScope(): TemplateTypeScope; diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 6d71e1dea5..c29a175d2c 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -25,6 +25,9 @@ final class TemplateTypeFactory { + /** + * @param non-empty-string $name + */ public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null): TemplateType { $strategy ??= new TemplateTypeParameterStrategy(); diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 1653b03415..166464a884 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\NonAcceptingNeverType; @@ -85,8 +86,35 @@ public static function resolveToBounds(Type $type): Type */ public static function toArgument(Type $type): Type { + $ownedTemplates = []; + /** @var T */ - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$ownedTemplates): Type { + if ($type instanceof ParametersAcceptor) { + $templateTypeMap = $type->getTemplateTypeMap(); + + foreach ($type->getParameters() as $parameter) { + $parameterType = $parameter->getType(); + if (!($parameterType instanceof TemplateType) || !$templateTypeMap->hasType($parameterType->getName())) { + continue; + } + + $ownedTemplates[] = $parameterType; + } + + $returnType = $type->getReturnType(); + + if ($returnType instanceof TemplateType && $templateTypeMap->hasType($returnType->getName())) { + $ownedTemplates[] = $returnType; + } + } + + foreach ($ownedTemplates as $ownedTemplate) { + if ($ownedTemplate === $type) { + return $traverse($type); + } + } + if ($type instanceof TemplateType) { return $traverse($type->toArgument()); } diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index f9a7625720..e40eab90d3 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -7,6 +7,11 @@ class TemplateTypeScope { + public static function createWithAnonymousFunction(): self + { + return new self(null, null); + } + public static function createWithFunction(string $functionName): self { return new self(null, $functionName); @@ -48,6 +53,10 @@ public function equals(self $other): bool /** @api */ public function describe(): string { + if ($this->className === null && $this->functionName === null) { + return 'anonymous function'; + } + if ($this->className === null) { return sprintf('function %s()', $this->functionName); } diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 803ec31348..66fd32a2fd 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -25,6 +25,7 @@ trait TemplateTypeTrait { + /** @var non-empty-string */ private string $name; private TemplateTypeScope $scope; @@ -36,6 +37,7 @@ trait TemplateTypeTrait /** @var TBound */ private Type $bound; + /** @return non-empty-string */ public function getName(): string { return $this->name; diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index 8e3bf90f5d..997fb21238 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -11,6 +11,9 @@ final class TemplateUnionType extends UnionType implements TemplateType /** @use TemplateTypeTrait */ use TemplateTypeTrait; + /** + * @param non-empty-string $name + */ public function __construct( TemplateTypeScope $scope, TemplateTypeStrategy $templateTypeStrategy, diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 1779cb41ad..b7962f71ae 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -9,6 +9,8 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use function array_filter; @@ -445,6 +447,38 @@ public function toBoolean(): BooleanType return new ConstantBooleanType(false); } + public function toAbsoluteNumber(): Type + { + if ($this->min !== null && $this->min >= 0) { + return $this; + } + + if ($this->max === null || $this->max >= 0) { + $inversedMin = $this->min !== null ? $this->min * -1 : null; + + return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); + } + + return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null); + } + + public function toString(): Type + { + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); + if ($isZero->no()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + /** * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType * @@ -616,6 +650,9 @@ public function exponentiate(Type $exponent): Type return parent::exponentiate($exponent); } + /** + * @return list + */ public function getFiniteTypes(): array { if ($this->min === null || $this->max === null) { diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 5e81e00519..f91a646c9d 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -62,6 +62,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } + public function toFloat(): Type { return new FloatType(); @@ -96,6 +101,11 @@ public function toArrayKey(): Type return $this; } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 3fe932552a..9b4fbf22ba 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -11,7 +11,6 @@ use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; @@ -27,6 +26,7 @@ use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -43,6 +43,7 @@ use function ksort; use function md5; use function sprintf; +use function str_starts_with; use function strlen; use function substr; @@ -389,7 +390,7 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) } if ( - substr($typeDescription, 0, strlen('array<')) === 'array<' + str_starts_with($typeDescription, 'array<') && in_array('array', $skipTypeNames, true) ) { $nonEmpty = false; @@ -671,8 +672,20 @@ public function isOffsetAccessible(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal()); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { + if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) { + $arrayKeyOffsetType = $offsetType->toArrayKey(); + if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + return TrinaryLogic::createYes(); + } + } + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } @@ -691,6 +704,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + } + public function unsetOffset(Type $offsetType): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); @@ -760,9 +778,6 @@ public function isCallable(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { @@ -899,6 +914,13 @@ public function toNumber(): Type return $type; } + public function toAbsoluteNumber(): Type + { + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); + + return $type; + } + public function toString(): Type { $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString()); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index c675aff51d..f36cab2e7f 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -217,6 +217,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -242,6 +247,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/MethodParameterClosureTypeExtension.php b/src/Type/MethodParameterClosureTypeExtension.php new file mode 100644 index 0000000000..6272a26ef8 --- /dev/null +++ b/src/Type/MethodParameterClosureTypeExtension.php @@ -0,0 +1,32 @@ +isExplicitMixed); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self($this->isExplicitMixed); + } + public function unsetOffset(Type $offsetType): Type { if ($this->subtractedType !== null) { @@ -264,9 +268,6 @@ public function getEnumCases(): array return []; } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; @@ -467,6 +468,11 @@ public function toNumber(): Type ]); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new IntegerType(); @@ -565,6 +571,16 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createMaybe(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isOffsetAccessible()->no()) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 18b5c2ba21..b619768b6d 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -8,9 +8,7 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; -use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -258,6 +256,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createYes(); @@ -273,6 +276,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { return new NeverType(); @@ -325,15 +333,12 @@ public function shuffleArray(): Type public function isCallable(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createNo(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - return [new TrivialParametersAcceptor()]; + throw new ShouldNotHappenException(); } public function isCloneable(): TrinaryLogic @@ -346,6 +351,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toString(): Type { return $this; diff --git a/src/Type/NewObjectType.php b/src/Type/NewObjectType.php new file mode 100644 index 0000000000..57b932a398 --- /dev/null +++ b/src/Type/NewObjectType.php @@ -0,0 +1,104 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('new<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getObjectTypeOrClassStringObjectType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('new'), [$this->type->toPhpDocNode()]); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + return new self( + $properties['type'], + ); + } + +} diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 5cf35ca527..bbf9dceec8 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -127,6 +127,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -152,6 +157,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isScalar(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index a796c70e12..e72cf25be2 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -144,6 +144,11 @@ public function toNumber(): Type return new ConstantIntegerType(0); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType(''); @@ -174,6 +179,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); @@ -190,6 +200,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $array->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 17bffaa7bb..a00324259a 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -428,6 +428,11 @@ public function describe(VerbosityLevel $level): string ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getEnumCases(): array { return []; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 1fd2236087..45a04563c9 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -18,11 +18,12 @@ 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\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; @@ -93,6 +94,9 @@ class ObjectType implements TypeWithClassName, SubtractableType private ?string $cachedDescription = null; + /** @var array> */ + private static array $enumCases = []; + /** @api */ public function __construct( private string $className, @@ -113,6 +117,7 @@ public static function resetCaches(): void self::$methods = []; self::$properties = []; self::$ancestors = []; + self::$enumCases = []; } private static function createFromReflection(ClassReflection $reflection): self @@ -258,6 +263,9 @@ public function getReferencedClasses(): array public function getObjectClassNames(): array { + if ($this->className === '') { + return []; + } return [$this->className]; } @@ -374,12 +382,12 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + $thisClassReflection = $this->getClassReflection(); - if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClassNames[0])) { + if ($thisClassReflection === null || !$reflectionProvider->hasClass($thatClassNames[0])) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); } - $thisClassReflection = $this->getClassReflection(); $thatClassReflection = $reflectionProvider->getClass($thatClassNames[0]); if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { @@ -555,6 +563,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { @@ -1098,6 +1111,11 @@ public function isOffsetAccessible(): TrinaryLogic ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->isOffsetAccessible(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { @@ -1180,6 +1198,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ($this->isOffsetAccessible()->no()) { + return new ErrorType(); + } + + return $this; + } + public function unsetOffset(Type $offsetType): Type { if ($this->isOffsetAccessible()->no()) { @@ -1200,30 +1227,46 @@ public function getEnumCases(): array return []; } - $subtracted = []; + $cacheKey = $this->describeCache(); + if (array_key_exists($cacheKey, self::$enumCases)) { + return self::$enumCases[$cacheKey]; + } + + $className = $classReflection->getName(); + if ($this->subtractedType !== null) { - foreach ($this->subtractedType->getEnumCases() as $enumCase) { - $subtracted[$enumCase->getEnumCaseName()] = true; + $subtractedEnumCaseNames = []; + + foreach ($this->subtractedType->getEnumCases() as $subtractedCase) { + $subtractedEnumCaseNames[$subtractedCase->getEnumCaseName()] = true; } - } - $cases = []; - foreach ($classReflection->getEnumCases() as $enumCase) { - if (array_key_exists($enumCase->getName(), $subtracted)) { - continue; + $cases = []; + foreach ($classReflection->getEnumCases() as $enumCase) { + if (array_key_exists($enumCase->getName(), $subtractedEnumCaseNames)) { + continue; + } + $cases[] = new EnumCaseObjectType($className, $enumCase->getName(), $classReflection); + } + } else { + $cases = []; + foreach ($classReflection->getEnumCases() as $enumCase) { + $cases[] = new EnumCaseObjectType($className, $enumCase->getName(), $classReflection); } - $cases[] = new EnumCaseObjectType($classReflection->getName(), $enumCase->getName(), $classReflection); } - return $cases; + return self::$enumCases[$cacheKey] = $cases; } public function isCallable(): TrinaryLogic { - $parametersAcceptors = $this->findCallableParametersAcceptors(); + $parametersAcceptors = RecursionGuard::run($this, fn () => $this->findCallableParametersAcceptors()); if ($parametersAcceptors === null) { return TrinaryLogic::createNo(); } + if ($parametersAcceptors instanceof ErrorType) { + return TrinaryLogic::createNo(); + } if ( count($parametersAcceptors) === 1 @@ -1235,13 +1278,10 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->className === Closure::class) { - return [new TrivialParametersAcceptor()]; + return [new TrivialParametersAcceptor('Closure')]; } $parametersAcceptors = $this->findCallableParametersAcceptors(); if ($parametersAcceptors === null) { @@ -1252,7 +1292,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) } /** - * @return ParametersAcceptor[]|null + * @return CallableParametersAcceptor[]|null */ private function findCallableParametersAcceptors(): ?array { @@ -1262,7 +1302,11 @@ private function findCallableParametersAcceptors(): ?array } if ($classReflection->hasNativeMethod('__invoke')) { - return $this->getMethod('__invoke', new OutOfClassScope())->getVariants(); + $method = $this->getMethod('__invoke', new OutOfClassScope()); + return FunctionCallableVariant::createFromVariants( + $method, + $method->getVariants(), + ); } if (!$classReflection->getNativeReflection()->isFinal()) { @@ -1332,27 +1376,20 @@ public function changeSubtractedType(?Type $subtractedType): Type { if ($subtractedType !== null) { $classReflection = $this->getClassReflection(); - $allowedSubTypesList = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null; - if ($allowedSubTypesList !== null) { - $allowedSubTypes = []; - foreach ($allowedSubTypesList as $allowedSubType) { - $allowedSubTypes[$allowedSubType->describe(VerbosityLevel::precise())] = $allowedSubType; - } + $allowedSubTypes = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null; + if ($allowedSubTypes !== null) { + $preciseVerbosity = VerbosityLevel::precise(); $originalAllowedSubTypes = $allowedSubTypes; $subtractedSubTypes = []; - $subtractedTypesList = TypeUtils::flattenTypes($subtractedType); - $subtractedTypes = []; - foreach ($subtractedTypesList as $type) { - $subtractedTypes[$type->describe(VerbosityLevel::precise())] = $type; - } - + $subtractedTypes = TypeUtils::flattenTypes($subtractedType); foreach ($subtractedTypes as $subType) { - foreach ($allowedSubTypes as $description => $allowedSubType) { + foreach ($allowedSubTypes as $key => $allowedSubType) { if ($subType->equals($allowedSubType)) { + $description = $allowedSubType->describe($preciseVerbosity); $subtractedSubTypes[$description] = $subType; - unset($allowedSubTypes[$description]); + unset($allowedSubTypes[$key]); continue 2; } } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index 51a3d07937..2d0cc64c1e 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -140,6 +140,11 @@ function () use ($level): string { ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getEnumCases(): array { return []; diff --git a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..d47d7f39eb --- /dev/null +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'abs'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; + } + + $inputType = $scope->getType($args[0]->value); + + $outputType = $inputType->toAbsoluteNumber(); + + if ($outputType instanceof ErrorType) { + return null; + } + + return $outputType; + } + +} diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index 73d0f31e36..d697380439 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -23,6 +23,7 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -85,8 +86,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } 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(self::createFunctionName($callbackArg->value), [new Arg($itemVar)]); + $expr = new FuncCall($funcName, [new Arg($itemVar)]); return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr); } } @@ -100,8 +106,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } 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(self::createFunctionName($callbackArg->value), [new Arg($keyVar)]); + $expr = new FuncCall($funcName, [new Arg($keyVar)]); return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr); } } @@ -115,9 +126,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } 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(); + } + $itemVar = new Variable('item'); $keyVar = new Variable('key'); - $expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]); + $expr = new FuncCall($funcName, [new Arg($itemVar), new Arg($keyVar)]); return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); } } @@ -242,10 +258,20 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type ]; } - private static function createFunctionName(string $funcName): Name + private static function createFunctionName(string $funcName): ?Name { + if ($funcName === '') { + return null; + } + if ($funcName[0] === '\\') { - return new Name\FullyQualified(substr($funcName, 1)); + $funcName = substr($funcName, 1); + + if ($funcName === '') { + return null; + } + + return new Name\FullyQualified($funcName); } return new Name($funcName); diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 0e17d33c35..af0f49d231 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -39,10 +39,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $callableIsNull = $callableType->isNull()->yes(); if ($callableType->isCallable()->yes()) { - $valueType = new NeverType(); + $valueTypes = [new NeverType()]; foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); + $valueTypes[] = $parametersAcceptor->getReturnType(); } + $valueType = TypeCombinator::union(...$valueTypes); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { @@ -65,23 +66,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantArrays = $arrayType->getConstantArrays(); if (count($constantArrays) > 0) { $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $returnedArrayBuilder->setOffsetValueType( - $keyType, - $valueType, - $constantArray->isOptionalKey($i), - ); + $totalCount = TypeCombinator::countConstantArrayValueTypes($constantArrays) * TypeCombinator::countConstantArrayValueTypes([$valueType]); + if ($totalCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + foreach ($constantArrays as $constantArray) { + $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $returnedArrayBuilder->setOffsetValueType( + $keyType, + $valueType, + $constantArray->isOptionalKey($i), + ); + } + $returnedArray = $returnedArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); + } + $arrayTypes[] = $returnedArray; } - $returnedArray = $returnedArrayBuilder->getArray(); - if ($constantArray->isList()->yes()) { - $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); - } - $arrayTypes[] = $returnedArray; - } - $mappedArrayType = TypeCombinator::union(...$arrayTypes); + $mappedArrayType = TypeCombinator::union(...$arrayTypes); + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } } elseif ($arrayType->isArray()->yes()) { $mappedArrayType = TypeCombinator::intersect(new ArrayType( $arrayType->getIterableKeyType(), diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 282c65d3b2..811727ae39 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -5,9 +5,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; @@ -22,24 +24,25 @@ 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; } - $valueType = $scope->getType($functionCall->getArgs()[0]->value); + $valueType = $scope->getType($args[0]->value); if (!$valueType->isArray()->yes()) { return null; } - $offsetType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; - $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0; + $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) { - $limitType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null; + $preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : null; - $preserveKeysType = isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null; + $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0; + $limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null; $preserveKeys = $preserveKeysType !== null && $preserveKeysType->isTrue()->yes(); $results = []; @@ -51,7 +54,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($valueType->isIterableAtLeastOnce()->yes()) { - return TypeCombinator::union($valueType, new ConstantArrayType([], [])); + $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; } return $valueType; diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php index 3c32b6c360..1e1075c812 100644 --- a/src/Type/Php/ConstantFunctionReturnTypeExtension.php +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; @@ -36,7 +37,12 @@ public function getTypeFromFunctionCall( $results = []; foreach ($nameType->getConstantStrings() as $constantName) { - $results[] = $scope->getType($this->constantHelper->createExprFromConstantName($constantName->getValue())); + $expr = $this->constantHelper->createExprFromConstantName($constantName->getValue()); + if ($expr === null) { + return new ErrorType(); + } + + $results[] = $scope->getType($expr); } if (count($results) > 0) { diff --git a/src/Type/Php/ConstantHelper.php b/src/Type/Php/ConstantHelper.php index 790f169ed9..7f056f90ec 100644 --- a/src/Type/Php/ConstantHelper.php +++ b/src/Type/Php/ConstantHelper.php @@ -15,11 +15,20 @@ class ConstantHelper { - public function createExprFromConstantName(string $constantName): Expr + public function createExprFromConstantName(string $constantName): ?Expr { + if ($constantName === '') { + return null; + } + $classConstParts = explode('::', $constantName); if (count($classConstParts) >= 2) { - $classConstName = new FullyQualified(ltrim($classConstParts[0], '\\')); + $fqcn = ltrim($classConstParts[0], '\\'); + if ($fqcn === '') { + return null; + } + + $classConstName = new FullyQualified($fqcn); if ($classConstName->isSpecialClassName()) { $classConstName = new Name($classConstName->toString()); } diff --git a/src/Type/Php/CurlInitReturnTypeExtension.php b/src/Type/Php/CurlInitReturnTypeExtension.php index e9564278fe..813b220159 100644 --- a/src/Type/Php/CurlInitReturnTypeExtension.php +++ b/src/Type/Php/CurlInitReturnTypeExtension.php @@ -4,17 +4,36 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; +use function array_map; use function count; +use function is_string; +use function parse_url; +use function str_contains; +use function strcasecmp; +use function strlen; class CurlInitReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + /** @see https://github.com/curl/curl/blob/curl-8_9_1/lib/urldata.h#L135 */ + private const CURL_MAX_INPUT_LENGTH = 8000000; + + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'curl_init'; @@ -26,13 +45,58 @@ public function getTypeFromFunctionCall( Scope $scope, ): Type { - $argsCount = count($functionCall->getArgs()); + $args = $functionCall->getArgs(); + $argsCount = count($args); $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $notFalseReturnType = TypeCombinator::remove($returnType, new ConstantBooleanType(false)); if ($argsCount === 0) { - return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); + 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(http://github.com/$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 b926ed181d..bae299abaa 100644 --- a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -14,6 +13,10 @@ class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date_format'; @@ -23,16 +26,15 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) < 2) { return new StringType(); } - return $scope->getType( - new FuncCall(new FullyQualified('date'), [ - $functionCall->getArgs()[1], - ]), + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($functionCall->getArgs()[1]->value), + true, ); } diff --git a/src/Type/Php/DateFormatMethodReturnTypeExtension.php b/src/Type/Php/DateFormatMethodReturnTypeExtension.php index f6b71786bc..f028fbea7b 100644 --- a/src/Type/Php/DateFormatMethodReturnTypeExtension.php +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -3,9 +3,7 @@ namespace PHPStan\Type\Php; use DateTimeInterface; -use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -16,6 +14,10 @@ class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function getClass(): string { return DateTimeInterface::class; @@ -26,16 +28,15 @@ 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(); } - return $scope->getType( - new FuncCall(new FullyQualified('date'), [ - $methodCall->getArgs()[0], - ]), + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($methodCall->getArgs()[0]->value), + true, ); } diff --git a/src/Type/Php/DateFunctionReturnTypeExtension.php b/src/Type/Php/DateFunctionReturnTypeExtension.php index 2147fd1b80..41432d1c3a 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -5,23 +5,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; -use PHPStan\Type\Accessory\AccessoryNumericStringType; -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 PHPStan\Type\UnionType; use function count; -use function date; -use function sprintf; class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date'; @@ -31,101 +25,16 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return new StringType(); - } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantStrings = $argType->getConstantStrings(); - - if (count($constantStrings) === 0) { - return new StringType(); - } - - if (count($constantStrings) === 1) { - $constantString = $constantStrings[0]->getValue(); - - // see see https://www.php.net/manual/en/datetime.format.php - switch ($constantString) { - case 'd': - return $this->buildNumericRangeType(1, 31, true); - case 'j': - return $this->buildNumericRangeType(1, 31, false); - case 'N': - return $this->buildNumericRangeType(1, 7, false); - case 'w': - return $this->buildNumericRangeType(0, 6, false); - case 'm': - return $this->buildNumericRangeType(1, 12, true); - case 'n': - return $this->buildNumericRangeType(1, 12, false); - case 't': - return $this->buildNumericRangeType(28, 31, false); - case 'L': - return $this->buildNumericRangeType(0, 1, false); - case 'g': - return $this->buildNumericRangeType(1, 12, false); - case 'G': - return $this->buildNumericRangeType(0, 23, false); - case 'h': - return $this->buildNumericRangeType(1, 12, true); - case 'H': - return $this->buildNumericRangeType(0, 23, true); - case 'I': - return $this->buildNumericRangeType(0, 1, false); - } - } - - $types = []; - foreach ($constantStrings as $constantString) { - $types[] = new ConstantStringType(date($constantString->getValue())); - } - - $type = TypeCombinator::union(...$types); - if ($type->isNumericString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } - - if ($type->isNonFalsyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); - } - - if ($type->isNonEmptyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); - } - - if ($type->isNonEmptyString()->no()) { - return new ConstantStringType(''); - } - - return new StringType(); - } - - private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type - { - $types = []; - - for ($i = $min; $i <= $max; $i++) { - $string = (string) $i; - - if ($zeroPad) { - $string = sprintf('%02s', $string); - } - - $types[] = new ConstantStringType($string); + return null; } - return new UnionType($types); + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($functionCall->getArgs()[0]->value), + false, + ); } } diff --git a/src/Type/Php/DateFunctionReturnTypeHelper.php b/src/Type/Php/DateFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..d6cadad384 --- /dev/null +++ b/src/Type/Php/DateFunctionReturnTypeHelper.php @@ -0,0 +1,114 @@ +getConstantStrings() as $formatString) { + $types[] = $this->buildReturnTypeFromFormat($formatString->getValue(), $useMicrosec); + } + + if (count($types) === 0) { + $types[] = $formatType->isNonEmptyString()->yes() + ? new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]) + : new StringType(); + } + + $type = TypeCombinator::union(...$types); + + if ($type->isNumericString()->no() && $formatType->isNonEmptyString()->yes()) { + $type = TypeCombinator::union($type, new IntersectionType([ + new StringType(), new AccessoryNonEmptyStringType(), + ])); + } + + return $type; + } + + public function buildReturnTypeFromFormat(string $formatString, bool $useMicrosec): Type + { + // see see https://www.php.net/manual/en/datetime.format.php + switch ($formatString) { + case 'd': + return $this->buildNumericRangeType(1, 31, true); + case 'j': + return $this->buildNumericRangeType(1, 31, false); + case 'N': + return $this->buildNumericRangeType(1, 7, false); + case 'w': + return $this->buildNumericRangeType(0, 6, false); + case 'm': + return $this->buildNumericRangeType(1, 12, true); + case 'n': + return $this->buildNumericRangeType(1, 12, false); + case 't': + return $this->buildNumericRangeType(28, 31, false); + case 'L': + return $this->buildNumericRangeType(0, 1, false); + case 'g': + return $this->buildNumericRangeType(1, 12, false); + case 'G': + return $this->buildNumericRangeType(0, 23, false); + case 'h': + return $this->buildNumericRangeType(1, 12, true); + case 'H': + return $this->buildNumericRangeType(0, 23, true); + case 'I': + return $this->buildNumericRangeType(0, 1, false); + case 'u': + return $useMicrosec + ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]) + : new ConstantStringType('000000'); + } + + $date = date($formatString); + + // If parameter string is not included, returned as ConstantStringType + if ($date === $formatString) { + return new ConstantStringType($date); + } + + if (is_numeric($date)) { + return new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + } + + return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } + + private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type + { + $types = []; + + for ($i = $min; $i <= $max; $i++) { + $string = (string) $i; + + if ($zeroPad) { + $string = str_pad($string, 2, '0', STR_PAD_LEFT); + } + + $types[] = new ConstantStringType($string); + } + + return new UnionType($types); + } + +} diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index 746fa0f2f5..cd7f63662c 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -5,9 +5,11 @@ use DateInterval; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; @@ -15,6 +17,10 @@ class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateInterval::class; @@ -33,17 +39,26 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateInterval($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedIntervalStringException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index d69fad1ef3..7b691b6257 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -6,9 +6,11 @@ use DateTimeImmutable; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; @@ -17,6 +19,10 @@ class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); @@ -35,17 +41,26 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateTime($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php new file mode 100644 index 0000000000..d22637d4e5 --- /dev/null +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -0,0 +1,71 @@ +getName() === 'modify' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + + if (!$this->phpVersion->hasDateTimeExceptions()) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + try { + $dateTime = new DateTime(); + $dateTime->modify($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $this->exceptionType(); + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $this->exceptionType(); + } + + return null; + } + + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); + } + +} diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index 17359ba45c..39a327157a 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -6,6 +6,7 @@ use DateTimeInterface; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -18,13 +19,12 @@ class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var class-string */ - private $dateTimeClass; - /** @param class-string $dateTimeClass */ - public function __construct(string $dateTimeClass = DateTime::class) + public function __construct( + private PhpVersion $phpVersion, + private string $dateTimeClass = DateTime::class, + ) { - $this->dateTimeClass = $dateTimeClass; } public function getClass(): string @@ -53,7 +53,6 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method try { $result = @(new DateTime())->modify($constantString->getValue()); } catch (Throwable) { - $hasFalse = true; $valueType = TypeCombinator::remove($valueType, $constantString); continue; } @@ -71,13 +70,20 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - if ($hasFalse && !$hasDateTime) { - return new ConstantBooleanType(false); - } - if ($hasDateTime && !$hasFalse) { + if ($hasFalse) { + if (!$hasDateTime) { + return new ConstantBooleanType(false); + } + + return null; + } elseif ($hasDateTime) { return $scope->getType($methodCall->var); } + if ($this->phpVersion->hasDateTimeExceptions()) { + return new NeverType(); + } + return null; } diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php index 3715cc4329..b0eee10181 100644 --- a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -5,9 +5,11 @@ use DateTimeZone; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; @@ -15,6 +17,10 @@ class DateTimeZoneConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateTimeZone::class; @@ -33,17 +39,26 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateTimeZone($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $methodReflection->getThrowType(); + return $this->exceptionType(); } return null; } + private function exceptionType(): Type + { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType('DateInvalidTimeZoneException'); + } + + return new ObjectType('Exception'); + } + } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 43ab4c48db..5f37995240 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -54,8 +54,13 @@ public function specifyTypes( return new SpecifiedTypes([], []); } + $expr = $this->constantHelper->createExprFromConstantName($constantName->getValue()); + if ($expr === null) { + return new SpecifiedTypes([], []); + } + return $this->typeSpecifier->create( - $this->constantHelper->createExprFromConstantName($constantName->getValue()), + $expr, new MixedType(), $context, false, diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 498904c528..b69b412e58 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -41,32 +40,34 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (count($functionCall->getArgs()) < 2) { + $args = $functionCall->getArgs(); + if (count($args) < 2) { return null; } - $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); - $isSuperset = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); - if ($isSuperset->yes()) { - if ($this->phpVersion->getVersionId() >= 80000) { + $delimiterType = $scope->getType($args[0]->value); + $isEmptyString = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); + if ($isEmptyString->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); - } elseif ($isSuperset->no()) { - $arrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); - if ( - !isset($functionCall->getArgs()[2]) - || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($functionCall->getArgs()[2]->value))->yes() - ) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); - } + } + + $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + if ( + !isset($args[2]) + || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes() + ) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } - return $arrayType; + if (!$this->phpVersion->throwsValueErrorForInternalFunctions() && $isEmptyString->maybe()) { + $returnType = TypeCombinator::union($returnType, new ConstantBooleanType(false)); } - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($delimiterType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($returnType); + $returnType = TypeUtils::toBenevolentUnion($returnType); } return $returnType; diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index a2ed6c792b..7ae801bfd9 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -235,6 +235,9 @@ private function getFilterTypeOptions(): array return $this->filterTypeOptions; } + /** + * @param non-empty-string $constantName + */ private function getConstant(string $constantName): int { $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); diff --git a/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..936f5b5adf --- /dev/null +++ b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php @@ -0,0 +1,91 @@ +getName() === 'get_debug_type'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + if ($argType instanceof UnionType) { + return new UnionType(array_map(Closure::fromCallable([self::class, 'resolveOneType']), $argType->getTypes())); + } + return self::resolveOneType($argType); + } + + /** + * @see https://www.php.net/manual/en/function.get-debug-type.php#refsect1-function.get-debug-type-returnvalues + */ + private static function resolveOneType(Type $type): Type + { + if ($type->isNull()->yes()) { + return new ConstantStringType('null'); + } + if ($type->isBoolean()->yes()) { + return new ConstantStringType('bool'); + } + if ($type->isInteger()->yes()) { + return new ConstantStringType('int'); + } + if ($type->isFloat()->yes()) { + return new ConstantStringType('float'); + } + if ($type->isString()->yes()) { + return new ConstantStringType('string'); + } + if ($type->isArray()->yes()) { + return new ConstantStringType('array'); + } + + // "resources" type+state is skipped since we cannot infer the state + + if ($type->isObject()->yes()) { + $reflections = $type->getObjectClassReflections(); + $types = []; + foreach ($reflections as $reflection) { + // if the class is not final, the actual returned string might be of a child class + if ($reflection->isFinal() && !$reflection->isAnonymous()) { + $types[] = new ConstantStringType($reflection->getName()); + } + + if ($reflection->isAnonymous()) { // phpcs:ignore + $types[] = new ConstantStringType('class@anonymous'); + } + } + + switch (count($types)) { + case 0: + return new StringType(); + case 1: + return $types[0]; + default: + return new UnionType($types); + } + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index b23663a994..3bf0ecfbb5 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\BinaryOp\Equal; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; @@ -52,7 +53,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $needleExpr = $node->getArgs()[0]->value; $arrayExpr = $node->getArgs()[1]->value; - if ($arrayExpr instanceof Array_ && $isStrictComparison) { + + $needleType = $scope->getType($needleExpr); + $arrayType = $scope->getType($arrayExpr); + $arrayValueType = $arrayType->getIterableValueType(); + + $isStrictComparison = $isStrictComparison + || $needleType->isEnum()->yes() + || $arrayValueType->isEnum()->yes(); + + if ($arrayExpr instanceof Array_) { $types = null; foreach ($arrayExpr->items as $item) { if ($item === null) { @@ -62,7 +72,12 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $types = null; break; } - $itemTypes = $this->typeSpecifier->resolveIdentical(new Identical($needleExpr, $item->value), $scope, $context, null); + + if ($isStrictComparison) { + $itemTypes = $this->typeSpecifier->resolveIdentical(new Identical($needleExpr, $item->value), $scope, $context, null); + } else { + $itemTypes = $this->typeSpecifier->resolveEqual(new Equal($needleExpr, $item->value), $scope, $context, null); + } if ($types === null) { $types = $itemTypes; @@ -77,20 +92,25 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } } - $needleType = $scope->getType($needleExpr); - $arrayType = $scope->getType($arrayExpr); - $arrayValueType = $arrayType->getIterableValueType(); - - $isStrictComparison = $isStrictComparison - || $needleType->isEnum()->yes() - || $arrayValueType->isEnum()->yes(); - if (!$isStrictComparison) { + if ( + $context->true() + && $arrayType->isArray()->yes() + && $arrayType->getIterableValueType()->isSuperTypeOf($needleType)->yes() + ) { + return $this->typeSpecifier->create( + $node->getArgs()[1]->value, + TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), + $context, + false, + $scope, + ); + } + return new SpecifiedTypes(); } $specifiedTypes = new SpecifiedTypes(); - if ( $context->true() || ( diff --git a/src/Type/Php/IntdivThrowTypeExtension.php b/src/Type/Php/IntdivThrowTypeExtension.php index aab1448733..ef8ac1ef54 100644 --- a/src/Type/Php/IntdivThrowTypeExtension.php +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -7,11 +7,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionThrowTypeExtension; -use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; use const PHP_INT_MIN; @@ -29,39 +28,19 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $functionReflection->getThrowType(); } - $containsMin = false; - $valueType = $scope->getType($funcCall->getArgs()[0]->value); - foreach ($valueType->getConstantScalarTypes() as $constantScalarType) { - if ($constantScalarType->getValue() === PHP_INT_MIN) { - $containsMin = true; - } - - $valueType = TypeCombinator::remove($valueType, $constantScalarType); - } - - if (!$valueType instanceof NeverType) { - $containsMin = true; - } + $valueType = $scope->getType($funcCall->getArgs()[0]->value)->toInteger(); + $containsMin = $valueType->isSuperTypeOf(new ConstantIntegerType(PHP_INT_MIN)); - $divisionByZero = false; - $divisorType = $scope->getType($funcCall->getArgs()[1]->value); - foreach ($divisorType->getConstantScalarTypes() as $constantScalarType) { - if ($containsMin && $constantScalarType->getValue() === -1) { + $divisorType = $scope->getType($funcCall->getArgs()[1]->value)->toInteger(); + if (!$containsMin->no()) { + $divisionByMinusOne = $divisorType->isSuperTypeOf(new ConstantIntegerType(-1)); + if (!$divisionByMinusOne->no()) { return new ObjectType(ArithmeticError::class); } - - if ($constantScalarType->getValue() === 0) { - $divisionByZero = true; - } - - $divisorType = TypeCombinator::remove($divisorType, $constantScalarType); - } - - if (!$divisorType instanceof NeverType) { - return new ObjectType($containsMin ? ArithmeticError::class : DivisionByZeroError::class); } - if ($divisionByZero) { + $divisionByZero = $divisorType->isSuperTypeOf(new ConstantIntegerType(0)); + if (!$divisionByZero->no()) { return new ObjectType(DivisionByZeroError::class); } diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index cf653e182e..a06e2b3370 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -14,10 +14,9 @@ use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use stdClass; use function is_bool; use function json_decode; @@ -45,7 +44,7 @@ public function isFunctionSupported( return true; } - return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && $functionReflection->getName() === 'json_encode'; + return $functionReflection->getName() === 'json_encode' && $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getTypeFromFunctionCall( @@ -76,18 +75,18 @@ public function getTypeFromFunctionCall( private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $fallbackType): Type { $args = $funcCall->getArgs(); - $isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall, $scope); + $isForceArray = $this->isForceArray($funcCall, $scope); if (!isset($args[0])) { return $fallbackType; } $firstValueType = $scope->getType($args[0]->value); if ($firstValueType instanceof ConstantStringType) { - return $this->resolveConstantStringType($firstValueType, $isArrayWithoutStdClass); + return $this->resolveConstantStringType($firstValueType, $isForceArray); } - if ($isArrayWithoutStdClass) { - return TypeCombinator::remove($fallbackType, new ObjectType(stdClass::class)); + if ($isForceArray) { + return TypeCombinator::remove($fallbackType, new ObjectWithoutClassType()); } return $fallbackType; @@ -96,7 +95,7 @@ private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type /** * Is "json_decode(..., true)"? */ - private function isForceArrayWithoutStdClass(FuncCall $funcCall, Scope $scope): bool + private function isForceArray(FuncCall $funcCall, Scope $scope): bool { $args = $funcCall->getArgs(); if (!isset($args[1])) { diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php index e58f80d814..29eaf4f290 100644 --- a/src/Type/Php/JsonThrowTypeExtension.php +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -32,14 +32,14 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( + return in_array( $functionReflection->getName(), [ 'json_encode', 'json_decode', ], true, - ); + ) && $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getThrowTypeFromFunctionCall( diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index a3e2ec18b8..2a3d43c9c0 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -86,7 +86,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($componentType->getValue() === -1) { - return $this->createAllComponentsReturnType(); + return TypeCombinator::union($this->createComponentsArray(), new ConstantBooleanType(false)); } return $this->componentTypesPairedConstants[$componentType->getValue()] ?? new ConstantBooleanType(false); @@ -97,24 +97,31 @@ private function createAllComponentsReturnType(): Type if ($this->allComponentsTogetherType === null) { $returnTypes = [ new ConstantBooleanType(false), + new NullType(), + IntegerRangeType::fromInterval(0, 65535), + new StringType(), + $this->createComponentsArray(), ]; - $builder = ConstantArrayTypeBuilder::createEmpty(); + $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); + } - if ($this->componentTypesPairedStrings === null) { - throw new ShouldNotHappenException(); - } + return $this->allComponentsTogetherType; + } - foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { - $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); - } + private function createComponentsArray(): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); - $returnTypes[] = $builder->getArray(); + if ($this->componentTypesPairedStrings === null) { + throw new ShouldNotHappenException(); + } - $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); + foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); } - return $this->allComponentsTogetherType; + return $builder->getArray(); } private function cacheReturnTypes(): void diff --git a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php index 2139200e0b..11a5f74752 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -54,17 +54,32 @@ public function getTypeFromFunctionCall( } $flagsType = $scope->getType($functionCall->getArgs()[1]->value); - if ($flagsType instanceof ConstantIntegerType) { - if ($flagsType->getValue() === $this->getConstant('PATHINFO_ALL')) { - return $arrayType; + + $scalarValues = $flagsType->getConstantScalarValues(); + if ($scalarValues !== []) { + $pathInfoAll = $this->getConstant('PATHINFO_ALL'); + if ($pathInfoAll === null) { + return null; + } + + $result = []; + foreach ($scalarValues as $scalarValue) { + if ($scalarValue === $pathInfoAll) { + $result[] = $arrayType; + } else { + $result[] = new StringType(); + } } - return new StringType(); + return TypeCombinator::union(...$result); } return TypeCombinator::union($arrayType, new StringType()); } + /** + * @param non-empty-string $constantName + */ private function getConstant(string $constantName): ?int { if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) { diff --git a/src/Type/Php/PregMatchParameterOutTypeExtension.php b/src/Type/Php/PregMatchParameterOutTypeExtension.php new file mode 100644 index 0000000000..9066351d9f --- /dev/null +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -0,0 +1,55 @@ +getName()), ['preg_match', 'preg_match_all'], true) + // the parameter is named different, depending on PHP version. + && in_array($parameter->getName(), ['subpatterns', 'matches'], true); + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $funcCall->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return null; + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + if ($functionReflection->getName() === 'preg_match') { + return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + } + return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); + } + +} diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php new file mode 100644 index 0000000000..2c7cad49be --- /dev/null +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -0,0 +1,81 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return in_array(strtolower($functionReflection->getName()), ['preg_match', 'preg_match_all'], true) && !$context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return new SpecifiedTypes(); + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + if ($functionReflection->getName() === 'preg_match') { + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + } else { + $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); + } + if ($matchedType === null) { + return new SpecifiedTypes(); + } + + $overwrite = false; + if ($context->false()) { + $overwrite = true; + $context = $context->negate(); + } + + return $this->typeSpecifier->create( + $matchesArg->value, + $matchedType, + $context, + $overwrite, + $scope, + $node, + ); + } + +} diff --git a/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php new file mode 100644 index 0000000000..a7ea4dc133 --- /dev/null +++ b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php @@ -0,0 +1,60 @@ +getName() === 'preg_replace_callback' && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + $patternArg = $args[0] ?? null; + $flagsArg = $args[5] ?? null; + + if ( + $patternArg === null + ) { + return null; + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($matchesType === null) { + return null; + } + + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new StringType(), + ); + } + +} diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 9d48e74c01..8a67f1d6ab 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -22,7 +22,9 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use ValueError; use function count; +use function is_array; use function range; class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -65,7 +67,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, continue; } - $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + try { + $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); + } catch (ValueError) { + continue; + } + + // @phpstan-ignore function.alreadyNarrowedType + if (!is_array($rangeValues)) { + continue; + } + if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) { if ($startConstant instanceof ConstantIntegerType && $endConstant instanceof ConstantIntegerType) { if ($startConstant->getValue() > $endConstant->getValue()) { diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index 078301bb12..56478c55c9 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -34,6 +34,10 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $valueType = $scope->getType($methodCall->getArgs()[0]->value); foreach ($valueType->getConstantStrings() as $constantString) { + if ($constantString->getValue() === '') { + return null; + } + if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) { return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php new file mode 100644 index 0000000000..628bcb0d3c --- /dev/null +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -0,0 +1,530 @@ +matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true); + } + + public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type + { + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); + } + + /** + * @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([], []); + } + + $constantStrings = $patternType->getConstantStrings(); + if (count($constantStrings) === 0) { + return null; + } + + $flags = null; + if ($flagsType !== null) { + if (!$flagsType instanceof ConstantIntegerType) { + return null; + } + + /** @var int-mask $flags */ + $flags = $flagsType->getValue() & (PREG_OFFSET_CAPTURE | PREG_PATTERN_ORDER | PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | self::PREG_UNMATCHED_AS_NULL_ON_72_73); + + // some other unsupported/unexpected flag was passed in + if ($flags !== $flagsType->getValue()) { + return null; + } + } + + $matchedTypes = []; + foreach ($constantStrings as $constantString) { + $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll); + if ($matched === null) { + return null; + } + + $matchedTypes[] = $matched; + } + + if (count($matchedTypes) === 1) { + return $matchedTypes[0]; + } + + return TypeCombinator::union(...$matchedTypes); + } + + /** + * @param int-mask|null $flags + */ + private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + { + $parseResult = $this->regexGroupParser->parseGroups($regex); + if ($parseResult === 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++; + } + + $onlyOptionalTopLevelGroup = $this->getOnlyOptionalTopLevelGroup($groupList); + $onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList); + $flags ??= 0; + + if ( + !$matchesAll + && $wasMatched->yes() + && $onlyOptionalTopLevelGroup !== null + ) { + // if only one top level capturing optional group exists + // we build a more precise tagged union of a empty-match and a match with the group + + $onlyOptionalTopLevelGroup->forceNonOptional(); + + $combiType = $this->buildArrayType( + $groupList, + $wasMatched, + $trailingOptionals, + $flags, + $markVerbs, + $matchesAll, + ); + + if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) { + // positive match has a subject but not any capturing group + $combiType = TypeCombinator::union( + new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [0], [], true), + $combiType, + ); + } + + $onlyOptionalTopLevelGroup->clearOverrides(); + + return $combiType; + } elseif ( + !$matchesAll + && $onlyOptionalTopLevelGroup === null + && $onlyTopLevelAlternation !== null + && !$wasMatched->no() + ) { + // if only a single top level alternation exist built a more precise tagged union + + $combiTypes = []; + $isOptionalAlternation = false; + foreach ($onlyTopLevelAlternation->getGroupCombinations() as $groupCombo) { + $comboList = $groupList; + + $beforeCurrentCombo = true; + foreach ($comboList as $groupId => $group) { + if (in_array($groupId, $groupCombo, true)) { + $isOptionalAlternation = $group->inOptionalAlternation(); + $group->forceNonOptional(); + $beforeCurrentCombo = false; + } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { + $group->forceNonOptional(); + $group->forceType( + $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), + ); + } elseif ( + $group->getAlternationId() === $onlyTopLevelAlternation->getId() + && !$this->containsUnmatchedAsNull($flags, $matchesAll) + ) { + unset($comboList[$groupId]); + } + } + + $combiType = $this->buildArrayType( + $comboList, + $wasMatched, + $trailingOptionals, + $flags, + $markVerbs, + $matchesAll, + ); + + $combiTypes[] = $combiType; + + foreach ($groupCombo as $groupId) { + $group = $comboList[$groupId]; + $group->clearOverrides(); + } + } + + if ( + !$this->containsUnmatchedAsNull($flags, $matchesAll) + && ( + $onlyTopLevelAlternation->getAlternationsCount() !== count($onlyTopLevelAlternation->getGroupCombinations()) + || $isOptionalAlternation + ) + ) { + // positive match has a subject but not any capturing group + $combiTypes[] = new ConstantArrayType([new ConstantIntegerType(0)], [$this->createSubjectValueType($flags, $matchesAll)], [0], [], true); + } + + return TypeCombinator::union(...$combiTypes); + } + + // the general case, which should work in all cases but does not yield the most + // precise result possible in some cases + return $this->buildArrayType( + $groupList, + $wasMatched, + $trailingOptionals, + $flags, + $markVerbs, + $matchesAll, + ); + } + + /** + * @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, + TrinaryLogic $wasMatched, + int $trailingOptionals, + int $flags, + array $markVerbs, + bool $matchesAll, + ): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + // first item in matches contains the overall match. + $builder->setOffsetValueType( + $this->getKeyType(0), + $this->createSubjectValueType($flags, $matchesAll), + $this->isSubjectOptional($wasMatched, $matchesAll), + ); + + $countGroups = count($captureGroups); + $i = 0; + foreach ($captureGroups as $captureGroup) { + $isTrailingOptional = $i >= $countGroups - $trailingOptionals; + $isLastGroup = $i === $countGroups - 1; + $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll); + $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); + + if ($captureGroup->isNamed()) { + $builder->setOffsetValueType( + $this->getKeyType($captureGroup->getName()), + $groupValueType, + $optional, + ); + } + + $builder->setOffsetValueType( + $this->getKeyType($i + 1), + $groupValueType, + $optional, + ); + + $i++; + } + + if (count($markVerbs) > 0) { + $markTypes = []; + foreach ($markVerbs as $mark) { + $markTypes[] = new ConstantStringType($mark); + } + $builder->setOffsetValueType( + $this->getKeyType('MARK'), + TypeCombinator::union(...$markTypes), + true, + ); + } + + if ($matchesAll && $this->containsSetOrder($flags)) { + $arrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $builder->getArray())); + if (!$wasMatched->yes()) { + $arrayType = TypeCombinator::union( + new ConstantArrayType([], []), + $arrayType, + ); + } + return $arrayType; + } + + return $builder->getArray(); + } + + private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): bool + { + if ($matchesAll) { + return false; + } + + return !$wasMatched->yes(); + } + + private function createSubjectValueType(int $flags, bool $matchesAll): Type + { + $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + + if ($matchesAll) { + if ($this->containsPatternOrder($flags)) { + $subjectValueType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $subjectValueType)); + } + } + + return $subjectValueType; + } + + private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll): bool + { + if ($matchesAll) { + if ($isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $this->containsSetOrder($flags)) { + return true; + } + + return false; + } + + if (!$wasMatched->yes()) { + $optional = true; + } else { + if (!$isTrailingOptional) { + $optional = false; + } elseif ($this->containsUnmatchedAsNull($flags, $matchesAll)) { + $optional = false; + } else { + $optional = $captureGroup->isOptional(); + } + } + + return $optional; + } + + private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type + { + if ($matchesAll) { + if (!$this->containsSetOrder($flags) && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { + $groupValueType = $this->getValueType( + TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), + $flags, + $matchesAll, + ); + $groupValueType = TypeCombinator::removeNull($groupValueType); + } else { + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + } + + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + $groupValueType = TypeCombinator::removeNull($groupValueType); + } + + if ($this->containsPatternOrder($flags)) { + $groupValueType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $groupValueType)); + } + + return $groupValueType; + } + + if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { + $groupValueType = $this->getValueType( + TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), + $flags, + $matchesAll, + ); + } else { + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + } + + if ($wasMatched->yes()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + $groupValueType = TypeCombinator::removeNull($groupValueType); + } + } + + return $groupValueType; + } + + private function containsOffsetCapture(int $flags): bool + { + return ($flags & PREG_OFFSET_CAPTURE) !== 0; + } + + private function containsPatternOrder(int $flags): bool + { + // If no order flag is given, PREG_PATTERN_ORDER is assumed. + return !$this->containsSetOrder($flags); + } + + private function containsSetOrder(int $flags): bool + { + return ($flags & PREG_SET_ORDER) !== 0; + } + + private function containsUnmatchedAsNull(int $flags, bool $matchesAll): bool + { + if ($matchesAll) { + // preg_match_all() with PREG_UNMATCHED_AS_NULL works consistently across php-versions + // https://3v4l.org/tKmPn + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0; + } + + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && (($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0 || $this->phpVersion->supportsPregUnmatchedAsNull()); + } + + private function getKeyType(int|string $key): Type + { + if (is_string($key)) { + return new ConstantStringType($key); + } + + return new ConstantIntegerType($key); + } + + private function getValueType(Type $baseType, int $flags, bool $matchesAll): Type + { + $valueType = $baseType; + + // unmatched groups return -1 as offset + $offsetType = IntegerRangeType::fromInterval(-1, null); + if ($this->containsUnmatchedAsNull($flags, $matchesAll)) { + $valueType = TypeCombinator::addNull($valueType); + } + + if ($this->containsOffsetCapture($flags)) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $builder->setOffsetValueType( + new ConstantIntegerType(0), + $valueType, + ); + $builder->setOffsetValueType( + new ConstantIntegerType(1), + $offsetType, + ); + + return $builder->getArray(); + } + + return $valueType; + } + + private function getPatternType(Expr $patternExpr, Scope $scope): Type + { + if ($patternExpr instanceof Expr\BinaryOp\Concat) { + return $this->regexExpressionHelper->resolvePatternConcat($patternExpr, $scope); + } + + return $scope->getType($patternExpr); + } + +} diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 1da17b48d8..797ee59bb4 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -52,15 +52,7 @@ public function getTypeFromFunctionCall( { $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); - $possibleTypes = ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->getArgs(), - $functionReflection->getVariants(), - )->getReturnType(); - // resolve conditional return types - $possibleTypes = TypeUtils::resolveLateResolvableTypes($possibleTypes); - - if (TypeCombinator::containsNull($possibleTypes)) { + if ($this->canReturnNull($functionReflection, $functionCall, $scope)) { $type = TypeCombinator::addNull($type); } @@ -73,18 +65,17 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( Scope $scope, ): Type { - $argumentPosition = self::FUNCTIONS_SUBJECT_POSITION[$functionReflection->getName()]; + $subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope); $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( $scope, $functionCall->getArgs(), $functionReflection->getVariants(), )->getReturnType(); - if (count($functionCall->getArgs()) <= $argumentPosition) { + if ($subjectArgumentType === null) { return $defaultReturnType; } - $subjectArgumentType = $scope->getType($functionCall->getArgs()[$argumentPosition]->value); if ($subjectArgumentType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } @@ -124,4 +115,35 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( return $defaultReturnType; } + private function getSubjectType( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $argumentPosition = self::FUNCTIONS_SUBJECT_POSITION[$functionReflection->getName()]; + if (count($functionCall->getArgs()) <= $argumentPosition) { + return null; + } + return $scope->getType($functionCall->getArgs()[$argumentPosition]->value); + } + + private function canReturnNull( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): bool + { + $possibleTypes = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + + // resolve conditional return types + $possibleTypes = TypeUtils::resolveLateResolvableTypes($possibleTypes); + + return TypeCombinator::containsNull($possibleTypes); + } + } diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 839c5c1587..e3e3441b3e 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -6,14 +6,17 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; @@ -39,21 +42,18 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo ); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { + // PHP 7 can return either a float or false. + // PHP 8 can either return a float or fatal. + $defaultReturnType = null; + if ($this->phpVersion->hasStricterRoundFunctions()) { // PHP 8 fatals with a missing parameter. $noArgsReturnType = new NeverType(true); - // PHP 8 can either return a float or fatal. - $defaultReturnType = new FloatType(); } else { // PHP 7 returns null with a missing parameter. $noArgsReturnType = new NullType(); - // PHP 7 can return either a float or false. - $defaultReturnType = new BenevolentUnionType([ - new FloatType(), - new ConstantBooleanType(false), - ]); } if (count($functionCall->getArgs()) < 1) { @@ -71,6 +71,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, new IntegerType(), new FloatType(), ); + + if (!$scope->isDeclareStrictTypes()) { + $allowed = TypeCombinator::union( + $allowed, + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + new NullType(), + new BooleanType(), + ); + } + if ($allowed->isSuperTypeOf($firstArgType)->no()) { // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); diff --git a/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..70526414bb --- /dev/null +++ b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php @@ -0,0 +1,91 @@ +getName()) === 'settype' + && count($node->getArgs()) > 1 + && $context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $value = $node->getArgs()[0]->value; + $valueType = $scope->getType($value); + $castType = $scope->getType($node->getArgs()[1]->value); + + $constantStrings = $castType->getConstantStrings(); + if (count($constantStrings) < 1) { + return new SpecifiedTypes(); + } + + $types = []; + + foreach ($constantStrings as $constantString) { + switch ($constantString->getValue()) { + case 'bool': + case 'boolean': + $types[] = $valueType->toBoolean(); + break; + case 'int': + case 'integer': + $types[] = $valueType->toInteger(); + break; + case 'float': + case 'double': + $types[] = $valueType->toFloat(); + break; + case 'string': + $types[] = $valueType->toString(); + break; + case 'array': + $types[] = $valueType->toArray(); + break; + case 'object': + $types[] = new ObjectType(stdClass::class); + break; + case 'null': + $types[] = new NullType(); + break; + default: + $types[] = new ErrorType(); + } + } + + return $this->typeSpecifier->create( + $value, + TypeCombinator::union(...$types), + TypeSpecifierContext::createTruthy(), + true, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index 41d903e5c5..ea5da83872 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -12,6 +12,7 @@ use SimpleXMLElement; use function count; use function extension_loaded; +use function libxml_use_internal_errors; class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { @@ -32,14 +33,20 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $valueType = $scope->getType($methodCall->getArgs()[0]->value); $constantStrings = $valueType->getConstantStrings(); - foreach ($constantStrings as $constantString) { - try { - new SimpleXMLElement($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); - } + $internalErrorsOld = libxml_use_internal_errors(true); + + try { + foreach ($constantStrings as $constantString) { + try { + new SimpleXMLElement($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $methodReflection->getThrowType(); + } - $valueType = TypeCombinator::remove($valueType, $constantString); + $valueType = TypeCombinator::remove($valueType, $constantString); + } + } finally { + libxml_use_internal_errors($internalErrorsOld); } if (!$valueType instanceof NeverType) { diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index b6084178b3..8829b4a4b8 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Internal\CombinationsHelper; @@ -10,19 +11,25 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use Throwable; +use function array_fill; use function array_key_exists; use function array_shift; use function count; use function in_array; +use function intval; use function is_string; use function preg_match; use function sprintf; +use function substr; use function vsprintf; class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -44,54 +51,197 @@ public function getTypeFromFunctionCall( return null; } + $constantType = $this->getConstantType($args, $functionReflection, $scope); + if ($constantType !== null) { + return $constantType; + } + $formatType = $scope->getType($args[0]->value); - if (count($formatType->getConstantStrings()) > 0) { - $skip = false; - foreach ($formatType->getConstantStrings() as $constantString) { - // The printf format is %[argnum$][flags][width][.precision] - if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*[bdeEfFgGhHouxX]$/', $constantString->getValue(), $matches) === 1) { + $formatStrings = $formatType->getConstantStrings(); + + $singlePlaceholderEarlyReturn = null; + $allPatternsNonEmpty = count($formatStrings) !== 0; + $allPatternsNonFalsy = count($formatStrings) !== 0; + foreach ($formatStrings as $constantString) { + $constantParts = $this->getFormatConstantParts( + $constantString->getValue(), + $functionReflection, + $functionCall, + $scope, + ); + if ($constantParts !== null) { + if ($constantParts->isNonFalsyString()->yes()) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf + // keep all bool flags as is + } elseif ($constantParts->isNonEmptyString()->yes()) { + $allPatternsNonFalsy = false; + } else { + $allPatternsNonEmpty = false; + $allPatternsNonFalsy = false; + } + } else { + $allPatternsNonEmpty = false; + $allPatternsNonFalsy = false; + } + + // 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] !== '') { // invalid positional argument - if (array_key_exists(1, $matches) && $matches[1] === '0$') { + if ($matches[1] === '0$') { return null; } + $checkArg = intval(substr($matches[1], 0, -1)); + } else { + $checkArg = 1; + } - continue; + // constant string specifies a numbered argument that does not exist + if (!array_key_exists($checkArg, $args)) { + return null; } - $skip = true; - break; - } + // 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' + && ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes()) + ) { + $singlePlaceholderEarlyReturn = $checkArgType->toString(); + } elseif ($matches[2] !== 's') { + $singlePlaceholderEarlyReturn = new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } - if (!$skip) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + continue; } + + $singlePlaceholderEarlyReturn = null; + break; + } + + if ($singlePlaceholderEarlyReturn !== null) { + return $singlePlaceholderEarlyReturn; } - if ($formatType->isNonFalsyString()->yes()) { - $returnType = new IntersectionType([ + if ($allPatternsNonFalsy) { + return new IntersectionType([ new StringType(), new AccessoryNonFalsyStringType(), ]); - } elseif ($formatType->isNonEmptyString()->yes()) { - $returnType = new IntersectionType([ + } + + $isNonEmpty = $allPatternsNonEmpty; + if ( + !$isNonEmpty + && $functionReflection->getName() === 'sprintf' + && count($args) >= 2 + && $formatType->isNonEmptyString()->yes() + ) { + $allArgsNonEmpty = true; + foreach ($args as $key => $arg) { + if ($key === 0) { + continue; + } + + if (!$scope->getType($arg->value)->toString()->isNonEmptyString()->yes()) { + $allArgsNonEmpty = false; + break; + } + } + + if ($allArgsNonEmpty) { + $isNonEmpty = true; + } + } + + if ($isNonEmpty) { + return new IntersectionType([ new StringType(), new AccessoryNonEmptyStringType(), ]); + } + + return new StringType(); + } + + /** + * Detect constant strings in the format which neither depend on placeholders nor on given value arguments. + */ + private function getFormatConstantParts( + string $format, + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?ConstantStringType + { + $args = $functionCall->getArgs(); + if ($functionReflection->getName() === 'sprintf') { + $valuesCount = count($args) - 1; + } elseif ( + $functionReflection->getName() === 'vsprintf' + && count($args) >= 2 + ) { + $arraySize = $scope->getType($args[1]->value)->getArraySize(); + if (!($arraySize instanceof ConstantIntegerType)) { + return null; + } + + $valuesCount = $arraySize->getValue(); } else { - $returnType = new StringType(); + return null; + } + + if ($valuesCount <= 0) { + return null; } + $dummyValues = array_fill(0, $valuesCount, ''); + try { + $formatted = @vsprintf($format, $dummyValues); + if ($formatted === false) { // @phpstan-ignore identical.alwaysFalse (PHP7.2 compat) + return null; + } + return new ConstantStringType($formatted); + } catch (Throwable) { + return null; + } + } + + /** + * @param Arg[] $args + */ + private function getConstantType(array $args, FunctionReflection $functionReflection, Scope $scope): ?Type + { $values = []; + $combinationsCount = 1; foreach ($args as $arg) { + if ($arg->unpack) { + return null; + } + $argType = $scope->getType($arg->value); - if (count($argType->getConstantScalarValues()) === 0) { - return $returnType; + $constantScalarValues = $argType->getConstantScalarValues(); + + if (count($constantScalarValues) === 0) { + if ($argType instanceof IntegerRangeType) { + foreach ($argType->getFiniteTypes() as $finiteType) { + $constantScalarValues[] = $finiteType->getValue(); + } + } + } + + if (count($constantScalarValues) === 0) { + return null; } - $values[] = $argType->getConstantScalarValues(); + $values[] = $constantScalarValues; + $combinationsCount *= count($constantScalarValues); + } + + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } $combinations = CombinationsHelper::combinations($values); @@ -99,7 +249,7 @@ public function getTypeFromFunctionCall( foreach ($combinations as $combination) { $format = array_shift($combination); if (!is_string($format)) { - return $returnType; + return null; } try { @@ -109,12 +259,12 @@ public function getTypeFromFunctionCall( $returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination)); } } catch (Throwable) { - return $returnType; + return null; } } if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { - return $returnType; + return null; } return TypeCombinator::union(...$returnTypes); diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index df1dfc88e9..ebb693672d 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -34,6 +34,8 @@ class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeEx 'mb_strtolower' => 1, 'lcfirst' => 1, 'ucfirst' => 1, + 'mb_lcfirst' => 1, + 'mb_ucfirst' => 1, 'ucwords' => 1, 'mb_convert_case' => 2, 'mb_convert_kana' => 1, diff --git a/src/Type/Php/StrContainingTypeSpecifyingExtension.php b/src/Type/Php/StrContainingTypeSpecifyingExtension.php index f7745f2128..8cf678ae56 100644 --- a/src/Type/Php/StrContainingTypeSpecifyingExtension.php +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -38,6 +38,11 @@ final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyi 'stripos' => [0, 1], 'strripos' => [0, 1], 'strstr' => [0, 1], + 'mb_strpos' => [0, 1], + 'mb_strrpos' => [0, 1], + 'mb_stripos' => [0, 1], + 'mb_strripos' => [0, 1], + 'mb_strstr' => [0, 1], ]; private TypeSpecifier $typeSpecifier; diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index 6bf696ac11..3ba27a7f9e 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -2,12 +2,14 @@ namespace PHPStan\Type\Php; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -39,17 +41,17 @@ public function getTypeFromFunctionCall( return new StringType(); } - $inputType = $scope->getType($args[0]->value); $multiplierType = $scope->getType($args[1]->value); if ((new ConstantIntegerType(0))->isSuperTypeOf($multiplierType)->yes()) { return new ConstantStringType(''); } - if ($multiplierType instanceof ConstantIntegerType && $multiplierType->getValue() < 0) { + if (IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($multiplierType)->yes()) { return new NeverType(); } + $inputType = $scope->getType($args[0]->value); if ( $inputType instanceof ConstantStringType && $multiplierType instanceof ConstantIntegerType @@ -72,13 +74,29 @@ public function getTypeFromFunctionCall( if ($inputType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); + + if ( + $inputType->isNumericString()->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes() + ) { + $onlyNumbers = true; + foreach ($inputType->getConstantStrings() as $constantString) { + if (Strings::match($constantString->getValue(), '#^[0-9]+$#') === null) { + $onlyNumbers = false; + break; + } + } + + if ($onlyNumbers) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + } } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); return new IntersectionType($accessoryTypes); } - return new StringType(); } diff --git a/src/Type/Php/StrlenFunctionReturnTypeExtension.php b/src/Type/Php/StrlenFunctionReturnTypeExtension.php index fa2c1eb2ff..e4a51cb833 100644 --- a/src/Type/Php/StrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -15,7 +15,13 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function array_unique; use function count; +use function max; +use function min; +use function range; +use function sort; use function strlen; class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -49,56 +55,41 @@ public function getTypeFromFunctionCall( $constantScalars = $argType->getConstantScalarTypes(); } - $min = null; - $max = null; + $lengths = []; foreach ($constantScalars as $constantScalar) { $stringScalar = $constantScalar->toString(); if (!($stringScalar instanceof ConstantStringType)) { - $min = $max = null; + $lengths = []; break; } - $len = strlen($stringScalar->getValue()); - - if ($min === null) { - $min = $len; - $max = $len; - } - - if ($len < $min) { - $min = $len; - } - if ($len <= $max) { - continue; - } - - $max = $len; - } - - // $max is always != null, when $min is != null - if ($min !== null) { - return IntegerRangeType::fromInterval($min, $max); - } - - $bool = new BooleanType(); - if ($bool->isSuperTypeOf($argType)->yes()) { - return IntegerRangeType::fromInterval(0, 1); + $length = strlen($stringScalar->getValue()); + $lengths[] = $length; } $isNonEmpty = $argType->isNonEmptyString(); $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); - if ( + $range = null; + if (count($lengths) > 0) { + $lengths = array_unique($lengths); + sort($lengths); + if ($lengths === range(min($lengths), max($lengths))) { + $range = IntegerRangeType::fromInterval(min($lengths), max($lengths)); + } else { + $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); + } + } elseif ($argType->isBoolean()->yes()) { + $range = IntegerRangeType::fromInterval(0, 1); + } elseif ( $isNonEmpty->yes() || $numeric->isSuperTypeOf($argType)->yes() || TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes() ) { - return IntegerRangeType::fromInterval(1, null); - } - - if ($argType->isString()->yes() && $isNonEmpty->no()) { - return new ConstantIntegerType(0); + $range = IntegerRangeType::fromInterval(1, null); + } elseif ($argType->isString()->yes() && $isNonEmpty->no()) { + $range = new ConstantIntegerType(0); } - return null; + return $range; } } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index de54d526fb..ae4bf10d5a 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -17,7 +17,9 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; +use function in_array; use function is_bool; +use function mb_substr; use function substr; class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -25,7 +27,7 @@ class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExten public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'substr'; + return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true); } public function getTypeFromFunctionCall( @@ -47,10 +49,12 @@ public function getTypeFromFunctionCall( $zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes(); $length = null; $positiveLength = false; + $maybeOneLength = false; if (count($args) === 3) { $length = $scope->getType($args[2]->value); $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + $maybeOneLength = !(new ConstantIntegerType(1))->isSuperTypeOf($length)->no(); } $constantStrings = $string->getConstantStrings(); @@ -62,16 +66,17 @@ public function getTypeFromFunctionCall( $results = []; foreach ($constantStrings as $constantString) { if ($length !== null) { - $substr = substr( - $constantString->getValue(), - $offset->getValue(), - $length->getValue(), - ); + if ($functionReflection->getName() === 'mb_substr') { + $substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()); + } else { + $substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue()); + } } else { - $substr = substr( - $constantString->getValue(), - $offset->getValue(), - ); + if ($functionReflection->getName() === 'mb_substr') { + $substr = mb_substr($constantString->getValue(), $offset->getValue()); + } else { + $substr = substr($constantString->getValue(), $offset->getValue()); + } } if (is_bool($substr)) { @@ -85,7 +90,7 @@ public function getTypeFromFunctionCall( } if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { - if ($string->isNonFalsyString()->yes()) { + if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { return new IntersectionType([ new StringType(), new AccessoryNonFalsyStringType(), diff --git a/src/Type/RecursionGuard.php b/src/Type/RecursionGuard.php index 2149fb1015..8fd995882c 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -9,10 +9,11 @@ class RecursionGuard private static array $context = []; /** - * @param callable(): Type $callback - * + * @template T + * @param callable(): T $callback + * @return T|ErrorType */ - public static function run(Type $type, callable $callback): Type + public static function run(Type $type, callable $callback) { $key = $type->describe(VerbosityLevel::value()); if (isset(self::$context[$key])) { diff --git a/src/Type/Regex/RegexAlternation.php b/src/Type/Regex/RegexAlternation.php new file mode 100644 index 0000000000..9a00411c10 --- /dev/null +++ b/src/Type/Regex/RegexAlternation.php @@ -0,0 +1,47 @@ +> */ + private array $groupCombinations = []; + + public function __construct( + private readonly int $alternationId, + private readonly int $alternationsCount, + ) + { + } + + public function getId(): int + { + return $this->alternationId; + } + + public function pushGroup(int $combinationIndex, RegexCapturingGroup $group): void + { + if (!array_key_exists($combinationIndex, $this->groupCombinations)) { + $this->groupCombinations[$combinationIndex] = []; + } + + $this->groupCombinations[$combinationIndex][] = $group->getId(); + } + + public function getAlternationsCount(): int + { + return $this->alternationsCount; + } + + /** + * @return array> + */ + public function getGroupCombinations(): array + { + return $this->groupCombinations; + } + +} diff --git a/src/Type/Regex/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php new file mode 100644 index 0000000000..62708a2de3 --- /dev/null +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -0,0 +1,126 @@ +id; + } + + public function forceNonOptional(): void + { + $this->forceNonOptional = true; + } + + public function forceType(Type $type): void + { + $this->forceType = $type; + } + + public function clearOverrides(): void + { + $this->forceNonOptional = false; + $this->forceType = null; + } + + public function resetsGroupCounter(): bool + { + return $this->parent instanceof RegexNonCapturingGroup && $this->parent->resetsGroupCounter(); + } + + /** + * @phpstan-assert-if-true !null $this->getAlternationId() + * @phpstan-assert-if-true !null $this->getAlternation() + */ + public function inAlternation(): bool + { + return $this->alternation !== null; + } + + public function getAlternation(): ?RegexAlternation + { + return $this->alternation; + } + + public function getAlternationId(): ?int + { + if ($this->alternation === null) { + return null; + } + + return $this->alternation->getId(); + } + + public function isOptional(): bool + { + if ($this->forceNonOptional) { + return false; + } + + return $this->inAlternation() + || $this->inOptionalQuantification + || $this->parent !== null && $this->parent->isOptional(); + } + + public function inOptionalAlternation(): bool + { + if (!$this->inAlternation()) { + return false; + } + + $parent = $this->parent; + while ($parent !== null && $parent->getAlternationId() === $this->getAlternationId()) { + if (!$parent instanceof RegexNonCapturingGroup) { + return false; + } + $parent = $parent->getParent(); + } + return $parent !== null && $parent->isOptional(); + } + + public function isTopLevel(): bool + { + return $this->parent === null + || $this->parent instanceof RegexNonCapturingGroup && $this->parent->isTopLevel(); + } + + /** @phpstan-assert-if-true !null $this->getName() */ + public function isNamed(): bool + { + return $this->name !== null; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getType(): Type + { + if ($this->forceType !== null) { + return $this->forceType; + } + return $this->type; + } + +} diff --git a/src/Type/Regex/RegexExpressionHelper.php b/src/Type/Regex/RegexExpressionHelper.php new file mode 100644 index 0000000000..2b94fda6e2 --- /dev/null +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -0,0 +1,157 @@ +name instanceof Name + && $expr->name->toLowerString() === 'preg_quote' + ) { + return new ConstantStringType('(?:.*)'); + } + + if ($expr instanceof Concat) { + $left = $this->resolve($expr->left); + $right = $this->resolve($expr->right); + + $strings = []; + foreach ($left->toString()->getConstantStrings() as $leftString) { + foreach ($right->toString()->getConstantStrings() as $rightString) { + $strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue()); + } + } + + return TypeCombinator::union(...$strings); + } + + return $this->scope->getType($expr); + } + + }; + + return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr)); + } + + public function getPatternModifiers(string $pattern): ?string + { + $endDelimiterPos = $this->getEndDelimiterPos($pattern); + + if ($endDelimiterPos === false) { + return null; + } + + return substr($pattern, $endDelimiterPos + 1); + } + + public function removeDelimitersAndModifiers(string $pattern): string + { + $endDelimiterPos = $this->getEndDelimiterPos($pattern); + + if ($endDelimiterPos === false) { + return $pattern; + } + + return substr($pattern, 1, $endDelimiterPos - 1); + } + + private function getEndDelimiterPos(string $pattern): false|int + { + $startDelimiter = $this->getPatternDelimiter($pattern); + if ($startDelimiter === null) { + return false; + } + + // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php + $bracketStyleDelimiters = [ + '{' => '}', + '(' => ')', + '[' => ']', + '<' => '>', + ]; + if (array_key_exists($startDelimiter, $bracketStyleDelimiters)) { + $endDelimiterPos = strrpos($pattern, $bracketStyleDelimiters[$startDelimiter]); + } else { + // same start and end delimiter + $endDelimiterPos = strrpos($pattern, $startDelimiter); + } + + return $endDelimiterPos; + } + + /** + * Get delimiters from non-constant patterns, if possible. + * + * @return string[] + */ + public function getPatternDelimiters(Concat $concat, Scope $scope): array + { + if ($concat->left instanceof Concat) { + return $this->getPatternDelimiters($concat->left, $scope); + } + + $left = $scope->getType($concat->left); + + $delimiters = []; + foreach ($left->getConstantStrings() as $leftString) { + $delimiter = $this->getPatternDelimiter($leftString->getValue()); + if ($delimiter === null) { + continue; + } + + $delimiters[] = $delimiter; + } + return $delimiters; + } + + private function getPatternDelimiter(string $regex): ?string + { + if ($regex === '') { + return null; + } + + return substr($regex, 0, 1); + } + +} diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php new file mode 100644 index 0000000000..6938ff8000 --- /dev/null +++ b/src/Type/Regex/RegexGroupParser.php @@ -0,0 +1,586 @@ +, list}|null + */ + public function parseGroups(string $regex): ?array + { + if (self::$parser === null) { + /** @throws void */ + self::$parser = Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); + } + + try { + Strings::match('', $regex); + } catch (RegexpException) { + // pattern is invalid, so let the RegularExpressionPatternRule report it + return null; + } + + $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); + try { + $ast = self::$parser->parse($rawRegex); + } catch (Exception) { + return null; + } + + $captureOnlyNamed = false; + $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; + if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { + $captureOnlyNamed = str_contains($modifiers, 'n'); + } + + $capturingGroups = []; + $alternationId = -1; + $captureGroupId = 100; + $markVerbs = []; + $this->walkRegexAst( + $ast, + null, + $alternationId, + 0, + false, + null, + $captureGroupId, + $capturingGroups, + $markVerbs, + $captureOnlyNamed, + false, + $modifiers, + ); + + return [$capturingGroups, $markVerbs]; + } + + /** + * @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 + { + $group = null; + if ($ast->getId() === '#capturing') { + $group = new RegexCapturingGroup( + $captureGroupId++, + null, + $alternation, + $inOptionalQuantification, + $parentGroup, + $this->createGroupType( + $ast, + $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup), + $patternModifiers, + ), + ); + $parentGroup = $group; + } elseif ($ast->getId() === '#namedcapturing') { + $name = $ast->getChild(0)->getValueValue(); + $group = new RegexCapturingGroup( + $captureGroupId++, + $name, + $alternation, + $inOptionalQuantification, + $parentGroup, + $this->createGroupType( + $ast, + $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup), + $patternModifiers, + ), + ); + $parentGroup = $group; + } elseif ($ast->getId() === '#noncapturing') { + $group = new RegexNonCapturingGroup( + $alternation, + $inOptionalQuantification, + $parentGroup, + false, + ); + $parentGroup = $group; + } elseif ($ast->getId() === '#noncapturingreset') { + $group = new RegexNonCapturingGroup( + $alternation, + $inOptionalQuantification, + $parentGroup, + true, + ); + $parentGroup = $group; + } + + $inOptionalQuantification = false; + if ($ast->getId() === '#quantification') { + [$min, $max] = $this->getQuantificationRange($ast); + + if ($min === 0) { + $inOptionalQuantification = true; + } + + if ($max === null || $max > 1) { + $repeatedMoreThanOnce = true; + } + } + + if ($ast->getId() === '#alternation') { + $alternationId++; + $alternation = new RegexAlternation($alternationId, count($ast->getChildren())); + } + + if ($ast->getId() === '#mark') { + $markVerbs[] = $ast->getChild(0)->getValueValue(); + return; + } + + if ( + $group instanceof RegexCapturingGroup && + (!$captureOnlyNamed || $group->isNamed()) + ) { + $capturingGroups[$group->getId()] = $group; + + if ($alternation !== null) { + $alternation->pushGroup($combinationIndex, $group); + } + } + + foreach ($ast->getChildren() as $child) { + $this->walkRegexAst( + $child, + $alternation, + $alternationId, + $combinationIndex, + $inOptionalQuantification, + $parentGroup, + $captureGroupId, + $capturingGroups, + $markVerbs, + $captureOnlyNamed, + $repeatedMoreThanOnce, + $patternModifiers, + ); + + if ($ast->getId() !== '#alternation') { + continue; + } + + $combinationIndex++; + } + } + + private function allowConstantTypes( + string $patternModifiers, + bool $repeatedMoreThanOnce, + RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, + ): bool + { + if (str_contains($patternModifiers, 'i')) { + // if caseless, we don't use constant types + // because it likely yields too many combinations + return false; + } + + if ($repeatedMoreThanOnce) { + return false; + } + + if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) { + return false; + } + + return true; + } + + /** @return array{?int, ?int} */ + private function getQuantificationRange(TreeNode $node): array + { + if ($node->getId() !== '#quantification') { + throw new ShouldNotHappenException(); + } + + $min = null; + $max = null; + + $lastChild = $node->getChild($node->getChildrenNumber() - 1); + $value = $lastChild->getValue(); + + // normalize away possessive and lazy quantifier-modifiers + $token = str_replace(['_possessive', '_lazy'], '', $value['token']); + $value = rtrim($value['value'], '+?'); + + if ($token === 'n_to_m') { + if (sscanf($value, '{%d,%d}', $n, $m) !== 2 || !is_int($n) || !is_int($m)) { + throw new ShouldNotHappenException(); + } + + $min = $n; + $max = $m; + } elseif ($token === 'n_or_more') { + if (sscanf($value, '{%d,}', $n) !== 1 || !is_int($n)) { + throw new ShouldNotHappenException(); + } + + $min = $n; + } elseif ($token === 'exactly_n') { + if (sscanf($value, '{%d}', $n) !== 1 || !is_int($n)) { + throw new ShouldNotHappenException(); + } + + $min = $n; + $max = $n; + } elseif ($token === 'zero_or_one') { + $min = 0; + $max = 1; + } elseif ($token === 'zero_or_more') { + $min = 0; + } elseif ($token === 'one_or_more') { + $min = 1; + } + + return [$min, $max]; + } + + private function createGroupType(TreeNode $group, bool $maybeConstant, string $patternModifiers): Type + { + $isNonEmpty = TrinaryLogic::createMaybe(); + $isNonFalsy = TrinaryLogic::createMaybe(); + $isNumeric = TrinaryLogic::createMaybe(); + $inOptionalQuantification = false; + $onlyLiterals = []; + + $this->walkGroupAst( + $group, + false, + $isNonEmpty, + $isNonFalsy, + $isNumeric, + $inOptionalQuantification, + $onlyLiterals, + false, + $patternModifiers, + ); + + if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) { + $result = []; + foreach ($onlyLiterals as $literal) { + $result[] = new ConstantStringType($literal); + + } + return TypeCombinator::union(...$result); + } + + if ($isNumeric->yes()) { + if ($isNonFalsy->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryNonFalsyStringType(), + ]); + } + + $result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + if (!$isNonEmpty->yes()) { + return TypeCombinator::union(new ConstantStringType(''), $result); + } + return $result; + } elseif ($isNonFalsy->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } elseif ($isNonEmpty->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return new StringType(); + } + + /** + * @param array|null $onlyLiterals + */ + private function walkGroupAst( + TreeNode $ast, + bool $inAlternation, + TrinaryLogic &$isNonEmpty, + TrinaryLogic &$isNonFalsy, + TrinaryLogic &$isNumeric, + bool &$inOptionalQuantification, + ?array &$onlyLiterals, + bool $inClass, + string $patternModifiers, + ): void + { + $children = $ast->getChildren(); + + if ( + $ast->getId() === '#concatenation' + && count($children) > 0 + ) { + $meaningfulTokens = 0; + foreach ($children as $child) { + $nonFalsy = false; + if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy)) { + continue; + } + + $meaningfulTokens++; + + if (!$nonFalsy || $inAlternation) { + continue; + } + + // a single token non-falsy on its own + $isNonFalsy = TrinaryLogic::createYes(); + break; + } + + if ($meaningfulTokens > 0) { + $isNonEmpty = TrinaryLogic::createYes(); + + // two non-empty tokens concatenated results in a non-falsy string + if ($meaningfulTokens > 1 && !$inAlternation) { + $isNonFalsy = TrinaryLogic::createYes(); + } + } + } elseif ($ast->getId() === '#quantification') { + [$min] = $this->getQuantificationRange($ast); + + if ($min === 0) { + $inOptionalQuantification = true; + } + if ($min >= 1) { + $isNonEmpty = TrinaryLogic::createYes(); + $inOptionalQuantification = false; + } + if ($min >= 2 && !$inAlternation) { + $isNonFalsy = TrinaryLogic::createYes(); + } + + $onlyLiterals = null; + } elseif ($ast->getId() === '#class' && $onlyLiterals !== null) { + $inClass = true; + + $newLiterals = []; + foreach ($children as $child) { + $oldLiterals = $onlyLiterals; + + $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers); + foreach ($oldLiterals ?? [] as $oldLiteral) { + $newLiterals[] = $oldLiteral; + } + } + $onlyLiterals = $newLiterals; + } elseif ($ast->getId() === 'token') { + $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers); + if ($literalValue !== null) { + if (Strings::match($literalValue, '/^\d+$/') === null) { + $isNumeric = TrinaryLogic::createNo(); + } elseif ($isNumeric->maybe()) { + $isNumeric = TrinaryLogic::createYes(); + } + + if (!$inOptionalQuantification) { + $isNonEmpty = TrinaryLogic::createYes(); + } + } + } elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing'], true)) { + $onlyLiterals = null; + } + + if ($ast->getId() === '#alternation') { + $inAlternation = true; + } + + // [^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(); + } + + foreach ($children as $child) { + $this->walkGroupAst( + $child, + $inAlternation, + $isNonEmpty, + $isNonFalsy, + $isNumeric, + $inOptionalQuantification, + $onlyLiterals, + $inClass, + $patternModifiers, + ); + } + } + + private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool + { + if ($node->getId() === '#quantification') { + [$min] = $this->getQuantificationRange($node); + + if ($min > 0) { + return false; + } + + if ($min === 0) { + return true; + } + } + + $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers); + if ($literal !== null) { + if ($literal !== '' && $literal !== '0') { + $isNonFalsy = true; + } + return false; + } + + foreach ($node->getChildren() as $child) { + if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy)) { + return false; + } + } + + return true; + } + + /** + * @param array|null $onlyLiterals + */ + private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers): ?string + { + if ($node->getId() !== 'token') { + return null; + } + + // token is the token name from grammar without the namespace so literal and class:literal are both called literal here + $token = $node->getValueToken(); + $value = $node->getValueValue(); + + if ( + in_array($token, [ + 'literal', 'escaped_end_class', + // 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', + ], true) + ) { + if (str_contains($patternModifiers, 'x') && trim($value) === '') { + return null; + } + + if (strlen($value) > 1 && $value[0] === '\\') { + $value = substr($value, 1) ?: ''; + } + + if ( + $appendLiterals + && in_array($token, ['literal', 'range', 'class_', '_class_literal'], true) + && $onlyLiterals !== null + && !in_array($value, ['.'], true) + ) { + if ($onlyLiterals === []) { + $onlyLiterals = [$value]; + } else { + foreach ($onlyLiterals as &$literal) { + $literal .= $value; + } + } + } + + return $value; + } + + if (!in_array($token, ['capturing_name'], true)) { + $onlyLiterals = null; + } + + // character escape sequences, just return a fixed string + if (in_array($token, ['character', 'dynamic_character', 'character_type'], true)) { + if ($token === 'character_type' && $value === '\d') { + return '0'; + } + + return $value; + } + + // [:digit:] and the like, more support coming later + if ($token === 'posix_class') { + if ($value === '[:digit:]') { + return '0'; + } + if (in_array($value, ['[:alpha:]', '[:alnum:]', '[:upper:]', '[:lower:]', '[:word:]', '[:ascii:]', '[:print:]', '[:xdigit:]', '[:graph:]'], true)) { + return 'a'; + } + if ($value === '[:blank:]') { + return " \t"; + } + if ($value === '[:cntrl:]') { + return "\x00\x1F"; + } + if ($value === '[:space:]') { + return " \t\r\n\v\f"; + } + if ($value === '[:punct:]') { + return '!"#$%&\'()*+,\-./:;<=>?@[\]^_`{|}~'; + } + } + + if ($token === 'anchor' || $token === 'match_point_reset') { + return ''; + } + + return null; + } + +} diff --git a/src/Type/Regex/RegexNonCapturingGroup.php b/src/Type/Regex/RegexNonCapturingGroup.php new file mode 100644 index 0000000000..79b4d8bc08 --- /dev/null +++ b/src/Type/Regex/RegexNonCapturingGroup.php @@ -0,0 +1,55 @@ +getAlternationId() */ + public function inAlternation(): bool + { + return $this->alternation !== null; + } + + public function getAlternationId(): ?int + { + if ($this->alternation === null) { + return null; + } + + return $this->alternation->getId(); + } + + public function isOptional(): bool + { + return $this->inAlternation() + || $this->inOptionalQuantification + || ($this->parent !== null && $this->parent->isOptional()); + } + + public function isTopLevel(): bool + { + return $this->parent === null + || $this->parent instanceof RegexNonCapturingGroup && $this->parent->isTopLevel(); + } + + public function getParent(): RegexCapturingGroup|RegexNonCapturingGroup|null + { + return $this->parent; + } + + public function resetsGroupCounter(): bool + { + return $this->resetGroupCounter; + } + +} diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 0eee8cea5f..4327f30bde 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -55,6 +55,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new StringType(); @@ -86,6 +91,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isScalar(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticMethodParameterClosureTypeExtension.php b/src/Type/StaticMethodParameterClosureTypeExtension.php new file mode 100644 index 0000000000..94727efb21 --- /dev/null +++ b/src/Type/StaticMethodParameterClosureTypeExtension.php @@ -0,0 +1,32 @@ +getStaticObjectType()->isOffsetAccessible(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->getStaticObjectType()->isOffsetAccessLegal(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->getStaticObjectType()->hasOffsetValueType($offsetType); @@ -391,6 +395,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->getStaticObjectType()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->getStaticObjectType()->setExistingOffsetValueType($offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { return $this->getStaticObjectType()->unsetOffset($offsetType); @@ -576,9 +585,6 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType return new BooleanType(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return $this->getStaticObjectType()->getCallableParametersAcceptors($scope); @@ -594,6 +600,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return $this->getStaticObjectType()->toString(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 71d93671a6..cc114d5c34 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -310,6 +310,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); @@ -325,6 +330,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -355,6 +365,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 7e08d4063b..5fe9ae444a 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -56,6 +56,11 @@ 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()); @@ -67,7 +72,10 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - return new StringType(); + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type @@ -82,12 +90,20 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni } if ($offsetType->isInteger()->yes() || $offsetType instanceof MixedType) { - return new StringType(); + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -133,6 +149,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php index 44cac2d98e..2b3b4a45c5 100644 --- a/src/Type/Traits/ConstantNumericComparisonTypeTrait.php +++ b/src/Type/Traits/ConstantNumericComparisonTypeTrait.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Traits; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; @@ -22,6 +23,7 @@ public function getSmallerType(): Type if (!(bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); @@ -31,6 +33,7 @@ public function getSmallerOrEqualType(): Type { $subtractedTypes = [ IntegerRangeType::createAllGreaterThan($this->value), + // subtract range when we support float-ranges ]; if (!(bool) $this->value) { @@ -45,6 +48,7 @@ public function getGreaterType(): Type $subtractedTypes = [ new NullType(), new ConstantBooleanType(false), + new ConstantFloatType(0.0), // subtract range when we support float-ranges IntegerRangeType::createAllSmallerThanOrEqualTo($this->value), ]; @@ -64,6 +68,7 @@ public function getGreaterOrEqualType(): Type if ((bool) $this->value) { $subtractedTypes[] = new NullType(); $subtractedTypes[] = new ConstantBooleanType(false); + $subtractedTypes[] = new ConstantFloatType(0.0); // subtract range when we support float-ranges } return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); diff --git a/src/Type/Traits/ConstantScalarTypeTrait.php b/src/Type/Traits/ConstantScalarTypeTrait.php index cca9b12008..5a8d6dcfc5 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -24,7 +24,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return AcceptsResult::createFromBoolean($this->value === $type->value); + return AcceptsResult::createFromBoolean($this->equals($type)); } if ($type instanceof CompoundType) { @@ -37,7 +37,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return TrinaryLogic::createFromBoolean($this->equals($type)); } if ($type instanceof parent) { diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 144938fd61..a749a6fae0 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -222,6 +222,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->resolve()->isOffsetAccessible(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->resolve()->isOffsetAccessLegal(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->resolve()->hasOffsetValueType($offsetType); @@ -237,6 +242,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->resolve()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->resolve()->setExistingOffsetValueType($offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { return $this->resolve()->unsetOffset($offsetType); @@ -317,6 +327,11 @@ public function toNumber(): Type return $this->resolve()->toNumber(); } + public function toAbsoluteNumber(): Type + { + return $this->resolve()->toAbsoluteNumber(); + } + public function toInteger(): Type { return $this->resolve()->toInteger(); diff --git a/src/Type/Traits/MaybeCallableTypeTrait.php b/src/Type/Traits/MaybeCallableTypeTrait.php index a6f5ee2ec3..cc22cac99b 100644 --- a/src/Type/Traits/MaybeCallableTypeTrait.php +++ b/src/Type/Traits/MaybeCallableTypeTrait.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Traits; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; @@ -15,9 +14,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; diff --git a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 55889eb574..6e83395e95 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -29,6 +29,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; diff --git a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php index 929d8c0002..d74dc8d0d1 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -29,6 +29,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 8105cd0555..0c37e439c7 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -233,6 +233,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 123b2892f7..3ac0a41c24 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -4,11 +4,11 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -32,7 +32,7 @@ interface Type */ public function getReferencedClasses(): array; - /** @return list */ + /** @return list */ public function getObjectClassNames(): array; /** @@ -131,12 +131,16 @@ public function isList(): TrinaryLogic; public function isOffsetAccessible(): TrinaryLogic; + public function isOffsetAccessLegal(): TrinaryLogic; + public function hasOffsetValueType(Type $offsetType): TrinaryLogic; public function getOffsetValueType(Type $offsetType): Type; public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type; + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type; + public function unsetOffset(Type $offsetType): Type; public function getKeysArray(): Type; @@ -183,7 +187,7 @@ public function exponentiate(Type $exponent): Type; public function isCallable(): TrinaryLogic; /** - * @return ParametersAcceptor[] + * @return CallableParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array; @@ -309,6 +313,8 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap; */ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array; + public function toAbsoluteNumber(): Type; + /** * Traverses inner types * diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 5c8e461cfa..c0564cb4a4 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -591,6 +591,12 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array if (!($innerType instanceof AccessoryType) && !($innerType instanceof CallableType)) { continue; } + if ($innerType instanceof HasOffsetType) { + $offset = $innerType->getOffsetType(); + if ($offset instanceof ConstantStringType || $offset instanceof ConstantIntegerType) { + $innerType = new HasOffsetValueType($offset, $arrayType->getIterableValueType()); + } + } if ($innerType instanceof HasOffsetValueType) { $accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType; continue; @@ -643,7 +649,7 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array } /** - * @param Type[] $arrayTypes + * @param list $arrayTypes * @return Type[] */ private static function processArrayTypes(array $arrayTypes): array @@ -664,14 +670,26 @@ private static function processArrayTypes(array $arrayTypes): array $valueTypesForGeneralArray = []; $generalArrayOccurred = false; $constantKeyTypesNumbered = []; + $filledArrays = 0; + $overflowed = false; /** @var int|float $nextConstantKeyTypeIndex */ $nextConstantKeyTypeIndex = 1; + $constantArraysMap = array_map( + static fn (Type $t) => $t->getConstantArrays(), + $arrayTypes, + ); - foreach ($arrayTypes as $arrayType) { - if ($generalArrayOccurred || !$arrayType->isConstantArray()->yes()) { + foreach ($arrayTypes as $arrayIdx => $arrayType) { + $constantArrays = $constantArraysMap[$arrayIdx]; + $isConstantArray = $constantArrays !== []; + if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) { + $filledArrays++; + } + + if ($generalArrayOccurred || !$isConstantArray) { foreach ($arrayType->getArrays() as $type) { - $keyTypesForGeneralArray[] = $type->getKeyType(); + $keyTypesForGeneralArray[] = $type->getIterableKeyType(); $valueTypesForGeneralArray[] = $type->getItemType(); $generalArrayOccurred = true; } @@ -693,13 +711,18 @@ private static function processArrayTypes(array $arrayTypes): array $nextConstantKeyTypeIndex *= 2; if (!is_int($nextConstantKeyTypeIndex)) { $generalArrayOccurred = true; + $overflowed = true; continue 2; } } } } - if ($generalArrayOccurred) { + if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) { + $reducedArrayTypes = self::reduceArrays($arrayTypes, false); + if (count($reducedArrayTypes) === 1) { + return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)]; + } $scopes = []; $useTemplateArray = true; foreach ($arrayTypes as $arrayType) { @@ -732,7 +755,7 @@ private static function processArrayTypes(array $arrayTypes): array ]; } - $reducedArrayTypes = self::reduceArrays($arrayTypes); + $reducedArrayTypes = self::reduceArrays($arrayTypes, true); return array_map( static fn (Type $arrayType) => self::intersect($arrayType, ...$accessoryTypes), @@ -748,68 +771,68 @@ private static function optimizeConstantArrays(array $types): array { $constantArrayValuesCount = self::countConstantArrayValueTypes($types); - if ($constantArrayValuesCount > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $results = []; - foreach ($types as $type) { - $results[] = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { - if ($type instanceof ConstantArrayType) { - if ($type->isIterableAtLeastOnce()->no()) { - return $type; - } + if ($constantArrayValuesCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + return $types; + } - $isList = true; - $valueTypes = []; - $keyTypes = []; - $nextAutoIndex = 0; - foreach ($type->getKeyTypes() as $i => $innerKeyType) { - if (!$innerKeyType instanceof ConstantIntegerType) { - $isList = false; - } elseif ($innerKeyType->getValue() !== $nextAutoIndex) { - $isList = false; - $nextAutoIndex = $innerKeyType->getValue() + 1; - } else { - $nextAutoIndex++; - } - - $generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific()); - $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; - - $innerValueType = $type->getValueTypes()[$i]; - $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type, callable $innerTraverse) use ($traverse): Type { - if ($type instanceof ArrayType) { - return TypeCombinator::intersect($type, new OversizedArrayType()); - } - - return $traverse($type); - }); - $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; - } + $results = []; + foreach ($types as $type) { + $results[] = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if (!$type instanceof ConstantArrayType) { + return $traverse($type); + } + + if ($type->isIterableAtLeastOnce()->no()) { + return $type; + } + + $isList = true; + $valueTypes = []; + $keyTypes = []; + $nextAutoIndex = 0; + foreach ($type->getKeyTypes() as $i => $innerKeyType) { + if (!$innerKeyType instanceof ConstantIntegerType) { + $isList = false; + } elseif ($innerKeyType->getValue() !== $nextAutoIndex) { + $isList = false; + $nextAutoIndex = $innerKeyType->getValue() + 1; + } else { + $nextAutoIndex++; + } - $keyType = TypeCombinator::union(...array_values($keyTypes)); - $valueType = TypeCombinator::union(...array_values($valueTypes)); + $generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific()); + $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; - $arrayType = new ArrayType($keyType, $valueType); - if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + $innerValueType = $type->getValueTypes()[$i]; + $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type, callable $innerTraverse) use ($traverse): Type { + if ($type instanceof ArrayType) { + return TypeCombinator::intersect($type, new OversizedArrayType()); } - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); - } + return $traverse($type); + }); + $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; + } - return $traverse($type); - }); - } + $keyType = TypeCombinator::union(...array_values($keyTypes)); + $valueType = TypeCombinator::union(...array_values($valueTypes)); - return $results; + $arrayType = new ArrayType($keyType, $valueType); + if ($isList) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } + + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); + }); } - return $types; + return $results; } /** * @param Type[] $types */ - private static function countConstantArrayValueTypes(array $types): int + public static function countConstantArrayValueTypes(array $types): int { $constantArrayValuesCount = 0; foreach ($types as $type) { @@ -825,16 +848,21 @@ private static function countConstantArrayValueTypes(array $types): int } /** - * @param Type[] $constantArrays - * @return Type[] + * @param list $constantArrays + * @return list */ - private static function reduceArrays(array $constantArrays): array + private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array { $newArrays = []; $arraysToProcess = []; $emptyArray = null; foreach ($constantArrays as $constantArray) { if (!$constantArray->isConstantArray()->yes()) { + // This is an optimization for current use-case of $preserveTaggedUnions=false, where we need + // one constant array as a result, or we generalize the $constantArrays. + if (!$preserveTaggedUnions) { + return $constantArrays; + } $newArrays[] = $constantArray; continue; } @@ -880,7 +908,8 @@ private static function reduceArrays(array $constantArrays): array } if ( - $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i]) ) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); @@ -889,13 +918,25 @@ private static function reduceArrays(array $constantArrays): array } if ( - $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j]) ) { $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); unset($arraysToProcess[$j]); continue 1; } + + if ( + !$preserveTaggedUnions + // both arrays have same keys + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + ) { + $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + unset($arraysToProcess[$i]); + continue 2; + } } } @@ -1003,7 +1044,7 @@ public static function intersect(Type ...$types): Type if ($hasOffsetValueTypeCount > 32) { $newTypes[] = new OversizedArrayType(); - $types = array_values($newTypes); + $types = $newTypes; $typesCount = count($types); } @@ -1200,7 +1241,7 @@ public static function intersect(Type ...$types): Type ($types[$i] instanceof ArrayType || $types[$i] instanceof IterableType) && ($types[$j] instanceof ArrayType || $types[$j] instanceof IterableType) ) { - $keyType = self::intersect($types[$i]->getKeyType(), $types[$j]->getKeyType()); + $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType()); $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType()); if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) { $types[$j] = new IterableType($keyType, $itemType); diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index 03cc1c9d5f..09e33f5c9b 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -184,7 +184,7 @@ public static function decideType( foreach ($phpDocType->getTypes() as $innerType) { if ($innerType instanceof ArrayType) { $innerTypes[] = new IterableType( - $innerType->getKeyType(), + $innerType->getIterableKeyType(), $innerType->getItemType(), ); } else { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f9c97c03aa..1a8ac35969 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -12,7 +12,6 @@ use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection; @@ -27,7 +26,11 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use function array_diff_assoc; +use function array_fill_keys; use function array_map; +use function array_merge; +use function array_slice; use function array_unique; use function array_values; use function count; @@ -120,27 +123,42 @@ public function getReferencedClasses(): array public function getObjectClassNames(): array { - return array_values(array_unique($this->pickFromTypes(static fn (Type $type) => $type->getObjectClassNames()))); + return array_values(array_unique($this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassNames(), + static fn (Type $type) => $type->isObject()->yes(), + ))); } public function getObjectClassReflections(): array { - return $this->pickFromTypes(static fn (Type $type) => $type->getObjectClassReflections()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassReflections(), + static fn (Type $type) => $type->isObject()->yes(), + ); } public function getArrays(): array { - return $this->pickFromTypes(static fn (Type $type) => $type->getArrays()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); } public function getConstantArrays(): array { - return $this->pickFromTypes(static fn (Type $type) => $type->getConstantArrays()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); } public function getConstantStrings(): array { - return $this->pickFromTypes(static fn (Type $type) => $type->getConstantStrings()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantStrings(), + static fn (Type $type) => $type->isString()->yes(), + ); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic @@ -160,10 +178,6 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return AcceptsResult::createYes(); } - if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { - return $type->isAcceptedWithReasonBy($this, $strictTypes); - } - $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))); @@ -172,6 +186,10 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $result; } + if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + if ($type instanceof TemplateUnionType) { return $result->or($type->isAcceptedWithReasonBy($this, $strictTypes)); } @@ -291,6 +309,26 @@ public function describe(VerbosityLevel $level): string } } + if ($level->isPrecise()) { + $duplicates = array_diff_assoc($typeNames, array_unique($typeNames)); + if (count($duplicates) > 0) { + $indexByDuplicate = array_fill_keys($duplicates, 0); + foreach ($typeNames as $key => $typeName) { + if (!isset($indexByDuplicate[$typeName])) { + continue; + } + + $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName]; + } + } + } else { + $typeNames = array_unique($typeNames); + } + + if (count($typeNames) > 1024) { + return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}"; + } + return implode('|', $typeNames); }; @@ -609,6 +647,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal()); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); @@ -638,6 +681,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + } + public function unsetOffset(Type $offsetType): Type { return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); @@ -690,7 +738,10 @@ public function shuffleArray(): Type public function getEnumCases(): array { - return $this->pickFromTypes(static fn (Type $type) => $type->getEnumCases()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getEnumCases(), + static fn (Type $type) => $type->isObject()->yes(), + ); } public function isCallable(): TrinaryLogic @@ -698,20 +749,23 @@ public function isCallable(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + $acceptors = []; + foreach ($this->types as $type) { if ($type->isCallable()->no()) { continue; } - return $type->getCallableParametersAcceptors($scope); + $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope)); } - throw new ShouldNotHappenException(); + if (count($acceptors) === 0) { + throw new ShouldNotHappenException(); + } + + return $acceptors; } public function isCloneable(): TrinaryLogic @@ -824,6 +878,13 @@ public function toNumber(): Type return $type; } + public function toAbsoluteNumber(): Type + { + $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); + + return $type; + } + public function toString(): Type { $type = $this->unionTypes(static fn (Type $type): Type => $type->toString()); @@ -1038,15 +1099,19 @@ protected function unionTypes(callable $getType): Type */ protected function pickTypes(callable $getTypes): array { - return $this->pickFromTypes($getTypes); + return $this->pickFromTypes($getTypes, static fn () => false); } /** * @template T * @param callable(Type $type): list $getValues + * @param callable(Type $type): bool $criteria * @return list */ - protected function pickFromTypes(callable $getValues): array + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array { $values = []; foreach ($this->types as $type) { diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index db153a6a01..a2c28b3153 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -109,6 +109,13 @@ public static function sortTypes(array $types): array return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } + if ( + ($a instanceof CallableType || $a instanceof ClosureType) + && ($b instanceof CallableType || $b instanceof ClosureType) + ) { + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + } + return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); }); return $types; diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 7a1e8707f6..c80bcbce99 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -78,6 +78,11 @@ public function isValue(): bool return $this->value === self::VALUE; } + public function isPrecise(): bool + { + return $this->value === self::PRECISE; + } + /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 07f755ce16..8cbd1076ba 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -97,6 +97,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); @@ -122,6 +127,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/stubs/ReflectionMethod.stub b/stubs/ReflectionMethod.stub index 8107e50667..cb060a6c97 100644 --- a/stubs/ReflectionMethod.stub +++ b/stubs/ReflectionMethod.stub @@ -8,4 +8,9 @@ class ReflectionMethod */ public $class; + /** + * @var non-falsy-string + */ + public $name; + } diff --git a/stubs/SplObjectStorage.stub b/stubs/SplObjectStorage.stub index 17910633ab..3f7c44f120 100644 --- a/stubs/SplObjectStorage.stub +++ b/stubs/SplObjectStorage.stub @@ -42,12 +42,12 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess public function getInfo() { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAll(SplObjectStorage $storage): void { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAllExcept(SplObjectStorage $storage): void { } diff --git a/stubs/WeakReference.stub b/stubs/WeakReference.stub index 5f23dbca9c..060dfe1a8d 100644 --- a/stubs/WeakReference.stub +++ b/stubs/WeakReference.stub @@ -26,4 +26,9 @@ final class WeakReference */ final class WeakMap implements \ArrayAccess, \Countable, \IteratorAggregate { + /** + * @param TKey $offset + * @return TValue + */ + public function offsetGet($offset) {} } diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index 65064e2327..25c73249ec 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -23,7 +23,6 @@ function array_reduce( * * @param TArray $array * @param callable(T,T):int $callback - * @param-out (TArray is non-empty-array ? non-empty-array : array) $array */ function uasort(array &$array, callable $callback): bool {} @@ -34,7 +33,6 @@ function uasort(array &$array, callable $callback): bool * * @param TArray $array * @param callable(T,T):int $callback - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function usort(array &$array, callable $callback): bool {} @@ -46,7 +44,6 @@ function usort(array &$array, callable $callback): bool * * @param TArray $array * @param callable(TKey,TKey):int $callback - * @param-out (TArray is non-empty-array ? non-empty-array : array) $array */ function uksort(array &$array, callable $callback): bool { diff --git a/stubs/core.stub b/stubs/core.stub index 02de5df68c..2cb6f29448 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -75,13 +75,13 @@ function str_shuffle(string $string): string {} /** * @param array $result - * @param-out array $result + * @param-out array|string> $result */ function parse_str(string $string, array &$result): void {} /** * @param array $result - * @param-out array $result + * @param-out array|string> $result */ function mb_parse_str(string $string, array &$result): bool {} @@ -119,7 +119,6 @@ function passthru(string $command, &$result_code): ?bool {} * @template TArray as array * * @param TArray $array - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function shuffle(array &$array): bool { @@ -130,7 +129,6 @@ function shuffle(array &$array): bool * @template TArray as array * * @param TArray $array - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function sort(array &$array, int $flags = SORT_REGULAR): bool { @@ -141,7 +139,6 @@ function sort(array &$array, int $flags = SORT_REGULAR): bool * @template TArray as array * * @param TArray $array - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function rsort(array &$array, int $flags = SORT_REGULAR): bool { @@ -196,7 +193,7 @@ function sscanf(string $string, string $format, &$war, &...$vars) {} * ? list> * : (TFlags is 770 * ? list> - * : array + * : (TFlags is 0 ? array> : array) * ) * ) * ) diff --git a/stubs/mysqli.stub b/stubs/mysqli.stub index 77823aaa54..cc88f4f6e1 100644 --- a/stubs/mysqli.stub +++ b/stubs/mysqli.stub @@ -14,8 +14,26 @@ class mysqli_result * @var int<0,max>|numeric-string */ public $num_rows; + + /** + * @template T of object + * @param class-string $class + * @param array $constructor_args + * @return T|null|false + */ + function fetch_object(string $class = 'stdClass', array $constructor_args = []) {} } + +/** + * @template T of object + * + * @param class-string $class + * @param array $constructor_args + * @return T|null|false + */ +function mysqli_fetch_object(mysqli_result $result, string $class = 'stdClass', array $constructor_args = []) {} + class mysqli_stmt { /** diff --git a/stubs/runtime/Enum/UnitEnum.php b/stubs/runtime/Enum/UnitEnum.php index a8d1a8d468..0a9e6282e6 100644 --- a/stubs/runtime/Enum/UnitEnum.php +++ b/stubs/runtime/Enum/UnitEnum.php @@ -8,7 +8,7 @@ interface UnitEnum { /** - * @return static[] + * @return list */ public static function cases(): array; } diff --git a/stubs/socket_select.stub b/stubs/socket_select.stub new file mode 100644 index 0000000000..25a6cb1f1a --- /dev/null +++ b/stubs/socket_select.stub @@ -0,0 +1,12 @@ +|null &$read + * @param array|null &$write + * @param array|null &$except + * @param-out ($read is not null ? array : null) $read + * @param-out ($write is not null ? array : null) $write + * @param-out ($except is not null ? array : null) $except + * @return int|false + */ +function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0) {} diff --git a/stubs/socket_select_php8.stub b/stubs/socket_select_php8.stub new file mode 100644 index 0000000000..f2a1704b42 --- /dev/null +++ b/stubs/socket_select_php8.stub @@ -0,0 +1,11 @@ +|null &$read + * @param array|null &$write + * @param array|null &$except + * @param-out ($read is not null ? array : null) $read + * @param-out ($write is not null ? array : null) $write + * @param-out ($except is not null ? array : null) $except + */ +function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0): int|false {} diff --git a/stubs/typeCheckingFunctions.stub b/stubs/typeCheckingFunctions.stub index 82d9c83cde..4f06338125 100644 --- a/stubs/typeCheckingFunctions.stub +++ b/stubs/typeCheckingFunctions.stub @@ -105,7 +105,7 @@ function is_long(mixed $value): bool } /** - * @phpstan-assert-if-true resource $value + * @phpstan-assert-if-true =resource $value * @return bool */ function is_resource(mixed $value): bool diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index a36e9bfe45..b84ff5feb0 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -5,7 +5,6 @@ use Bug4288\MyClass; use Bug4713\Service; use ExtendingKnownClassWithCheck\Foo; -use PHPStan\File\FileHelper; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -264,7 +263,7 @@ public function testBug3686(): void public function testBug3379(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-3379.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3379.php'); $this->assertCount(1, $errors); $this->assertSame('Constant SOME_UNKNOWN_CONST not found.', $errors[0]->getMessage()); } @@ -443,7 +442,7 @@ public function testBug5231Two(): void public function testBug5529(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-5529.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-5529.php'); $this->assertNoErrors($errors); } @@ -716,7 +715,7 @@ public function testBug6192(): void public function testBug7068(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-7068.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7068.php'); $this->assertNoErrors($errors); } @@ -726,7 +725,7 @@ public function testDiscussion6993(): void $this->markTestSkipped('Test requires PHP 8.0.'); } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-6993.php'); + $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()); } @@ -743,7 +742,7 @@ public function testBug7078(): void $this->markTestSkipped('Test requires PHP 8.0.'); } - $errors = $this->runAnalyse(__DIR__ . '/data/bug-7078.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7078.php'); $this->assertNoErrors($errors); } @@ -759,7 +758,7 @@ public function testBug7116(): void public function testBug3853(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-3853.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-3853.php'); $this->assertNoErrors($errors); } @@ -828,7 +827,7 @@ public function testBug7094(): void public function testOffsetAccess(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/offset-access.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/offset-access.php'); $this->assertCount(1, $errors); $this->assertSame('PHPDoc tag @return contains unresolvable type.', $errors[0]->getMessage()); $this->assertSame(42, $errors[0]->getLine()); @@ -866,7 +865,7 @@ public function testBug7381(): void public function testBug7153(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/bug-7153.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7153.php'); $this->assertNoErrors($errors); } @@ -1053,7 +1052,7 @@ public function testBug7110(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-7110.php'); $this->assertCount(1, $errors); $this->assertSame('Parameter #1 $s of function Bug7110\takesInt expects int, string given.', $errors[0]->getMessage()); - $this->assertSame(30, $errors[0]->getLine()); + $this->assertSame(34, $errors[0]->getLine()); } public function testBug8376(): void @@ -1064,7 +1063,7 @@ public function testBug8376(): void public function testAssertDocblock(): void { - $errors = $this->runAnalyse(__DIR__ . '/data/assert-docblock.php'); + $errors = $this->runAnalyse(__DIR__ . '/nsrt/assert-docblock.php'); $this->assertCount(4, $errors); $this->assertSame('Call to method AssertDocblock\A::testInt() with string will always evaluate to false.', $errors[0]->getMessage()); $this->assertSame(218, $errors[0]->getLine()); @@ -1268,7 +1267,7 @@ public function testBug9994(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-9994.php'); $this->assertCount(2, $errors); $this->assertSame('Negated boolean expression is always false.', $errors[0]->getMessage()); - $this->assertSame('Parameter #2 $callback of function array_filter expects (callable(1|2|3|null): mixed)|null, false given.', $errors[1]->getMessage()); + $this->assertSame('Parameter #2 $callback of function array_filter expects (callable(1|2|3|null): bool)|null, false given.', $errors[1]->getMessage()); } public function testBug10049(): void @@ -1307,6 +1306,137 @@ public function testBug10302(): void $this->assertNoErrors($errors); } + public function testBug10358(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10358.php'); + $this->assertCount(1, $errors); + $this->assertSame('Cannot use Ns\Foo2 as Foo because the name is already in use', $errors[0]->getMessage()); + $this->assertSame(6, $errors[0]->getLine()); + } + + public function testBug10509(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10509.php'); + $this->assertCount(2, $errors); + $this->assertSame('Method Bug10509\Foo::doFoo() has no return type specified.', $errors[0]->getMessage()); + $this->assertSame('PHPDoc tag @return contains unresolvable type.', $errors[1]->getMessage()); + } + + public function testBug10538(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10538.php'); + $this->assertNoErrors($errors); + } + + public function testBug10772(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10772.php'); + $this->assertNoErrors($errors); + } + + public function testBug10985(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10985.php'); + $this->assertNoErrors($errors); + } + + public function testBug10979(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10979.php'); + $this->assertNoErrors($errors); + } + + public function testBug11026(): void + { + if (PHP_VERSION_ID < 70300) { + $this->markTestSkipped('Test requires PHP 7.3.'); + } + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11026.php'); + $this->assertNoErrors($errors); + } + + public function testBug10867(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10867.php'); + $this->assertNoErrors($errors); + } + + public function testBug11263(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11263.php'); + $this->assertNoErrors($errors); + } + + public function testBug11147(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11147.php'); + $this->assertCount(1, $errors); + $this->assertSame('Method Bug11147\RedisAdapter::createConnection() has invalid return type Bug11147\NonExistentClass.', $errors[0]->getMessage()); + } + + public function testBug11283(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11283.php'); + $this->assertNoErrors($errors); + } + + public function testBug11292(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11292.php'); + $this->assertNoErrors($errors); + } + + public function testBug11297(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11297.php'); + $this->assertNoErrors($errors); + } + + public function testBug5597(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5597.php'); + $this->assertNoErrors($errors); + } + + public function testBug11511(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11511.php'); + $this->assertCount(1, $errors); + $this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage()); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] @@ -1314,13 +1444,16 @@ public function testBug10302(): void private function runAnalyse(string $file, ?array $allAnalysedFiles = null): array { $file = $this->getFileHelper()->normalizePath($file); - /** @var Analyser $analyser */ + $analyser = self::getContainer()->getByType(Analyser::class); - /** @var FileHelper $fileHelper */ - $fileHelper = self::getContainer()->getByType(FileHelper::class); - $errors = $analyser->analyse([$file], null, null, true, $allAnalysedFiles)->getErrors(); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + $errors = $finalizer->finalize( + $analyser->analyse([$file], null, null, true, $allAnalysedFiles), + false, + true, + )->getErrors(); foreach ($errors as $error) { - $this->assertSame($fileHelper->normalizePath($file), $error->getFilePath()); + $this->assertSame($file, $error->getFilePath()); } return $errors; diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 535afd7119..b149c39ab0 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -11,6 +11,8 @@ use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\Printer\Printer; use PHPStan\Parser\RichParser; @@ -206,6 +208,32 @@ public function dataTrueAndFalse(): array ]; } + /** + * @dataProvider dataTrueAndFalse + */ + public function testIgnoreErrorByPathAndIdentifierCountsCorrectly(bool $onlyFiles): void + { + $ignoreErrors = [ + [ + 'identifier' => 'tests.alwaysFail', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'identifier' => 'tests.alwaysFail', + 'count' => 2, + 'path' => __DIR__ . '/data/two-different-fails.php', + ], + ]; + + $filesToAnalyze = [ + __DIR__ . '/data/two-fails.php', + __DIR__ . '/data/two-different-fails.php', + ]; + $result = $this->runAnalyser($ignoreErrors, true, $filesToAnalyze, $onlyFiles); + $this->assertNoErrors($result); + } + /** * @dataProvider dataTrueAndFalse */ @@ -628,7 +656,7 @@ private function runAnalyser( bool $enableIgnoreErrorsWithinPhpDocs = true, ): array { - $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors, $enableIgnoreErrorsWithinPhpDocs); + $analyser = $this->createAnalyser($enableIgnoreErrorsWithinPhpDocs); if (is_string($filePaths)) { $filePaths = [$filePaths]; @@ -648,6 +676,18 @@ private function runAnalyser( $analyserResult = $analyser->analyse($normalizedFilePaths); + $finalizer = new AnalyserResultFinalizer( + new DirectRuleRegistry([]), + new RuleErrorTransformer(), + $this->createScopeFactory( + $this->createReflectionProvider(), + self::getContainer()->getService('typeSpecifier'), + ), + new LocalIgnoresProcessor(), + $reportUnmatchedIgnoredErrors, + ); + $analyserResult = $finalizer->finalize($analyserResult, $onlyFiles, false)->getAnalyserResult(); + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $normalizedFilePaths, $analyserResult->hasReachedInternalErrorsCountLimit()); $errors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); $errors = array_merge($errors, $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages()); @@ -657,11 +697,11 @@ private function runAnalyser( return array_merge( $errors, - $analyserResult->getInternalErrors(), + array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $analyserResult->getInternalErrors()), ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enableIgnoreErrorsWithinPhpDocs): Analyser + private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser { $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), @@ -680,6 +720,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enable self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), self::getClassReflectionExtensionRegistryProvider(), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), $this->getParser(), $fileTypeMapper, self::getContainer()->getByType(StubPhpDocProvider::class), @@ -690,6 +731,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enable $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), false, true, @@ -699,6 +741,8 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enable true, $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], + self::getContainer()->getParameter('featureToggles')['paramOutType'], + self::getContainer()->getParameter('featureToggles')['preciseMissingReturn'], ); $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); $fileAnalyser = new FileAnalyser( @@ -714,7 +758,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enable ), new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), new RuleErrorTransformer(), - $reportUnmatchedIgnoredErrors, + new LocalIgnoresProcessor(), ); return new Analyser( diff --git a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php index 311d189ca6..3d28594e7e 100644 --- a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php @@ -202,6 +202,46 @@ public function dataReorderValid(): iterable new StringType(), ], ]; + + yield [ + [ + ['one', true, false, new IntegerType()], + ['two', true, false, new StringType()], + ['three', true, false, new FloatType()], + ], + [], + [], + ]; + + yield [ + [ + ['one', true, false, new IntegerType()], + ['two', true, false, new StringType()], + ['three', true, false, new FloatType()], + ], + [ + [new StringType(), 'onee'], + ], + [ + new StringType(), + ], + ]; + + yield [ + [ + ['one', true, false, new IntegerType()], + ['two', true, false, new StringType()], + ['three', true, false, new FloatType()], + ], + [ + [new IntegerType(), null], + [new StringType(), 'onee'], + ], + [ + new IntegerType(), + new StringType(), + ], + ]; } /** @@ -282,29 +322,6 @@ public function dataReorderInvalid(): iterable [new StringType(), 'three'], ], ]; - - yield [ - [ - ['one', true, false, new IntegerType()], - ['two', true, false, new StringType()], - ['three', true, false, new FloatType()], - ], - [ - [new StringType(), 'onee'], - ], - ]; - - yield [ - [ - ['one', true, false, new IntegerType()], - ['two', true, false, new StringType()], - ['three', true, false, new FloatType()], - ], - [ - [new IntegerType(), null], - [new StringType(), 'onee'], - ], - ]; } /** diff --git a/tests/PHPStan/Analyser/Bug10922Test.php b/tests/PHPStan/Analyser/Bug10922Test.php new file mode 100644 index 0000000000..2dccf946c8 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10922Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-10922.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-10922.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug10980Test.php b/tests/PHPStan/Analyser/Bug10980Test.php new file mode 100644 index 0000000000..6c3821447f --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10980Test.php @@ -0,0 +1,28 @@ +assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/Bug11009Test.php b/tests/PHPStan/Analyser/Bug11009Test.php new file mode 100644 index 0000000000..480e170756 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug11009Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-11009.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-11009.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php new file mode 100644 index 0000000000..ff0a0daa9e --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -0,0 +1,43 @@ + + */ +class Bug9307CallMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, 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, true), + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-9307.php'], []); + } + +} diff --git a/tests/PHPStan/Analyser/Bug9307Test.php b/tests/PHPStan/Analyser/Bug9307Test.php new file mode 100644 index 0000000000..6502ac2701 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-9307.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-9307.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php index 68075be52d..168b6799b1 100644 --- a/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php +++ b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Analyser\Ignore; use PHPStan\Testing\PHPStanTestCase; +use function array_pop; +use function substr_count; use const PHP_EOL; class IgnoreLexerTest extends PHPStanTestCase @@ -69,7 +71,7 @@ public function dataTokenize(): iterable [ ['return.ref', IgnoreLexer::TOKEN_IDENTIFIER, 1], [' ', IgnoreLexer::TOKEN_WHITESPACE, 1], - [PHP_EOL . ' ', IgnoreLexer::TOKEN_EOL, 1], + [PHP_EOL . ' ', IgnoreLexer::TOKEN_END, 1], ['(', IgnoreLexer::TOKEN_OPEN_PARENTHESIS, 2], ['čičí', IgnoreLexer::TOKEN_OTHER, 2], [')', IgnoreLexer::TOKEN_CLOSE_PARENTHESIS, 2], @@ -84,7 +86,11 @@ public function dataTokenize(): iterable public function testTokenize(string $input, array $expectedTokens): void { $lexer = new IgnoreLexer(); - $this->assertSame($expectedTokens, $lexer->tokenize($input)); + $tokens = $lexer->tokenize($input); + $lastToken = array_pop($tokens); + + $this->assertSame(['', IgnoreLexer::TOKEN_END, substr_count($input, PHP_EOL) + 1], $lastToken); + $this->assertSame($expectedTokens, $tokens); } } diff --git a/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php new file mode 100644 index 0000000000..f810ed04a2 --- /dev/null +++ b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/immediately-called-function-without-implicit-throw.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/data/immediately-called-function-without-implicit-throw.neon'], + ); + } + +} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 430e94505c..4d0ca33581 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -302,7 +302,7 @@ public function dataAssignInIf(): array $testScope, 'matches', TrinaryLogic::createYes(), - 'array', + 'array{0?: string}', ], [ $testScope, @@ -343,7 +343,7 @@ public function dataAssignInIf(): array $testScope, 'matches2', TrinaryLogic::createMaybe(), - 'array', + 'array{0?: string}', ], [ $testScope, @@ -355,13 +355,13 @@ public function dataAssignInIf(): array $testScope, 'matches3', TrinaryLogic::createYes(), - 'array', + 'array{0?: string}', ], [ $testScope, 'matches4', TrinaryLogic::createMaybe(), - 'array', + 'array{}|array{string}', ], [ $testScope, @@ -415,7 +415,7 @@ public function dataAssignInIf(): array $testScope, 'ternaryMatches', TrinaryLogic::createYes(), - 'array', + 'array{0?: string}', ], [ $testScope, @@ -1073,7 +1073,7 @@ public function dataArrayDestructuring(): array '$secondStringArray', ], [ - 'string', + 'non-empty-string', '$thirdStringArray', ], [ @@ -1089,7 +1089,7 @@ public function dataArrayDestructuring(): array '$secondStringArrayList', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayList', ], [ @@ -1097,35 +1097,35 @@ public function dataArrayDestructuring(): array '$fourthStringArrayList', ], [ - 'string', + 'non-empty-string', '$firstStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$secondStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$fourthStringArrayForeach', ], [ - 'string', + 'non-empty-string', '$firstStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$secondStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$thirdStringArrayForeachList', ], [ - 'string', + 'non-empty-string', '$fourthStringArrayForeachList', ], [ @@ -2904,7 +2904,7 @@ public function dataBinaryOperations(): array '$fooString[4]', ], [ - 'string', + 'non-empty-string', '$fooString[$integer]', ], [ @@ -2956,19 +2956,19 @@ public function dataBinaryOperations(): array '$string--', ], [ - 'string', + '(float|int|string)', '++$string', ], [ - 'string', + '(float|int|string)', '--$string', ], [ - 'string', + '(float|int|string)', '$incrementedString', ], [ - 'string', + '(float|int|string)', '$decrementedString', ], [ @@ -3079,14 +3079,6 @@ public function dataBinaryOperations(): array 'bool', 'array_key_exists(\'foo\', $generalArray)', ], - [ - PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle', - 'curl_init()', - ], - [ - PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false', - 'curl_init($string)', - ], [ 'string', 'sprintf($string, $string, 1)', @@ -3620,6 +3612,23 @@ public function testTypeFromFunctionPhpDocsPhpstanPrefix( ); } + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromFunctionPrefixedPhpDocs + */ + public function testTypeFromFunctionPhpDocsPhanPrefix( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/functionPhpDocs-phanPrefix.php'; + $this->assertTypes( + __DIR__ . '/data/functionPhpDocs-phanPrefix.php', + $description, + $expression, + ); + } + public function dataTypeFromMethodPhpDocs(): array { return [ @@ -3829,6 +3838,31 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix( ); } + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsPhanPrefix( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhanPrefix)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhanPrefix)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooPhanPrefix'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-phanPrefix.php', + $description, + $expression, + ); + } + /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs @@ -5458,7 +5492,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}|false', + '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', '$parseUrlConstantUrlUnknownComponent', ], [ @@ -7250,15 +7284,15 @@ public function dataExplode(): array '$sureFalse', ], [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', '$arrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + PHP_VERSION_ID < 80000 ? 'non-empty-list|false' : 'non-empty-list', '$anotherArrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? '(list|false)' : 'list', + PHP_VERSION_ID < 80000 ? '(non-empty-list|false)' : 'non-empty-list', '$benevolentArrayOrFalse', ], ]; @@ -7965,7 +7999,7 @@ public function dataPassedByReference(): array '$matches', ], [ - 'mixed', + 'string', '$s', ], ]; @@ -8367,6 +8401,44 @@ public function testAnonymousClassNameInTrait( ); } + public function dataAnonymousClassNameSameLine(): array + { + return [ + [ + 'AnonymousClass0d7d08272ba2f0a6ef324bb65c679e02', + '$foo', + '$bar', + ], + [ + 'AnonymousClass464f64cbdca25b4af842cae65615bca9', + '$bar', + '$baz', + ], + [ + 'AnonymousClassa9fb472ec9acc5cae3bee4355c296bfa', + '$baz', + 'die', + ], + ]; + } + + /** + * @dataProvider dataAnonymousClassNameSameLine + */ + public function testAnonymousClassNameSameLine( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/anonymous-class-name-same-line.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + public function dataDynamicConstants(): array { return [ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 06ed7c7c45..6a8c6b517d 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -6,7 +6,6 @@ use PHPStan\Testing\TypeInferenceTestCase; use stdClass; use function define; -use function extension_loaded; use const PHP_INT_SIZE; use const PHP_VERSION_ID; @@ -15,644 +14,66 @@ class NodeScopeResolverTest extends TypeInferenceTestCase public function dataFileAsserts(): iterable { - require_once __DIR__ . '/data/implode.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/implode.php'); + yield from $this->gatherAssertTypesFromDirectory(__DIR__ . '/nsrt'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/invalid_type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/json_object_as_array.php'); - - require_once __DIR__ . '/data/bug2574.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php'); - - require_once __DIR__ . '/data/bug2577.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2577.php'); - - require_once __DIR__ . '/data/generics.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics.php'); - - require_once __DIR__ . '/data/generic-class-string.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-class-string.php'); - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-enum-class-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7162.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10201.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10445.php'); - } - - require_once __DIR__ . '/data/generic-generalization.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-generalization.php'); - - require_once __DIR__ . '/data/instanceof.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/named-arguments.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/date.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); - if (PHP_INT_SIZE === 8) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/strtotime-return-type-extensions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9972.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2612.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2677.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2676.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/psalm-prefix-unresolvable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/complex-generics-example.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2648.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2740.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-parameter-remapping.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inheritdoc-constructors.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/list-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2835.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2443.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2750.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2850.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2863.php'); - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/reflection-type.php'); + if (PHP_VERSION_ID < 80200 && PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection-php81.php'); } - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types-first-class-callables.php'); + if (PHP_VERSION_ID < 80000 && PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); } - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types-ftp-connect.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types-ftp-connect-resource.php'); + if (PHP_VERSION_ID < 80300) { + if (PHP_VERSION_ID >= 80200) { + yield from $this->gatherAssertTypes(__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'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php73.php'); + } } - yield from $this->gatherAssertTypes(__DIR__ . '/data/type-change-after-array-access-assignment.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator_to_array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/key-of.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/value-of.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/ext-ds.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-return-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/is-numeric.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/is-a.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/is-subclass-of.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3142.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shapes-keys-strings.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-expr-phpdoc-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3226.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2001.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2232.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3009.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-var.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-param.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-return.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-template.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3266.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3269.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5086.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assign-nested-arrays.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3276.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6856.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/shadowed-trait-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions-namespaced.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/root-scope-maybe-defined.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3336.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/catch-without-variable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/mixed-typehint.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2600-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2600.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-typehint-without-null-in-phpdoc.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/override-root-scope-variable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bitwise-not.php'); - if (extension_loaded('gd')) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/graphics-draw-return-types.php'); - } - - require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'; - 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 from $this->gatherAssertTypes(__DIR__ . '/data/minmax.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); - } if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-php8.php'); + 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 from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array-key-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3133.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-2550.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shape-list-optional.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return-php7.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return-php8.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3875.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10327.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2611.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3548.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10131.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3866.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1014.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-pr-339.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/pow.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-expr.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5351.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-on-expr.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3961-php8.php'); + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-64bit.php'); } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3961.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1924.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/extra-int-types.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/count-type.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-sprintf.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/get-class-static-class.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816-2.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3985.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shift.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-slice.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3990.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3991.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3993.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3997.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4016.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8127.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7944.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6196.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7301.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9472.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9764.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10092.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9084.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/promoted-properties-types.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/early-termination-phpdoc.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3915.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2378.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-do-not-generalize.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9985.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6294.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6462.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2580.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9753.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9721.php'); - } - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9734.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection.php'); - } - if (PHP_VERSION_ID >= 80200) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection-php82.php'); - } elseif (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection-php81.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expr.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/nullsafe.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/specified-types-closure-use.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/specified-types-closure-edge.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/cast-to-numeric-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2539.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2733.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3132.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1233.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/comparison-operators.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3880.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/inc-dec-in-conditions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4099.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3760.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2997.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1657.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2945.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4207.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4206.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-empty-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4205.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variable-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-expression-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1865.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-non-empty-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-dependent-key-value.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variables-type-guard-same-as-type.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variables-arrow-function.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-801.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1209.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9784.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2980.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3986.php'); - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4188.php'); - } - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4339.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-32bit.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4343.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-method.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-constructor.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4351.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-use.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-declare.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4398.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4415.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/compact.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4500.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4504.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4436.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/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__ . '/data/bug-2549.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1945.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2003.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-651.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1283.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4538.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/proc_get_status.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4552.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1897.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1801.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2927.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4558.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4557.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4209.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4209-2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2869.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3024.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3134.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/infer-array-key.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/offset-value-after-assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2112.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-callables.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-string-callables.php'); - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-arrow-functions.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map-closure.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-merge.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-merge2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-sum.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-plus.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4573.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9881.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4577.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4579.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3321.php'); - - require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php'; yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Generics/data/bug-6301.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof-class-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4498.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4587.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4606.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3922.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types-unwrapping.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-types-unwrapping-covariant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nested-generic-incomplete-constructor.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator-iterator.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4642.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-4643.php'); - require_once __DIR__ . '/data/throw-points/helpers.php'; + if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/php8/null-safe-method-call.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4857.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/and.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/array-dim-fetch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/assign-op.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/do-while.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/for.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/foreach.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/func-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/if.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/method-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/or.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/property-fetch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/static-call.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/switch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/throw.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/try-catch-finally.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/variable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/while.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-points/try-catch.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-override.php'); - require_once __DIR__ . '/data/phpdoc-pseudotype-namespace.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-namespace.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-pseudotype-global.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-traits.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4423.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-unions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-parent.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4247.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4267.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2231.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3558.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3351.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4213.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4657.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4707.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4545.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4714.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4725.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4733.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4326.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-987.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3677.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4215.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10224.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4695.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2977.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3190.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ternary-specified-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-560.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/do-not-remember-impure-functions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4190.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/clear-stat-cache.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument-static.php'); - - require_once __DIR__ . '/data/invalidate-object-argument-function.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-object-argument-function.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4588.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4091.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3382.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4177.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2288.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1157.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1597.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3617.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-778.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2969.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3004.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3710.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-505.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1670.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1219.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3302.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1511.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4434.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4231.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4287.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4700.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpdoc-in-closure-bind.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/multi-assign.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-reduce-types-first.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4803.php'); - - require_once __DIR__ . '/data/type-aliases.php'; - - yield from $this->gatherAssertTypes(__DIR__ . '/data/type-aliases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4650.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2906.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeCreateDynamicReturnTypes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeDynamicReturnTypes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeModifyReturnTypes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4821.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4838.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4879.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4820.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4822.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4816.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4757.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4814.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4982.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4761.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3331.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3106.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2640.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2413.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3446.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/getopt.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-default.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4985.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5000.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/number_format.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5140.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4857.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/empty-array-shape.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5089.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3158.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/unable-to-resolve-callback-parameter-type.php'); - require_once __DIR__ . '/../Rules/Functions/data/varying-acceptor.php'; yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/varying-acceptor.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/uksort-bug.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-types.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5219.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/strval.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-next.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-replace-functions.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-substr.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-substr-pre-80.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3981.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4711.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/sscanf.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-offset-get.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-object-lower-bound.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-reflection-interfaces.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4415.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5259.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5293.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5129.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4970.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5322.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5336.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6845.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/splfixedarray-iterator-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5372_2.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-implements.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3379.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/reflectionclass-issue-5511-php8.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/modulo-operator.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/literal-string.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-returns-non-empty-string.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/model-mixin.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5529.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof-php8.php'); - } - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php'); - - if (PHP_INT_SIZE === 8) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5072.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5530.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1861.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4843.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4602.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4499.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2142.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5584.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/math.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1870.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5615.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-unshift.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_map_multiple.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/range-numeric-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/range-int-range.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/missing-closure-native-return-typehint.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4741.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/more-type-strings.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/variadic-parameter-php8.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/no-named-arguments.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4896.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5843.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/eval-implicit-throw.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5628.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5501.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4743.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5992.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6001.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/round-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/round.php'); - } - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287-php81.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287.php'); - } - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5458.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php'); } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/never.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-intersection.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2760.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/new-in-initializers.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5372_2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.php'); if (PHP_VERSION_ID >= 80100) { define('TEST_OBJECT_CONSTANT', new stdClass()); @@ -664,494 +85,55 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/new-in-initializers-runtime.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/first-class-callables.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-is-list-type-specifying.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-is-list-unset.php'); - } - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-unpacking-string-keys.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-array.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/enums-import-alias.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7176.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-enum.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6293.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php72.php'); - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php74.php'); - } - if (PHP_INT_SIZE === 8) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-64bit.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-32bit.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs-phpstanPropertyPrefix.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-destructuring-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-prepare.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-type-set.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/for-loop-i-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5316.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3858.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2806.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5328.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3044.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-readonly-properties.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-array_key_exists-issue.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/equal.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/identical.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5698-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5698-php7.php'); - } - - if (PHP_VERSION_ID >= 70304) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/date-period-return-types.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6404.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6399.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk-php8.php'); - } - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk-php81.php'); - } - - if (PHP_VERSION_ID < 80200) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php82.php'); - } - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6497.php'); - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type-root.php'); - } - - if (PHP_VERSION_ID < 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type-pre-81.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type-post-81.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/template-null-bound.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4592.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4903.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2420.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2718.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3126.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4586.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4887.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions-80.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions-74.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6308.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6329.php'); - if (PHP_VERSION_ID >= 70400) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-6473.php'); } - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6566-types.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6500.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6488.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6624.php'); - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/property-template-tag.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6672.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6687.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-in-union.php'); - - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_match_php7.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_match_php8.php'); - } - - if (PHP_VERSION_ID >= 70300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6654.php'); - } - - require_once __DIR__ . '/data/countable.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6704.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6695.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6433.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6698.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/date-format.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6070.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6108.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1516.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6138.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6174.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5749.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5675.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6505.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6305.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6699.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6715.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6682.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_filter.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5759.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5783.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5668.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-empty-array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5757.php'); - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/nullable-closure-parameter.php'); - } if (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6635.php'); } - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6591.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6790.php'); - } - - if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6859.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/curl_getinfo.php'); - if (PHP_VERSION_ID >= 70300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/curl_getinfo_7.3.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6251.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6870.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4885.php'); - } - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/value-of-enum.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6576.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-strlen-cases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-string-unions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys-php8.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-php8.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-php8.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-php8.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_keys.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_values.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys-php7.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-php7.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-php7.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-php7.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_keys-php7.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_values-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-type-specifying.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-pop.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-push.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-replace.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-reverse.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6889.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6891.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10088.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/shuffle.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/simplexml.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6904.php'); - } - if (PHP_VERSION_ID >= 80300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-native-type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-10212.php'); } - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6936-limit.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5262.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2471.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5846.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5896.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6927.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3853.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-optional-set.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7000.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6383.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-3284.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/int-mask.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-types-constant.php'); - - require_once __DIR__ . '/data/constant-phpdoc-type.php'; - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-phpdoc-type.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6993.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7078.php'); - } - - if (PHP_VERSION_ID >= 80300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php83.php'); - } elseif (PHP_VERSION_ID >= 80200) { - yield from $this->gatherAssertTypes(__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'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php73.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7096.php'); - } - if (PHP_VERSION_ID >= 80300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/json-validate.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/return-type-class-constant.php'); } - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7167.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6864.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7776.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7068.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7115.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-type-identical.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-str-containing-fns.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/fizz-buzz.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4875.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6609.php'); - //define('ALREADY_DEFINED_CONSTANT', true); //yield from $this->gatherAssertTypes(__DIR__ . '/data/already-defined-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/value-of-generic.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/key-of-generic.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7106.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4950.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-reflection-default-values.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/pr-1244.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7144.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7144-composer-integration.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4371.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/initializer-expr-type-resolver.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/offset-access.php'); - - if (PHP_VERSION_ID >= 70300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/str-casing.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-substr-specifying.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/unset-conditional-expressions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-types-inference.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7210.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7341.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-strstr-specifying.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-strrchr-specifying.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-strcasing-specifying.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7374.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/template-constant-bound.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7391.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/finally-scope.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7387.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7353.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7031.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-intersect.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7153.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-non-empty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-haystack-subtract.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4117.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7490.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/remember-possibly-impure-function-values.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/emptyiterator.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/collected-data.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7550.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7580.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/this-subtractable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expression-inference.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1519.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7663-php7.php'); - } - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7663-php8.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7663.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7688.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7689.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/has-offset-type-bug.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5920.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-1.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-3.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-7511.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7224.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6556.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-4708.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4708.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2911.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-7156.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6728.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__ . '/data/bug-5223.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7698.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/non-falsy-string.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7483.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7056.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 from $this->gatherAssertTypes(__DIR__ . '/data/bug-6901.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/var-in-and-out-of-function.php'); if (PHP_VERSION_ID >= 70400) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-argument-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-argument-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ctype-digit.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7788.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7809.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-non-empty-array-after-unset.php'); - if (PHP_VERSION_ID >= 80200) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/true-typehint.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-6000.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/prestashop-breakdowns-empty-array.php'); - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-comparisons-php7.php'); - } - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-comparisons-php8.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10084.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-comparisons.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7563.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7764.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5845.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-array-bug.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7492.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7877.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1021.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6170.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key-exists.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/key-exists.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7909.php'); if (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-7898.php'); @@ -1161,269 +143,44 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-7823.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7921.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7928.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Analyser/data/is-resource-specified.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7949.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7639.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5304.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7244.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7501.php'); - if (PHP_VERSION_ID >= 80200) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/standalone-types.php'); - } yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-7954.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/scope-generalization.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8015.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7993.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7996.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7141.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/cli-globals.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8033.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-union-unshift.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7987.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7963-three.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8017.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/global-namespace.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/dnf.php'); - } - - if (PHP_VERSION_ID >= 70300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/fpm-get-status.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-offset-unset.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-docblock.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-empty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-method.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-property.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-this.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-methods.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-intersected.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-invariant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-conditional.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/docblock-assert-equality.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8008.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-class-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5552.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/extra-extra-int-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/list-count.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-7839.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/self-out.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-expressions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Classes/data/bug-5333.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-subtypes-enum.php'); - } - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10071.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9394.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-subtypes-datetime.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-subtypes-throwable.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__ . '/data/bug-7519.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8087.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5785.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-object.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-string.php'); - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8225.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8242.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-treatPhpDocTypesAsCertainBug.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-retain-expression-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7913.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-8280.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8272.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8277.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/strtr.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/static-has-method.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/mixed-to-number.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-8113.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/phpunit-integration.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8361.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8373.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-8389.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8421.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/imagick-pixel.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-8467a.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8467b.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8442.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/PDOStatement.php'); if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8485.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-8447.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9134.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7805.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-82.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4565.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3789.php'); - - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8543.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8520.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10002.php'); if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9007.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-dynamic-return-type-extension-regression.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo-php8.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/always-true-elseif.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7547.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9341.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8568.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/DeadCode/data/bug-8620.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8635.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8625.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7239-php8.php'); - } - if (PHP_VERSION_ID < 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7239.php'); - } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8621.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8084.php'); - - if (PHP_VERSION_ID >= 80300) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/str_increment.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/str_decrement.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3019.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/get-native-type.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/callsite-cast-narrowing.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8775.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8752.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-affected-rows.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-num-rows.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-stmt-affected-rows-and-num-rows.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/list-shapes.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3013.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7607.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10373.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ibm_db2.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/benevolent-union-math.php'); if (PHP_VERSION_ID >= 80200) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-8957.php'); } - if (PHP_VERSION_ID >= 80100) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8486.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9000.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-from.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8917.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ds-copy.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8803.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8827.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4907.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8924.php'); - if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9499.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9939.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5998.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8609.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9131.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/more-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/invalid-type-aliases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/asymmetric-properties.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9062.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8092.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__ . '/data/object-shape.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/rule-error-builder.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/memcache-get.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4302b.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/ini-get.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9274.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/extract.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/image-size.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/base64_decode.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9404.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-partially-non-iterable.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/globals.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9208.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/finite-types.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5782b-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5782b-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func.php'); - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/gettype.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array_splice.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-9542.php'); - - if (PHP_VERSION_ID >= 80000) { - yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php8.php'); - } else { - yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php7.php'); - } - - yield from $this->gatherAssertTypes(__DIR__ . '/data/falsy-isset.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-coalesce.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-ternary-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7915.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9714.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9105.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5172.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9293.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/nullsafe-vs-scalar.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8517.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-9803.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-connection-fns.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9963.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/str-shuffle.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9995.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/enum_exists.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9778.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9867.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-isset-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-empty-certainty.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8366.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7291.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10264.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-vars.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5961.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10189.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10317.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-interface-extends.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-extends.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-implements.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-10594.php'); } /** diff --git a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php new file mode 100644 index 0000000000..795707aabb --- /dev/null +++ b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/param-closure-this-stubs.php'); + } + + /** + * @dataProvider dataAsserts + * @param mixed ...$args + */ + public function testAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/param-closure-this-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php new file mode 100644 index 0000000000..025513c4bf --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionArrowFunctionTest.php @@ -0,0 +1,44 @@ +gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension-arrow-function.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + 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); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/parameter-closure-type-extension-arrow-function.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php new file mode 100644 index 0000000000..4b990f1f72 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureTypeExtensionTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/parameter-closure-type-extension.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/parameter-closure-type-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php new file mode 100644 index 0000000000..e32c7c3ca3 --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterOutTypeExtensionTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/param-out/parameter-out-types.php'); + } + + /** + * @dataProvider dataAsserts + * @param mixed ...$args + */ + public function testAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/parameter-out.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/PathConstantsTest.php b/tests/PHPStan/Analyser/PathConstantsTest.php index c22864f698..39832befa4 100644 --- a/tests/PHPStan/Analyser/PathConstantsTest.php +++ b/tests/PHPStan/Analyser/PathConstantsTest.php @@ -3,13 +3,18 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use const DIRECTORY_SEPARATOR; class PathConstantsTest extends TypeInferenceTestCase { public function dataFileAsserts(): iterable { - yield from $this->gatherAssertTypes(__DIR__ . '/data/pathConstants.php'); + if (DIRECTORY_SEPARATOR === '\\') { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pathConstants-win.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pathConstants.php'); + } } /** diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index 7ae319c0e3..99bba5ea9b 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -245,7 +245,7 @@ public function testGetConstantType(): void $scope = $scopeFactory->create(ScopeContext::create(__DIR__ . '/data/compiler-halt-offset.php')); $node = new ConstFetch(new FullyQualified('__COMPILER_HALT_OFFSET__')); $type = $scope->getType($node); - $this->assertSame('int<0, max>', $type->describe(VerbosityLevel::precise())); + $this->assertSame('int<1, max>', $type->describe(VerbosityLevel::precise())); } } diff --git a/tests/PHPStan/Analyser/TestClosureTypeRule.php b/tests/PHPStan/Analyser/TestClosureTypeRule.php new file mode 100644 index 0000000000..9d3128b1c7 --- /dev/null +++ b/tests/PHPStan/Analyser/TestClosureTypeRule.php @@ -0,0 +1,39 @@ + + */ +class TestClosureTypeRule implements Rule +{ + + public function getNodeType(): string + { + return FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Closure && !$node instanceof Node\Expr\ArrowFunction) { + return []; + } + + $type = $scope->getType($node); + + return [ + RuleErrorBuilder::message(sprintf('Closure type: %s', $type->describe(VerbosityLevel::precise()))) + ->identifier('tests.closureType') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php new file mode 100644 index 0000000000..aebfdc606f --- /dev/null +++ b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class TestClosureTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TestClosureTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/nsrt/closure-passed-to-type.php'], [ + [ + 'Closure type: Closure(mixed): (1|2|3)', + 25, + ], + [ + 'Closure type: Closure(mixed): (1|2|3)', + 35, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/TraitStubFilesTest.php b/tests/PHPStan/Analyser/TraitStubFilesTest.php index 9858c6389d..9107f5f45c 100644 --- a/tests/PHPStan/Analyser/TraitStubFilesTest.php +++ b/tests/PHPStan/Analyser/TraitStubFilesTest.php @@ -1,4 +1,4 @@ - 'Foo', 'get_class($foo)' => '\'Foo\''], ['get_class($foo)' => '~\'Foo\''], ], + [ + new Equal( + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + new String_('Foo'), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], + ], + [ + new Equal( + new String_('Foo'), + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], + ], [ new BooleanNot( new Expr\Instanceof_( @@ -802,7 +822,7 @@ public function dataCondition(): iterable '$n' => 'mixed~int<3, max>|true', ], [ - '$n' => 'mixed~int|false|null', + '$n' => 'mixed~0.0|int|false|null', ], ], [ @@ -814,7 +834,7 @@ public function dataCondition(): iterable '$n' => 'mixed~int<' . PHP_INT_MIN . ', max>|true', ], [ - '$n' => 'mixed~false|null', + '$n' => 'mixed~0.0|false|null', ], ], [ @@ -823,7 +843,7 @@ public function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~bool|int|null', + '$n' => 'mixed~0.0|bool|int|null', ], [ '$n' => 'mixed', @@ -838,7 +858,7 @@ public function dataCondition(): iterable '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', ], [ - '$n' => 'mixed~bool|int|null', + '$n' => 'mixed~0.0|bool|int|null', ], ], [ @@ -847,7 +867,7 @@ public function dataCondition(): iterable new LNumber(PHP_INT_MAX), ), [ - '$n' => 'mixed~int|false|null', + '$n' => 'mixed~0.0|int|false|null', ], [ '$n' => 'mixed~int<' . PHP_INT_MAX . ', max>|true', @@ -865,7 +885,7 @@ public function dataCondition(): iterable ), ), [ - '$n' => 'mixed~int|int<6, max>|false|null', + '$n' => 'mixed~0.0|int|int<6, max>|false|null', ], [ '$n' => 'mixed~int<3, 5>|true', @@ -1230,7 +1250,7 @@ public function dataCondition(): iterable ), [ '$foo' => 'non-empty-array', - 'count($foo)' => 'mixed~int|false|null', + 'count($foo)' => 'mixed~0.0|int|false|null', ], [], ], @@ -1324,11 +1344,17 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array return $descriptions; } + /** + * @param non-empty-string $className + */ private function createInstanceOf(string $className, string $variableName = 'foo'): Expr\Instanceof_ { return new Expr\Instanceof_(new Variable($variableName), new Name($className)); } + /** + * @param non-empty-string $functionName + */ private function createFunctionCall(string $functionName, string $variableName = 'foo'): FuncCall { return new FuncCall(new Name($functionName), [new Arg(new Variable($variableName))]); diff --git a/tests/PHPStan/Analyser/bug-10922.neon b/tests/PHPStan/Analyser/bug-10922.neon new file mode 100644 index 0000000000..3ee516d3be --- /dev/null +++ b/tests/PHPStan/Analyser/bug-10922.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Analyser/bug-11009.neon b/tests/PHPStan/Analyser/bug-11009.neon new file mode 100644 index 0000000000..d9a90c70b4 --- /dev/null +++ b/tests/PHPStan/Analyser/bug-11009.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/bug-11009.stub diff --git a/tests/PHPStan/Analyser/bug-9307.neon b/tests/PHPStan/Analyser/bug-9307.neon new file mode 100644 index 0000000000..c551b84f1f --- /dev/null +++ b/tests/PHPStan/Analyser/bug-9307.neon @@ -0,0 +1,2 @@ +parameters: + treatPhpDocTypesAsCertain: false diff --git a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php index 3ee8656661..b66f99e76b 100644 --- a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php +++ b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php @@ -1,7 +1,7 @@ $list */ - $list = []; - - assertType('list', $list); - - assert((count($list) <= 1) === true); - assertType('list', $list); - } - - /** @param list $c */ - public function sayHello(array $c): void - { - assertType('list', $c); - if (count($c) > 0) { - $c = array_map(fn () => new stdClass(), $c); - assertType('non-empty-list', $c); - } else { - assertType('array{}', $c); - } - - assertType('list', $c); - } -} diff --git a/tests/PHPStan/Analyser/data/bug-10358.php b/tests/PHPStan/Analyser/data/bug-10358.php new file mode 100644 index 0000000000..fc8a94f01c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10358.php @@ -0,0 +1,8 @@ + + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10538.php b/tests/PHPStan/Analyser/data/bug-10538.php new file mode 100644 index 0000000000..24fc1f1be2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10538.php @@ -0,0 +1,44 @@ + 1, + 'BY' => 2, + 'BE' => 3, + 'BB' => 4, + 'HB' => 5, + 'HH' => 6, + 'HE' => 7, + 'MV' => 8, + 'NI' => 9, + 'NW' => 10, + 'RP' => 11, + 'SL' => 12, + 'ST' => 13, + 'SN' => 14, + 'SH' => 15, + 'TH' => 16, + ]; + + protected static function test(): void + { + for ($i = 0; $i < 10; $i++) { + foreach (self::CHANGESET as $stateCode => $changesets) { + $stateId = self::STATES[$stateCode]; + foreach ($changesets as $changeset) { + echo sprintf( + '%s %s %s %s', + $changeset['new']['Gemarkung'], + $changeset['old']['Gemeinde'], + $changeset['old']['Gemarkung'], + $stateId + ); + } + } + } + } + + protected const CHANGESET = ['BB' => [['old' => ['Gemeinde' => 'Alt Zauche - Wußmerk', 'Gemarkung' => 'Alt Zauche'],'new' => ['Gemeinde' => 'Alt Zauche - Wußwerk', 'Gemarkung' => 'Alt Zauche'],],['old' => ['Gemeinde' => 'Alt Zauche - Wußmerk', 'Gemarkung' => 'Wußwerk'],'new' => ['Gemeinde' => 'Alt Zauche - Wußwerk', 'Gemarkung' => 'Wußwerk'],],['old' => ['Gemeinde' => 'Doberlug - Kirchhain', 'Gemarkung' => 'Doberlug-Kirchhainrchhain'],'new' => ['Gemeinde' => 'Doberlug - Kirchhain', 'Gemarkung' => 'Doberlug-Kirchhain'],],],'BE' => [['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Charlottenburg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Charlottenburg'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Grunewald-Forst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Grunewald-Forst'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Schmargendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schmargendorf'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Wilmersdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wilmersdorf'],],['old' => ['Gemeindeschluessel' => '11000002', 'Gemeinde' => 'Friedrichshain-Kreuzberg', 'Gemarkung' => 'Friedrichshain'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedrichshain'],],['old' => ['Gemeindeschluessel' => '11000002', 'Gemeinde' => 'Friedrichshain-Kreuzberg', 'Gemarkung' => 'Kreuzberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kreuzberg'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Falkenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Falkenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Hohenschönhausen'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hohenschönhausen'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Lichtenberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichtenberg'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Malchow Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Malchow Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Malchow Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Malchow Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Wartenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wartenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Wartenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wartenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Weißensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Ahrensfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Ahrensfelde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Biesdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Biesdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Dahlwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Dahlwitz'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Falkenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Falkenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Friedrichsfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedrichsfelde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Hellersdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hellersdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Kaulsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kaulsdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Köpenick'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Köpenick'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Mahlsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mahlsdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Marzahn'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Marzahn'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Mitte'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mitte'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Tiergarten'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tiergarten'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Wedding'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wedding'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Britz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Britz'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Buckow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Buckow'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Neukölln'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Neukölln'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Rudow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Rudow'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 01'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 01'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 02'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 02'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 03'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 03'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Prenzlauer Berg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Prenzlauer Berg'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Weißensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Weißensee 01'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee 01'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Frohnau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Frohnau'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Heiligensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Heiligensee'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Hermsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hermsdorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Lübars'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lübars'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Reinickendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Reinickendorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Schulzendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schulzendorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Forst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Forst'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Gut'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Valentinswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Valentinswerder'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Wilhelmsruh'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wilhelmsruh'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Wittenau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wittenau'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Eiswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Eiswerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Gatow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Gatow'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Gewehrplan u. Pulverfabrik'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Gewehrplan u. Pulverfabrik'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Groß-Glienicke'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Groß-Glienicke'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Haselhorst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Haselhorst'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Heerstraße'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Heerstraße'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Kladow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kladow'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Klosterfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Klosterfelde'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Pichelsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pichelsdorf'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Pichelswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pichelswerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Seeburg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Seeburg'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Spandau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Spandau'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Staaken'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Staaken'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Teufelsbruch'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Teufelsbruch'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Tiefwerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tiefwerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Zitadelle'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Zitadelle'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Dahlem'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Dahlem'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Düppel'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Düppel'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Lankwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lankwitz'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Lichterfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichterfelde'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Nikolassee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Nikolassee'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Schwanenwerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schwanenwerder'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Steglitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Steglitz'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Wannsee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wannsee'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Zehlendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Zehlendorf'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Friedenau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedenau'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Lichtenrade'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichtenrade'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Mariendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mariendorf'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Marienfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Marienfelde'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Schöneberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schöneberg'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Tempelhof'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tempelhof'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Bohnsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Bohnsdorf'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Britz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Britz'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Buckow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Buckow'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Fahlenberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Fahlenberg'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Glienicke'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Glienicke'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Grünau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Grünau'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Johannisthal'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Johannisthal'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Kanne'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kanne'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Köpenick'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Köpenick'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Neukölln'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Neukölln'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Oberschöneweide'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Oberschöneweide'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Rudow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Rudow'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Schmöckwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schmöckwitz'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Treptow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Treptow'],],],'HB' => [['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt1'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 1'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt3'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 3'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt4'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 4'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Überseehafen', 'Gemarkungsnummer' => '040008'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Überseehafen', 'Gemarkungsnummer' => '040009'],],],'HE' => [['old' => ['Gemeindeschluessel' => '06635005', 'Gemeinde' => 'Bromskirchen', 'Gemarkung' => 'Bromskirchen'],'new' => ['Gemeindeschluessel' => '06635001', 'Gemeinde' => 'Allendorf (Eder)', 'Gemarkung' => 'Bromskirchen'],],['old' => ['Gemeindeschluessel' => '06635005', 'Gemeinde' => 'Bromskirchen', 'Gemarkung' => 'Somplar'],'new' => ['Gemeindeschluessel' => '06635001', 'Gemeinde' => 'Allendorf (Eder)', 'Gemarkung' => 'Somplar'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Gelnhausen'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Gelnhausen'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Hailer'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Hailer'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Haitz'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Haitz'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Höchst'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Höchst'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Meerholz'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Meerholz'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Roth'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Roth'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Ahlbach'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Ahlbach'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Dietkirchen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Dietkirchen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Eschhofen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Eschhofen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Limburg'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Limburg'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Lindenholzhausen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Lindenholzhausen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Linter'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Linter'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Offheim'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Offheim'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Staffel'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Staffel'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Arnoldshain'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Arnoldshain'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Brombach'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Brombach'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Dorfweil'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Dorfweil'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Hunoldstal'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Hunoldstal'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Niederreifenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Niederreifenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Oberreifenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Oberreifenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Schmitten'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Schmitten'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Seelenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Seelenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Treisberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Treisberg'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Alraft'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Alraft'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Dehringhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Dehringhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Freienhagen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Freienhagen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Höringhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Höringhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Netze'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Netze'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Nieder-Werbe'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Nieder-Werbe'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Ober-Werbe'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Ober-Werbe'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Oberwerba'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Oberwerba'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Sachsenhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Sachsenhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Waldeck'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Waldeck'],],],'MV' => [['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Buschmühlen'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Buschmühlen'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Malpendorf'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Malpendorf'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Neubukow'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Neubukow'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Panzow'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Panzow'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Spriehusen'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Spriehusen'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Helmstorf'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Helmstorf'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Klein Tessin'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Klein Tessin'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Tessin'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Tessin'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Vilz'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Vilz'],],],'NI' => [['old' => ['Gemeindeschluessel' => '03153006', 'Gemeinde' => 'Hahausen', 'Gemarkung' => 'Hahausen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Hahausen'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Astfeld'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Astfeld'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bredelem'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bredelem'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Langelsheim'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Langelsheim'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lautenthal'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lautenthal'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen-Mispliet'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen-Mispliet'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Lutter am Barenberge'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lutter am Barenberge'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Lutter-Westerberg'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lutter-Westerberg'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Nauen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Nauen'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Ostlutter'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Ostlutter'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Alt Wallmoden'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Alt Wallmoden'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Bodenstein'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bodenstein'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Neuwallmoden'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Neuwallmoden'],],],'ST' => [['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Nempitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Nempitz'],],['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Oebles-Schlechtewitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Oebles-Schlechtewitz'],],['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Tollwitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Tollwitz'],],['old' => ['Gemeinde' => 'Harsleben', 'Gemarkung' => 'Harsleben'],'new' => ['Gemeinde' => 'Harsleben / Harschlewe', 'Gemarkung' => 'Harsleben'],],['old' => ['Gemeindeschluessel' => '15083575', 'Gemeinde' => 'Westheide', 'Gemarkung' => 'Born'],'new' => ['Gemeindeschluessel' => '15083557', 'Gemeinde' => 'Westheide', 'Gemarkung' => 'Born'],],],'TH' => [['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Berteroda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Berteroda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Eisenach'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Eisenach'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Frohnishof'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Frohnishof'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Göringen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Göringen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hörschel'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hörschel'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hötzelsroda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hötzelsroda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Madelungen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Madelungen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neuenhof'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neuenhof'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neukirchen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neukirchen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stedtfeld'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stedtfeld'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stockhausen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stockhausen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stregda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stregda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Wartha'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Wartha'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Bliederstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Bliederstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Feldengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Feldengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Großenehrich'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Großenehrich'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Holzengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Holzengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Kirchengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Kirchengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Niederspier'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Niederspier'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Otterstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Otterstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Rohnstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Rohnstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Wenigenehrich'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Wenigenehrich'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Westerengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Westerengel'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Etterwinden'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Etterwinden'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Gräfen-Nitzendorf'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Gräfen-Nitzendorf'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Gumpelstadt'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Gumpelstadt'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Kupfersuhl'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Kupfersuhl'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Möhra'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Möhra'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Neuendorf'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Neuendorf'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Wackenhof'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Wackenhof'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Waldfisch'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Waldfisch'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Witzelroda'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Witzelroda'],],['old' => ['Gemeinde' => 'Roßleben-Wiehe, Stadt', 'Gemarkung' => 'Bottendorf'],'new' => ['Gemeinde' => 'Roßleben-Wiehe', 'Gemarkung' => 'Bottendorf'],],['old' => ['Gemeinde' => 'Wolferschwenda', 'Gemarkung' => 'Wolferschwenda'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Wolferschwenda'],],],]; +} diff --git a/tests/PHPStan/Analyser/data/bug-10772.php b/tests/PHPStan/Analyser/data/bug-10772.php new file mode 100644 index 0000000000..76ea079046 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10772.php @@ -0,0 +1,9393 @@ +getNameInLanguage(LanguageAlpha2::English); + + // part B, all right + // $value->toCountryAlpha2()->getNameInLanguage(LanguageAlpha2::English); +} + +enum CountryAlpha3: string +{ + case Afghanistan = 'AFG'; + case Aland_Islands = 'ALA'; + case Albania = 'ALB'; + case Algeria = 'DZA'; + case American_Samoa = 'ASM'; + case Andorra = 'AND'; + case Angola = 'AGO'; + case Anguilla = 'AIA'; + case Antarctica = 'ATA'; + case Antigua_and_Barbuda = 'ATG'; + case Argentina = 'ARG'; + case Armenia = 'ARM'; + case Aruba = 'ABW'; + case Australia = 'AUS'; + case Austria = 'AUT'; + case Azerbaijan = 'AZE'; + case Bahamas = 'BHS'; + case Bahrain = 'BHR'; + case Bangladesh = 'BGD'; + case Barbados = 'BRB'; + case Belarus = 'BLR'; + case Belgium = 'BEL'; + case Belize = 'BLZ'; + case Benin = 'BEN'; + case Bermuda = 'BMU'; + case Bhutan = 'BTN'; + case Bolivia = 'BOL'; + case Bonaire_Sint_Eustatius_and_Saba = 'BES'; + case Bosnia_and_Herzegovina = 'BIH'; + case Botswana = 'BWA'; + case Bouvet_Island = 'BVT'; + case Brazil = 'BRA'; + case British_Indian_Ocean_Territory = 'IOT'; + case Brunei_Darussalam = 'BRN'; + case Bulgaria = 'BGR'; + case Burkina_Faso = 'BFA'; + case Burundi = 'BDI'; + case Cabo_Verde = 'CPV'; + case Cambodia = 'KHM'; + case Cameroon = 'CMR'; + case Canada = 'CAN'; + case Cayman_Islands = 'CYM'; + case Central_African_Republic = 'CAF'; + case Chad = 'TCD'; + case Chile = 'CHL'; + case China = 'CHN'; + case Christmas_Island = 'CXR'; + case Cocos_Islands = 'CCK'; + case Colombia = 'COL'; + case Comoros = 'COM'; + case Congo = 'COG'; + case Congo_Democratic_Republic = 'COD'; + case Cook_Islands = 'COK'; + case Costa_Rica = 'CRI'; + case Cote_d_Ivoire = 'CIV'; + case Croatia = 'HRV'; + case Cuba = 'CUB'; + case Curacao = 'CUW'; + case Cyprus = 'CYP'; + case Czechia = 'CZE'; + case Denmark = 'DNK'; + case Djibouti = 'DJI'; + case Dominica = 'DMA'; + case Dominican_Republic = 'DOM'; + case Ecuador = 'ECU'; + case Egypt = 'EGY'; + case El_Salvador = 'SLV'; + case Equatorial_Guinea = 'GNQ'; + case Eritrea = 'ERI'; + case Estonia = 'EST'; + case Eswatini = 'SWZ'; + case Ethiopia = 'ETH'; + case Falkland_Islands = 'FLK'; + case Faroe_Islands = 'FRO'; + case Fiji = 'FJI'; + case Finland = 'FIN'; + case France = 'FRA'; + case French_Guiana = 'GUF'; + case French_Polynesia = 'PYF'; + case French_Southern_Territories = 'ATF'; + case Gabon = 'GAB'; + case Gambia = 'GMB'; + case Georgia = 'GEO'; + case Germany = 'DEU'; + case Ghana = 'GHA'; + case Gibraltar = 'GIB'; + case Greece = 'GRC'; + case Greenland = 'GRL'; + case Grenada = 'GRD'; + case Guadeloupe = 'GLP'; + case Guam = 'GUM'; + case Guatemala = 'GTM'; + case Guernsey = 'GGY'; + case Guinea = 'GIN'; + case Guinea_Bissau = 'GNB'; + case Guyana = 'GUY'; + case Haiti = 'HTI'; + case Heard_Island_and_McDonald_Islands = 'HMD'; + case Holy_See = 'VAT'; + case Honduras = 'HND'; + case Hong_Kong = 'HKG'; + case Hungary = 'HUN'; + case Iceland = 'ISL'; + case India = 'IND'; + case Indonesia = 'IDN'; + case Iran = 'IRN'; + case Iraq = 'IRQ'; + case Ireland = 'IRL'; + case Isle_of_Man = 'IMN'; + case Israel = 'ISR'; + case Italy = 'ITA'; + case Jamaica = 'JAM'; + case Japan = 'JPN'; + case Jersey = 'JEY'; + case Jordan = 'JOR'; + case Kazakhstan = 'KAZ'; + case Kenya = 'KEN'; + case Kiribati = 'KIR'; + case Korea_Democratic_Peoples_Republic = 'PRK'; + case Korea_Republic = 'KOR'; + case Kuwait = 'KWT'; + case Kyrgyzstan = 'KGZ'; + case Lao_Peoples_Democratic_Republic = 'LAO'; + case Latvia = 'LVA'; + case Lebanon = 'LBN'; + case Lesotho = 'LSO'; + case Liberia = 'LBR'; + case Libya = 'LBY'; + case Liechtenstein = 'LIE'; + case Lithuania = 'LTU'; + case Luxembourg = 'LUX'; + case Macao = 'MAC'; + case Madagascar = 'MDG'; + case Malawi = 'MWI'; + case Malaysia = 'MYS'; + case Maldives = 'MDV'; + case Mali = 'MLI'; + case Malta = 'MLT'; + case Marshall_Islands = 'MHL'; + case Martinique = 'MTQ'; + case Mauritania = 'MRT'; + case Mauritius = 'MUS'; + case Mayotte = 'MYT'; + case Mexico = 'MEX'; + case Micronesia = 'FSM'; + case Moldova = 'MDA'; + case Monaco = 'MCO'; + case Mongolia = 'MNG'; + case Montenegro = 'MNE'; + case Montserrat = 'MSR'; + case Morocco = 'MAR'; + case Mozambique = 'MOZ'; + case Myanmar = 'MMR'; + case Namibia = 'NAM'; + case Nauru = 'NRU'; + case Nepal = 'NPL'; + case Netherlands = 'NLD'; + case New_Caledonia = 'NCL'; + case New_Zealand = 'NZL'; + case Nicaragua = 'NIC'; + case Niger = 'NER'; + case Nigeria = 'NGA'; + case Niue = 'NIU'; + case Norfolk_Island = 'NFK'; + case North_Macedonia = 'MKD'; + case Northern_Mariana_Islands = 'MNP'; + case Norway = 'NOR'; + case Oman = 'OMN'; + case Pakistan = 'PAK'; + case Palau = 'PLW'; + case Palestine = 'PSE'; + case Panama = 'PAN'; + case Papua_New_Guinea = 'PNG'; + case Paraguay = 'PRY'; + case Peru = 'PER'; + case Philippines = 'PHL'; + case Pitcairn = 'PCN'; + case Poland = 'POL'; + case Portugal = 'PRT'; + case Puerto_Rico = 'PRI'; + case Qatar = 'QAT'; + case Reunion = 'REU'; + case Romania = 'ROU'; + case Russian_Federation = 'RUS'; + case Rwanda = 'RWA'; + case Saint_Barthelemy = 'BLM'; + case Saint_Helena_Ascension_Tristan_da_Cunha = 'SHN'; + case Saint_Kitts_and_Nevis = 'KNA'; + case Saint_Lucia = 'LCA'; + case Saint_Martin_French_part = 'MAF'; + case Saint_Pierre_and_Miquelon = 'SPM'; + case Saint_Vincent_and_the_Grenadines = 'VCT'; + case Samoa = 'WSM'; + case San_Marino = 'SMR'; + case Sao_Tome_and_Principe = 'STP'; + case Saudi_Arabia = 'SAU'; + case Senegal = 'SEN'; + case Serbia = 'SRB'; + case Seychelles = 'SYC'; + case Sierra_Leone = 'SLE'; + case Singapore = 'SGP'; + case Sint_Maarten_Dutch_part = 'SXM'; + case Slovakia = 'SVK'; + case Slovenia = 'SVN'; + case Solomon_Islands = 'SLB'; + case Somalia = 'SOM'; + case South_Africa = 'ZAF'; + case South_Georgia_South_Sandwich_Islands = 'SGS'; + case South_Sudan = 'SSD'; + case Spain = 'ESP'; + case Sri_Lanka = 'LKA'; + case Sudan = 'SDN'; + case Suriname = 'SUR'; + case Svalbard_Jan_Mayen = 'SJM'; + case Sweden = 'SWE'; + case Switzerland = 'CHE'; + case Syrian_Arab_Republic = 'SYR'; + case Taiwan_Province_of_China = 'TWN'; + case Tajikistan = 'TJK'; + case Tanzania = 'TZA'; + case Thailand = 'THA'; + case Timor_Leste = 'TLS'; + case Togo = 'TGO'; + case Tokelau = 'TKL'; + case Tonga = 'TON'; + case Trinidad_and_Tobago = 'TTO'; + case Tunisia = 'TUN'; + case Turkey = 'TUR'; + case Turkmenistan = 'TKM'; + case Turks_and_Caicos_Islands = 'TCA'; + case Tuvalu = 'TUV'; + case Uganda = 'UGA'; + case Ukraine = 'UKR'; + case United_Arab_Emirates = 'ARE'; + case United_Kingdom = 'GBR'; + case United_States_Outlying_Islands = 'UMI'; + case United_States_of_America = 'USA'; + case Uruguay = 'URY'; + case Uzbekistan = 'UZB'; + case Vanuatu = 'VUT'; + case Venezuela = 'VEN'; + case Viet_Nam = 'VNM'; + case Virgin_Islands_British = 'VGB'; + case Virgin_Islands_U_S = 'VIR'; + case Wallis_and_Futuna = 'WLF'; + case Western_Sahara = 'ESH'; + case Yemen = 'YEM'; + case Zambia = 'ZMB'; + case Zimbabwe = 'ZWE'; + + + + public function getNameInLanguage(LanguageAlpha2|LanguageAlpha3Terminology|LanguageAlpha3Bibliographic|LanguageAlpha3Extensive $language): ?string + { + return $this->toCountryAlpha2()->getNameInLanguage($language); + } + + public function toCountryAlpha2(): mixed + { + return BackedEnum::fromName('x', $this->name); + } +} + +enum LanguageAlpha2: string +{ + case Abkhazian = 'ab'; + case Afar = 'aa'; + case Afrikaans = 'af'; + case Akan = 'ak'; + case Albanian = 'sq'; + case Amharic = 'am'; + case Arabic = 'ar'; + case Aragonese = 'an'; + case Armenian = 'hy'; + case Assamese = 'as'; + case Avaric = 'av'; + case Avestan = 'ae'; + case Aymara = 'ay'; + case Azerbaijani = 'az'; + case Bambara = 'bm'; + case Bashkir = 'ba'; + case Basque = 'eu'; + case Belarusian = 'be'; + case Bengali = 'bn'; + case Bihari_languages = 'bh'; + case Bislama = 'bi'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nb'; + case Bosnian = 'bs'; + case Breton = 'br'; + case Bulgarian = 'bg'; + case Burmese = 'my'; + case Catalan_Valencian = 'ca'; + case Central_Khmer = 'km'; + case Chamorro = 'ch'; + case Chechen = 'ce'; + case Chichewa_Chewa_Nyanja = 'ny'; + case Chinese = 'zh'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'cu'; + case Chuvash = 'cv'; + case Cornish = 'kw'; + case Corsican = 'co'; + case Cree = 'cr'; + case Croatian = 'hr'; + case Czech = 'cs'; + case Danish = 'da'; + case Divehi_Dhivehi_Maldivian = 'dv'; + case Dutch_Flemish = 'nl'; + case Dzongkha = 'dz'; + case English = 'en'; + case Esperanto = 'eo'; + case Estonian = 'et'; + case Ewe = 'ee'; + case Faroese = 'fo'; + case Fijian = 'fj'; + case Finnish = 'fi'; + case French = 'fr'; + case Fulah = 'ff'; + case Gaelic_Scottish_Gaelic = 'gd'; + case Galician = 'gl'; + case Ganda = 'lg'; + case Georgian = 'ka'; + case German = 'de'; + case Greek_Modern_1453 = 'el'; + case Guarani = 'gn'; + case Gujarati = 'gu'; + case Haitian_Haitian_Creole = 'ht'; + case Hausa = 'ha'; + case Hebrew = 'he'; + case Herero = 'hz'; + case Hindi = 'hi'; + case Hiri_Motu = 'ho'; + case Hungarian = 'hu'; + case Icelandic = 'is'; + case Ido = 'io'; + case Igbo = 'ig'; + case Indonesian = 'id'; + case Interlingua_International_Auxiliary_Language_Association = 'ia'; + case Interlingue_Occidental = 'ie'; + case Inuktitut = 'iu'; + case Inupiaq = 'ik'; + case Irish = 'ga'; + case Italian = 'it'; + case Japanese = 'ja'; + case Javanese = 'jv'; + case Kalaallisut_Greenlandic = 'kl'; + case Kannada = 'kn'; + case Kanuri = 'kr'; + case Kashmiri = 'ks'; + case Kazakh = 'kk'; + case Kikuyu_Gikuyu = 'ki'; + case Kinyarwanda = 'rw'; + case Kirghiz_Kyrgyz = 'ky'; + case Komi = 'kv'; + case Kongo = 'kg'; + case Korean = 'ko'; + case Kuanyama_Kwanyama = 'kj'; + case Kurdish = 'ku'; + case Lao = 'lo'; + case Latin = 'la'; + case Latvian = 'lv'; + case Limburgan_Limburger_Limburgish = 'li'; + case Lingala = 'ln'; + case Lithuanian = 'lt'; + case Luba_Katanga = 'lu'; + case Luxembourgish_Letzeburgesch = 'lb'; + case Macedonian = 'mk'; + case Malagasy = 'mg'; + case Malay = 'ms'; + case Malayalam = 'ml'; + case Maltese = 'mt'; + case Manx = 'gv'; + case Maori = 'mi'; + case Marathi = 'mr'; + case Marshallese = 'mh'; + case Mongolian = 'mn'; + case Nauru = 'na'; + case Navajo_Navaho = 'nv'; + case Ndebele_North_North_Ndebele = 'nd'; + case Ndebele_South_South_Ndebele = 'nr'; + case Ndonga = 'ng'; + case Nepali = 'ne'; + case Northern_Sami = 'se'; + case Norwegian = 'no'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nn'; + case Occitan_post_1500 = 'oc'; + case Ojibwa = 'oj'; + case Oriya = 'or'; + case Oromo = 'om'; + case Ossetian_Ossetic = 'os'; + case Pali = 'pi'; + case Panjabi_Punjabi = 'pa'; + case Persian = 'fa'; + case Polish = 'pl'; + case Portuguese = 'pt'; + case Pushto_Pashto = 'ps'; + case Quechua = 'qu'; + case Romanian_Moldavian_Moldovan = 'ro'; + case Romansh = 'rm'; + case Rundi = 'rn'; + case Russian = 'ru'; + case Samoan = 'sm'; + case Sango = 'sg'; + case Sanskrit = 'sa'; + case Sardinian = 'sc'; + case Serbian = 'sr'; + case Shona = 'sn'; + case Sichuan_Yi_Nuosu = 'ii'; + case Sindhi = 'sd'; + case Sinhala_Sinhalese = 'si'; + case Slovak = 'sk'; + case Slovenian = 'sl'; + case Somali = 'so'; + case Sotho_Southern = 'st'; + case Spanish_Castilian = 'es'; + case Sundanese = 'su'; + case Swahili = 'sw'; + case Swati = 'ss'; + case Swedish = 'sv'; + case Tagalog = 'tl'; + case Tahitian = 'ty'; + case Tajik = 'tg'; + case Tamil = 'ta'; + case Tatar = 'tt'; + case Telugu = 'te'; + case Thai = 'th'; + case Tibetan = 'bo'; + case Tigrinya = 'ti'; + case Tonga_Tonga_Islands = 'to'; + case Tsonga = 'ts'; + case Tswana = 'tn'; + case Turkish = 'tr'; + case Turkmen = 'tk'; + case Twi = 'tw'; + case Uighur_Uyghur = 'ug'; + case Ukrainian = 'uk'; + case Urdu = 'ur'; + case Uzbek = 'uz'; + case Venda = 've'; + case Vietnamese = 'vi'; + case Volapuk = 'vo'; + case Walloon = 'wa'; + case Welsh = 'cy'; + case Western_Frisian = 'fy'; + case Wolof = 'wo'; + case Xhosa = 'xh'; + case Yiddish = 'yi'; + case Yoruba = 'yo'; + case Zhuang_Chuang = 'za'; + case Zulu = 'zu'; + + /** @deprecated Will be removed in v4. Please use ::getNameInLanguage(LanguageAlpha2::English) instead */ + public function toLanguageName(): LanguageName + { + return BackedEnum::fromName(LanguageName::class, $this->name); + } +} + +enum LanguageAlpha3Terminology: string +{ + case Abkhazian = 'abk'; + case Achinese = 'ace'; + case Acoli = 'ach'; + case Adangme = 'ada'; + case Adyghe_Adygei = 'ady'; + case Afar = 'aar'; + case Afrihili = 'afh'; + case Afrikaans = 'afr'; + case Afro_Asiatic_languages = 'afa'; + case Ainu = 'ain'; + case Akan = 'aka'; + case Akkadian = 'akk'; + case Albanian = 'sqi'; + case Aleut = 'ale'; + case Algonquian_languages = 'alg'; + case Altaic_languages = 'tut'; + case Amharic = 'amh'; + case Angika = 'anp'; + case Apache_languages = 'apa'; + case Arabic = 'ara'; + case Aragonese = 'arg'; + case Arapaho = 'arp'; + case Arawak = 'arw'; + case Armenian = 'hye'; + case Aromanian_Arumanian_Macedo_Romanian = 'rup'; + case Artificial_languages = 'art'; + case Assamese = 'asm'; + case Asturian_Bable_Leonese_Asturleonese = 'ast'; + case Athapascan_languages = 'ath'; + case Australian_languages = 'aus'; + case Austronesian_languages = 'map'; + case Avaric = 'ava'; + case Avestan = 'ave'; + case Awadhi = 'awa'; + case Aymara = 'aym'; + case Azerbaijani = 'aze'; + case Balinese = 'ban'; + case Baltic_languages = 'bat'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Bamileke_languages = 'bai'; + case Banda_languages = 'bad'; + case Bantu_languages = 'bnt'; + case Basa = 'bas'; + case Bashkir = 'bak'; + case Basque = 'eus'; + case Batak_languages = 'btk'; + case Beja_Bedawiyet = 'bej'; + case Belarusian = 'bel'; + case Bemba = 'bem'; + case Bengali = 'ben'; + case Berber_languages = 'ber'; + case Bhojpuri = 'bho'; + case Bihari_languages = 'bih'; + case Bikol = 'bik'; + case Bini_Edo = 'bin'; + case Bislama = 'bis'; + case Blin_Bilin = 'byn'; + case Blissymbols_Blissymbolics_Bliss = 'zbl'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nob'; + case Bosnian = 'bos'; + case Braj = 'bra'; + case Breton = 'bre'; + case Buginese = 'bug'; + case Bulgarian = 'bul'; + case Buriat = 'bua'; + case Burmese = 'mya'; + case Caddo = 'cad'; + case Catalan_Valencian = 'cat'; + case Caucasian_languages = 'cau'; + case Cebuano = 'ceb'; + case Celtic_languages = 'cel'; + case Central_American_Indian_languages = 'cai'; + case Central_Khmer = 'khm'; + case Chagatai = 'chg'; + case Chamic_languages = 'cmc'; + case Chamorro = 'cha'; + case Chechen = 'che'; + case Cherokee = 'chr'; + case Cheyenne = 'chy'; + case Chibcha = 'chb'; + case Chichewa_Chewa_Nyanja = 'nya'; + case Chinese = 'zho'; + case Chinook_jargon = 'chn'; + case Chipewyan_Dene_Suline = 'chp'; + case Choctaw = 'cho'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'chu'; + case Chuukese = 'chk'; + case Chuvash = 'chv'; + case Classical_Newari_Old_Newari_Classical_Nepal_Bhasa = 'nwc'; + case Classical_Syriac = 'syc'; + case Coptic = 'cop'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Cree = 'cre'; + case Creek = 'mus'; + case Creoles_and_pidgins = 'crp'; + case Creoles_and_pidgins_English_based = 'cpe'; + case Creoles_and_pidgins_French_based = 'cpf'; + case Creoles_and_pidgins_Portuguese_based = 'cpp'; + case Crimean_Tatar_Crimean_Turkish = 'crh'; + case Croatian = 'hrv'; + case Cushitic_languages = 'cus'; + case Czech = 'ces'; + case Dakota = 'dak'; + case Danish = 'dan'; + case Dargwa = 'dar'; + case Delaware = 'del'; + case Dinka = 'din'; + case Divehi_Dhivehi_Maldivian = 'div'; + case Dogri = 'doi'; + case Dogrib = 'dgr'; + case Dravidian_languages = 'dra'; + case Duala = 'dua'; + case Dutch_Flemish = 'nld'; + case Dutch_Middle_ca_1050_1350 = 'dum'; + case Dyula = 'dyu'; + case Dzongkha = 'dzo'; + case Eastern_Frisian = 'frs'; + case Efik = 'efi'; + case Egyptian_Ancient = 'egy'; + case Ekajuk = 'eka'; + case Elamite = 'elx'; + case English = 'eng'; + case English_Middle_1100_1500 = 'enm'; + case English_Old_ca_450_1100 = 'ang'; + case Erzya = 'myv'; + case Esperanto = 'epo'; + case Estonian = 'est'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Fang = 'fan'; + case Fanti = 'fat'; + case Faroese = 'fao'; + case Fijian = 'fij'; + case Filipino_Pilipino = 'fil'; + case Finnish = 'fin'; + case Finno_Ugrian_languages = 'fiu'; + case Fon = 'fon'; + case French = 'fra'; + case French_Middle_ca_1400_1600 = 'frm'; + case French_Old_842_ca_1400 = 'fro'; + case Friulian = 'fur'; + case Fulah = 'ful'; + case Ga = 'gaa'; + case Gaelic_Scottish_Gaelic = 'gla'; + case Galibi_Carib = 'car'; + case Galician = 'glg'; + case Ganda = 'lug'; + case Gayo = 'gay'; + case Gbaya = 'gba'; + case Geez = 'gez'; + case Georgian = 'kat'; + case German = 'deu'; + case German_Middle_High_ca_1050_1500 = 'gmh'; + case German_Old_High_ca_750_1050 = 'goh'; + case Germanic_languages = 'gem'; + case Gilbertese = 'gil'; + case Gondi = 'gon'; + case Gorontalo = 'gor'; + case Gothic = 'got'; + case Grebo = 'grb'; + case Greek_Ancient_to_1453 = 'grc'; + case Greek_Modern_1453 = 'ell'; + case Guarani = 'grn'; + case Gujarati = 'guj'; + case Gwich_in = 'gwi'; + case Haida = 'hai'; + case Haitian_Haitian_Creole = 'hat'; + case Hausa = 'hau'; + case Hawaiian = 'haw'; + case Hebrew = 'heb'; + case Herero = 'her'; + case Hiligaynon = 'hil'; + case Himachali_languages_Western_Pahari_languages = 'him'; + case Hindi = 'hin'; + case Hiri_Motu = 'hmo'; + case Hittite = 'hit'; + case Hmong_Mong = 'hmn'; + case Hungarian = 'hun'; + case Hupa = 'hup'; + case Iban = 'iba'; + case Icelandic = 'isl'; + case Ido = 'ido'; + case Igbo = 'ibo'; + case Ijo_languages = 'ijo'; + case Iloko = 'ilo'; + case Inari_Sami = 'smn'; + case Indic_languages = 'inc'; + case Indo_European_languages = 'ine'; + case Indonesian = 'ind'; + case Ingush = 'inh'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Interlingue_Occidental = 'ile'; + case Inuktitut = 'iku'; + case Inupiaq = 'ipk'; + case Iranian_languages = 'ira'; + case Irish = 'gle'; + case Irish_Middle_900_1200 = 'mga'; + case Irish_Old_to_900 = 'sga'; + case Iroquoian_languages = 'iro'; + case Italian = 'ita'; + case Japanese = 'jpn'; + case Javanese = 'jav'; + case Judeo_Arabic = 'jrb'; + case Judeo_Persian = 'jpr'; + case Kabardian = 'kbd'; + case Kabyle = 'kab'; + case Kachin_Jingpho = 'kac'; + case Kalaallisut_Greenlandic = 'kal'; + case Kalmyk_Oirat = 'xal'; + case Kamba = 'kam'; + case Kannada = 'kan'; + case Kanuri = 'kau'; + case Kara_Kalpak = 'kaa'; + case Karachay_Balkar = 'krc'; + case Karelian = 'krl'; + case Karen_languages = 'kar'; + case Kashmiri = 'kas'; + case Kashubian = 'csb'; + case Kawi = 'kaw'; + case Kazakh = 'kaz'; + case Khasi = 'kha'; + case Khoisan_languages = 'khi'; + case Khotanese_Sakan = 'kho'; + case Kikuyu_Gikuyu = 'kik'; + case Kimbundu = 'kmb'; + case Kinyarwanda = 'kin'; + case Kirghiz_Kyrgyz = 'kir'; + case Klingon_tlhIngan_Hol = 'tlh'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konkani = 'kok'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Kpelle = 'kpe'; + case Kru_languages = 'kro'; + case Kuanyama_Kwanyama = 'kua'; + case Kumyk = 'kum'; + case Kurdish = 'kur'; + case Kurukh = 'kru'; + case Kutenai = 'kut'; + case Ladino = 'lad'; + case Lahnda = 'lah'; + case Lamba = 'lam'; + case Land_Dayak_languages = 'day'; + case Lao = 'lao'; + case Latin = 'lat'; + case Latvian = 'lav'; + case Lezghian = 'lez'; + case Limburgan_Limburger_Limburgish = 'lim'; + case Lingala = 'lin'; + case Lithuanian = 'lit'; + case Lojban = 'jbo'; + case Low_German_Low_Saxon_German_Low_Saxon_Low = 'nds'; + case Lower_Sorbian = 'dsb'; + case Lozi = 'loz'; + case Luba_Katanga = 'lub'; + case Luba_Lulua = 'lua'; + case Luiseno = 'lui'; + case Lule_Sami = 'smj'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lushai = 'lus'; + case Luxembourgish_Letzeburgesch = 'ltz'; + case Macedonian = 'mkd'; + case Madurese = 'mad'; + case Magahi = 'mag'; + case Maithili = 'mai'; + case Makasar = 'mak'; + case Malagasy = 'mlg'; + case Malay = 'msa'; + case Malayalam = 'mal'; + case Maltese = 'mlt'; + case Manchu = 'mnc'; + case Mandar = 'mdr'; + case Mandingo = 'man'; + case Manipuri = 'mni'; + case Manobo_languages = 'mno'; + case Manx = 'glv'; + case Maori = 'mri'; + case Mapudungun_Mapuche = 'arn'; + case Marathi = 'mar'; + case Mari = 'chm'; + case Marshallese = 'mah'; + case Marwari = 'mwr'; + case Masai = 'mas'; + case Mayan_languages = 'myn'; + case Mende = 'men'; + case Mi_kmaq_Micmac = 'mic'; + case Minangkabau = 'min'; + case Mirandese = 'mwl'; + case Mohawk = 'moh'; + case Moksha = 'mdf'; + case Mon_Khmer_languages = 'mkh'; + case Mongo = 'lol'; + case Mongolian = 'mon'; + case Montenegrin = 'cnr'; + case Mossi = 'mos'; + case Multiple_languages = 'mul'; + case Munda_languages = 'mun'; + case N_Ko = 'nqo'; + case Nahuatl_languages = 'nah'; + case Nauru = 'nau'; + case Navajo_Navaho = 'nav'; + case Ndebele_North_North_Ndebele = 'nde'; + case Ndebele_South_South_Ndebele = 'nbl'; + case Ndonga = 'ndo'; + case Neapolitan = 'nap'; + case Nepal_Bhasa_Newari = 'new'; + case Nepali = 'nep'; + case Nias = 'nia'; + case Niger_Kordofanian_languages = 'nic'; + case Nilo_Saharan_languages = 'ssa'; + case Niuean = 'niu'; + case No_linguistic_content_Not_applicable = 'zxx'; + case Nogai = 'nog'; + case Norse_Old = 'non'; + case North_American_Indian_languages = 'nai'; + case Northern_Frisian = 'frr'; + case Northern_Sami = 'sme'; + case Norwegian = 'nor'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nno'; + case Nubian_languages = 'nub'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nzima = 'nzi'; + case Occitan_post_1500 = 'oci'; + case Official_Aramaic_700_300_BCE_Imperial_Aramaic_700_300_BCE = 'arc'; + case Ojibwa = 'oji'; + case Oriya = 'ori'; + case Oromo = 'orm'; + case Osage = 'osa'; + case Ossetian_Ossetic = 'oss'; + case Otomian_languages = 'oto'; + case Pahlavi = 'pal'; + case Palauan = 'pau'; + case Pali = 'pli'; + case Pampanga_Kapampangan = 'pam'; + case Pangasinan = 'pag'; + case Panjabi_Punjabi = 'pan'; + case Papiamento = 'pap'; + case Papuan_languages = 'paa'; + case Pedi_Sepedi_Northern_Sotho = 'nso'; + case Persian = 'fas'; + case Persian_Old_ca_600_400_B_C = 'peo'; + case Philippine_languages = 'phi'; + case Phoenician = 'phn'; + case Pohnpeian = 'pon'; + case Polish = 'pol'; + case Portuguese = 'por'; + case Prakrit_languages = 'pra'; + case Provencal_Old_to_1500_Occitan_Old_to_1500 = 'pro'; + case Pushto_Pashto = 'pus'; + case Quechua = 'que'; + case Rajasthani = 'raj'; + case Rapanui = 'rap'; + case Rarotongan_Cook_Islands_Maori = 'rar'; + case Romance_languages = 'roa'; + case Romanian_Moldavian_Moldovan = 'ron'; + case Romansh = 'roh'; + case Romany = 'rom'; + case Rundi = 'run'; + case Russian = 'rus'; + case Salishan_languages = 'sal'; + case Samaritan_Aramaic = 'sam'; + case Sami_languages = 'smi'; + case Samoan = 'smo'; + case Sandawe = 'sad'; + case Sango = 'sag'; + case Sanskrit = 'san'; + case Santali = 'sat'; + case Sardinian = 'srd'; + case Sasak = 'sas'; + case Scots = 'sco'; + case Selkup = 'sel'; + case Semitic_languages = 'sem'; + case Serbian = 'srp'; + case Serer = 'srr'; + case Shan = 'shn'; + case Shona = 'sna'; + case Sichuan_Yi_Nuosu = 'iii'; + case Sicilian = 'scn'; + case Sidamo = 'sid'; + case Sign_Languages = 'sgn'; + case Siksika = 'bla'; + case Sindhi = 'snd'; + case Sinhala_Sinhalese = 'sin'; + case Sino_Tibetan_languages = 'sit'; + case Siouan_languages = 'sio'; + case Skolt_Sami = 'sms'; + case Slave_Athapascan = 'den'; + case Slavic_languages = 'sla'; + case Slovak = 'slk'; + case Slovenian = 'slv'; + case Sogdian = 'sog'; + case Somali = 'som'; + case Songhai_languages = 'son'; + case Soninke = 'snk'; + case Sorbian_languages = 'wen'; + case Sotho_Southern = 'sot'; + case South_American_Indian_languages = 'sai'; + case Southern_Altai = 'alt'; + case Southern_Sami = 'sma'; + case Spanish_Castilian = 'spa'; + case Sranan_Tongo = 'srn'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Sukuma = 'suk'; + case Sumerian = 'sux'; + case Sundanese = 'sun'; + case Susu = 'sus'; + case Swahili = 'swa'; + case Swati = 'ssw'; + case Swedish = 'swe'; + case Swiss_German_Alemannic_Alsatian = 'gsw'; + case Syriac = 'syr'; + case Tagalog = 'tgl'; + case Tahitian = 'tah'; + case Tai_languages = 'tai'; + case Tajik = 'tgk'; + case Tamashek = 'tmh'; + case Tamil = 'tam'; + case Tatar = 'tat'; + case Telugu = 'tel'; + case Tereno = 'ter'; + case Tetum = 'tet'; + case Thai = 'tha'; + case Tibetan = 'bod'; + case Tigre = 'tig'; + case Tigrinya = 'tir'; + case Timne = 'tem'; + case Tiv = 'tiv'; + case Tlingit = 'tli'; + case Tok_Pisin = 'tpi'; + case Tokelau = 'tkl'; + case Tonga_Nyasa = 'tog'; + case Tonga_Tonga_Islands = 'ton'; + case Tsimshian = 'tsi'; + case Tsonga = 'tso'; + case Tswana = 'tsn'; + case Tumbuka = 'tum'; + case Tupi_languages = 'tup'; + case Turkish = 'tur'; + case Turkish_Ottoman_1500_1928 = 'ota'; + case Turkmen = 'tuk'; + case Tuvalu = 'tvl'; + case Tuvinian = 'tyv'; + case Twi = 'twi'; + case Udmurt = 'udm'; + case Ugaritic = 'uga'; + case Uighur_Uyghur = 'uig'; + case Ukrainian = 'ukr'; + case Umbundu = 'umb'; + case Uncoded_languages = 'mis'; + case Undetermined = 'und'; + case Upper_Sorbian = 'hsb'; + case Urdu = 'urd'; + case Uzbek = 'uzb'; + case Vai = 'vai'; + case Venda = 'ven'; + case Vietnamese = 'vie'; + case Volapuk = 'vol'; + case Votic = 'vot'; + case Wakashan_languages = 'wak'; + case Walloon = 'wln'; + case Waray = 'war'; + case Washo = 'was'; + case Welsh = 'cym'; + case Western_Frisian = 'fry'; + case Wolaitta_Wolaytta = 'wal'; + case Wolof = 'wol'; + case Xhosa = 'xho'; + case Yakut = 'sah'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yiddish = 'yid'; + case Yoruba = 'yor'; + case Yupik_languages = 'ypk'; + case Zande_languages = 'znd'; + case Zapotec = 'zap'; + case Zaza_Dimili_Dimli_Kirdki_Kirmanjki_Zazaki = 'zza'; + case Zenaga = 'zen'; + case Zhuang_Chuang = 'zha'; + case Zulu = 'zul'; + case Zuni = 'zun'; +} + + +enum LanguageAlpha3Bibliographic: string +{ + case Abkhazian = 'abk'; + case Achinese = 'ace'; + case Acoli = 'ach'; + case Adangme = 'ada'; + case Adyghe_Adygei = 'ady'; + case Afar = 'aar'; + case Afrihili = 'afh'; + case Afrikaans = 'afr'; + case Afro_Asiatic_languages = 'afa'; + case Ainu = 'ain'; + case Akan = 'aka'; + case Akkadian = 'akk'; + case Albanian = 'alb'; + case Aleut = 'ale'; + case Algonquian_languages = 'alg'; + case Altaic_languages = 'tut'; + case Amharic = 'amh'; + case Angika = 'anp'; + case Apache_languages = 'apa'; + case Arabic = 'ara'; + case Aragonese = 'arg'; + case Arapaho = 'arp'; + case Arawak = 'arw'; + case Armenian = 'arm'; + case Aromanian_Arumanian_Macedo_Romanian = 'rup'; + case Artificial_languages = 'art'; + case Assamese = 'asm'; + case Asturian_Bable_Leonese_Asturleonese = 'ast'; + case Athapascan_languages = 'ath'; + case Australian_languages = 'aus'; + case Austronesian_languages = 'map'; + case Avaric = 'ava'; + case Avestan = 'ave'; + case Awadhi = 'awa'; + case Aymara = 'aym'; + case Azerbaijani = 'aze'; + case Balinese = 'ban'; + case Baltic_languages = 'bat'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Bamileke_languages = 'bai'; + case Banda_languages = 'bad'; + case Bantu_languages = 'bnt'; + case Basa = 'bas'; + case Bashkir = 'bak'; + case Basque = 'baq'; + case Batak_languages = 'btk'; + case Beja_Bedawiyet = 'bej'; + case Belarusian = 'bel'; + case Bemba = 'bem'; + case Bengali = 'ben'; + case Berber_languages = 'ber'; + case Bhojpuri = 'bho'; + case Bihari_languages = 'bih'; + case Bikol = 'bik'; + case Bini_Edo = 'bin'; + case Bislama = 'bis'; + case Blin_Bilin = 'byn'; + case Blissymbols_Blissymbolics_Bliss = 'zbl'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nob'; + case Bosnian = 'bos'; + case Braj = 'bra'; + case Breton = 'bre'; + case Buginese = 'bug'; + case Bulgarian = 'bul'; + case Buriat = 'bua'; + case Burmese = 'bur'; + case Caddo = 'cad'; + case Catalan_Valencian = 'cat'; + case Caucasian_languages = 'cau'; + case Cebuano = 'ceb'; + case Celtic_languages = 'cel'; + case Central_American_Indian_languages = 'cai'; + case Central_Khmer = 'khm'; + case Chagatai = 'chg'; + case Chamic_languages = 'cmc'; + case Chamorro = 'cha'; + case Chechen = 'che'; + case Cherokee = 'chr'; + case Cheyenne = 'chy'; + case Chibcha = 'chb'; + case Chichewa_Chewa_Nyanja = 'nya'; + case Chinese = 'chi'; + case Chinook_jargon = 'chn'; + case Chipewyan_Dene_Suline = 'chp'; + case Choctaw = 'cho'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'chu'; + case Chuukese = 'chk'; + case Chuvash = 'chv'; + case Classical_Newari_Old_Newari_Classical_Nepal_Bhasa = 'nwc'; + case Classical_Syriac = 'syc'; + case Coptic = 'cop'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Cree = 'cre'; + case Creek = 'mus'; + case Creoles_and_pidgins = 'crp'; + case Creoles_and_pidgins_English_based = 'cpe'; + case Creoles_and_pidgins_French_based = 'cpf'; + case Creoles_and_pidgins_Portuguese_based = 'cpp'; + case Crimean_Tatar_Crimean_Turkish = 'crh'; + case Croatian = 'hrv'; + case Cushitic_languages = 'cus'; + case Czech = 'cze'; + case Dakota = 'dak'; + case Danish = 'dan'; + case Dargwa = 'dar'; + case Delaware = 'del'; + case Dinka = 'din'; + case Divehi_Dhivehi_Maldivian = 'div'; + case Dogri = 'doi'; + case Dogrib = 'dgr'; + case Dravidian_languages = 'dra'; + case Duala = 'dua'; + case Dutch_Flemish = 'dut'; + case Dutch_Middle_ca_1050_1350 = 'dum'; + case Dyula = 'dyu'; + case Dzongkha = 'dzo'; + case Eastern_Frisian = 'frs'; + case Efik = 'efi'; + case Egyptian_Ancient = 'egy'; + case Ekajuk = 'eka'; + case Elamite = 'elx'; + case English = 'eng'; + case English_Middle_1100_1500 = 'enm'; + case English_Old_ca_450_1100 = 'ang'; + case Erzya = 'myv'; + case Esperanto = 'epo'; + case Estonian = 'est'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Fang = 'fan'; + case Fanti = 'fat'; + case Faroese = 'fao'; + case Fijian = 'fij'; + case Filipino_Pilipino = 'fil'; + case Finnish = 'fin'; + case Finno_Ugrian_languages = 'fiu'; + case Fon = 'fon'; + case French = 'fre'; + case French_Middle_ca_1400_1600 = 'frm'; + case French_Old_842_ca_1400 = 'fro'; + case Friulian = 'fur'; + case Fulah = 'ful'; + case Ga = 'gaa'; + case Gaelic_Scottish_Gaelic = 'gla'; + case Galibi_Carib = 'car'; + case Galician = 'glg'; + case Ganda = 'lug'; + case Gayo = 'gay'; + case Gbaya = 'gba'; + case Geez = 'gez'; + case Georgian = 'geo'; + case German = 'ger'; + case German_Middle_High_ca_1050_1500 = 'gmh'; + case German_Old_High_ca_750_1050 = 'goh'; + case Germanic_languages = 'gem'; + case Gilbertese = 'gil'; + case Gondi = 'gon'; + case Gorontalo = 'gor'; + case Gothic = 'got'; + case Grebo = 'grb'; + case Greek_Ancient_to_1453 = 'grc'; + case Greek_Modern_1453 = 'gre'; + case Guarani = 'grn'; + case Gujarati = 'guj'; + case Gwich_in = 'gwi'; + case Haida = 'hai'; + case Haitian_Haitian_Creole = 'hat'; + case Hausa = 'hau'; + case Hawaiian = 'haw'; + case Hebrew = 'heb'; + case Herero = 'her'; + case Hiligaynon = 'hil'; + case Himachali_languages_Western_Pahari_languages = 'him'; + case Hindi = 'hin'; + case Hiri_Motu = 'hmo'; + case Hittite = 'hit'; + case Hmong_Mong = 'hmn'; + case Hungarian = 'hun'; + case Hupa = 'hup'; + case Iban = 'iba'; + case Icelandic = 'ice'; + case Ido = 'ido'; + case Igbo = 'ibo'; + case Ijo_languages = 'ijo'; + case Iloko = 'ilo'; + case Inari_Sami = 'smn'; + case Indic_languages = 'inc'; + case Indo_European_languages = 'ine'; + case Indonesian = 'ind'; + case Ingush = 'inh'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Interlingue_Occidental = 'ile'; + case Inuktitut = 'iku'; + case Inupiaq = 'ipk'; + case Iranian_languages = 'ira'; + case Irish = 'gle'; + case Irish_Middle_900_1200 = 'mga'; + case Irish_Old_to_900 = 'sga'; + case Iroquoian_languages = 'iro'; + case Italian = 'ita'; + case Japanese = 'jpn'; + case Javanese = 'jav'; + case Judeo_Arabic = 'jrb'; + case Judeo_Persian = 'jpr'; + case Kabardian = 'kbd'; + case Kabyle = 'kab'; + case Kachin_Jingpho = 'kac'; + case Kalaallisut_Greenlandic = 'kal'; + case Kalmyk_Oirat = 'xal'; + case Kamba = 'kam'; + case Kannada = 'kan'; + case Kanuri = 'kau'; + case Kara_Kalpak = 'kaa'; + case Karachay_Balkar = 'krc'; + case Karelian = 'krl'; + case Karen_languages = 'kar'; + case Kashmiri = 'kas'; + case Kashubian = 'csb'; + case Kawi = 'kaw'; + case Kazakh = 'kaz'; + case Khasi = 'kha'; + case Khoisan_languages = 'khi'; + case Khotanese_Sakan = 'kho'; + case Kikuyu_Gikuyu = 'kik'; + case Kimbundu = 'kmb'; + case Kinyarwanda = 'kin'; + case Kirghiz_Kyrgyz = 'kir'; + case Klingon_tlhIngan_Hol = 'tlh'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konkani = 'kok'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Kpelle = 'kpe'; + case Kru_languages = 'kro'; + case Kuanyama_Kwanyama = 'kua'; + case Kumyk = 'kum'; + case Kurdish = 'kur'; + case Kurukh = 'kru'; + case Kutenai = 'kut'; + case Ladino = 'lad'; + case Lahnda = 'lah'; + case Lamba = 'lam'; + case Land_Dayak_languages = 'day'; + case Lao = 'lao'; + case Latin = 'lat'; + case Latvian = 'lav'; + case Lezghian = 'lez'; + case Limburgan_Limburger_Limburgish = 'lim'; + case Lingala = 'lin'; + case Lithuanian = 'lit'; + case Lojban = 'jbo'; + case Low_German_Low_Saxon_German_Low_Saxon_Low = 'nds'; + case Lower_Sorbian = 'dsb'; + case Lozi = 'loz'; + case Luba_Katanga = 'lub'; + case Luba_Lulua = 'lua'; + case Luiseno = 'lui'; + case Lule_Sami = 'smj'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lushai = 'lus'; + case Luxembourgish_Letzeburgesch = 'ltz'; + case Macedonian = 'mac'; + case Madurese = 'mad'; + case Magahi = 'mag'; + case Maithili = 'mai'; + case Makasar = 'mak'; + case Malagasy = 'mlg'; + case Malay = 'may'; + case Malayalam = 'mal'; + case Maltese = 'mlt'; + case Manchu = 'mnc'; + case Mandar = 'mdr'; + case Mandingo = 'man'; + case Manipuri = 'mni'; + case Manobo_languages = 'mno'; + case Manx = 'glv'; + case Maori = 'mao'; + case Mapudungun_Mapuche = 'arn'; + case Marathi = 'mar'; + case Mari = 'chm'; + case Marshallese = 'mah'; + case Marwari = 'mwr'; + case Masai = 'mas'; + case Mayan_languages = 'myn'; + case Mende = 'men'; + case Mi_kmaq_Micmac = 'mic'; + case Minangkabau = 'min'; + case Mirandese = 'mwl'; + case Mohawk = 'moh'; + case Moksha = 'mdf'; + case Mon_Khmer_languages = 'mkh'; + case Mongo = 'lol'; + case Mongolian = 'mon'; + case Montenegrin = 'cnr'; + case Mossi = 'mos'; + case Multiple_languages = 'mul'; + case Munda_languages = 'mun'; + case N_Ko = 'nqo'; + case Nahuatl_languages = 'nah'; + case Nauru = 'nau'; + case Navajo_Navaho = 'nav'; + case Ndebele_North_North_Ndebele = 'nde'; + case Ndebele_South_South_Ndebele = 'nbl'; + case Ndonga = 'ndo'; + case Neapolitan = 'nap'; + case Nepal_Bhasa_Newari = 'new'; + case Nepali = 'nep'; + case Nias = 'nia'; + case Niger_Kordofanian_languages = 'nic'; + case Nilo_Saharan_languages = 'ssa'; + case Niuean = 'niu'; + case No_linguistic_content_Not_applicable = 'zxx'; + case Nogai = 'nog'; + case Norse_Old = 'non'; + case North_American_Indian_languages = 'nai'; + case Northern_Frisian = 'frr'; + case Northern_Sami = 'sme'; + case Norwegian = 'nor'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nno'; + case Nubian_languages = 'nub'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nzima = 'nzi'; + case Occitan_post_1500 = 'oci'; + case Official_Aramaic_700_300_BCE_Imperial_Aramaic_700_300_BCE = 'arc'; + case Ojibwa = 'oji'; + case Oriya = 'ori'; + case Oromo = 'orm'; + case Osage = 'osa'; + case Ossetian_Ossetic = 'oss'; + case Otomian_languages = 'oto'; + case Pahlavi = 'pal'; + case Palauan = 'pau'; + case Pali = 'pli'; + case Pampanga_Kapampangan = 'pam'; + case Pangasinan = 'pag'; + case Panjabi_Punjabi = 'pan'; + case Papiamento = 'pap'; + case Papuan_languages = 'paa'; + case Pedi_Sepedi_Northern_Sotho = 'nso'; + case Persian = 'per'; + case Persian_Old_ca_600_400_B_C = 'peo'; + case Philippine_languages = 'phi'; + case Phoenician = 'phn'; + case Pohnpeian = 'pon'; + case Polish = 'pol'; + case Portuguese = 'por'; + case Prakrit_languages = 'pra'; + case Provencal_Old_to_1500_Occitan_Old_to_1500 = 'pro'; + case Pushto_Pashto = 'pus'; + case Quechua = 'que'; + case Rajasthani = 'raj'; + case Rapanui = 'rap'; + case Rarotongan_Cook_Islands_Maori = 'rar'; + case Romance_languages = 'roa'; + case Romanian_Moldavian_Moldovan = 'rum'; + case Romansh = 'roh'; + case Romany = 'rom'; + case Rundi = 'run'; + case Russian = 'rus'; + case Salishan_languages = 'sal'; + case Samaritan_Aramaic = 'sam'; + case Sami_languages = 'smi'; + case Samoan = 'smo'; + case Sandawe = 'sad'; + case Sango = 'sag'; + case Sanskrit = 'san'; + case Santali = 'sat'; + case Sardinian = 'srd'; + case Sasak = 'sas'; + case Scots = 'sco'; + case Selkup = 'sel'; + case Semitic_languages = 'sem'; + case Serbian = 'srp'; + case Serer = 'srr'; + case Shan = 'shn'; + case Shona = 'sna'; + case Sichuan_Yi_Nuosu = 'iii'; + case Sicilian = 'scn'; + case Sidamo = 'sid'; + case Sign_Languages = 'sgn'; + case Siksika = 'bla'; + case Sindhi = 'snd'; + case Sinhala_Sinhalese = 'sin'; + case Sino_Tibetan_languages = 'sit'; + case Siouan_languages = 'sio'; + case Skolt_Sami = 'sms'; + case Slave_Athapascan = 'den'; + case Slavic_languages = 'sla'; + case Slovak = 'slo'; + case Slovenian = 'slv'; + case Sogdian = 'sog'; + case Somali = 'som'; + case Songhai_languages = 'son'; + case Soninke = 'snk'; + case Sorbian_languages = 'wen'; + case Sotho_Southern = 'sot'; + case South_American_Indian_languages = 'sai'; + case Southern_Altai = 'alt'; + case Southern_Sami = 'sma'; + case Spanish_Castilian = 'spa'; + case Sranan_Tongo = 'srn'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Sukuma = 'suk'; + case Sumerian = 'sux'; + case Sundanese = 'sun'; + case Susu = 'sus'; + case Swahili = 'swa'; + case Swati = 'ssw'; + case Swedish = 'swe'; + case Swiss_German_Alemannic_Alsatian = 'gsw'; + case Syriac = 'syr'; + case Tagalog = 'tgl'; + case Tahitian = 'tah'; + case Tai_languages = 'tai'; + case Tajik = 'tgk'; + case Tamashek = 'tmh'; + case Tamil = 'tam'; + case Tatar = 'tat'; + case Telugu = 'tel'; + case Tereno = 'ter'; + case Tetum = 'tet'; + case Thai = 'tha'; + case Tibetan = 'tib'; + case Tigre = 'tig'; + case Tigrinya = 'tir'; + case Timne = 'tem'; + case Tiv = 'tiv'; + case Tlingit = 'tli'; + case Tok_Pisin = 'tpi'; + case Tokelau = 'tkl'; + case Tonga_Nyasa = 'tog'; + case Tonga_Tonga_Islands = 'ton'; + case Tsimshian = 'tsi'; + case Tsonga = 'tso'; + case Tswana = 'tsn'; + case Tumbuka = 'tum'; + case Tupi_languages = 'tup'; + case Turkish = 'tur'; + case Turkish_Ottoman_1500_1928 = 'ota'; + case Turkmen = 'tuk'; + case Tuvalu = 'tvl'; + case Tuvinian = 'tyv'; + case Twi = 'twi'; + case Udmurt = 'udm'; + case Ugaritic = 'uga'; + case Uighur_Uyghur = 'uig'; + case Ukrainian = 'ukr'; + case Umbundu = 'umb'; + case Uncoded_languages = 'mis'; + case Undetermined = 'und'; + case Upper_Sorbian = 'hsb'; + case Urdu = 'urd'; + case Uzbek = 'uzb'; + case Vai = 'vai'; + case Venda = 'ven'; + case Vietnamese = 'vie'; + case Volapuk = 'vol'; + case Votic = 'vot'; + case Wakashan_languages = 'wak'; + case Walloon = 'wln'; + case Waray = 'war'; + case Washo = 'was'; + case Welsh = 'wel'; + case Western_Frisian = 'fry'; + case Wolaitta_Wolaytta = 'wal'; + case Wolof = 'wol'; + case Xhosa = 'xho'; + case Yakut = 'sah'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yiddish = 'yid'; + case Yoruba = 'yor'; + case Yupik_languages = 'ypk'; + case Zande_languages = 'znd'; + case Zapotec = 'zap'; + case Zaza_Dimili_Dimli_Kirdki_Kirmanjki_Zazaki = 'zza'; + case Zenaga = 'zen'; + case Zhuang_Chuang = 'zha'; + case Zulu = 'zul'; + case Zuni = 'zun'; + +} + + +enum LanguageAlpha3Extensive: string +{ + case Ghotuo = 'aaa'; + case Alumu_Tesu = 'aab'; + case Ari = 'aac'; + case Amal = 'aad'; + case Arbereshe_Albanian = 'aae'; + case Aranadan = 'aaf'; + case Ambrak = 'aag'; + case Abu_Arapesh = 'aah'; + case Arifama_Miniafia = 'aai'; + case Ankave = 'aak'; + case Afade = 'aal'; + case Anambe = 'aan'; + case Algerian_Saharan_Arabic = 'aao'; + case Para_Arara = 'aap'; + case Eastern_Abnaki = 'aaq'; + case Afar = 'aar'; + case Aasax = 'aas'; + case Arvanitika_Albanian = 'aat'; + case Abau = 'aau'; + case Solong = 'aaw'; + case Mandobo_Atas = 'aax'; + case Amarasi = 'aaz'; + case Abe = 'aba'; + case Bankon = 'abb'; + case Ambala_Ayta = 'abc'; + case Manide = 'abd'; + case Western_Abnaki = 'abe'; + case Abai_Sungai = 'abf'; + case Abaga = 'abg'; + case Tajiki_Arabic = 'abh'; + case Abidji = 'abi'; + case Aka_Bea = 'abj'; + case Abkhazian = 'abk'; + case Lampung_Nyo = 'abl'; + case Abanyom = 'abm'; + case Abua = 'abn'; + case Abon = 'abo'; + case Abellen_Ayta = 'abp'; + case Abaza = 'abq'; + case Abron = 'abr'; + case Ambonese_Malay = 'abs'; + case Ambulas = 'abt'; + case Abure = 'abu'; + case Baharna_Arabic = 'abv'; + case Pal = 'abw'; + case Inabaknon = 'abx'; + case Aneme_Wake = 'aby'; + case Abui = 'abz'; + case Achagua = 'aca'; + case Anca = 'acb'; + case Gikyode = 'acd'; + case Achinese = 'ace'; + case Saint_Lucian_Creole_French = 'acf'; + case Acoli = 'ach'; + case Aka_Cari = 'aci'; + case Aka_Kora = 'ack'; + case Akar_Bale = 'acl'; + case Mesopotamian_Arabic = 'acm'; + case Achang = 'acn'; + case Eastern_Acipa = 'acp'; + case Ta_izzi_Adeni_Arabic = 'acq'; + case Achi = 'acr'; + case Acroa = 'acs'; + case Achterhoeks = 'act'; + case Achuar_Shiwiar = 'acu'; + case Achumawi = 'acv'; + case Hijazi_Arabic = 'acw'; + case Omani_Arabic = 'acx'; + case Cypriot_Arabic = 'acy'; + case Acheron = 'acz'; + case Adangme = 'ada'; + case Atauran = 'adb'; + case Lidzonka = 'add'; + case Adele = 'ade'; + case Dhofari_Arabic = 'adf'; + case Andegerebinha = 'adg'; + case Adhola = 'adh'; + case Adi = 'adi'; + case Adioukrou = 'adj'; + case Galo = 'adl'; + case Adang = 'adn'; + case Abu = 'ado'; + case Adangbe = 'adq'; + case Adonara = 'adr'; + case Adamorobe_Sign_Language = 'ads'; + case Adnyamathanha = 'adt'; + case Aduge = 'adu'; + case Amundava = 'adw'; + case Amdo_Tibetan = 'adx'; + case Adyghe = 'ady'; + case Adzera = 'adz'; + case Areba = 'aea'; + case Tunisian_Arabic = 'aeb'; + case Saidi_Arabic = 'aec'; + case Argentine_Sign_Language = 'aed'; + case Northeast_Pashai = 'aee'; + case Haeke = 'aek'; + case Ambele = 'ael'; + case Arem = 'aem'; + case Armenian_Sign_Language = 'aen'; + case Aer = 'aeq'; + case Eastern_Arrernte = 'aer'; + case Alsea = 'aes'; + case Akeu = 'aeu'; + case Ambakich = 'aew'; + case Amele = 'aey'; + case Aeka = 'aez'; + case Gulf_Arabic = 'afb'; + case Andai = 'afd'; + case Putukwam = 'afe'; + case Afghan_Sign_Language = 'afg'; + case Afrihili = 'afh'; + case Akrukay = 'afi'; + case Nanubae = 'afk'; + case Defaka = 'afn'; + case Eloyi = 'afo'; + case Tapei = 'afp'; + case Afrikaans = 'afr'; + case Afro_Seminole_Creole = 'afs'; + case Afitti = 'aft'; + case Awutu = 'afu'; + case Obokuitai = 'afz'; + case Aguano = 'aga'; + case Legbo = 'agb'; + case Agatu = 'agc'; + case Agarabi = 'agd'; + case Angal = 'age'; + case Arguni = 'agf'; + case Angor = 'agg'; + case Ngelima = 'agh'; + case Agariya = 'agi'; + case Argobba = 'agj'; + case Isarog_Agta = 'agk'; + case Fembe = 'agl'; + case Angaataha = 'agm'; + case Agutaynen = 'agn'; + case Tainae = 'ago'; + case Aghem = 'agq'; + case Aguaruna = 'agr'; + case Esimbi = 'ags'; + case Central_Cagayan_Agta = 'agt'; + case Aguacateco = 'agu'; + case Remontado_Dumagat = 'agv'; + case Kahua = 'agw'; + case Aghul = 'agx'; + case Southern_Alta = 'agy'; + case Mt_Iriga_Agta = 'agz'; + case Ahanta = 'aha'; + case Axamb = 'ahb'; + case Qimant = 'ahg'; + case Aghu = 'ahh'; + case Tiagbamrin_Aizi = 'ahi'; + case Akha = 'ahk'; + case Igo = 'ahl'; + case Mobumrin_Aizi = 'ahm'; + case Ahan = 'ahn'; + case Ahom = 'aho'; + case Aproumu_Aizi = 'ahp'; + case Ahirani = 'ahr'; + case Ashe = 'ahs'; + case Ahtena = 'aht'; + case Arosi = 'aia'; + case Ainu_China = 'aib'; + case Ainbai = 'aic'; + case Alngith = 'aid'; + case Amara = 'aie'; + case Agi = 'aif'; + case Antigua_and_Barbuda_Creole_English = 'aig'; + case Ai_Cham = 'aih'; + case Assyrian_Neo_Aramaic = 'aii'; + case Lishanid_Noshan = 'aij'; + case Ake = 'aik'; + case Aimele = 'ail'; + case Aimol = 'aim'; + case Ainu_Japan = 'ain'; + case Aiton = 'aio'; + case Burumakok = 'aip'; + case Aimaq = 'aiq'; + case Airoran = 'air'; + case Arikem = 'ait'; + case Aari = 'aiw'; + case Aighon = 'aix'; + case Ali = 'aiy'; + case Aja_South_Sudan = 'aja'; + case Aja_Benin = 'ajg'; + case Ajie = 'aji'; + case Andajin = 'ajn'; + case Algerian_Jewish_Sign_Language = 'ajs'; + case Judeo_Moroccan_Arabic = 'aju'; + case Ajawa = 'ajw'; + case Amri_Karbi = 'ajz'; + case Akan = 'aka'; + case Batak_Angkola = 'akb'; + case Mpur = 'akc'; + case Ukpet_Ehom = 'akd'; + case Akawaio = 'ake'; + case Akpa = 'akf'; + case Anakalangu = 'akg'; + case Angal_Heneng = 'akh'; + case Aiome = 'aki'; + case Aka_Jeru = 'akj'; + case Akkadian = 'akk'; + case Aklanon = 'akl'; + case Aka_Bo = 'akm'; + case Akurio = 'ako'; + case Siwu = 'akp'; + case Ak = 'akq'; + case Araki = 'akr'; + case Akaselem = 'aks'; + case Akolet = 'akt'; + case Akum = 'aku'; + case Akhvakh = 'akv'; + case Akwa = 'akw'; + case Aka_Kede = 'akx'; + case Aka_Kol = 'aky'; + case Alabama = 'akz'; + case Alago = 'ala'; + case Qawasqar = 'alc'; + case Alladian = 'ald'; + case Aleut = 'ale'; + case Alege = 'alf'; + case Alawa = 'alh'; + case Amaimon = 'ali'; + case Alangan = 'alj'; + case Alak = 'alk'; + case Allar = 'all'; + case Amblong = 'alm'; + case Gheg_Albanian = 'aln'; + case Larike_Wakasihu = 'alo'; + case Alune = 'alp'; + case Algonquin = 'alq'; + case Alutor = 'alr'; + case Tosk_Albanian = 'als'; + case Southern_Altai = 'alt'; + case Are_are = 'alu'; + case Alaba_K_abeena = 'alw'; + case Amol = 'alx'; + case Alyawarr = 'aly'; + case Alur = 'alz'; + case Amanaye = 'ama'; + case Ambo = 'amb'; + case Amahuaca = 'amc'; + case Yanesha = 'ame'; + case Hamer_Banna = 'amf'; + case Amurdak = 'amg'; + case Amharic = 'amh'; + case Amis = 'ami'; + case Amdang = 'amj'; + case Ambai = 'amk'; + case War_Jaintia = 'aml'; + case Ama_Papua_New_Guinea = 'amm'; + case Amanab = 'amn'; + case Amo = 'amo'; + case Alamblak = 'amp'; + case Amahai = 'amq'; + case Amarakaeri = 'amr'; + case Southern_Amami_Oshima = 'ams'; + case Amto = 'amt'; + case Guerrero_Amuzgo = 'amu'; + case Ambelau = 'amv'; + case Western_Neo_Aramaic = 'amw'; + case Anmatyerre = 'amx'; + case Ami = 'amy'; + case Atampaya = 'amz'; + case Andaqui = 'ana'; + case Andoa = 'anb'; + case Ngas = 'anc'; + case Ansus = 'and'; + case Xaracuu = 'ane'; + case Animere = 'anf'; + case Old_English_ca_450_1100 = 'ang'; + case Nend = 'anh'; + case Andi = 'ani'; + case Anor = 'anj'; + case Goemai = 'ank'; + case Anu_Hkongso_Chin = 'anl'; + case Anal = 'anm'; + case Obolo = 'ann'; + case Andoque = 'ano'; + case Angika = 'anp'; + case Jarawa_India = 'anq'; + case Andh = 'anr'; + case Anserma = 'ans'; + case Antakarinya = 'ant'; + case Anuak = 'anu'; + case Denya = 'anv'; + case Anaang = 'anw'; + case Andra_Hus = 'anx'; + case Anyin = 'any'; + case Anem = 'anz'; + case Angolar = 'aoa'; + case Abom = 'aob'; + case Pemon = 'aoc'; + case Andarum = 'aod'; + case Angal_Enen = 'aoe'; + case Bragat = 'aof'; + case Angoram = 'aog'; + case Anindilyakwa = 'aoi'; + case Mufian = 'aoj'; + case Arho = 'aok'; + case Alor = 'aol'; + case Omie = 'aom'; + case Bumbita_Arapesh = 'aon'; + case Aore = 'aor'; + case Taikat = 'aos'; + case Atong_India = 'aot'; + case A_ou = 'aou'; + case Atorada = 'aox'; + case Uab_Meto = 'aoz'; + case Sa_a = 'apb'; + case Levantine_Arabic = 'apc'; + case Sudanese_Arabic = 'apd'; + case Bukiyip = 'ape'; + case Pahanan_Agta = 'apf'; + case Ampanang = 'apg'; + case Athpariya = 'aph'; + case Apiaka = 'api'; + case Jicarilla_Apache = 'apj'; + case Kiowa_Apache = 'apk'; + case Lipan_Apache = 'apl'; + case Mescalero_Chiricahua_Apache = 'apm'; + case Apinaye = 'apn'; + case Ambul = 'apo'; + case Apma = 'app'; + case A_Pucikwar = 'apq'; + case Arop_Lokep = 'apr'; + case Arop_Sissano = 'aps'; + case Apatani = 'apt'; + case Apurina = 'apu'; + case Alapmunte = 'apv'; + case Western_Apache = 'apw'; + case Aputai = 'apx'; + case Apalai = 'apy'; + case Safeyoka = 'apz'; + case Archi = 'aqc'; + case Ampari_Dogon = 'aqd'; + case Arigidi = 'aqg'; + case Aninka = 'aqk'; + case Atohwaim = 'aqm'; + case Northern_Alta = 'aqn'; + case Atakapa = 'aqp'; + case Arha = 'aqr'; + case Angaite = 'aqt'; + case Akuntsu = 'aqz'; + case Arabic = 'ara'; + case Standard_Arabic = 'arb'; + case Official_Aramaic_700_300_BCE = 'arc'; + case Arabana = 'ard'; + case Western_Arrarnta = 'are'; + case Aragonese = 'arg'; + case Arhuaco = 'arh'; + case Arikara = 'ari'; + case Arapaso = 'arj'; + case Arikapu = 'ark'; + case Arabela = 'arl'; + case Mapudungun = 'arn'; + case Araona = 'aro'; + case Arapaho = 'arp'; + case Algerian_Arabic = 'arq'; + case Karo_Brazil = 'arr'; + case Najdi_Arabic = 'ars'; + case Arua_Amazonas_State = 'aru'; + case Arbore = 'arv'; + case Arawak = 'arw'; + case Arua_Rodonia_State = 'arx'; + case Moroccan_Arabic = 'ary'; + case Egyptian_Arabic = 'arz'; + case Asu_Tanzania = 'asa'; + case Assiniboine = 'asb'; + case Casuarina_Coast_Asmat = 'asc'; + case American_Sign_Language = 'ase'; + case Auslan = 'asf'; + case Cishingini = 'asg'; + case Abishira = 'ash'; + case Buruwai = 'asi'; + case Sari = 'asj'; + case Ashkun = 'ask'; + case Asilulu = 'asl'; + case Assamese = 'asm'; + case Xingu_Asurini = 'asn'; + case Dano = 'aso'; + case Algerian_Sign_Language = 'asp'; + case Austrian_Sign_Language = 'asq'; + case Asuri = 'asr'; + case Ipulo = 'ass'; + case Asturian = 'ast'; + case Tocantins_Asurini = 'asu'; + case Asoa = 'asv'; + case Australian_Aborigines_Sign_Language = 'asw'; + case Muratayak = 'asx'; + case Yaosakor_Asmat = 'asy'; + case As = 'asz'; + case Pele_Ata = 'ata'; + case Zaiwa = 'atb'; + case Atsahuaca = 'atc'; + case Ata_Manobo = 'atd'; + case Atemble = 'ate'; + case Ivbie_North_Okpela_Arhe = 'atg'; + case Attie = 'ati'; + case Atikamekw = 'atj'; + case Ati = 'atk'; + case Mt_Iraya_Agta = 'atl'; + case Ata = 'atm'; + case Ashtiani = 'atn'; + case Atong_Cameroon = 'ato'; + case Pudtol_Atta = 'atp'; + case Aralle_Tabulahan = 'atq'; + case Waimiri_Atroari = 'atr'; + case Gros_Ventre = 'ats'; + case Pamplona_Atta = 'att'; + case Reel = 'atu'; + case Northern_Altai = 'atv'; + case Atsugewi = 'atw'; + case Arutani = 'atx'; + case Aneityum = 'aty'; + case Arta = 'atz'; + case Asumboa = 'aua'; + case Alugu = 'aub'; + case Waorani = 'auc'; + case Anuta = 'aud'; + case Aguna = 'aug'; + case Aushi = 'auh'; + case Anuki = 'aui'; + case Awjilah = 'auj'; + case Heyo = 'auk'; + case Aulua = 'aul'; + case Asu_Nigeria = 'aum'; + case Molmo_One = 'aun'; + case Auyokawa = 'auo'; + case Makayam = 'aup'; + case Anus = 'auq'; + case Aruek = 'aur'; + case Austral = 'aut'; + case Auye = 'auu'; + case Awyi = 'auw'; + case Aura = 'aux'; + case Awiyaana = 'auy'; + case Uzbeki_Arabic = 'auz'; + case Avaric = 'ava'; + case Avau = 'avb'; + case Alviri_Vidari = 'avd'; + case Avestan = 'ave'; + case Avikam = 'avi'; + case Kotava = 'avk'; + case Eastern_Egyptian_Bedawi_Arabic = 'avl'; + case Angkamuthi = 'avm'; + case Avatime = 'avn'; + case Agavotaguerra = 'avo'; + case Aushiri = 'avs'; + case Au = 'avt'; + case Avokaya = 'avu'; + case Ava_Canoeiro = 'avv'; + case Awadhi = 'awa'; + case Awa_Papua_New_Guinea = 'awb'; + case Cicipu = 'awc'; + case Aweti = 'awe'; + case Anguthimri = 'awg'; + case Awbono = 'awh'; + case Aekyom = 'awi'; + case Awabakal = 'awk'; + case Arawum = 'awm'; + case Awngi = 'awn'; + case Awak = 'awo'; + case Awera = 'awr'; + case South_Awyu = 'aws'; + case Arawete = 'awt'; + case Central_Awyu = 'awu'; + case Jair_Awyu = 'awv'; + case Awun = 'aww'; + case Awara = 'awx'; + case Edera_Awyu = 'awy'; + case Abipon = 'axb'; + case Ayerrerenge = 'axe'; + case Mato_Grosso_Arara = 'axg'; + case Yaka_Central_African_Republic = 'axk'; + case Lower_Southern_Aranda = 'axl'; + case Middle_Armenian = 'axm'; + case Xaragure = 'axx'; + case Awar = 'aya'; + case Ayizo_Gbe = 'ayb'; + case Southern_Aymara = 'ayc'; + case Ayabadhu = 'ayd'; + case Ayere = 'aye'; + case Ginyanga = 'ayg'; + case Hadrami_Arabic = 'ayh'; + case Leyigha = 'ayi'; + case Akuku = 'ayk'; + case Libyan_Arabic = 'ayl'; + case Aymara = 'aym'; + case Sanaani_Arabic = 'ayn'; + case Ayoreo = 'ayo'; + case North_Mesopotamian_Arabic = 'ayp'; + case Ayi_Papua_New_Guinea = 'ayq'; + case Central_Aymara = 'ayr'; + case Sorsogon_Ayta = 'ays'; + case Magbukun_Ayta = 'ayt'; + case Ayu = 'ayu'; + case Mai_Brat = 'ayz'; + case Azha = 'aza'; + case South_Azerbaijani = 'azb'; + case Eastern_Durango_Nahuatl = 'azd'; + case Azerbaijani = 'aze'; + case San_Pedro_Amuzgos_Amuzgo = 'azg'; + case North_Azerbaijani = 'azj'; + case Ipalapa_Amuzgo = 'azm'; + case Western_Durango_Nahuatl = 'azn'; + case Awing = 'azo'; + case Faire_Atta = 'azt'; + case Highland_Puebla_Nahuatl = 'azz'; + case Babatana = 'baa'; + case Bainouk_Gunyuno = 'bab'; + case Badui = 'bac'; + case Bare = 'bae'; + case Nubaca = 'baf'; + case Tuki = 'bag'; + case Bahamas_Creole_English = 'bah'; + case Barakai = 'baj'; + case Bashkir = 'bak'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Balinese = 'ban'; + case Waimaha = 'bao'; + case Bantawa = 'bap'; + case Bavarian = 'bar'; + case Basa_Cameroon = 'bas'; + case Bada_Nigeria = 'bau'; + case Vengo = 'bav'; + case Bambili_Bambui = 'baw'; + case Bamun = 'bax'; + case Batuley = 'bay'; + case Baatonum = 'bba'; + case Barai = 'bbb'; + case Batak_Toba = 'bbc'; + case Bau = 'bbd'; + case Bangba = 'bbe'; + case Baibai = 'bbf'; + case Barama = 'bbg'; + case Bugan = 'bbh'; + case Barombi = 'bbi'; + case Ghomala = 'bbj'; + case Babanki = 'bbk'; + case Bats = 'bbl'; + case Babango = 'bbm'; + case Uneapa = 'bbn'; + case Northern_Bobo_Madare = 'bbo'; + case West_Central_Banda = 'bbp'; + case Bamali = 'bbq'; + case Girawa = 'bbr'; + case Bakpinka = 'bbs'; + case Mburku = 'bbt'; + case Kulung_Nigeria = 'bbu'; + case Karnai = 'bbv'; + case Baba = 'bbw'; + case Bubia = 'bbx'; + case Befang = 'bby'; + case Central_Bai = 'bca'; + case Bainouk_Samik = 'bcb'; + case Southern_Balochi = 'bcc'; + case North_Babar = 'bcd'; + case Bamenyam = 'bce'; + case Bamu = 'bcf'; + case Baga_Pokur = 'bcg'; + case Bariai = 'bch'; + case Baoule = 'bci'; + case Bardi = 'bcj'; + case Bunuba = 'bck'; + case Central_Bikol = 'bcl'; + case Bannoni = 'bcm'; + case Bali_Nigeria = 'bcn'; + case Kaluli = 'bco'; + case Bali_Democratic_Republic_of_Congo = 'bcp'; + case Bench = 'bcq'; + case Babine = 'bcr'; + case Kohumono = 'bcs'; + case Bendi = 'bct'; + case Awad_Bing = 'bcu'; + case Shoo_Minda_Nye = 'bcv'; + case Bana = 'bcw'; + case Bacama = 'bcy'; + case Bainouk_Gunyaamolo = 'bcz'; + case Bayot = 'bda'; + case Basap = 'bdb'; + case Embera_Baudo = 'bdc'; + case Bunama = 'bdd'; + case Bade = 'bde'; + case Biage = 'bdf'; + case Bonggi = 'bdg'; + case Baka_South_Sudan = 'bdh'; + case Burun = 'bdi'; + case Bai_South_Sudan = 'bdj'; + case Budukh = 'bdk'; + case Indonesian_Bajau = 'bdl'; + case Buduma = 'bdm'; + case Baldemu = 'bdn'; + case Morom = 'bdo'; + case Bende = 'bdp'; + case Bahnar = 'bdq'; + case West_Coast_Bajau = 'bdr'; + case Burunge = 'bds'; + case Bokoto = 'bdt'; + case Oroko = 'bdu'; + case Bodo_Parja = 'bdv'; + case Baham = 'bdw'; + case Budong_Budong = 'bdx'; + case Bandjalang = 'bdy'; + case Badeshi = 'bdz'; + case Beaver = 'bea'; + case Bebele = 'beb'; + case Iceve_Maci = 'bec'; + case Bedoanas = 'bed'; + case Byangsi = 'bee'; + case Benabena = 'bef'; + case Belait = 'beg'; + case Biali = 'beh'; + case Bekati = 'bei'; + case Beja = 'bej'; + case Bebeli = 'bek'; + case Belarusian = 'bel'; + case Bemba_Zambia = 'bem'; + case Bengali = 'ben'; + case Beami = 'beo'; + case Besoa = 'bep'; + case Beembe = 'beq'; + case Besme = 'bes'; + case Guiberoua_Bete = 'bet'; + case Blagar = 'beu'; + case Daloa_Bete = 'bev'; + case Betawi = 'bew'; + case Jur_Modo = 'bex'; + case Beli_Papua_New_Guinea = 'bey'; + case Bena_Tanzania = 'bez'; + case Bari = 'bfa'; + case Pauri_Bareli = 'bfb'; + case Panyi_Bai = 'bfc'; + case Bafut = 'bfd'; + case Betaf = 'bfe'; + case Bofi = 'bff'; + case Busang_Kayan = 'bfg'; + case Blafe = 'bfh'; + case British_Sign_Language = 'bfi'; + case Bafanji = 'bfj'; + case Ban_Khor_Sign_Language = 'bfk'; + case Banda_Ndele = 'bfl'; + case Mmen = 'bfm'; + case Bunak = 'bfn'; + case Malba_Birifor = 'bfo'; + case Beba = 'bfp'; + case Badaga = 'bfq'; + case Bazigar = 'bfr'; + case Southern_Bai = 'bfs'; + case Balti = 'bft'; + case Gahri = 'bfu'; + case Bondo = 'bfw'; + case Bantayanon = 'bfx'; + case Bagheli = 'bfy'; + case Mahasu_Pahari = 'bfz'; + case Gwamhi_Wuri = 'bga'; + case Bobongko = 'bgb'; + case Haryanvi = 'bgc'; + case Rathwi_Bareli = 'bgd'; + case Bauria = 'bge'; + case Bangandu = 'bgf'; + case Bugun = 'bgg'; + case Giangan = 'bgi'; + case Bangolan = 'bgj'; + case Bit = 'bgk'; + case Bo_Laos = 'bgl'; + case Western_Balochi = 'bgn'; + case Baga_Koga = 'bgo'; + case Eastern_Balochi = 'bgp'; + case Bagri = 'bgq'; + case Bawm_Chin = 'bgr'; + case Tagabawa = 'bgs'; + case Bughotu = 'bgt'; + case Mbongno = 'bgu'; + case Warkay_Bipim = 'bgv'; + case Bhatri = 'bgw'; + case Balkan_Gagauz_Turkish = 'bgx'; + case Benggoi = 'bgy'; + case Banggai = 'bgz'; + case Bharia = 'bha'; + case Bhili = 'bhb'; + case Biga = 'bhc'; + case Bhadrawahi = 'bhd'; + case Bhaya = 'bhe'; + case Odiai = 'bhf'; + case Binandere = 'bhg'; + case Bukharic = 'bhh'; + case Bhilali = 'bhi'; + case Bahing = 'bhj'; + case Bimin = 'bhl'; + case Bathari = 'bhm'; + case Bohtan_Neo_Aramaic = 'bhn'; + case Bhojpuri = 'bho'; + case Bima = 'bhp'; + case Tukang_Besi_South = 'bhq'; + case Bara_Malagasy = 'bhr'; + case Buwal = 'bhs'; + case Bhattiyali = 'bht'; + case Bhunjia = 'bhu'; + case Bahau = 'bhv'; + case Biak = 'bhw'; + case Bhalay = 'bhx'; + case Bhele = 'bhy'; + case Bada_Indonesia = 'bhz'; + case Badimaya = 'bia'; + case Bissa = 'bib'; + case Bidiyo = 'bid'; + case Bepour = 'bie'; + case Biafada = 'bif'; + case Biangai = 'big'; + case Bikol = 'bik'; + case Bile = 'bil'; + case Bimoba = 'bim'; + case Bini = 'bin'; + case Nai = 'bio'; + case Bila = 'bip'; + case Bipi = 'biq'; + case Bisorio = 'bir'; + case Bislama = 'bis'; + case Berinomo = 'bit'; + case Biete = 'biu'; + case Southern_Birifor = 'biv'; + case Kol_Cameroon = 'biw'; + case Bijori = 'bix'; + case Birhor = 'biy'; + case Baloi = 'biz'; + case Budza = 'bja'; + case Banggarla = 'bjb'; + case Bariji = 'bjc'; + case Biao_Jiao_Mien = 'bje'; + case Barzani_Jewish_Neo_Aramaic = 'bjf'; + case Bidyogo = 'bjg'; + case Bahinemo = 'bjh'; + case Burji = 'bji'; + case Kanauji = 'bjj'; + case Barok = 'bjk'; + case Bulu_Papua_New_Guinea = 'bjl'; + case Bajelani = 'bjm'; + case Banjar = 'bjn'; + case Mid_Southern_Banda = 'bjo'; + case Fanamaket = 'bjp'; + case Binumarien = 'bjr'; + case Bajan = 'bjs'; + case Balanta_Ganja = 'bjt'; + case Busuu = 'bju'; + case Bedjond = 'bjv'; + case Bakwe = 'bjw'; + case Banao_Itneg = 'bjx'; + case Bayali = 'bjy'; + case Baruga = 'bjz'; + case Kyak = 'bka'; + case Baka_Cameroon = 'bkc'; + case Binukid = 'bkd'; + case Beeke = 'bkf'; + case Buraka = 'bkg'; + case Bakoko = 'bkh'; + case Baki = 'bki'; + case Pande = 'bkj'; + case Brokskat = 'bkk'; + case Berik = 'bkl'; + case Kom_Cameroon = 'bkm'; + case Bukitan = 'bkn'; + case Kwa = 'bko'; + case Boko_Democratic_Republic_of_Congo = 'bkp'; + case Bakairi = 'bkq'; + case Bakumpai = 'bkr'; + case Northern_Sorsoganon = 'bks'; + case Boloki = 'bkt'; + case Buhid = 'bku'; + case Bekwarra = 'bkv'; + case Bekwel = 'bkw'; + case Baikeno = 'bkx'; + case Bokyi = 'bky'; + case Bungku = 'bkz'; + case Siksika = 'bla'; + case Bilua = 'blb'; + case Bella_Coola = 'blc'; + case Bolango = 'bld'; + case Balanta_Kentohe = 'ble'; + case Buol = 'blf'; + case Kuwaa = 'blh'; + case Bolia = 'bli'; + case Bolongan = 'blj'; + case Pa_o_Karen = 'blk'; + case Biloxi = 'bll'; + case Beli_South_Sudan = 'blm'; + case Southern_Catanduanes_Bikol = 'bln'; + case Anii = 'blo'; + case Blablanga = 'blp'; + case Baluan_Pam = 'blq'; + case Blang = 'blr'; + case Balaesang = 'bls'; + case Tai_Dam = 'blt'; + case Kibala = 'blv'; + case Balangao = 'blw'; + case Mag_Indi_Ayta = 'blx'; + case Notre = 'bly'; + case Balantak = 'blz'; + case Lame = 'bma'; + case Bembe = 'bmb'; + case Biem = 'bmc'; + case Baga_Manduri = 'bmd'; + case Limassa = 'bme'; + case Bom_Kim = 'bmf'; + case Bamwe = 'bmg'; + case Kein = 'bmh'; + case Bagirmi = 'bmi'; + case Bote_Majhi = 'bmj'; + case Ghayavi = 'bmk'; + case Bomboli = 'bml'; + case Northern_Betsimisaraka_Malagasy = 'bmm'; + case Bina_Papua_New_Guinea = 'bmn'; + case Bambalang = 'bmo'; + case Bulgebi = 'bmp'; + case Bomu = 'bmq'; + case Muinane = 'bmr'; + case Bilma_Kanuri = 'bms'; + case Biao_Mon = 'bmt'; + case Somba_Siawari = 'bmu'; + case Bum = 'bmv'; + case Bomwali = 'bmw'; + case Baimak = 'bmx'; + case Baramu = 'bmz'; + case Bonerate = 'bna'; + case Bookan = 'bnb'; + case Bontok = 'bnc'; + case Banda_Indonesia = 'bnd'; + case Bintauna = 'bne'; + case Masiwang = 'bnf'; + case Benga = 'bng'; + case Bangi = 'bni'; + case Eastern_Tawbuid = 'bnj'; + case Bierebo = 'bnk'; + case Boon = 'bnl'; + case Batanga = 'bnm'; + case Bunun = 'bnn'; + case Bantoanon = 'bno'; + case Bola = 'bnp'; + case Bantik = 'bnq'; + case Butmas_Tur = 'bnr'; + case Bundeli = 'bns'; + case Bentong = 'bnu'; + case Bonerif = 'bnv'; + case Bisis = 'bnw'; + case Bangubangu = 'bnx'; + case Bintulu = 'bny'; + case Beezen = 'bnz'; + case Bora = 'boa'; + case Aweer = 'bob'; + case Tibetan = 'bod'; + case Mundabli = 'boe'; + case Bolon = 'bof'; + case Bamako_Sign_Language = 'bog'; + case Boma = 'boh'; + case Barbareno = 'boi'; + case Anjam = 'boj'; + case Bonjo = 'bok'; + case Bole = 'bol'; + case Berom = 'bom'; + case Bine = 'bon'; + case Tiemacewe_Bozo = 'boo'; + case Bonkiman = 'bop'; + case Bogaya = 'boq'; + case Bororo = 'bor'; + case Bosnian = 'bos'; + case Bongo = 'bot'; + case Bondei = 'bou'; + case Tuwuli = 'bov'; + case Rema = 'bow'; + case Buamu = 'box'; + case Bodo_Central_African_Republic = 'boy'; + case Tieyaxo_Bozo = 'boz'; + case Daakaka = 'bpa'; + case Mbuk = 'bpc'; + case Banda_Banda = 'bpd'; + case Bauni = 'bpe'; + case Bonggo = 'bpg'; + case Botlikh = 'bph'; + case Bagupi = 'bpi'; + case Binji = 'bpj'; + case Orowe = 'bpk'; + case Broome_Pearling_Lugger_Pidgin = 'bpl'; + case Biyom = 'bpm'; + case Dzao_Min = 'bpn'; + case Anasi = 'bpo'; + case Kaure = 'bpp'; + case Banda_Malay = 'bpq'; + case Koronadal_Blaan = 'bpr'; + case Sarangani_Blaan = 'bps'; + case Barrow_Point = 'bpt'; + case Bongu = 'bpu'; + case Bian_Marind = 'bpv'; + case Bo_Papua_New_Guinea = 'bpw'; + case Palya_Bareli = 'bpx'; + case Bishnupriya = 'bpy'; + case Bilba = 'bpz'; + case Tchumbuli = 'bqa'; + case Bagusa = 'bqb'; + case Boko_Benin = 'bqc'; + case Bung = 'bqd'; + case Baga_Kaloum = 'bqf'; + case Bago_Kusuntu = 'bqg'; + case Baima = 'bqh'; + case Bakhtiari = 'bqi'; + case Bandial = 'bqj'; + case Banda_Mbres = 'bqk'; + case Bilakura = 'bql'; + case Wumboko = 'bqm'; + case Bulgarian_Sign_Language = 'bqn'; + case Balo = 'bqo'; + case Busa = 'bqp'; + case Biritai = 'bqq'; + case Burusu = 'bqr'; + case Bosngun = 'bqs'; + case Bamukumbit = 'bqt'; + case Boguru = 'bqu'; + case Koro_Wachi = 'bqv'; + case Buru_Nigeria = 'bqw'; + case Baangi = 'bqx'; + case Bengkala_Sign_Language = 'bqy'; + case Bakaka = 'bqz'; + case Braj = 'bra'; + case Brao = 'brb'; + case Berbice_Creole_Dutch = 'brc'; + case Baraamu = 'brd'; + case Breton = 'bre'; + case Bira = 'brf'; + case Baure = 'brg'; + case Brahui = 'brh'; + case Mokpwe = 'bri'; + case Bieria = 'brj'; + case Birked = 'brk'; + case Birwa = 'brl'; + case Barambu = 'brm'; + case Boruca = 'brn'; + case Brokkat = 'bro'; + case Barapasi = 'brp'; + case Breri = 'brq'; + case Birao = 'brr'; + case Baras = 'brs'; + case Bitare = 'brt'; + case Eastern_Bru = 'bru'; + case Western_Bru = 'brv'; + case Bellari = 'brw'; + case Bodo_India = 'brx'; + case Burui = 'bry'; + case Bilbil = 'brz'; + case Abinomn = 'bsa'; + case Brunei_Bisaya = 'bsb'; + case Bassari = 'bsc'; + case Wushi = 'bse'; + case Bauchi = 'bsf'; + case Bashkardi = 'bsg'; + case Kati = 'bsh'; + case Bassossi = 'bsi'; + case Bangwinji = 'bsj'; + case Burushaski = 'bsk'; + case Basa_Gumna = 'bsl'; + case Busami = 'bsm'; + case Barasana_Eduria = 'bsn'; + case Buso = 'bso'; + case Baga_Sitemu = 'bsp'; + case Bassa = 'bsq'; + case Bassa_Kontagora = 'bsr'; + case Akoose = 'bss'; + case Basketo = 'bst'; + case Bahonsuai = 'bsu'; + case Baga_Sobane = 'bsv'; + case Baiso = 'bsw'; + case Yangkam = 'bsx'; + case Sabah_Bisaya = 'bsy'; + case Bata = 'bta'; + case Bati_Cameroon = 'btc'; + case Batak_Dairi = 'btd'; + case Gamo_Ningi = 'bte'; + case Birgit = 'btf'; + case Gagnoa_Bete = 'btg'; + case Biatah_Bidayuh = 'bth'; + case Burate = 'bti'; + case Bacanese_Malay = 'btj'; + case Batak_Mandailing = 'btm'; + case Ratagnon = 'btn'; + case Rinconada_Bikol = 'bto'; + case Budibud = 'btp'; + case Batek = 'btq'; + case Baetora = 'btr'; + case Batak_Simalungun = 'bts'; + case Bete_Bendi = 'btt'; + case Batu = 'btu'; + case Bateri = 'btv'; + case Butuanon = 'btw'; + case Batak_Karo = 'btx'; + case Bobot = 'bty'; + case Batak_Alas_Kluet = 'btz'; + case Buriat = 'bua'; + case Bua = 'bub'; + case Bushi = 'buc'; + case Ntcham = 'bud'; + case Beothuk = 'bue'; + case Bushoong = 'buf'; + case Buginese = 'bug'; + case Younuo_Bunu = 'buh'; + case Bongili = 'bui'; + case Basa_Gurmana = 'buj'; + case Bugawac = 'buk'; + case Bulgarian = 'bul'; + case Bulu_Cameroon = 'bum'; + case Sherbro = 'bun'; + case Terei = 'buo'; + case Busoa = 'bup'; + case Brem = 'buq'; + case Bokobaru = 'bus'; + case Bungain = 'but'; + case Budu = 'buu'; + case Bun = 'buv'; + case Bubi = 'buw'; + case Boghom = 'bux'; + case Bullom_So = 'buy'; + case Bukwen = 'buz'; + case Barein = 'bva'; + case Bube = 'bvb'; + case Baelelea = 'bvc'; + case Baeggu = 'bvd'; + case Berau_Malay = 'bve'; + case Boor = 'bvf'; + case Bonkeng = 'bvg'; + case Bure = 'bvh'; + case Belanda_Viri = 'bvi'; + case Baan = 'bvj'; + case Bukat = 'bvk'; + case Bolivian_Sign_Language = 'bvl'; + case Bamunka = 'bvm'; + case Buna = 'bvn'; + case Bolgo = 'bvo'; + case Bumang = 'bvp'; + case Birri = 'bvq'; + case Burarra = 'bvr'; + case Bati_Indonesia = 'bvt'; + case Bukit_Malay = 'bvu'; + case Baniva = 'bvv'; + case Boga = 'bvw'; + case Dibole = 'bvx'; + case Baybayanon = 'bvy'; + case Bauzi = 'bvz'; + case Bwatoo = 'bwa'; + case Namosi_Naitasiri_Serua = 'bwb'; + case Bwile = 'bwc'; + case Bwaidoka = 'bwd'; + case Bwe_Karen = 'bwe'; + case Boselewa = 'bwf'; + case Barwe = 'bwg'; + case Bishuo = 'bwh'; + case Baniwa = 'bwi'; + case Laa_Laa_Bwamu = 'bwj'; + case Bauwaki = 'bwk'; + case Bwela = 'bwl'; + case Biwat = 'bwm'; + case Wunai_Bunu = 'bwn'; + case Boro_Ethiopia = 'bwo'; + case Mandobo_Bawah = 'bwp'; + case Southern_Bobo_Madare = 'bwq'; + case Bura_Pabir = 'bwr'; + case Bomboma = 'bws'; + case Bafaw_Balong = 'bwt'; + case Buli_Ghana = 'bwu'; + case Bwa = 'bww'; + case Bu_Nao_Bunu = 'bwx'; + case Cwi_Bwamu = 'bwy'; + case Bwisi = 'bwz'; + case Tairaha = 'bxa'; + case Belanda_Bor = 'bxb'; + case Molengue = 'bxc'; + case Pela = 'bxd'; + case Birale = 'bxe'; + case Bilur = 'bxf'; + case Bangala = 'bxg'; + case Buhutu = 'bxh'; + case Pirlatapa = 'bxi'; + case Bayungu = 'bxj'; + case Bukusu = 'bxk'; + case Jalkunan = 'bxl'; + case Mongolia_Buriat = 'bxm'; + case Burduna = 'bxn'; + case Barikanchi = 'bxo'; + case Bebil = 'bxp'; + case Beele = 'bxq'; + case Russia_Buriat = 'bxr'; + case Busam = 'bxs'; + case China_Buriat = 'bxu'; + case Berakou = 'bxv'; + case Bankagooma = 'bxw'; + case Binahari = 'bxz'; + case Batak = 'bya'; + case Bikya = 'byb'; + case Ubaghara = 'byc'; + case Benyadu = 'byd'; + case Pouye = 'bye'; + case Bete = 'byf'; + case Baygo = 'byg'; + case Bhujel = 'byh'; + case Buyu = 'byi'; + case Bina_Nigeria = 'byj'; + case Biao = 'byk'; + case Bayono = 'byl'; + case Bidjara = 'bym'; + case Bilin = 'byn'; + case Biyo = 'byo'; + case Bumaji = 'byp'; + case Basay = 'byq'; + case Baruya = 'byr'; + case Burak = 'bys'; + case Berti = 'byt'; + case Medumba = 'byv'; + case Belhariya = 'byw'; + case Qaqet = 'byx'; + case Banaro = 'byz'; + case Bandi = 'bza'; + case Andio = 'bzb'; + case Southern_Betsimisaraka_Malagasy = 'bzc'; + case Bribri = 'bzd'; + case Jenaama_Bozo = 'bze'; + case Boikin = 'bzf'; + case Babuza = 'bzg'; + case Mapos_Buang = 'bzh'; + case Bisu = 'bzi'; + case Belize_Kriol_English = 'bzj'; + case Nicaragua_Creole_English = 'bzk'; + case Boano_Sulawesi = 'bzl'; + case Bolondo = 'bzm'; + case Boano_Maluku = 'bzn'; + case Bozaba = 'bzo'; + case Kemberano = 'bzp'; + case Buli_Indonesia = 'bzq'; + case Biri = 'bzr'; + case Brazilian_Sign_Language = 'bzs'; + case Brithenig = 'bzt'; + case Burmeso = 'bzu'; + case Naami = 'bzv'; + case Basa_Nigeria = 'bzw'; + case Kelengaxo_Bozo = 'bzx'; + case Obanliku = 'bzy'; + case Evant = 'bzz'; + case Chorti = 'caa'; + case Garifuna = 'cab'; + case Chuj = 'cac'; + case Caddo = 'cad'; + case Lehar = 'cae'; + case Southern_Carrier = 'caf'; + case Nivacle = 'cag'; + case Cahuarano = 'cah'; + case Chane = 'caj'; + case Kaqchikel = 'cak'; + case Carolinian = 'cal'; + case Cemuhi = 'cam'; + case Chambri = 'can'; + case Chacobo = 'cao'; + case Chipaya = 'cap'; + case Car_Nicobarese = 'caq'; + case Galibi_Carib = 'car'; + case Tsimane = 'cas'; + case Catalan = 'cat'; + case Cavinena = 'cav'; + case Callawalla = 'caw'; + case Chiquitano = 'cax'; + case Cayuga = 'cay'; + case Canichana = 'caz'; + case Cabiyari = 'cbb'; + case Carapana = 'cbc'; + case Carijona = 'cbd'; + case Chimila = 'cbg'; + case Chachi = 'cbi'; + case Ede_Cabe = 'cbj'; + case Chavacano = 'cbk'; + case Bualkhaw_Chin = 'cbl'; + case Nyahkur = 'cbn'; + case Izora = 'cbo'; + case Tsucuba = 'cbq'; + case Cashibo_Cacataibo = 'cbr'; + case Cashinahua = 'cbs'; + case Chayahuita = 'cbt'; + case Candoshi_Shapra = 'cbu'; + case Cacua = 'cbv'; + case Kinabalian = 'cbw'; + case Carabayo = 'cby'; + case Chamicuro = 'ccc'; + case Cafundo_Creole = 'ccd'; + case Chopi = 'cce'; + case Samba_Daka = 'ccg'; + case Atsam = 'cch'; + case Kasanga = 'ccj'; + case Cutchi_Swahili = 'ccl'; + case Malaccan_Creole_Malay = 'ccm'; + case Comaltepec_Chinantec = 'cco'; + case Chakma = 'ccp'; + case Cacaopera = 'ccr'; + case Choni = 'cda'; + case Chenchu = 'cde'; + case Chiru = 'cdf'; + case Chambeali = 'cdh'; + case Chodri = 'cdi'; + case Churahi = 'cdj'; + case Chepang = 'cdm'; + case Chaudangsi = 'cdn'; + case Min_Dong_Chinese = 'cdo'; + case Cinda_Regi_Tiyal = 'cdr'; + case Chadian_Sign_Language = 'cds'; + case Chadong = 'cdy'; + case Koda = 'cdz'; + case Lower_Chehalis = 'cea'; + case Cebuano = 'ceb'; + case Chamacoco = 'ceg'; + case Eastern_Khumi_Chin = 'cek'; + case Cen = 'cen'; + case Czech = 'ces'; + case Centuum = 'cet'; + case Ekai_Chin = 'cey'; + case Dijim_Bwilim = 'cfa'; + case Cara = 'cfd'; + case Como_Karim = 'cfg'; + case Falam_Chin = 'cfm'; + case Changriwa = 'cga'; + case Kagayanen = 'cgc'; + case Chiga = 'cgg'; + case Chocangacakha = 'cgk'; + case Chamorro = 'cha'; + case Chibcha = 'chb'; + case Catawba = 'chc'; + case Highland_Oaxaca_Chontal = 'chd'; + case Chechen = 'che'; + case Tabasco_Chontal = 'chf'; + case Chagatai = 'chg'; + case Chinook = 'chh'; + case Ojitlan_Chinantec = 'chj'; + case Chuukese = 'chk'; + case Cahuilla = 'chl'; + case Mari_Russia = 'chm'; + case Chinook_jargon = 'chn'; + case Choctaw = 'cho'; + case Chipewyan = 'chp'; + case Quiotepec_Chinantec = 'chq'; + case Cherokee = 'chr'; + case Cholon = 'cht'; + case Church_Slavic = 'chu'; + case Chuvash = 'chv'; + case Chuwabu = 'chw'; + case Chantyal = 'chx'; + case Cheyenne = 'chy'; + case Ozumacin_Chinantec = 'chz'; + case Cia_Cia = 'cia'; + case Ci_Gbe = 'cib'; + case Chickasaw = 'cic'; + case Chimariko = 'cid'; + case Cineni = 'cie'; + case Chinali = 'cih'; + case Chitkuli_Kinnauri = 'cik'; + case Cimbrian = 'cim'; + case Cinta_Larga = 'cin'; + case Chiapanec = 'cip'; + case Tiri = 'cir'; + case Chippewa = 'ciw'; + case Chaima = 'ciy'; + case Western_Cham = 'cja'; + case Chru = 'cje'; + case Upper_Chehalis = 'cjh'; + case Chamalal = 'cji'; + case Chokwe = 'cjk'; + case Eastern_Cham = 'cjm'; + case Chenapian = 'cjn'; + case Asheninka_Pajonal = 'cjo'; + case Cabecar = 'cjp'; + case Shor = 'cjs'; + case Chuave = 'cjv'; + case Jinyu_Chinese = 'cjy'; + case Central_Kurdish = 'ckb'; + case Chak = 'ckh'; + case Cibak = 'ckl'; + case Chakavian = 'ckm'; + case Kaang_Chin = 'ckn'; + case Anufo = 'cko'; + case Kajakse = 'ckq'; + case Kairak = 'ckr'; + case Tayo = 'cks'; + case Chukot = 'ckt'; + case Koasati = 'cku'; + case Kavalan = 'ckv'; + case Caka = 'ckx'; + case Cakfem_Mushere = 'cky'; + case Cakchiquel_Quiche_Mixed_Language = 'ckz'; + case Ron = 'cla'; + case Chilcotin = 'clc'; + case Chaldean_Neo_Aramaic = 'cld'; + case Lealao_Chinantec = 'cle'; + case Chilisso = 'clh'; + case Chakali = 'cli'; + case Laitu_Chin = 'clj'; + case Idu_Mishmi = 'clk'; + case Chala = 'cll'; + case Clallam = 'clm'; + case Lowland_Oaxaca_Chontal = 'clo'; + case Classical_Sanskrit = 'cls'; + case Lautu_Chin = 'clt'; + case Caluyanun = 'clu'; + case Chulym = 'clw'; + case Eastern_Highland_Chatino = 'cly'; + case Maa = 'cma'; + case Cerma = 'cme'; + case Classical_Mongolian = 'cmg'; + case Embera_Chami = 'cmi'; + case Campalagian = 'cml'; + case Michigamea = 'cmm'; + case Mandarin_Chinese = 'cmn'; + case Central_Mnong = 'cmo'; + case Mro_Khimi_Chin = 'cmr'; + case Messapic = 'cms'; + case Camtho = 'cmt'; + case Changthang = 'cna'; + case Chinbon_Chin = 'cnb'; + case Coong = 'cnc'; + case Northern_Qiang = 'cng'; + case Hakha_Chin = 'cnh'; + case Ashaninka = 'cni'; + case Khumi_Chin = 'cnk'; + case Lalana_Chinantec = 'cnl'; + case Con = 'cno'; + case Northern_Ping_Chinese = 'cnp'; + case Chung = 'cnq'; + case Montenegrin = 'cnr'; + case Central_Asmat = 'cns'; + case Tepetotutla_Chinantec = 'cnt'; + case Chenoua = 'cnu'; + case Ngawn_Chin = 'cnw'; + case Middle_Cornish = 'cnx'; + case Cocos_Islands_Malay = 'coa'; + case Chicomuceltec = 'cob'; + case Cocopa = 'coc'; + case Cocama_Cocamilla = 'cod'; + case Koreguaje = 'coe'; + case Colorado = 'cof'; + case Chong = 'cog'; + case Chonyi_Dzihana_Kauma = 'coh'; + case Cochimi = 'coj'; + case Santa_Teresa_Cora = 'cok'; + case Columbia_Wenatchi = 'col'; + case Comanche = 'com'; + case Cofan = 'con'; + case Comox = 'coo'; + case Coptic = 'cop'; + case Coquille = 'coq'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Caquinte = 'cot'; + case Wamey = 'cou'; + case Cao_Miao = 'cov'; + case Cowlitz = 'cow'; + case Nanti = 'cox'; + case Chochotec = 'coz'; + case Palantla_Chinantec = 'cpa'; + case Ucayali_Yurua_Asheninka = 'cpb'; + case Ajyininka_Apurucayali = 'cpc'; + case Cappadocian_Greek = 'cpg'; + case Chinese_Pidgin_English = 'cpi'; + case Cherepon = 'cpn'; + case Kpeego = 'cpo'; + case Capiznon = 'cps'; + case Pichis_Asheninka = 'cpu'; + case Pu_Xian_Chinese = 'cpx'; + case South_Ucayali_Asheninka = 'cpy'; + case Chuanqiandian_Cluster_Miao = 'cqd'; + case Chara = 'cra'; + case Island_Carib = 'crb'; + case Lonwolwol = 'crc'; + case Coeur_d_Alene = 'crd'; + case Cree = 'cre'; + case Caramanta = 'crf'; + case Michif = 'crg'; + case Crimean_Tatar = 'crh'; + case Saotomense = 'cri'; + case Southern_East_Cree = 'crj'; + case Plains_Cree = 'crk'; + case Northern_East_Cree = 'crl'; + case Moose_Cree = 'crm'; + case El_Nayar_Cora = 'crn'; + case Crow = 'cro'; + case Iyo_wujwa_Chorote = 'crq'; + case Carolina_Algonquian = 'crr'; + case Seselwa_Creole_French = 'crs'; + case Iyojwa_ja_Chorote = 'crt'; + case Chaura = 'crv'; + case Chrau = 'crw'; + case Carrier = 'crx'; + case Cori = 'cry'; + case Cruzeno = 'crz'; + case Chiltepec_Chinantec = 'csa'; + case Kashubian = 'csb'; + case Catalan_Sign_Language = 'csc'; + case Chiangmai_Sign_Language = 'csd'; + case Czech_Sign_Language = 'cse'; + case Cuba_Sign_Language = 'csf'; + case Chilean_Sign_Language = 'csg'; + case Asho_Chin = 'csh'; + case Coast_Miwok = 'csi'; + case Songlai_Chin = 'csj'; + case Jola_Kasa = 'csk'; + case Chinese_Sign_Language = 'csl'; + case Central_Sierra_Miwok = 'csm'; + case Colombian_Sign_Language = 'csn'; + case Sochiapam_Chinantec = 'cso'; + case Southern_Ping_Chinese = 'csp'; + case Croatia_Sign_Language = 'csq'; + case Costa_Rican_Sign_Language = 'csr'; + case Southern_Ohlone = 'css'; + case Northern_Ohlone = 'cst'; + case Sumtu_Chin = 'csv'; + case Swampy_Cree = 'csw'; + case Cambodian_Sign_Language = 'csx'; + case Siyin_Chin = 'csy'; + case Coos = 'csz'; + case Tataltepec_Chatino = 'cta'; + case Chetco = 'ctc'; + case Tedim_Chin = 'ctd'; + case Tepinapa_Chinantec = 'cte'; + case Chittagonian = 'ctg'; + case Thaiphum_Chin = 'cth'; + case Tlacoatzintepec_Chinantec = 'ctl'; + case Chitimacha = 'ctm'; + case Chhintange = 'ctn'; + case Embera_Catio = 'cto'; + case Western_Highland_Chatino = 'ctp'; + case Northern_Catanduanes_Bikol = 'cts'; + case Wayanad_Chetti = 'ctt'; + case Chol = 'ctu'; + case Moundadan_Chetty = 'cty'; + case Zacatepec_Chatino = 'ctz'; + case Cua = 'cua'; + case Cubeo = 'cub'; + case Usila_Chinantec = 'cuc'; + case Chuka = 'cuh'; + case Cuiba = 'cui'; + case Mashco_Piro = 'cuj'; + case San_Blas_Kuna = 'cuk'; + case Culina = 'cul'; + case Cumanagoto = 'cuo'; + case Cupeno = 'cup'; + case Cun = 'cuq'; + case Chhulung = 'cur'; + case Teutila_Cuicatec = 'cut'; + case Tai_Ya = 'cuu'; + case Cuvok = 'cuv'; + case Chukwa = 'cuw'; + case Tepeuxila_Cuicatec = 'cux'; + case Cuitlatec = 'cuy'; + case Chug = 'cvg'; + case Valle_Nacional_Chinantec = 'cvn'; + case Kabwa = 'cwa'; + case Maindo = 'cwb'; + case Woods_Cree = 'cwd'; + case Kwere = 'cwe'; + case Chewong = 'cwg'; + case Kuwaataay = 'cwt'; + case Cha_ari = 'cxh'; + case Nopala_Chatino = 'cya'; + case Cayubaba = 'cyb'; + case Welsh = 'cym'; + case Cuyonon = 'cyo'; + case Huizhou_Chinese = 'czh'; + case Knaanic = 'czk'; + case Zenzontepec_Chatino = 'czn'; + case Min_Zhong_Chinese = 'czo'; + case Zotung_Chin = 'czt'; + case Dangaleat = 'daa'; + case Dambi = 'dac'; + case Marik = 'dad'; + case Duupa = 'dae'; + case Dagbani = 'dag'; + case Gwahatike = 'dah'; + case Day = 'dai'; + case Dar_Fur_Daju = 'daj'; + case Dakota = 'dak'; + case Dahalo = 'dal'; + case Damakawa = 'dam'; + case Danish = 'dan'; + case Daai_Chin = 'dao'; + case Dandami_Maria = 'daq'; + case Dargwa = 'dar'; + case Daho_Doo = 'das'; + case Dar_Sila_Daju = 'dau'; + case Taita = 'dav'; + case Davawenyo = 'daw'; + case Dayi = 'dax'; + case Dao = 'daz'; + case Bangime = 'dba'; + case Deno = 'dbb'; + case Dadiya = 'dbd'; + case Dabe = 'dbe'; + case Edopi = 'dbf'; + case Dogul_Dom_Dogon = 'dbg'; + case Doka = 'dbi'; + case Ida_an = 'dbj'; + case Dyirbal = 'dbl'; + case Duguri = 'dbm'; + case Duriankere = 'dbn'; + case Dulbu = 'dbo'; + case Duwai = 'dbp'; + case Daba = 'dbq'; + case Dabarre = 'dbr'; + case Ben_Tey_Dogon = 'dbt'; + case Bondum_Dom_Dogon = 'dbu'; + case Dungu = 'dbv'; + case Bankan_Tey_Dogon = 'dbw'; + case Dibiyaso = 'dby'; + case Deccan = 'dcc'; + case Negerhollands = 'dcr'; + case Dadi_Dadi = 'dda'; + case Dongotono = 'ddd'; + case Doondo = 'dde'; + case Fataluku = 'ddg'; + case West_Goodenough = 'ddi'; + case Jaru = 'ddj'; + case Dendi_Benin = 'ddn'; + case Dido = 'ddo'; + case Dhudhuroa = 'ddr'; + case Donno_So_Dogon = 'dds'; + case Dawera_Daweloor = 'ddw'; + case Dagik = 'dec'; + case Dedua = 'ded'; + case Dewoin = 'dee'; + case Dezfuli = 'def'; + case Degema = 'deg'; + case Dehwari = 'deh'; + case Demisa = 'dei'; + case Dek = 'dek'; + case Delaware = 'del'; + case Dem = 'dem'; + case Slave_Athapascan = 'den'; + case Pidgin_Delaware = 'dep'; + case Dendi_Central_African_Republic = 'deq'; + case Deori = 'der'; + case Desano = 'des'; + case German = 'deu'; + case Domung = 'dev'; + case Dengese = 'dez'; + case Southern_Dagaare = 'dga'; + case Bunoge_Dogon = 'dgb'; + case Casiguran_Dumagat_Agta = 'dgc'; + case Dagaari_Dioula = 'dgd'; + case Degenan = 'dge'; + case Doga = 'dgg'; + case Dghwede = 'dgh'; + case Northern_Dagara = 'dgi'; + case Dagba = 'dgk'; + case Andaandi = 'dgl'; + case Dagoman = 'dgn'; + case Dogri_individual_language = 'dgo'; + case Dogrib = 'dgr'; + case Dogoso = 'dgs'; + case Ndra_ngith = 'dgt'; + case Daungwurrung = 'dgw'; + case Doghoro = 'dgx'; + case Daga = 'dgz'; + case Dhundari = 'dhd'; + case Dhangu_Djangu = 'dhg'; + case Dhimal = 'dhi'; + case Dhalandji = 'dhl'; + case Zemba = 'dhm'; + case Dhanki = 'dhn'; + case Dhodia = 'dho'; + case Dhargari = 'dhr'; + case Dhaiso = 'dhs'; + case Dhurga = 'dhu'; + case Dehu = 'dhv'; + case Dhanwar_Nepal = 'dhw'; + case Dhungaloo = 'dhx'; + case Dia = 'dia'; + case South_Central_Dinka = 'dib'; + case Lakota_Dida = 'dic'; + case Didinga = 'did'; + case Dieri = 'dif'; + case Digo = 'dig'; + case Kumiai = 'dih'; + case Dimbong = 'dii'; + case Dai = 'dij'; + case Southwestern_Dinka = 'dik'; + case Dilling = 'dil'; + case Dime = 'dim'; + case Dinka = 'din'; + case Dibo = 'dio'; + case Northeastern_Dinka = 'dip'; + case Dimli_individual_language = 'diq'; + case Dirim = 'dir'; + case Dimasa = 'dis'; + case Diriku = 'diu'; + case Dhivehi = 'div'; + case Northwestern_Dinka = 'diw'; + case Dixon_Reef = 'dix'; + case Diuwe = 'diy'; + case Ding = 'diz'; + case Djadjawurrung = 'dja'; + case Djinba = 'djb'; + case Dar_Daju_Daju = 'djc'; + case Djamindjung = 'djd'; + case Zarma = 'dje'; + case Djangun = 'djf'; + case Djinang = 'dji'; + case Djeebbana = 'djj'; + case Eastern_Maroon_Creole = 'djk'; + case Jamsay_Dogon = 'djm'; + case Jawoyn = 'djn'; + case Jangkang = 'djo'; + case Djambarrpuyngu = 'djr'; + case Kapriman = 'dju'; + case Djawi = 'djw'; + case Dakpakha = 'dka'; + case Kadung = 'dkg'; + case Dakka = 'dkk'; + case Kuijau = 'dkr'; + case Southeastern_Dinka = 'dks'; + case Mazagway = 'dkx'; + case Dolgan = 'dlg'; + case Dahalik = 'dlk'; + case Dalmatian = 'dlm'; + case Darlong = 'dln'; + case Duma = 'dma'; + case Mombo_Dogon = 'dmb'; + case Gavak = 'dmc'; + case Madhi_Madhi = 'dmd'; + case Dugwor = 'dme'; + case Medefaidrin = 'dmf'; + case Upper_Kinabatangan = 'dmg'; + case Domaaki = 'dmk'; + case Dameli = 'dml'; + case Dama = 'dmm'; + case Kemedzung = 'dmo'; + case East_Damar = 'dmr'; + case Dampelas = 'dms'; + case Dubu = 'dmu'; + case Dumpas = 'dmv'; + case Mudburra = 'dmw'; + case Dema = 'dmx'; + case Demta = 'dmy'; + case Upper_Grand_Valley_Dani = 'dna'; + case Daonda = 'dnd'; + case Ndendeule = 'dne'; + case Dungan = 'dng'; + case Lower_Grand_Valley_Dani = 'dni'; + case Dan = 'dnj'; + case Dengka = 'dnk'; + case Dzuungoo = 'dnn'; + case Ndrulo = 'dno'; + case Danaru = 'dnr'; + case Mid_Grand_Valley_Dani = 'dnt'; + case Danau = 'dnu'; + case Danu = 'dnv'; + case Western_Dani = 'dnw'; + case Deni = 'dny'; + case Dom = 'doa'; + case Dobu = 'dob'; + case Northern_Dong = 'doc'; + case Doe = 'doe'; + case Domu = 'dof'; + case Dong = 'doh'; + case Dogri_macrolanguage = 'doi'; + case Dondo = 'dok'; + case Doso = 'dol'; + case Toura_Papua_New_Guinea = 'don'; + case Dongo = 'doo'; + case Lukpa = 'dop'; + case Dominican_Sign_Language = 'doq'; + case Dori_o = 'dor'; + case Dogose = 'dos'; + case Dass = 'dot'; + case Dombe = 'dov'; + case Doyayo = 'dow'; + case Bussa = 'dox'; + case Dompo = 'doy'; + case Dorze = 'doz'; + case Papar = 'dpp'; + case Dair = 'drb'; + case Minderico = 'drc'; + case Darmiya = 'drd'; + case Dolpo = 'dre'; + case Rungus = 'drg'; + case C_Lela = 'dri'; + case Paakantyi = 'drl'; + case West_Damar = 'drn'; + case Daro_Matu_Melanau = 'dro'; + case Dura = 'drq'; + case Gedeo = 'drs'; + case Drents = 'drt'; + case Rukai = 'dru'; + case Darai = 'dry'; + case Lower_Sorbian = 'dsb'; + case Dutch_Sign_Language = 'dse'; + case Daasanach = 'dsh'; + case Disa = 'dsi'; + case Dokshi = 'dsk'; + case Danish_Sign_Language = 'dsl'; + case Dusner = 'dsn'; + case Desiya = 'dso'; + case Tadaksahak = 'dsq'; + case Mardin_Sign_Language = 'dsz'; + case Daur = 'dta'; + case Labuk_Kinabatangan_Kadazan = 'dtb'; + case Ditidaht = 'dtd'; + case Adithinngithigh = 'dth'; + case Ana_Tinga_Dogon = 'dti'; + case Tene_Kan_Dogon = 'dtk'; + case Tomo_Kan_Dogon = 'dtm'; + case Daats_iin = 'dtn'; + case Tommo_So_Dogon = 'dto'; + case Kadazan_Dusun = 'dtp'; + case Lotud = 'dtr'; + case Toro_So_Dogon = 'dts'; + case Toro_Tegu_Dogon = 'dtt'; + case Tebul_Ure_Dogon = 'dtu'; + case Dotyali = 'dty'; + case Duala = 'dua'; + case Dubli = 'dub'; + case Duna = 'duc'; + case Umiray_Dumaget_Agta = 'due'; + case Dumbea = 'duf'; + case Duruma = 'dug'; + case Dungra_Bhil = 'duh'; + case Dumun = 'dui'; + case Uyajitaya = 'duk'; + case Alabat_Island_Agta = 'dul'; + case Middle_Dutch_ca_1050_1350 = 'dum'; + case Dusun_Deyah = 'dun'; + case Dupaninan_Agta = 'duo'; + case Duano = 'dup'; + case Dusun_Malang = 'duq'; + case Dii = 'dur'; + case Dumi = 'dus'; + case Drung = 'duu'; + case Duvle = 'duv'; + case Dusun_Witu = 'duw'; + case Duungooma = 'dux'; + case Dicamay_Agta = 'duy'; + case Duli_Gey = 'duz'; + case Duau = 'dva'; + case Diri = 'dwa'; + case Dawik_Kui = 'dwk'; + case Dawro = 'dwr'; + case Dutton_World_Speedwords = 'dws'; + case Dhuwal = 'dwu'; + case Dawawa = 'dww'; + case Dhuwaya = 'dwy'; + case Dewas_Rai = 'dwz'; + case Dyan = 'dya'; + case Dyaberdyaber = 'dyb'; + case Dyugun = 'dyd'; + case Villa_Viciosa_Agta = 'dyg'; + case Djimini_Senoufo = 'dyi'; + case Yanda_Dom_Dogon = 'dym'; + case Dyangadi = 'dyn'; + case Jola_Fonyi = 'dyo'; + case Dyarim = 'dyr'; + case Dyula = 'dyu'; + case Djabugay = 'dyy'; + case Tunzu = 'dza'; + case Daza = 'dzd'; + case Djiwarli = 'dze'; + case Dazaga = 'dzg'; + case Dzalakha = 'dzl'; + case Dzando = 'dzn'; + case Dzongkha = 'dzo'; + case Karenggapa = 'eaa'; + case Beginci = 'ebc'; + case Ebughu = 'ebg'; + case Eastern_Bontok = 'ebk'; + case Teke_Ebo = 'ebo'; + case Ebrie = 'ebr'; + case Embu = 'ebu'; + case Eteocretan = 'ecr'; + case Ecuadorian_Sign_Language = 'ecs'; + case Eteocypriot = 'ecy'; + case E = 'eee'; + case Efai = 'efa'; + case Efe = 'efe'; + case Efik = 'efi'; + case Ega = 'ega'; + case Emilian = 'egl'; + case Benamanga = 'egm'; + case Eggon = 'ego'; + case Egyptian_Ancient = 'egy'; + case Miyakubo_Sign_Language = 'ehs'; + case Ehueun = 'ehu'; + case Eipomek = 'eip'; + case Eitiep = 'eit'; + case Askopan = 'eiv'; + case Ejamat = 'eja'; + case Ekajuk = 'eka'; + case Ekit = 'eke'; + case Ekari = 'ekg'; + case Eki = 'eki'; + case Standard_Estonian = 'ekk'; + case Kol_Bangladesh = 'ekl'; + case Elip = 'ekm'; + case Koti = 'eko'; + case Ekpeye = 'ekp'; + case Yace = 'ekr'; + case Eastern_Kayah = 'eky'; + case Elepi = 'ele'; + case El_Hugeirat = 'elh'; + case Nding = 'eli'; + case Elkei = 'elk'; + case Modern_Greek_1453 = 'ell'; + case Eleme = 'elm'; + case El_Molo = 'elo'; + case Elu = 'elu'; + case Elamite = 'elx'; + case Emai_Iuleha_Ora = 'ema'; + case Embaloh = 'emb'; + case Emerillon = 'eme'; + case Eastern_Meohang = 'emg'; + case Mussau_Emira = 'emi'; + case Eastern_Maninkakan = 'emk'; + case Mamulique = 'emm'; + case Eman = 'emn'; + case Northern_Embera = 'emp'; + case Eastern_Minyag = 'emq'; + case Pacific_Gulf_Yupik = 'ems'; + case Eastern_Muria = 'emu'; + case Emplawas = 'emw'; + case Erromintxela = 'emx'; + case Epigraphic_Mayan = 'emy'; + case Mbessa = 'emz'; + case Apali = 'ena'; + case Markweeta = 'enb'; + case En = 'enc'; + case Ende = 'end'; + case Forest_Enets = 'enf'; + case English = 'eng'; + case Tundra_Enets = 'enh'; + case Enlhet = 'enl'; + case Middle_English_1100_1500 = 'enm'; + case Engenni = 'enn'; + case Enggano = 'eno'; + case Enga = 'enq'; + case Emumu = 'enr'; + case Enu = 'enu'; + case Enwan_Edo_State = 'env'; + case Enwan_Akwa_Ibom_State = 'enw'; + case Enxet = 'enx'; + case Beti_Cote_d_Ivoire = 'eot'; + case Epie = 'epi'; + case Esperanto = 'epo'; + case Eravallan = 'era'; + case Sie = 'erg'; + case Eruwa = 'erh'; + case Ogea = 'eri'; + case South_Efate = 'erk'; + case Horpa = 'ero'; + case Erre = 'err'; + case Ersu = 'ers'; + case Eritai = 'ert'; + case Erokwanas = 'erw'; + case Ese_Ejja = 'ese'; + case Aheri_Gondi = 'esg'; + case Eshtehardi = 'esh'; + case North_Alaskan_Inupiatun = 'esi'; + case Northwest_Alaska_Inupiatun = 'esk'; + case Egypt_Sign_Language = 'esl'; + case Esuma = 'esm'; + case Salvadoran_Sign_Language = 'esn'; + case Estonian_Sign_Language = 'eso'; + case Esselen = 'esq'; + case Central_Siberian_Yupik = 'ess'; + case Estonian = 'est'; + case Central_Yupik = 'esu'; + case Eskayan = 'esy'; + case Etebi = 'etb'; + case Etchemin = 'etc'; + case Ethiopian_Sign_Language = 'eth'; + case Eton_Vanuatu = 'etn'; + case Eton_Cameroon = 'eto'; + case Edolo = 'etr'; + case Yekhee = 'ets'; + case Etruscan = 'ett'; + case Ejagham = 'etu'; + case Eten = 'etx'; + case Semimi = 'etz'; + case Eudeve = 'eud'; + case Basque = 'eus'; + case Even = 'eve'; + case Uvbie = 'evh'; + case Evenki = 'evn'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Extremaduran = 'ext'; + case Eyak = 'eya'; + case Keiyo = 'eyo'; + case Ezaa = 'eza'; + case Uzekwe = 'eze'; + case Fasu = 'faa'; + case Fa_d_Ambu = 'fab'; + case Wagi = 'fad'; + case Fagani = 'faf'; + case Finongan = 'fag'; + case Baissa_Fali = 'fah'; + case Faiwol = 'fai'; + case Faita = 'faj'; + case Fang_Cameroon = 'fak'; + case South_Fali = 'fal'; + case Fam = 'fam'; + case Fang_Equatorial_Guinea = 'fan'; + case Faroese = 'fao'; + case Paloor = 'fap'; + case Fataleka = 'far'; + case Persian = 'fas'; + case Fanti = 'fat'; + case Fayu = 'fau'; + case Fala = 'fax'; + case Southwestern_Fars = 'fay'; + case Northwestern_Fars = 'faz'; + case West_Albay_Bikol = 'fbl'; + case Quebec_Sign_Language = 'fcs'; + case Feroge = 'fer'; + case Foia_Foia = 'ffi'; + case Maasina_Fulfulde = 'ffm'; + case Fongoro = 'fgr'; + case Nobiin = 'fia'; + case Fyer = 'fie'; + case Faifi = 'fif'; + case Fijian = 'fij'; + case Filipino = 'fil'; + case Finnish = 'fin'; + case Fipa = 'fip'; + case Firan = 'fir'; + case Tornedalen_Finnish = 'fit'; + case Fiwaga = 'fiw'; + case Kirya_Konzel = 'fkk'; + case Kven_Finnish = 'fkv'; + case Kalispel_Pend_d_Oreille = 'fla'; + case Foau = 'flh'; + case Fali = 'fli'; + case North_Fali = 'fll'; + case Flinders_Island = 'fln'; + case Fuliiru = 'flr'; + case Flaaitaal = 'fly'; + case Fe_fe = 'fmp'; + case Far_Western_Muria = 'fmu'; + case Fanbak = 'fnb'; + case Fanagalo = 'fng'; + case Fania = 'fni'; + case Foodo = 'fod'; + case Foi = 'foi'; + case Foma = 'fom'; + case Fon = 'fon'; + case Fore = 'for'; + case Siraya = 'fos'; + case Fernando_Po_Creole_English = 'fpe'; + case Fas = 'fqs'; + case French = 'fra'; + case Cajun_French = 'frc'; + case Fordata = 'frd'; + case Frankish = 'frk'; + case Middle_French_ca_1400_1600 = 'frm'; + case Old_French_842_ca_1400 = 'fro'; + case Arpitan = 'frp'; + case Forak = 'frq'; + case Northern_Frisian = 'frr'; + case Eastern_Frisian = 'frs'; + case Fortsenal = 'frt'; + case Western_Frisian = 'fry'; + case Finnish_Sign_Language = 'fse'; + case French_Sign_Language = 'fsl'; + case Finland_Swedish_Sign_Language = 'fss'; + case Adamawa_Fulfulde = 'fub'; + case Pulaar = 'fuc'; + case East_Futuna = 'fud'; + case Borgu_Fulfulde = 'fue'; + case Pular = 'fuf'; + case Western_Niger_Fulfulde = 'fuh'; + case Bagirmi_Fulfulde = 'fui'; + case Ko = 'fuj'; + case Fulah = 'ful'; + case Fum = 'fum'; + case Fulnio = 'fun'; + case Central_Eastern_Niger_Fulfulde = 'fuq'; + case Friulian = 'fur'; + case Futuna_Aniwa = 'fut'; + case Furu = 'fuu'; + case Nigerian_Fulfulde = 'fuv'; + case Fuyug = 'fuy'; + case Fur = 'fvr'; + case Fwai = 'fwa'; + case Fwe = 'fwe'; + case Ga = 'gaa'; + case Gabri = 'gab'; + case Mixed_Great_Andamanese = 'gac'; + case Gaddang = 'gad'; + case Guarequena = 'gae'; + case Gende = 'gaf'; + case Gagauz = 'gag'; + case Alekano = 'gah'; + case Borei = 'gai'; + case Gadsup = 'gaj'; + case Gamkonora = 'gak'; + case Galolen = 'gal'; + case Kandawo = 'gam'; + case Gan_Chinese = 'gan'; + case Gants = 'gao'; + case Gal = 'gap'; + case Gata = 'gaq'; + case Galeya = 'gar'; + case Adiwasi_Garasia = 'gas'; + case Kenati = 'gat'; + case Mudhili_Gadaba = 'gau'; + case Nobonob = 'gaw'; + case Borana_Arsi_Guji_Oromo = 'gax'; + case Gayo = 'gay'; + case West_Central_Oromo = 'gaz'; + case Gbaya_Central_African_Republic = 'gba'; + case Kaytetye = 'gbb'; + case Karajarri = 'gbd'; + case Niksek = 'gbe'; + case Gaikundi = 'gbf'; + case Gbanziri = 'gbg'; + case Defi_Gbe = 'gbh'; + case Galela = 'gbi'; + case Bodo_Gadaba = 'gbj'; + case Gaddi = 'gbk'; + case Gamit = 'gbl'; + case Garhwali = 'gbm'; + case Mo_da = 'gbn'; + case Northern_Grebo = 'gbo'; + case Gbaya_Bossangoa = 'gbp'; + case Gbaya_Bozoum = 'gbq'; + case Gbagyi = 'gbr'; + case Gbesi_Gbe = 'gbs'; + case Gagadu = 'gbu'; + case Gbanu = 'gbv'; + case Gabi_Gabi = 'gbw'; + case Eastern_Xwla_Gbe = 'gbx'; + case Gbari = 'gby'; + case Zoroastrian_Dari = 'gbz'; + case Mali = 'gcc'; + case Ganggalida = 'gcd'; + case Galice = 'gce'; + case Guadeloupean_Creole_French = 'gcf'; + case Grenadian_Creole_English = 'gcl'; + case Gaina = 'gcn'; + case Guianese_Creole_French = 'gcr'; + case Colonia_Tovar_German = 'gct'; + case Gade_Lohar = 'gda'; + case Pottangi_Ollar_Gadaba = 'gdb'; + case Gugu_Badhun = 'gdc'; + case Gedaged = 'gdd'; + case Gude = 'gde'; + case Guduf_Gava = 'gdf'; + case Ga_dang = 'gdg'; + case Gadjerawang = 'gdh'; + case Gundi = 'gdi'; + case Gurdjar = 'gdj'; + case Gadang = 'gdk'; + case Dirasha = 'gdl'; + case Laal = 'gdm'; + case Umanakaina = 'gdn'; + case Ghodoberi = 'gdo'; + case Mehri = 'gdq'; + case Wipi = 'gdr'; + case Ghandruk_Sign_Language = 'gds'; + case Kungardutyi = 'gdt'; + case Gudu = 'gdu'; + case Godwari = 'gdx'; + case Geruma = 'gea'; + case Kire = 'geb'; + case Gboloo_Grebo = 'gec'; + case Gade = 'ged'; + case Gerai = 'gef'; + case Gengle = 'geg'; + case Hutterite_German = 'geh'; + case Gebe = 'gei'; + case Gen = 'gej'; + case Ywom = 'gek'; + case ut_Ma_in = 'gel'; + case Geme = 'geq'; + case Geser_Gorom = 'ges'; + case Eviya = 'gev'; + case Gera = 'gew'; + case Garre = 'gex'; + case Enya = 'gey'; + case Geez = 'gez'; + case Patpatar = 'gfk'; + case Gafat = 'gft'; + case Gao = 'gga'; + case Gbii = 'ggb'; + case Gugadj = 'ggd'; + case Gurr_goni = 'gge'; + case Gurgula = 'ggg'; + case Kungarakany = 'ggk'; + case Ganglau = 'ggl'; + case Gitua = 'ggt'; + case Gagu = 'ggu'; + case Gogodala = 'ggw'; + case Ghadames = 'gha'; + case Hiberno_Scottish_Gaelic = 'ghc'; + case Southern_Ghale = 'ghe'; + case Northern_Ghale = 'ghh'; + case Geko_Karen = 'ghk'; + case Ghulfan = 'ghl'; + case Ghanongga = 'ghn'; + case Ghomara = 'gho'; + case Ghera = 'ghr'; + case Guhu_Samane = 'ghs'; + case Kuke = 'ght'; + case Kija = 'gia'; + case Gibanawa = 'gib'; + case Gail = 'gic'; + case Gidar = 'gid'; + case Gabogbo = 'gie'; + case Goaria = 'gig'; + case Githabul = 'gih'; + case Girirra = 'gii'; + case Gilbertese = 'gil'; + case Gimi_Eastern_Highlands = 'gim'; + case Hinukh = 'gin'; + case Gimi_West_New_Britain = 'gip'; + case Green_Gelao = 'giq'; + case Red_Gelao = 'gir'; + case North_Giziga = 'gis'; + case Gitxsan = 'git'; + case Mulao = 'giu'; + case White_Gelao = 'giw'; + case Gilima = 'gix'; + case Giyug = 'giy'; + case South_Giziga = 'giz'; + case Kachi_Koli = 'gjk'; + case Gunditjmara = 'gjm'; + case Gonja = 'gjn'; + case Gurindji_Kriol = 'gjr'; + case Gujari = 'gju'; + case Guya = 'gka'; + case Magi_Madang_Province = 'gkd'; + case Ndai = 'gke'; + case Gokana = 'gkn'; + case Kok_Nar = 'gko'; + case Guinea_Kpelle = 'gkp'; + case Ungkue = 'gku'; + case Scottish_Gaelic = 'gla'; + case Belning = 'glb'; + case Bon_Gula = 'glc'; + case Nanai = 'gld'; + case Irish = 'gle'; + case Galician = 'glg'; + case Northwest_Pashai = 'glh'; + case Gula_Iro = 'glj'; + case Gilaki = 'glk'; + case Garlali = 'gll'; + case Galambu = 'glo'; + case Glaro_Twabo = 'glr'; + case Gula_Chad = 'glu'; + case Manx = 'glv'; + case Glavda = 'glw'; + case Gule = 'gly'; + case Gambera = 'gma'; + case Gula_alaa = 'gmb'; + case Maghdi = 'gmd'; + case Magiyi = 'gmg'; + case Middle_High_German_ca_1050_1500 = 'gmh'; + case Middle_Low_German = 'gml'; + case Gbaya_Mbodomo = 'gmm'; + case Gimnime = 'gmn'; + case Mirning = 'gmr'; + case Gumalu = 'gmu'; + case Gamo = 'gmv'; + case Magoma = 'gmx'; + case Mycenaean_Greek = 'gmy'; + case Mgbolizhia = 'gmz'; + case Kaansa = 'gna'; + case Gangte = 'gnb'; + case Guanche = 'gnc'; + case Zulgo_Gemzek = 'gnd'; + case Ganang = 'gne'; + case Ngangam = 'gng'; + case Lere = 'gnh'; + case Gooniyandi = 'gni'; + case Ngen = 'gnj'; + case Gana = 'gnk'; + case Gangulu = 'gnl'; + case Ginuman = 'gnm'; + case Gumatj = 'gnn'; + case Northern_Gondi = 'gno'; + case Gana_2 = 'gnq'; + case Gureng_Gureng = 'gnr'; + case Guntai = 'gnt'; + case Gnau = 'gnu'; + case Western_Bolivian_Guarani = 'gnw'; + case Ganzi = 'gnz'; + case Guro = 'goa'; + case Playero = 'gob'; + case Gorakor = 'goc'; + case Godie = 'god'; + case Gongduk = 'goe'; + case Gofa = 'gof'; + case Gogo = 'gog'; + case Old_High_German_ca_750_1050 = 'goh'; + case Gobasi = 'goi'; + case Gowlan = 'goj'; + case Gowli = 'gok'; + case Gola = 'gol'; + case Goan_Konkani = 'gom'; + case Gondi = 'gon'; + case Gone_Dau = 'goo'; + case Yeretuar = 'gop'; + case Gorap = 'goq'; + case Gorontalo = 'gor'; + case Gronings = 'gos'; + case Gothic = 'got'; + case Gavar = 'gou'; + case Goo = 'gov'; + case Gorowa = 'gow'; + case Gobu = 'gox'; + case Goundo = 'goy'; + case Gozarkhani = 'goz'; + case Gupa_Abawa = 'gpa'; + case Ghanaian_Pidgin_English = 'gpe'; + case Taiap = 'gpn'; + case Ga_anda = 'gqa'; + case Guiqiong = 'gqi'; + case Guana_Brazil = 'gqn'; + case Gor = 'gqr'; + case Qau = 'gqu'; + case Rajput_Garasia = 'gra'; + case Grebo = 'grb'; + case Ancient_Greek_to_1453 = 'grc'; + case Guruntum_Mbaaru = 'grd'; + case Madi = 'grg'; + case Gbiri_Niragu = 'grh'; + case Ghari = 'gri'; + case Southern_Grebo = 'grj'; + case Kota_Marudu_Talantang = 'grm'; + case Guarani = 'grn'; + case Groma = 'gro'; + case Gorovu = 'grq'; + case Taznatit = 'grr'; + case Gresi = 'grs'; + case Garo = 'grt'; + case Kistane = 'gru'; + case Central_Grebo = 'grv'; + case Gweda = 'grw'; + case Guriaso = 'grx'; + case Barclayville_Grebo = 'gry'; + case Guramalum = 'grz'; + case Ghanaian_Sign_Language = 'gse'; + case German_Sign_Language = 'gsg'; + case Gusilay = 'gsl'; + case Guatemalan_Sign_Language = 'gsm'; + case Nema = 'gsn'; + case Southwest_Gbaya = 'gso'; + case Wasembo = 'gsp'; + case Greek_Sign_Language = 'gss'; + case Swiss_German = 'gsw'; + case Guato = 'gta'; + case Aghu_Tharnggala = 'gtu'; + case Shiki = 'gua'; + case Guajajara = 'gub'; + case Wayuu = 'guc'; + case Yocoboue_Dida = 'gud'; + case Gurindji = 'gue'; + case Gupapuyngu = 'guf'; + case Paraguayan_Guarani = 'gug'; + case Guahibo = 'guh'; + case Eastern_Bolivian_Guarani = 'gui'; + case Gujarati = 'guj'; + case Gumuz = 'guk'; + case Sea_Island_Creole_English = 'gul'; + case Guambiano = 'gum'; + case Mbya_Guarani = 'gun'; + case Guayabero = 'guo'; + case Gunwinggu = 'gup'; + case Ache = 'guq'; + case Farefare = 'gur'; + case Guinean_Sign_Language = 'gus'; + case Maleku_Jaika = 'gut'; + case Yanomamo = 'guu'; + case Gun = 'guw'; + case Gourmanchema = 'gux'; + case Gusii = 'guz'; + case Guana_Paraguay = 'gva'; + case Guanano = 'gvc'; + case Duwet = 'gve'; + case Golin = 'gvf'; + case Guaja = 'gvj'; + case Gulay = 'gvl'; + case Gurmana = 'gvm'; + case Kuku_Yalanji = 'gvn'; + case Gaviao_Do_Jiparana = 'gvo'; + case Para_Gaviao = 'gvp'; + case Gurung = 'gvr'; + case Gumawana = 'gvs'; + case Guyani = 'gvy'; + case Mbato = 'gwa'; + case Gwa = 'gwb'; + case Gawri = 'gwc'; + case Gawwada = 'gwd'; + case Gweno = 'gwe'; + case Gowro = 'gwf'; + case Moo = 'gwg'; + case Gwich_in = 'gwi'; + case Gwi = 'gwj'; + case Awngthim = 'gwm'; + case Gwandara = 'gwn'; + case Gwere = 'gwr'; + case Gawar_Bati = 'gwt'; + case Guwamu = 'gwu'; + case Kwini = 'gww'; + case Gua = 'gwx'; + case We_Southern = 'gxx'; + case Northwest_Gbaya = 'gya'; + case Garus = 'gyb'; + case Kayardild = 'gyd'; + case Gyem = 'gye'; + case Gungabula = 'gyf'; + case Gbayi = 'gyg'; + case Gyele = 'gyi'; + case Gayil = 'gyl'; + case Ngabere = 'gym'; + case Guyanese_Creole_English = 'gyn'; + case Gyalsumdo = 'gyo'; + case Guarayu = 'gyr'; + case Gunya = 'gyy'; + case Geji = 'gyz'; + case Ganza = 'gza'; + case Gazi = 'gzi'; + case Gane = 'gzn'; + case Han = 'haa'; + case Hanoi_Sign_Language = 'hab'; + case Gurani = 'hac'; + case Hatam = 'had'; + case Eastern_Oromo = 'hae'; + case Haiphong_Sign_Language = 'haf'; + case Hanga = 'hag'; + case Hahon = 'hah'; + case Haida = 'hai'; + case Hajong = 'haj'; + case Hakka_Chinese = 'hak'; + case Halang = 'hal'; + case Hewa = 'ham'; + case Hangaza = 'han'; + case Hako = 'hao'; + case Hupla = 'hap'; + case Ha = 'haq'; + case Harari = 'har'; + case Haisla = 'has'; + case Haitian = 'hat'; + case Hausa = 'hau'; + case Havu = 'hav'; + case Hawaiian = 'haw'; + case Southern_Haida = 'hax'; + case Haya = 'hay'; + case Hazaragi = 'haz'; + case Hamba = 'hba'; + case Huba = 'hbb'; + case Heiban = 'hbn'; + case Ancient_Hebrew = 'hbo'; + case Serbo_Croatian = 'hbs'; + case Habu = 'hbu'; + case Andaman_Creole_Hindi = 'hca'; + case Huichol = 'hch'; + case Northern_Haida = 'hdn'; + case Honduras_Sign_Language = 'hds'; + case Hadiyya = 'hdy'; + case Northern_Qiandong_Miao = 'hea'; + case Hebrew = 'heb'; + case Herde = 'hed'; + case Helong = 'heg'; + case Hehe = 'heh'; + case Heiltsuk = 'hei'; + case Hemba = 'hem'; + case Herero = 'her'; + case Hai_om = 'hgm'; + case Haigwai = 'hgw'; + case Hoia_Hoia = 'hhi'; + case Kerak = 'hhr'; + case Hoyahoya = 'hhy'; + case Lamang = 'hia'; + case Hibito = 'hib'; + case Hidatsa = 'hid'; + case Fiji_Hindi = 'hif'; + case Kamwe = 'hig'; + case Pamosu = 'hih'; + case Hinduri = 'hii'; + case Hijuk = 'hij'; + case Seit_Kaitetu = 'hik'; + case Hiligaynon = 'hil'; + case Hindi = 'hin'; + case Tsoa = 'hio'; + case Himarima = 'hir'; + case Hittite = 'hit'; + case Hiw = 'hiw'; + case Hixkaryana = 'hix'; + case Haji = 'hji'; + case Kahe = 'hka'; + case Hunde = 'hke'; + case Khah = 'hkh'; + case Hunjara_Kaina_Ke = 'hkk'; + case Mel_Khaonh = 'hkn'; + case Hong_Kong_Sign_Language = 'hks'; + case Halia = 'hla'; + case Halbi = 'hlb'; + case Halang_Doan = 'hld'; + case Hlersu = 'hle'; + case Matu_Chin = 'hlt'; + case Hieroglyphic_Luwian = 'hlu'; + case Southern_Mashan_Hmong = 'hma'; + case Humburi_Senni_Songhay = 'hmb'; + case Central_Huishui_Hmong = 'hmc'; + case Large_Flowery_Miao = 'hmd'; + case Eastern_Huishui_Hmong = 'hme'; + case Hmong_Don = 'hmf'; + case Southwestern_Guiyang_Hmong = 'hmg'; + case Southwestern_Huishui_Hmong = 'hmh'; + case Northern_Huishui_Hmong = 'hmi'; + case Ge = 'hmj'; + case Maek = 'hmk'; + case Luopohe_Hmong = 'hml'; + case Central_Mashan_Hmong = 'hmm'; + case Hmong = 'hmn'; + case Hiri_Motu = 'hmo'; + case Northern_Mashan_Hmong = 'hmp'; + case Eastern_Qiandong_Miao = 'hmq'; + case Hmar = 'hmr'; + case Southern_Qiandong_Miao = 'hms'; + case Hamtai = 'hmt'; + case Hamap = 'hmu'; + case Hmong_Do = 'hmv'; + case Western_Mashan_Hmong = 'hmw'; + case Southern_Guiyang_Hmong = 'hmy'; + case Hmong_Shua = 'hmz'; + case Mina_Cameroon = 'hna'; + case Southern_Hindko = 'hnd'; + case Chhattisgarhi = 'hne'; + case Hungu = 'hng'; + case Ani = 'hnh'; + case Hani = 'hni'; + case Hmong_Njua = 'hnj'; + case Hanunoo = 'hnn'; + case Northern_Hindko = 'hno'; + case Caribbean_Hindustani = 'hns'; + case Hung = 'hnu'; + case Hoava = 'hoa'; + case Mari_Madang_Province = 'hob'; + case Ho = 'hoc'; + case Holma = 'hod'; + case Horom = 'hoe'; + case Hobyot = 'hoh'; + case Holikachuk = 'hoi'; + case Hadothi = 'hoj'; + case Holu = 'hol'; + case Homa = 'hom'; + case Holoholo = 'hoo'; + case Hopi = 'hop'; + case Horo = 'hor'; + case Ho_Chi_Minh_City_Sign_Language = 'hos'; + case Hote = 'hot'; + case Hovongan = 'hov'; + case Honi = 'how'; + case Holiya = 'hoy'; + case Hozo = 'hoz'; + case Hpon = 'hpo'; + case Hawai_i_Sign_Language_HSL = 'hps'; + case Hrangkhol = 'hra'; + case Niwer_Mil = 'hrc'; + case Hre = 'hre'; + case Haruku = 'hrk'; + case Horned_Miao = 'hrm'; + case Haroi = 'hro'; + case Nhirrpi = 'hrp'; + case Hertevin = 'hrt'; + case Hruso = 'hru'; + case Croatian = 'hrv'; + case Warwar_Feni = 'hrw'; + case Hunsrik = 'hrx'; + case Harzani = 'hrz'; + case Upper_Sorbian = 'hsb'; + case Hungarian_Sign_Language = 'hsh'; + case Hausa_Sign_Language = 'hsl'; + case Xiang_Chinese = 'hsn'; + case Harsusi = 'hss'; + case Hoti = 'hti'; + case Minica_Huitoto = 'hto'; + case Hadza = 'hts'; + case Hitu = 'htu'; + case Middle_Hittite = 'htx'; + case Huambisa = 'hub'; + case Hua = 'huc'; + case Huaulu = 'hud'; + case San_Francisco_Del_Mar_Huave = 'hue'; + case Humene = 'huf'; + case Huachipaeri = 'hug'; + case Huilliche = 'huh'; + case Huli = 'hui'; + case Northern_Guiyang_Hmong = 'huj'; + case Hulung = 'huk'; + case Hula = 'hul'; + case Hungana = 'hum'; + case Hungarian = 'hun'; + case Hu = 'huo'; + case Hupa = 'hup'; + case Tsat = 'huq'; + case Halkomelem = 'hur'; + case Huastec = 'hus'; + case Humla = 'hut'; + case Murui_Huitoto = 'huu'; + case San_Mateo_Del_Mar_Huave = 'huv'; + case Hukumina = 'huw'; + case Nupode_Huitoto = 'hux'; + case Hulaula = 'huy'; + case Hunzib = 'huz'; + case Haitian_Vodoun_Culture_Language = 'hvc'; + case San_Dionisio_Del_Mar_Huave = 'hve'; + case Haveke = 'hvk'; + case Sabu = 'hvn'; + case Santa_Maria_Del_Mar_Huave = 'hvv'; + case Wane = 'hwa'; + case Hawai_i_Creole_English = 'hwc'; + case Hwana = 'hwo'; + case Hya = 'hya'; + case Armenian = 'hye'; + case Western_Armenian = 'hyw'; + case Iaai = 'iai'; + case Iatmul = 'ian'; + case Purari = 'iar'; + case Iban = 'iba'; + case Ibibio = 'ibb'; + case Iwaidja = 'ibd'; + case Akpes = 'ibe'; + case Ibanag = 'ibg'; + case Bih = 'ibh'; + case Ibaloi = 'ibl'; + case Agoi = 'ibm'; + case Ibino = 'ibn'; + case Igbo = 'ibo'; + case Ibuoro = 'ibr'; + case Ibu = 'ibu'; + case Ibani = 'iby'; + case Ede_Ica = 'ica'; + case Etkywan = 'ich'; + case Icelandic_Sign_Language = 'icl'; + case Islander_Creole_English = 'icr'; + case Idakho_Isukha_Tiriki = 'ida'; + case Indo_Portuguese = 'idb'; + case Idon = 'idc'; + case Ede_Idaca = 'idd'; + case Idere = 'ide'; + case Idi = 'idi'; + case Ido = 'ido'; + case Indri = 'idr'; + case Idesa = 'ids'; + case Idate = 'idt'; + case Idoma = 'idu'; + case Amganad_Ifugao = 'ifa'; + case Batad_Ifugao = 'ifb'; + case Ife = 'ife'; + case Ifo = 'iff'; + case Tuwali_Ifugao = 'ifk'; + case Teke_Fuumu = 'ifm'; + case Mayoyao_Ifugao = 'ifu'; + case Keley_I_Kallahan = 'ify'; + case Ebira = 'igb'; + case Igede = 'ige'; + case Igana = 'igg'; + case Igala = 'igl'; + case Kanggape = 'igm'; + case Ignaciano = 'ign'; + case Isebe = 'igo'; + case Interglossa = 'igs'; + case Igwe = 'igw'; + case Iha_Based_Pidgin = 'ihb'; + case Ihievbe = 'ihi'; + case Iha = 'ihp'; + case Bidhawal = 'ihw'; + case Sichuan_Yi = 'iii'; + case Thiin = 'iin'; + case Izon = 'ijc'; + case Biseni = 'ije'; + case Ede_Ije = 'ijj'; + case Kalabari = 'ijn'; + case Southeast_Ijo = 'ijs'; + case Eastern_Canadian_Inuktitut = 'ike'; + case Ikhin_Arokho = 'ikh'; + case Iko = 'iki'; + case Ika = 'ikk'; + case Ikulu = 'ikl'; + case Olulumo_Ikom = 'iko'; + case Ikpeshi = 'ikp'; + case Ikaranggal = 'ikr'; + case Inuit_Sign_Language = 'iks'; + case Inuinnaqtun = 'ikt'; + case Inuktitut = 'iku'; + case Iku_Gora_Ankwa = 'ikv'; + case Ikwere = 'ikw'; + case Ik = 'ikx'; + case Ikizu = 'ikz'; + case Ile_Ape = 'ila'; + case Ila = 'ilb'; + case Interlingue = 'ile'; + case Garig_Ilgar = 'ilg'; + case Ili_Turki = 'ili'; + case Ilongot = 'ilk'; + case Iranun_Malaysia = 'ilm'; + case Iloko = 'ilo'; + case Iranun_Philippines = 'ilp'; + case International_Sign = 'ils'; + case Ili_uun = 'ilu'; + case Ilue = 'ilv'; + case Mala_Malasar = 'ima'; + case Anamgura = 'imi'; + case Miluk = 'iml'; + case Imonda = 'imn'; + case Imbongu = 'imo'; + case Imroing = 'imr'; + case Marsian = 'ims'; + case Imotong = 'imt'; + case Milyan = 'imy'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Inga = 'inb'; + case Indonesian = 'ind'; + case Degexit_an = 'ing'; + case Ingush = 'inh'; + case Jungle_Inga = 'inj'; + case Indonesian_Sign_Language = 'inl'; + case Minaean = 'inm'; + case Isinai = 'inn'; + case Inoke_Yate = 'ino'; + case Inapari = 'inp'; + case Indian_Sign_Language = 'ins'; + case Intha = 'int'; + case Ineseno = 'inz'; + case Inor = 'ior'; + case Tuma_Irumu = 'iou'; + case Iowa_Oto = 'iow'; + case Ipili = 'ipi'; + case Inupiaq = 'ipk'; + case Ipiko = 'ipo'; + case Iquito = 'iqu'; + case Ikwo = 'iqw'; + case Iresim = 'ire'; + case Irarutu = 'irh'; + case Rigwe = 'iri'; + case Iraqw = 'irk'; + case Irantxe = 'irn'; + case Ir = 'irr'; + case Irula = 'iru'; + case Kamberau = 'irx'; + case Iraya = 'iry'; + case Isabi = 'isa'; + case Isconahua = 'isc'; + case Isnag = 'isd'; + case Italian_Sign_Language = 'ise'; + case Irish_Sign_Language = 'isg'; + case Esan = 'ish'; + case Nkem_Nkum = 'isi'; + case Ishkashimi = 'isk'; + case Icelandic = 'isl'; + case Masimasi = 'ism'; + case Isanzu = 'isn'; + case Isoko = 'iso'; + case Israeli_Sign_Language = 'isr'; + case Istriot = 'ist'; + case Isu_Menchum_Division = 'isu'; + case Italian = 'ita'; + case Binongan_Itneg = 'itb'; + case Southern_Tidung = 'itd'; + case Itene = 'ite'; + case Inlaod_Itneg = 'iti'; + case Judeo_Italian = 'itk'; + case Itelmen = 'itl'; + case Itu_Mbon_Uzo = 'itm'; + case Itonama = 'ito'; + case Iteri = 'itr'; + case Isekiri = 'its'; + case Maeng_Itneg = 'itt'; + case Itawit = 'itv'; + case Ito = 'itw'; + case Itik = 'itx'; + case Moyadan_Itneg = 'ity'; + case Itza = 'itz'; + case Iu_Mien = 'ium'; + case Ibatan = 'ivb'; + case Ivatan = 'ivv'; + case I_Wak = 'iwk'; + case Iwam = 'iwm'; + case Iwur = 'iwo'; + case Sepik_Iwam = 'iws'; + case Ixcatec = 'ixc'; + case Ixil = 'ixl'; + case Iyayu = 'iya'; + case Mesaka = 'iyo'; + case Yaka_Congo = 'iyx'; + case Ingrian = 'izh'; + case Kizamani = 'izm'; + case Izere = 'izr'; + case Izii = 'izz'; + case Jamamadi = 'jaa'; + case Hyam = 'jab'; + case Popti = 'jac'; + case Jahanka = 'jad'; + case Yabem = 'jae'; + case Jara = 'jaf'; + case Jah_Hut = 'jah'; + case Zazao = 'jaj'; + case Jakun = 'jak'; + case Yalahatan = 'jal'; + case Jamaican_Creole_English = 'jam'; + case Jandai = 'jan'; + case Yanyuwa = 'jao'; + case Yaqay = 'jaq'; + case New_Caledonian_Javanese = 'jas'; + case Jakati = 'jat'; + case Yaur = 'jau'; + case Javanese = 'jav'; + case Jambi_Malay = 'jax'; + case Yan_nhangu = 'jay'; + case Jawe = 'jaz'; + case Judeo_Berber = 'jbe'; + case Badjiri = 'jbi'; + case Arandai = 'jbj'; + case Barikewa = 'jbk'; + case Bijim = 'jbm'; + case Nafusi = 'jbn'; + case Lojban = 'jbo'; + case Jofotek_Bromnya = 'jbr'; + case Jabuti = 'jbt'; + case Jukun_Takum = 'jbu'; + case Yawijibaya = 'jbw'; + case Jamaican_Country_Sign_Language = 'jcs'; + case Krymchak = 'jct'; + case Jad = 'jda'; + case Jadgali = 'jdg'; + case Judeo_Tat = 'jdt'; + case Jebero = 'jeb'; + case Jerung = 'jee'; + case Jeh = 'jeh'; + case Yei = 'jei'; + case Jeri_Kuo = 'jek'; + case Yelmek = 'jel'; + case Dza = 'jen'; + case Jere = 'jer'; + case Manem = 'jet'; + case Jonkor_Bourmataguil = 'jeu'; + case Ngbee = 'jgb'; + case Judeo_Georgian = 'jge'; + case Gwak = 'jgk'; + case Ngomba = 'jgo'; + case Jehai = 'jhi'; + case Jhankot_Sign_Language = 'jhs'; + case Jina = 'jia'; + case Jibu = 'jib'; + case Tol = 'jic'; + case Bu_Kaduna_State = 'jid'; + case Jilbe = 'jie'; + case Jingulu = 'jig'; + case sTodsde = 'jih'; + case Jiiddu = 'jii'; + case Jilim = 'jil'; + case Jimi_Cameroon = 'jim'; + case Jiamao = 'jio'; + case Guanyinqiao = 'jiq'; + case Jita = 'jit'; + case Youle_Jinuo = 'jiu'; + case Shuar = 'jiv'; + case Buyuan_Jinuo = 'jiy'; + case Jejueo = 'jje'; + case Bankal = 'jjr'; + case Kaera = 'jka'; + case Mobwa_Karen = 'jkm'; + case Kubo = 'jko'; + case Paku_Karen = 'jkp'; + case Koro_India = 'jkr'; + case Amami_Koniya_Sign_Language = 'jks'; + case Labir = 'jku'; + case Ngile = 'jle'; + case Jamaican_Sign_Language = 'jls'; + case Dima = 'jma'; + case Zumbun = 'jmb'; + case Machame = 'jmc'; + case Yamdena = 'jmd'; + case Jimi_Nigeria = 'jmi'; + case Jumli = 'jml'; + case Makuri_Naga = 'jmn'; + case Kamara = 'jmr'; + case Mashi_Nigeria = 'jms'; + case Mouwase = 'jmw'; + case Western_Juxtlahuaca_Mixtec = 'jmx'; + case Jangshung = 'jna'; + case Jandavra = 'jnd'; + case Yangman = 'jng'; + case Janji = 'jni'; + case Yemsa = 'jnj'; + case Rawat = 'jnl'; + case Jaunsari = 'jns'; + case Joba = 'job'; + case Wojenaka = 'jod'; + case Jogi = 'jog'; + case Jora = 'jor'; + case Jordanian_Sign_Language = 'jos'; + case Jowulu = 'jow'; + case Jewish_Palestinian_Aramaic = 'jpa'; + case Japanese = 'jpn'; + case Judeo_Persian = 'jpr'; + case Jaqaru = 'jqr'; + case Jarai = 'jra'; + case Judeo_Arabic = 'jrb'; + case Jiru = 'jrr'; + case Jakattoe = 'jrt'; + case Japreria = 'jru'; + case Japanese_Sign_Language = 'jsl'; + case Juma = 'jua'; + case Wannu = 'jub'; + case Jurchen = 'juc'; + case Worodougou = 'jud'; + case Hone = 'juh'; + case Ngadjuri = 'jui'; + case Wapan = 'juk'; + case Jirel = 'jul'; + case Jumjum = 'jum'; + case Juang = 'jun'; + case Jiba = 'juo'; + case Hupde = 'jup'; + case Juruna = 'jur'; + case Jumla_Sign_Language = 'jus'; + case Jutish = 'jut'; + case Ju = 'juu'; + case Wapha = 'juw'; + case Juray = 'juy'; + case Javindo = 'jvd'; + case Caribbean_Javanese = 'jvn'; + case Jwira_Pepesa = 'jwi'; + case Jiarong = 'jya'; + case Judeo_Yemeni_Arabic = 'jye'; + case Jaya = 'jyy'; + case Kara_Kalpak = 'kaa'; + case Kabyle = 'kab'; + case Kachin = 'kac'; + case Adara = 'kad'; + case Ketangalan = 'kae'; + case Katso = 'kaf'; + case Kajaman = 'kag'; + case Kara_Central_African_Republic = 'kah'; + case Karekare = 'kai'; + case Jju = 'kaj'; + case Kalanguya = 'kak'; + case Kalaallisut = 'kal'; + case Kamba_Kenya = 'kam'; + case Kannada = 'kan'; + case Xaasongaxango = 'kao'; + case Bezhta = 'kap'; + case Capanahua = 'kaq'; + case Kashmiri = 'kas'; + case Georgian = 'kat'; + case Kanuri = 'kau'; + case Katukina = 'kav'; + case Kawi = 'kaw'; + case Kao = 'kax'; + case Kamayura = 'kay'; + case Kazakh = 'kaz'; + case Kalarko = 'kba'; + case Kaxuiana = 'kbb'; + case Kadiweu = 'kbc'; + case Kabardian = 'kbd'; + case Kanju = 'kbe'; + case Khamba = 'kbg'; + case Camsa = 'kbh'; + case Kaptiau = 'kbi'; + case Kari = 'kbj'; + case Grass_Koiari = 'kbk'; + case Kanembu = 'kbl'; + case Iwal = 'kbm'; + case Kare_Central_African_Republic = 'kbn'; + case Keliko = 'kbo'; + case Kabiye = 'kbp'; + case Kamano = 'kbq'; + case Kafa = 'kbr'; + case Kande = 'kbs'; + case Abadi = 'kbt'; + case Kabutra = 'kbu'; + case Dera_Indonesia = 'kbv'; + case Kaiep = 'kbw'; + case Ap_Ma = 'kbx'; + case Manga_Kanuri = 'kby'; + case Duhwa = 'kbz'; + case Khanty = 'kca'; + case Kawacha = 'kcb'; + case Lubila = 'kcc'; + case Ngkalmpw_Kanum = 'kcd'; + case Kaivi = 'kce'; + case Ukaan = 'kcf'; + case Tyap = 'kcg'; + case Vono = 'kch'; + case Kamantan = 'kci'; + case Kobiana = 'kcj'; + case Kalanga = 'kck'; + case Kela_Papua_New_Guinea = 'kcl'; + case Gula_Central_African_Republic = 'kcm'; + case Nubi = 'kcn'; + case Kinalakna = 'kco'; + case Kanga = 'kcp'; + case Kamo = 'kcq'; + case Katla = 'kcr'; + case Koenoem = 'kcs'; + case Kaian = 'kct'; + case Kami_Tanzania = 'kcu'; + case Kete = 'kcv'; + case Kabwari = 'kcw'; + case Kachama_Ganjule = 'kcx'; + case Korandje = 'kcy'; + case Konongo = 'kcz'; + case Worimi = 'kda'; + case Kutu = 'kdc'; + case Yankunytjatjara = 'kdd'; + case Makonde = 'kde'; + case Mamusi = 'kdf'; + case Seba = 'kdg'; + case Tem = 'kdh'; + case Kumam = 'kdi'; + case Karamojong = 'kdj'; + case Numee = 'kdk'; + case Tsikimba = 'kdl'; + case Kagoma = 'kdm'; + case Kunda = 'kdn'; + case Kaningdon_Nindem = 'kdp'; + case Koch = 'kdq'; + case Karaim = 'kdr'; + case Kuy = 'kdt'; + case Kadaru = 'kdu'; + case Koneraw = 'kdw'; + case Kam = 'kdx'; + case Keder = 'kdy'; + case Kwaja = 'kdz'; + case Kabuverdianu = 'kea'; + case Kele = 'keb'; + case Keiga = 'kec'; + case Kerewe = 'ked'; + case Eastern_Keres = 'kee'; + case Kpessi = 'kef'; + case Tese = 'keg'; + case Keak = 'keh'; + case Kei = 'kei'; + case Kadar = 'kej'; + case Kekchi = 'kek'; + case Kela_Democratic_Republic_of_Congo = 'kel'; + case Kemak = 'kem'; + case Kenyang = 'ken'; + case Kakwa = 'keo'; + case Kaikadi = 'kep'; + case Kamar = 'keq'; + case Kera = 'ker'; + case Kugbo = 'kes'; + case Ket = 'ket'; + case Akebu = 'keu'; + case Kanikkaran = 'kev'; + case West_Kewa = 'kew'; + case Kukna = 'kex'; + case Kupia = 'key'; + case Kukele = 'kez'; + case Kodava = 'kfa'; + case Northwestern_Kolami = 'kfb'; + case Konda_Dora = 'kfc'; + case Korra_Koraga = 'kfd'; + case Kota_India = 'kfe'; + case Koya = 'kff'; + case Kudiya = 'kfg'; + case Kurichiya = 'kfh'; + case Kannada_Kurumba = 'kfi'; + case Kemiehua = 'kfj'; + case Kinnauri = 'kfk'; + case Kung = 'kfl'; + case Khunsari = 'kfm'; + case Kuk = 'kfn'; + case Koro_Cote_d_Ivoire = 'kfo'; + case Korwa = 'kfp'; + case Korku = 'kfq'; + case Kachhi = 'kfr'; + case Bilaspuri = 'kfs'; + case Kanjari = 'kft'; + case Katkari = 'kfu'; + case Kurmukar = 'kfv'; + case Kharam_Naga = 'kfw'; + case Kullu_Pahari = 'kfx'; + case Kumaoni = 'kfy'; + case Koromfe = 'kfz'; + case Koyaga = 'kga'; + case Kawe = 'kgb'; + case Komering = 'kge'; + case Kube = 'kgf'; + case Kusunda = 'kgg'; + case Selangor_Sign_Language = 'kgi'; + case Gamale_Kham = 'kgj'; + case Kaiwa = 'kgk'; + case Kunggari = 'kgl'; + case Karingani = 'kgn'; + case Krongo = 'kgo'; + case Kaingang = 'kgp'; + case Kamoro = 'kgq'; + case Abun = 'kgr'; + case Kumbainggar = 'kgs'; + case Somyev = 'kgt'; + case Kobol = 'kgu'; + case Karas = 'kgv'; + case Karon_Dori = 'kgw'; + case Kamaru = 'kgx'; + case Kyerung = 'kgy'; + case Khasi = 'kha'; + case Lu = 'khb'; + case Tukang_Besi_North = 'khc'; + case Badi_Kanum = 'khd'; + case Korowai = 'khe'; + case Khuen = 'khf'; + case Khams_Tibetan = 'khg'; + case Kehu = 'khh'; + case Kuturmi = 'khj'; + case Halh_Mongolian = 'khk'; + case Lusi = 'khl'; + case Khmer = 'khm'; + case Khandesi = 'khn'; + case Khotanese = 'kho'; + case Kapori = 'khp'; + case Koyra_Chiini_Songhay = 'khq'; + case Kharia = 'khr'; + case Kasua = 'khs'; + case Khamti = 'kht'; + case Nkhumbi = 'khu'; + case Khvarshi = 'khv'; + case Khowar = 'khw'; + case Kanu = 'khx'; + case Kele_Democratic_Republic_of_Congo = 'khy'; + case Keapara = 'khz'; + case Kim = 'kia'; + case Koalib = 'kib'; + case Kickapoo = 'kic'; + case Koshin = 'kid'; + case Kibet = 'kie'; + case Eastern_Parbate_Kham = 'kif'; + case Kimaama = 'kig'; + case Kilmeri = 'kih'; + case Kitsai = 'kii'; + case Kilivila = 'kij'; + case Kikuyu = 'kik'; + case Kariya = 'kil'; + case Karagas = 'kim'; + case Kinyarwanda = 'kin'; + case Kiowa = 'kio'; + case Sheshi_Kham = 'kip'; + case Kosadle = 'kiq'; + case Kirghiz = 'kir'; + case Kis = 'kis'; + case Agob = 'kit'; + case Kirmanjki_individual_language = 'kiu'; + case Kimbu = 'kiv'; + case Northeast_Kiwai = 'kiw'; + case Khiamniungan_Naga = 'kix'; + case Kirikiri = 'kiy'; + case Kisi = 'kiz'; + case Mlap = 'kja'; + case Q_anjob_al = 'kjb'; + case Coastal_Konjo = 'kjc'; + case Southern_Kiwai = 'kjd'; + case Kisar = 'kje'; + case Khmu = 'kjg'; + case Khakas = 'kjh'; + case Zabana = 'kji'; + case Khinalugh = 'kjj'; + case Highland_Konjo = 'kjk'; + case Western_Parbate_Kham = 'kjl'; + case Khang = 'kjm'; + case Kunjen = 'kjn'; + case Harijan_Kinnauri = 'kjo'; + case Pwo_Eastern_Karen = 'kjp'; + case Western_Keres = 'kjq'; + case Kurudu = 'kjr'; + case East_Kewa = 'kjs'; + case Phrae_Pwo_Karen = 'kjt'; + case Kashaya = 'kju'; + case Kaikavian_Literary_Language = 'kjv'; + case Ramopa = 'kjx'; + case Erave = 'kjy'; + case Bumthangkha = 'kjz'; + case Kakanda = 'kka'; + case Kwerisa = 'kkb'; + case Odoodee = 'kkc'; + case Kinuku = 'kkd'; + case Kakabe = 'kke'; + case Kalaktang_Monpa = 'kkf'; + case Mabaka_Valley_Kalinga = 'kkg'; + case Khun = 'kkh'; + case Kagulu = 'kki'; + case Kako = 'kkj'; + case Kokota = 'kkk'; + case Kosarek_Yale = 'kkl'; + case Kiong = 'kkm'; + case Kon_Keu = 'kkn'; + case Karko = 'kko'; + case Gugubera = 'kkp'; + case Kaeku = 'kkq'; + case Kir_Balar = 'kkr'; + case Giiwo = 'kks'; + case Koi = 'kkt'; + case Tumi = 'kku'; + case Kangean = 'kkv'; + case Teke_Kukuya = 'kkw'; + case Kohin = 'kkx'; + case Guugu_Yimidhirr = 'kky'; + case Kaska = 'kkz'; + case Klamath_Modoc = 'kla'; + case Kiliwa = 'klb'; + case Kolbila = 'klc'; + case Gamilaraay = 'kld'; + case Kulung_Nepal = 'kle'; + case Kendeje = 'klf'; + case Tagakaulo = 'klg'; + case Weliki = 'klh'; + case Kalumpang = 'kli'; + case Khalaj = 'klj'; + case Kono_Nigeria = 'klk'; + case Kagan_Kalagan = 'kll'; + case Migum = 'klm'; + case Kalenjin = 'kln'; + case Kapya = 'klo'; + case Kamasa = 'klp'; + case Rumu = 'klq'; + case Khaling = 'klr'; + case Kalasha = 'kls'; + case Nukna = 'klt'; + case Klao = 'klu'; + case Maskelynes = 'klv'; + case Tado = 'klw'; + case Koluwawa = 'klx'; + case Kalao = 'kly'; + case Kabola = 'klz'; + case Konni = 'kma'; + case Kimbundu = 'kmb'; + case Southern_Dong = 'kmc'; + case Majukayang_Kalinga = 'kmd'; + case Bakole = 'kme'; + case Kare_Papua_New_Guinea = 'kmf'; + case Kate = 'kmg'; + case Kalam = 'kmh'; + case Kami_Nigeria = 'kmi'; + case Kumarbhag_Paharia = 'kmj'; + case Limos_Kalinga = 'kmk'; + case Tanudan_Kalinga = 'kml'; + case Kom_India = 'kmm'; + case Awtuw = 'kmn'; + case Kwoma = 'kmo'; + case Gimme = 'kmp'; + case Kwama = 'kmq'; + case Northern_Kurdish = 'kmr'; + case Kamasau = 'kms'; + case Kemtuik = 'kmt'; + case Kanite = 'kmu'; + case Karipuna_Creole_French = 'kmv'; + case Komo_Democratic_Republic_of_Congo = 'kmw'; + case Waboda = 'kmx'; + case Koma = 'kmy'; + case Khorasani_Turkish = 'kmz'; + case Dera_Nigeria = 'kna'; + case Lubuagan_Kalinga = 'knb'; + case Central_Kanuri = 'knc'; + case Konda = 'knd'; + case Kankanaey = 'kne'; + case Mankanya = 'knf'; + case Koongo = 'kng'; + case Kanufi = 'kni'; + case Western_Kanjobal = 'knj'; + case Kuranko = 'knk'; + case Keninjal = 'knl'; + case Kanamari = 'knm'; + case Konkani_individual_language = 'knn'; + case Kono_Sierra_Leone = 'kno'; + case Kwanja = 'knp'; + case Kintaq = 'knq'; + case Kaningra = 'knr'; + case Kensiu = 'kns'; + case Panoan_Katukina = 'knt'; + case Kono_Guinea = 'knu'; + case Tabo = 'knv'; + case Kung_Ekoka = 'knw'; + case Kendayan = 'knx'; + case Kanyok = 'kny'; + case Kalamse = 'knz'; + case Konomala = 'koa'; + case Kpati = 'koc'; + case Kodi = 'kod'; + case Kacipo_Bale_Suri = 'koe'; + case Kubi = 'kof'; + case Cogui = 'kog'; + case Koyo = 'koh'; + case Komi_Permyak = 'koi'; + case Konkani_macrolanguage = 'kok'; + case Kol_Papua_New_Guinea = 'kol'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konzo = 'koo'; + case Waube = 'kop'; + case Kota_Gabon = 'koq'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Lagwan = 'kot'; + case Koke = 'kou'; + case Kudu_Camo = 'kov'; + case Kugama = 'kow'; + case Koyukon = 'koy'; + case Korak = 'koz'; + case Kutto = 'kpa'; + case Mullu_Kurumba = 'kpb'; + case Curripaco = 'kpc'; + case Koba = 'kpd'; + case Kpelle = 'kpe'; + case Komba = 'kpf'; + case Kapingamarangi = 'kpg'; + case Kplang = 'kph'; + case Kofei = 'kpi'; + case Karaja = 'kpj'; + case Kpan = 'kpk'; + case Kpala = 'kpl'; + case Koho = 'kpm'; + case Kepkiriwat = 'kpn'; + case Ikposo = 'kpo'; + case Korupun_Sela = 'kpq'; + case Korafe_Yegha = 'kpr'; + case Tehit = 'kps'; + case Karata = 'kpt'; + case Kafoa = 'kpu'; + case Komi_Zyrian = 'kpv'; + case Kobon = 'kpw'; + case Mountain_Koiali = 'kpx'; + case Koryak = 'kpy'; + case Kupsabiny = 'kpz'; + case Mum = 'kqa'; + case Kovai = 'kqb'; + case Doromu_Koki = 'kqc'; + case Koy_Sanjaq_Surat = 'kqd'; + case Kalagan = 'kqe'; + case Kakabai = 'kqf'; + case Khe = 'kqg'; + case Kisankasa = 'kqh'; + case Koitabu = 'kqi'; + case Koromira = 'kqj'; + case Kotafon_Gbe = 'kqk'; + case Kyenele = 'kql'; + case Khisa = 'kqm'; + case Kaonde = 'kqn'; + case Eastern_Krahn = 'kqo'; + case Kimre = 'kqp'; + case Krenak = 'kqq'; + case Kimaragang = 'kqr'; + case Northern_Kissi = 'kqs'; + case Klias_River_Kadazan = 'kqt'; + case Seroa = 'kqu'; + case Okolod = 'kqv'; + case Kandas = 'kqw'; + case Mser = 'kqx'; + case Koorete = 'kqy'; + case Korana = 'kqz'; + case Kumhali = 'kra'; + case Karkin = 'krb'; + case Karachay_Balkar = 'krc'; + case Kairui_Midiki = 'krd'; + case Panara = 'kre'; + case Koro_Vanuatu = 'krf'; + case Kurama = 'krh'; + case Krio = 'kri'; + case Kinaray_A = 'krj'; + case Kerek = 'krk'; + case Karelian = 'krl'; + case Sapo = 'krn'; + case Durop = 'krp'; + case Krung = 'krr'; + case Gbaya_Sudan = 'krs'; + case Tumari_Kanuri = 'krt'; + case Kurukh = 'kru'; + case Kavet = 'krv'; + case Western_Krahn = 'krw'; + case Karon = 'krx'; + case Kryts = 'kry'; + case Sota_Kanum = 'krz'; + case Shambala = 'ksb'; + case Southern_Kalinga = 'ksc'; + case Kuanua = 'ksd'; + case Kuni = 'kse'; + case Bafia = 'ksf'; + case Kusaghe = 'ksg'; + case Kolsch = 'ksh'; + case Krisa = 'ksi'; + case Uare = 'ksj'; + case Kansa = 'ksk'; + case Kumalu = 'ksl'; + case Kumba = 'ksm'; + case Kasiguranin = 'ksn'; + case Kofa = 'kso'; + case Kaba = 'ksp'; + case Kwaami = 'ksq'; + case Borong = 'ksr'; + case Southern_Kisi = 'kss'; + case Winye = 'kst'; + case Khamyang = 'ksu'; + case Kusu = 'ksv'; + case S_gaw_Karen = 'ksw'; + case Kedang = 'ksx'; + case Kharia_Thar = 'ksy'; + case Kodaku = 'ksz'; + case Katua = 'kta'; + case Kambaata = 'ktb'; + case Kholok = 'ktc'; + case Kokata = 'ktd'; + case Nubri = 'kte'; + case Kwami = 'ktf'; + case Kalkutung = 'ktg'; + case Karanga = 'kth'; + case North_Muyu = 'kti'; + case Plapo_Krumen = 'ktj'; + case Kaniet = 'ktk'; + case Koroshi = 'ktl'; + case Kurti = 'ktm'; + case Karitiana = 'ktn'; + case Kuot = 'kto'; + case Kaduo = 'ktp'; + case Katabaga = 'ktq'; + case South_Muyu = 'kts'; + case Ketum = 'ktt'; + case Kituba_Democratic_Republic_of_Congo = 'ktu'; + case Eastern_Katu = 'ktv'; + case Kato = 'ktw'; + case Kaxarari = 'ktx'; + case Kango_Bas_Uele_District = 'kty'; + case Ju_hoan = 'ktz'; + case Kuanyama = 'kua'; + case Kutep = 'kub'; + case Kwinsu = 'kuc'; + case Auhelawa = 'kud'; + case Kuman_Papua_New_Guinea = 'kue'; + case Western_Katu = 'kuf'; + case Kupa = 'kug'; + case Kushi = 'kuh'; + case Kuikuro_Kalapalo = 'kui'; + case Kuria = 'kuj'; + case Kepo = 'kuk'; + case Kulere = 'kul'; + case Kumyk = 'kum'; + case Kunama = 'kun'; + case Kumukio = 'kuo'; + case Kunimaipa = 'kup'; + case Karipuna = 'kuq'; + case Kurdish = 'kur'; + case Kusaal = 'kus'; + case Kutenai = 'kut'; + case Upper_Kuskokwim = 'kuu'; + case Kur = 'kuv'; + case Kpagua = 'kuw'; + case Kukatja = 'kux'; + case Kuuku_Ya_u = 'kuy'; + case Kunza = 'kuz'; + case Bagvalal = 'kva'; + case Kubu = 'kvb'; + case Kove = 'kvc'; + case Kui_Indonesia = 'kvd'; + case Kalabakan = 'kve'; + case Kabalai = 'kvf'; + case Kuni_Boazi = 'kvg'; + case Komodo = 'kvh'; + case Kwang = 'kvi'; + case Psikye = 'kvj'; + case Korean_Sign_Language = 'kvk'; + case Kayaw = 'kvl'; + case Kendem = 'kvm'; + case Border_Kuna = 'kvn'; + case Dobel = 'kvo'; + case Kompane = 'kvp'; + case Geba_Karen = 'kvq'; + case Kerinci = 'kvr'; + case Lahta_Karen = 'kvt'; + case Yinbaw_Karen = 'kvu'; + case Kola = 'kvv'; + case Wersing = 'kvw'; + case Parkari_Koli = 'kvx'; + case Yintale_Karen = 'kvy'; + case Tsakwambo = 'kvz'; + case Daw = 'kwa'; + case Kwa_2 = 'kwb'; + case Likwala = 'kwc'; + case Kwaio = 'kwd'; + case Kwerba = 'kwe'; + case Kwara_ae = 'kwf'; + case Sara_Kaba_Deme = 'kwg'; + case Kowiai = 'kwh'; + case Awa_Cuaiquer = 'kwi'; + case Kwanga = 'kwj'; + case Kwakiutl = 'kwk'; + case Kofyar = 'kwl'; + case Kwambi = 'kwm'; + case Kwangali = 'kwn'; + case Kwomtari = 'kwo'; + case Kodia = 'kwp'; + case Kwer = 'kwr'; + case Kwese = 'kws'; + case Kwesten = 'kwt'; + case Kwakum = 'kwu'; + case Sara_Kaba_Naa = 'kwv'; + case Kwinti = 'kww'; + case Khirwar = 'kwx'; + case San_Salvador_Kongo = 'kwy'; + case Kwadi = 'kwz'; + case Kairiru = 'kxa'; + case Krobu = 'kxb'; + case Konso = 'kxc'; + case Brunei = 'kxd'; + case Manumanaw_Karen = 'kxf'; + case Karo_Ethiopia = 'kxh'; + case Keningau_Murut = 'kxi'; + case Kulfa = 'kxj'; + case Zayein_Karen = 'kxk'; + case Northern_Khmer = 'kxm'; + case Kanowit_Tanjong_Melanau = 'kxn'; + case Kanoe = 'kxo'; + case Wadiyara_Koli = 'kxp'; + case Smarky_Kanum = 'kxq'; + case Koro_Papua_New_Guinea = 'kxr'; + case Kangjia = 'kxs'; + case Koiwat = 'kxt'; + case Kuvi = 'kxv'; + case Konai = 'kxw'; + case Likuba = 'kxx'; + case Kayong = 'kxy'; + case Kerewo = 'kxz'; + case Kwaya = 'kya'; + case Butbut_Kalinga = 'kyb'; + case Kyaka = 'kyc'; + case Karey = 'kyd'; + case Krache = 'kye'; + case Kouya = 'kyf'; + case Keyagana = 'kyg'; + case Karok = 'kyh'; + case Kiput = 'kyi'; + case Karao = 'kyj'; + case Kamayo = 'kyk'; + case Kalapuya = 'kyl'; + case Kpatili = 'kym'; + case Northern_Binukidnon = 'kyn'; + case Kelon = 'kyo'; + case Kang = 'kyp'; + case Kenga = 'kyq'; + case Kuruaya = 'kyr'; + case Baram_Kayan = 'kys'; + case Kayagar = 'kyt'; + case Western_Kayah = 'kyu'; + case Kayort = 'kyv'; + case Kudmali = 'kyw'; + case Rapoisi = 'kyx'; + case Kambaira = 'kyy'; + case Kayabi = 'kyz'; + case Western_Karaboro = 'kza'; + case Kaibobo = 'kzb'; + case Bondoukou_Kulango = 'kzc'; + case Kadai = 'kzd'; + case Kosena = 'kze'; + case Da_a_Kaili = 'kzf'; + case Kikai = 'kzg'; + case Kelabit = 'kzi'; + case Kazukuru = 'kzk'; + case Kayeli = 'kzl'; + case Kais = 'kzm'; + case Kokola = 'kzn'; + case Kaningi = 'kzo'; + case Kaidipang = 'kzp'; + case Kaike = 'kzq'; + case Karang = 'kzr'; + case Sugut_Dusun = 'kzs'; + case Kayupulau = 'kzu'; + case Komyandaret = 'kzv'; + case Kariri_Xoco = 'kzw'; + case Kamarian = 'kzx'; + case Kango_Tshopo_District = 'kzy'; + case Kalabra = 'kzz'; + case Southern_Subanen = 'laa'; + case Linear_A = 'lab'; + case Lacandon = 'lac'; + case Ladino = 'lad'; + case Pattani = 'lae'; + case Lafofa = 'laf'; + case Rangi = 'lag'; + case Lahnda = 'lah'; + case Lambya = 'lai'; + case Lango_Uganda = 'laj'; + case Lalia = 'lal'; + case Lamba = 'lam'; + case Laru = 'lan'; + case Lao = 'lao'; + case Laka_Chad = 'lap'; + case Qabiao = 'laq'; + case Larteh = 'lar'; + case Lama_Togo = 'las'; + case Latin = 'lat'; + case Laba = 'lau'; + case Latvian = 'lav'; + case Lauje = 'law'; + case Tiwa = 'lax'; + case Lama_Bai = 'lay'; + case Aribwatsa = 'laz'; + case Label = 'lbb'; + case Lakkia = 'lbc'; + case Lak = 'lbe'; + case Tinani = 'lbf'; + case Laopang = 'lbg'; + case La_bi = 'lbi'; + case Ladakhi = 'lbj'; + case Central_Bontok = 'lbk'; + case Libon_Bikol = 'lbl'; + case Lodhi = 'lbm'; + case Rmeet = 'lbn'; + case Laven = 'lbo'; + case Wampar = 'lbq'; + case Lohorung = 'lbr'; + case Libyan_Sign_Language = 'lbs'; + case Lachi = 'lbt'; + case Labu = 'lbu'; + case Lavatbura_Lamusong = 'lbv'; + case Tolaki = 'lbw'; + case Lawangan = 'lbx'; + case Lamalama = 'lby'; + case Lardil = 'lbz'; + case Legenyem = 'lcc'; + case Lola = 'lcd'; + case Loncong = 'lce'; + case Lubu = 'lcf'; + case Luchazi = 'lch'; + case Lisela = 'lcl'; + case Tungag = 'lcm'; + case Western_Lawa = 'lcp'; + case Luhu = 'lcq'; + case Lisabata_Nuniali = 'lcs'; + case Kla_Dan = 'lda'; + case Du_ya = 'ldb'; + case Luri = 'ldd'; + case Lenyima = 'ldg'; + case Lamja_Dengsa_Tola = 'ldh'; + case Laari = 'ldi'; + case Lemoro = 'ldj'; + case Leelau = 'ldk'; + case Kaan = 'ldl'; + case Landoma = 'ldm'; + case Laadan = 'ldn'; + case Loo = 'ldo'; + case Tso = 'ldp'; + case Lufu = 'ldq'; + case Lega_Shabunda = 'lea'; + case Lala_Bisa = 'leb'; + case Leco = 'lec'; + case Lendu = 'led'; + case Lyele = 'lee'; + case Lelemi = 'lef'; + case Lenje = 'leh'; + case Lemio = 'lei'; + case Lengola = 'lej'; + case Leipon = 'lek'; + case Lele_Democratic_Republic_of_Congo = 'lel'; + case Nomaande = 'lem'; + case Lenca = 'len'; + case Leti_Cameroon = 'leo'; + case Lepcha = 'lep'; + case Lembena = 'leq'; + case Lenkau = 'ler'; + case Lese = 'les'; + case Lesing_Gelimi = 'let'; + case Kara_Papua_New_Guinea = 'leu'; + case Lamma = 'lev'; + case Ledo_Kaili = 'lew'; + case Luang = 'lex'; + case Lemolang = 'ley'; + case Lezghian = 'lez'; + case Lefa = 'lfa'; + case Lingua_Franca_Nova = 'lfn'; + case Lungga = 'lga'; + case Laghu = 'lgb'; + case Lugbara = 'lgg'; + case Laghuu = 'lgh'; + case Lengilu = 'lgi'; + case Lingarak = 'lgk'; + case Wala = 'lgl'; + case Lega_Mwenga = 'lgm'; + case T_apo = 'lgn'; + case Lango_South_Sudan = 'lgo'; + case Logba = 'lgq'; + case Lengo = 'lgr'; + case Guinea_Bissau_Sign_Language = 'lgs'; + case Pahi = 'lgt'; + case Longgu = 'lgu'; + case Ligenza = 'lgz'; + case Laha_Viet_Nam = 'lha'; + case Laha_Indonesia = 'lhh'; + case Lahu_Shi = 'lhi'; + case Lahul_Lohar = 'lhl'; + case Lhomi = 'lhm'; + case Lahanan = 'lhn'; + case Lhokpu = 'lhp'; + case Mlahso = 'lhs'; + case Lo_Toga = 'lht'; + case Lahu = 'lhu'; + case West_Central_Limba = 'lia'; + case Likum = 'lib'; + case Hlai = 'lic'; + case Nyindrou = 'lid'; + case Likila = 'lie'; + case Limbu = 'lif'; + case Ligbi = 'lig'; + case Lihir = 'lih'; + case Ligurian = 'lij'; + case Lika = 'lik'; + case Lillooet = 'lil'; + case Limburgan = 'lim'; + case Lingala = 'lin'; + case Liki = 'lio'; + case Sekpele = 'lip'; + case Libido = 'liq'; + case Liberian_English = 'lir'; + case Lisu = 'lis'; + case Lithuanian = 'lit'; + case Logorik = 'liu'; + case Liv = 'liv'; + case Col = 'liw'; + case Liabuku = 'lix'; + case Banda_Bambari = 'liy'; + case Libinza = 'liz'; + case Golpa = 'lja'; + case Rampi = 'lje'; + case Laiyolo = 'lji'; + case Li_o = 'ljl'; + case Lampung_Api = 'ljp'; + case Yirandali = 'ljw'; + case Yuru = 'ljx'; + case Lakalei = 'lka'; + case Kabras = 'lkb'; + case Kucong = 'lkc'; + case Lakonde = 'lkd'; + case Kenyi = 'lke'; + case Lakha = 'lkh'; + case Laki = 'lki'; + case Remun = 'lkj'; + case Laeko_Libuat = 'lkl'; + case Kalaamaya = 'lkm'; + case Lakon = 'lkn'; + case Khayo = 'lko'; + case Pari = 'lkr'; + case Kisa = 'lks'; + case Lakota = 'lkt'; + case Kungkari = 'lku'; + case Lokoya = 'lky'; + case Lala_Roba = 'lla'; + case Lolo = 'llb'; + case Lele_Guinea = 'llc'; + case Ladin = 'lld'; + case Lele_Papua_New_Guinea = 'lle'; + case Hermit = 'llf'; + case Lole = 'llg'; + case Lamu = 'llh'; + case Teke_Laali = 'lli'; + case Ladji_Ladji = 'llj'; + case Lelak = 'llk'; + case Lilau = 'lll'; + case Lasalimu = 'llm'; + case Lele_Chad = 'lln'; + case North_Efate = 'llp'; + case Lolak = 'llq'; + case Lithuanian_Sign_Language = 'lls'; + case Lau = 'llu'; + case Lauan = 'llx'; + case East_Limba = 'lma'; + case Merei = 'lmb'; + case Limilngan = 'lmc'; + case Lumun = 'lmd'; + case Peve = 'lme'; + case South_Lembata = 'lmf'; + case Lamogai = 'lmg'; + case Lambichhong = 'lmh'; + case Lombi = 'lmi'; + case West_Lembata = 'lmj'; + case Lamkang = 'lmk'; + case Hano = 'lml'; + case Lambadi = 'lmn'; + case Lombard = 'lmo'; + case Limbum = 'lmp'; + case Lamatuka = 'lmq'; + case Lamalera = 'lmr'; + case Lamenu = 'lmu'; + case Lomaiviti = 'lmv'; + case Lake_Miwok = 'lmw'; + case Laimbue = 'lmx'; + case Lamboya = 'lmy'; + case Langbashe = 'lna'; + case Mbalanhu = 'lnb'; + case Lundayeh = 'lnd'; + case Langobardic = 'lng'; + case Lanoh = 'lnh'; + case Daantanai = 'lni'; + case Leningitij = 'lnj'; + case South_Central_Banda = 'lnl'; + case Langam = 'lnm'; + case Lorediakarkar = 'lnn'; + case Lamnso = 'lns'; + case Longuda = 'lnu'; + case Lanima = 'lnw'; + case Lonzo = 'lnz'; + case Loloda = 'loa'; + case Lobi = 'lob'; + case Inonhan = 'loc'; + case Saluan = 'loe'; + case Logol = 'lof'; + case Logo = 'log'; + case Laarim = 'loh'; + case Loma_Cote_d_Ivoire = 'loi'; + case Lou = 'loj'; + case Loko = 'lok'; + case Mongo = 'lol'; + case Loma_Liberia = 'lom'; + case Malawi_Lomwe = 'lon'; + case Lombo = 'loo'; + case Lopa = 'lop'; + case Lobala = 'loq'; + case Teen = 'lor'; + case Loniu = 'los'; + case Otuho = 'lot'; + case Louisiana_Creole = 'lou'; + case Lopi = 'lov'; + case Tampias_Lobu = 'low'; + case Loun = 'lox'; + case Loke = 'loy'; + case Lozi = 'loz'; + case Lelepa = 'lpa'; + case Lepki = 'lpe'; + case Long_Phuri_Naga = 'lpn'; + case Lipo = 'lpo'; + case Lopit = 'lpx'; + case Logir = 'lqr'; + case Rara_Bakati = 'lra'; + case Northern_Luri = 'lrc'; + case Laurentian = 'lre'; + case Laragia = 'lrg'; + case Marachi = 'lri'; + case Loarki = 'lrk'; + case Lari = 'lrl'; + case Marama = 'lrm'; + case Lorang = 'lrn'; + case Laro = 'lro'; + case Southern_Yamphu = 'lrr'; + case Larantuka_Malay = 'lrt'; + case Larevat = 'lrv'; + case Lemerig = 'lrz'; + case Lasgerdi = 'lsa'; + case Burundian_Sign_Language = 'lsb'; + case Albarradas_Sign_Language = 'lsc'; + case Lishana_Deni = 'lsd'; + case Lusengo = 'lse'; + case Lish = 'lsh'; + case Lashi = 'lsi'; + case Latvian_Sign_Language = 'lsl'; + case Saamia = 'lsm'; + case Tibetan_Sign_Language = 'lsn'; + case Laos_Sign_Language = 'lso'; + case Panamanian_Sign_Language = 'lsp'; + case Aruop = 'lsr'; + case Lasi = 'lss'; + case Trinidad_and_Tobago_Sign_Language = 'lst'; + case Sivia_Sign_Language = 'lsv'; + case Seychelles_Sign_Language = 'lsw'; + case Mauritian_Sign_Language = 'lsy'; + case Late_Middle_Chinese = 'ltc'; + case Latgalian = 'ltg'; + case Thur = 'lth'; + case Leti_Indonesia = 'lti'; + case Latunde = 'ltn'; + case Tsotso = 'lto'; + case Tachoni = 'lts'; + case Latu = 'ltu'; + case Luxembourgish = 'ltz'; + case Luba_Lulua = 'lua'; + case Luba_Katanga = 'lub'; + case Aringa = 'luc'; + case Ludian = 'lud'; + case Luvale = 'lue'; + case Laua = 'luf'; + case Ganda = 'lug'; + case Luiseno = 'lui'; + case Luna = 'luj'; + case Lunanakha = 'luk'; + case Olu_bo = 'lul'; + case Luimbi = 'lum'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lumbu = 'lup'; + case Lucumi = 'luq'; + case Laura = 'lur'; + case Lushai = 'lus'; + case Lushootseed = 'lut'; + case Lumba_Yakkha = 'luu'; + case Luwati = 'luv'; + case Luo_Cameroon = 'luw'; + case Luyia = 'luy'; + case Southern_Luri = 'luz'; + case Maku_a = 'lva'; + case Lavi = 'lvi'; + case Lavukaleve = 'lvk'; + case Lwel = 'lvl'; + case Standard_Latvian = 'lvs'; + case Levuka = 'lvu'; + case Lwalu = 'lwa'; + case Lewo_Eleng = 'lwe'; + case Wanga = 'lwg'; + case White_Lachi = 'lwh'; + case Eastern_Lawa = 'lwl'; + case Laomian = 'lwm'; + case Luwo = 'lwo'; + case Malawian_Sign_Language = 'lws'; + case Lewotobi = 'lwt'; + case Lawu = 'lwu'; + case Lewo = 'lww'; + case Lakurumau = 'lxm'; + case Layakha = 'lya'; + case Lyngngam = 'lyg'; + case Luyana = 'lyn'; + case Literary_Chinese = 'lzh'; + case Litzlitz = 'lzl'; + case Leinong_Naga = 'lzn'; + case Laz = 'lzz'; + case San_Jeronimo_Tecoatl_Mazatec = 'maa'; + case Yutanduchi_Mixtec = 'mab'; + case Madurese = 'mad'; + case Bo_Rukul = 'mae'; + case Mafa = 'maf'; + case Magahi = 'mag'; + case Marshallese = 'mah'; + case Maithili = 'mai'; + case Jalapa_De_Diaz_Mazatec = 'maj'; + case Makasar = 'mak'; + case Malayalam = 'mal'; + case Mam = 'mam'; + case Mandingo = 'man'; + case Chiquihuitlan_Mazatec = 'maq'; + case Marathi = 'mar'; + case Masai = 'mas'; + case San_Francisco_Matlatzinca = 'mat'; + case Huautla_Mazatec = 'mau'; + case Satere_Mawe = 'mav'; + case Mampruli = 'maw'; + case North_Moluccan_Malay = 'max'; + case Central_Mazahua = 'maz'; + case Higaonon = 'mba'; + case Western_Bukidnon_Manobo = 'mbb'; + case Macushi = 'mbc'; + case Dibabawon_Manobo = 'mbd'; + case Molale = 'mbe'; + case Baba_Malay = 'mbf'; + case Mangseng = 'mbh'; + case Ilianen_Manobo = 'mbi'; + case Nadeb = 'mbj'; + case Malol = 'mbk'; + case Maxakali = 'mbl'; + case Ombamba = 'mbm'; + case Macaguan = 'mbn'; + case Mbo_Cameroon = 'mbo'; + case Malayo = 'mbp'; + case Maisin = 'mbq'; + case Nukak_Maku = 'mbr'; + case Sarangani_Manobo = 'mbs'; + case Matigsalug_Manobo = 'mbt'; + case Mbula_Bwazza = 'mbu'; + case Mbulungish = 'mbv'; + case Maring = 'mbw'; + case Mari_East_Sepik_Province = 'mbx'; + case Memoni = 'mby'; + case Amoltepec_Mixtec = 'mbz'; + case Maca = 'mca'; + case Machiguenga = 'mcb'; + case Bitur = 'mcc'; + case Sharanahua = 'mcd'; + case Itundujia_Mixtec = 'mce'; + case Matses = 'mcf'; + case Mapoyo = 'mcg'; + case Maquiritari = 'mch'; + case Mese = 'mci'; + case Mvanip = 'mcj'; + case Mbunda = 'mck'; + case Macaguaje = 'mcl'; + case Malaccan_Creole_Portuguese = 'mcm'; + case Masana = 'mcn'; + case Coatlan_Mixe = 'mco'; + case Makaa = 'mcp'; + case Ese = 'mcq'; + case Menya = 'mcr'; + case Mambai = 'mcs'; + case Mengisa = 'mct'; + case Cameroon_Mambila = 'mcu'; + case Minanibai = 'mcv'; + case Mawa_Chad = 'mcw'; + case Mpiemo = 'mcx'; + case South_Watut = 'mcy'; + case Mawan = 'mcz'; + case Mada_Nigeria = 'mda'; + case Morigi = 'mdb'; + case Male_Papua_New_Guinea = 'mdc'; + case Mbum = 'mdd'; + case Maba_Chad = 'mde'; + case Moksha = 'mdf'; + case Massalat = 'mdg'; + case Maguindanaon = 'mdh'; + case Mamvu = 'mdi'; + case Mangbetu = 'mdj'; + case Mangbutu = 'mdk'; + case Maltese_Sign_Language = 'mdl'; + case Mayogo = 'mdm'; + case Mbati = 'mdn'; + case Mbala = 'mdp'; + case Mbole = 'mdq'; + case Mandar = 'mdr'; + case Maria_Papua_New_Guinea = 'mds'; + case Mbere = 'mdt'; + case Mboko = 'mdu'; + case Santa_Lucia_Monteverde_Mixtec = 'mdv'; + case Mbosi = 'mdw'; + case Dizin = 'mdx'; + case Male_Ethiopia = 'mdy'; + case Surui_Do_Para = 'mdz'; + case Menka = 'mea'; + case Ikobi = 'meb'; + case Marra = 'mec'; + case Melpa = 'med'; + case Mengen = 'mee'; + case Megam = 'mef'; + case Southwestern_Tlaxiaco_Mixtec = 'meh'; + case Midob = 'mei'; + case Meyah = 'mej'; + case Mekeo = 'mek'; + case Central_Melanau = 'mel'; + case Mangala = 'mem'; + case Mende_Sierra_Leone = 'men'; + case Kedah_Malay = 'meo'; + case Miriwoong = 'mep'; + case Merey = 'meq'; + case Meru = 'mer'; + case Masmaje = 'mes'; + case Mato = 'met'; + case Motu = 'meu'; + case Mano = 'mev'; + case Maaka = 'mew'; + case Hassaniyya = 'mey'; + case Menominee = 'mez'; + case Pattani_Malay = 'mfa'; + case Bangka = 'mfb'; + case Mba = 'mfc'; + case Mendankwe_Nkwen = 'mfd'; + case Morisyen = 'mfe'; + case Naki = 'mff'; + case Mogofin = 'mfg'; + case Matal = 'mfh'; + case Wandala = 'mfi'; + case Mefele = 'mfj'; + case North_Mofu = 'mfk'; + case Putai = 'mfl'; + case Marghi_South = 'mfm'; + case Cross_River_Mbembe = 'mfn'; + case Mbe = 'mfo'; + case Makassar_Malay = 'mfp'; + case Moba = 'mfq'; + case Marrithiyel = 'mfr'; + case Mexican_Sign_Language = 'mfs'; + case Mokerang = 'mft'; + case Mbwela = 'mfu'; + case Mandjak = 'mfv'; + case Mulaha = 'mfw'; + case Melo = 'mfx'; + case Mayo = 'mfy'; + case Mabaan = 'mfz'; + case Middle_Irish_900_1200 = 'mga'; + case Mararit = 'mgb'; + case Morokodo = 'mgc'; + case Moru = 'mgd'; + case Mango = 'mge'; + case Maklew = 'mgf'; + case Mpumpong = 'mgg'; + case Makhuwa_Meetto = 'mgh'; + case Lijili = 'mgi'; + case Abureni = 'mgj'; + case Mawes = 'mgk'; + case Maleu_Kilenge = 'mgl'; + case Mambae = 'mgm'; + case Mbangi = 'mgn'; + case Meta = 'mgo'; + case Eastern_Magar = 'mgp'; + case Malila = 'mgq'; + case Mambwe_Lungu = 'mgr'; + case Manda_Tanzania = 'mgs'; + case Mongol = 'mgt'; + case Mailu = 'mgu'; + case Matengo = 'mgv'; + case Matumbi = 'mgw'; + case Mbunga = 'mgy'; + case Mbugwe = 'mgz'; + case Manda_India = 'mha'; + case Mahongwe = 'mhb'; + case Mocho = 'mhc'; + case Mbugu = 'mhd'; + case Besisi = 'mhe'; + case Mamaa = 'mhf'; + case Margu = 'mhg'; + case Ma_di = 'mhi'; + case Mogholi = 'mhj'; + case Mungaka = 'mhk'; + case Mauwake = 'mhl'; + case Makhuwa_Moniga = 'mhm'; + case Mocheno = 'mhn'; + case Mashi_Zambia = 'mho'; + case Balinese_Malay = 'mhp'; + case Mandan = 'mhq'; + case Eastern_Mari = 'mhr'; + case Buru_Indonesia = 'mhs'; + case Mandahuaca = 'mht'; + case Digaro_Mishmi = 'mhu'; + case Mbukushu = 'mhw'; + case Maru = 'mhx'; + case Ma_anyan = 'mhy'; + case Mor_Mor_Islands = 'mhz'; + case Miami = 'mia'; + case Atatlahuca_Mixtec = 'mib'; + case Mi_kmaq = 'mic'; + case Mandaic = 'mid'; + case Ocotepec_Mixtec = 'mie'; + case Mofu_Gudur = 'mif'; + case San_Miguel_El_Grande_Mixtec = 'mig'; + case Chayuco_Mixtec = 'mih'; + case Chigmecatitlan_Mixtec = 'mii'; + case Abar = 'mij'; + case Mikasuki = 'mik'; + case Penoles_Mixtec = 'mil'; + case Alacatlatzala_Mixtec = 'mim'; + case Minangkabau = 'min'; + case Pinotepa_Nacional_Mixtec = 'mio'; + case Apasco_Apoala_Mixtec = 'mip'; + case Miskito = 'miq'; + case Isthmus_Mixe = 'mir'; + case Uncoded_languages = 'mis'; + case Southern_Puebla_Mixtec = 'mit'; + case Cacaloxtepec_Mixtec = 'miu'; + case Akoye = 'miw'; + case Mixtepec_Mixtec = 'mix'; + case Ayutla_Mixtec = 'miy'; + case Coatzospan_Mixtec = 'miz'; + case Makalero = 'mjb'; + case San_Juan_Colorado_Mixtec = 'mjc'; + case Northwest_Maidu = 'mjd'; + case Muskum = 'mje'; + case Tu = 'mjg'; + case Mwera_Nyasa = 'mjh'; + case Kim_Mun = 'mji'; + case Mawak = 'mjj'; + case Matukar = 'mjk'; + case Mandeali = 'mjl'; + case Medebur = 'mjm'; + case Ma_Papua_New_Guinea = 'mjn'; + case Malankuravan = 'mjo'; + case Malapandaram = 'mjp'; + case Malaryan = 'mjq'; + case Malavedan = 'mjr'; + case Miship = 'mjs'; + case Sauria_Paharia = 'mjt'; + case Manna_Dora = 'mju'; + case Mannan = 'mjv'; + case Karbi = 'mjw'; + case Mahali = 'mjx'; + case Mahican = 'mjy'; + case Majhi = 'mjz'; + case Mbre = 'mka'; + case Mal_Paharia = 'mkb'; + case Siliput = 'mkc'; + case Macedonian = 'mkd'; + case Mawchi = 'mke'; + case Miya = 'mkf'; + case Mak_China = 'mkg'; + case Dhatki = 'mki'; + case Mokilese = 'mkj'; + case Byep = 'mkk'; + case Mokole = 'mkl'; + case Moklen = 'mkm'; + case Kupang_Malay = 'mkn'; + case Mingang_Doso = 'mko'; + case Moikodi = 'mkp'; + case Bay_Miwok = 'mkq'; + case Malas = 'mkr'; + case Silacayoapan_Mixtec = 'mks'; + case Vamale = 'mkt'; + case Konyanka_Maninka = 'mku'; + case Mafea = 'mkv'; + case Kituba_Congo = 'mkw'; + case Kinamiging_Manobo = 'mkx'; + case East_Makian = 'mky'; + case Makasae = 'mkz'; + case Malo = 'mla'; + case Mbule = 'mlb'; + case Cao_Lan = 'mlc'; + case Manambu = 'mle'; + case Mal = 'mlf'; + case Malagasy = 'mlg'; + case Mape = 'mlh'; + case Malimpung = 'mli'; + case Miltu = 'mlj'; + case Ilwana = 'mlk'; + case Malua_Bay = 'mll'; + case Mulam = 'mlm'; + case Malango = 'mln'; + case Mlomp = 'mlo'; + case Bargam = 'mlp'; + case Western_Maninkakan = 'mlq'; + case Vame = 'mlr'; + case Masalit = 'mls'; + case Maltese = 'mlt'; + case To_abaita = 'mlu'; + case Motlav = 'mlv'; + case Moloko = 'mlw'; + case Malfaxal = 'mlx'; + case Malaynon = 'mlz'; + case Mama = 'mma'; + case Momina = 'mmb'; + case Michoacan_Mazahua = 'mmc'; + case Maonan = 'mmd'; + case Mae = 'mme'; + case Mundat = 'mmf'; + case North_Ambrym = 'mmg'; + case Mehinaku = 'mmh'; + case Musar = 'mmi'; + case Majhwar = 'mmj'; + case Mukha_Dora = 'mmk'; + case Man_Met = 'mml'; + case Maii = 'mmm'; + case Mamanwa = 'mmn'; + case Mangga_Buang = 'mmo'; + case Siawi = 'mmp'; + case Musak = 'mmq'; + case Western_Xiangxi_Miao = 'mmr'; + case Malalamai = 'mmt'; + case Mmaala = 'mmu'; + case Miriti = 'mmv'; + case Emae = 'mmw'; + case Madak = 'mmx'; + case Migaama = 'mmy'; + case Mabaale = 'mmz'; + case Mbula = 'mna'; + case Muna = 'mnb'; + case Manchu = 'mnc'; + case Monde = 'mnd'; + case Naba = 'mne'; + case Mundani = 'mnf'; + case Eastern_Mnong = 'mng'; + case Mono_Democratic_Republic_of_Congo = 'mnh'; + case Manipuri = 'mni'; + case Munji = 'mnj'; + case Mandinka = 'mnk'; + case Tiale = 'mnl'; + case Mapena = 'mnm'; + case Southern_Mnong = 'mnn'; + case Min_Bei_Chinese = 'mnp'; + case Minriq = 'mnq'; + case Mono_USA = 'mnr'; + case Mansi = 'mns'; + case Mer = 'mnu'; + case Rennell_Bellona = 'mnv'; + case Mon = 'mnw'; + case Manikion = 'mnx'; + case Manyawa = 'mny'; + case Moni = 'mnz'; + case Mwan = 'moa'; + case Mocovi = 'moc'; + case Mobilian = 'mod'; + case Innu = 'moe'; + case Mongondow = 'mog'; + case Mohawk = 'moh'; + case Mboi = 'moi'; + case Monzombo = 'moj'; + case Morori = 'mok'; + case Mangue = 'mom'; + case Mongolian = 'mon'; + case Monom = 'moo'; + case Mopan_Maya = 'mop'; + case Mor_Bomberai_Peninsula = 'moq'; + case Moro = 'mor'; + case Mossi = 'mos'; + case Bari_2 = 'mot'; + case Mogum = 'mou'; + case Mohave = 'mov'; + case Moi_Congo = 'mow'; + case Molima = 'mox'; + case Shekkacho = 'moy'; + case Mukulu = 'moz'; + case Mpoto = 'mpa'; + case Malak_Malak = 'mpb'; + case Mangarrayi = 'mpc'; + case Machinere = 'mpd'; + case Majang = 'mpe'; + case Marba = 'mpg'; + case Maung = 'mph'; + case Mpade = 'mpi'; + case Martu_Wangka = 'mpj'; + case Mbara_Chad = 'mpk'; + case Middle_Watut = 'mpl'; + case Yosondua_Mixtec = 'mpm'; + case Mindiri = 'mpn'; + case Miu = 'mpo'; + case Migabac = 'mpp'; + case Matis = 'mpq'; + case Vangunu = 'mpr'; + case Dadibi = 'mps'; + case Mian = 'mpt'; + case Makurap = 'mpu'; + case Mungkip = 'mpv'; + case Mapidian = 'mpw'; + case Misima_Panaeati = 'mpx'; + case Mapia = 'mpy'; + case Mpi = 'mpz'; + case Maba_Indonesia = 'mqa'; + case Mbuko = 'mqb'; + case Mangole = 'mqc'; + case Matepi = 'mqe'; + case Momuna = 'mqf'; + case Kota_Bangun_Kutai_Malay = 'mqg'; + case Tlazoyaltepec_Mixtec = 'mqh'; + case Mariri = 'mqi'; + case Mamasa = 'mqj'; + case Rajah_Kabunsuwan_Manobo = 'mqk'; + case Mbelime = 'mql'; + case South_Marquesan = 'mqm'; + case Moronene = 'mqn'; + case Modole = 'mqo'; + case Manipa = 'mqp'; + case Minokok = 'mqq'; + case Mander = 'mqr'; + case West_Makian = 'mqs'; + case Mok = 'mqt'; + case Mandari = 'mqu'; + case Mosimo = 'mqv'; + case Murupi = 'mqw'; + case Mamuju = 'mqx'; + case Manggarai = 'mqy'; + case Pano = 'mqz'; + case Mlabri = 'mra'; + case Marino = 'mrb'; + case Maricopa = 'mrc'; + case Western_Magar = 'mrd'; + case Martha_s_Vineyard_Sign_Language = 'mre'; + case Elseng = 'mrf'; + case Mising = 'mrg'; + case Mara_Chin = 'mrh'; + case Maori = 'mri'; + case Western_Mari = 'mrj'; + case Hmwaveke = 'mrk'; + case Mortlockese = 'mrl'; + case Merlav = 'mrm'; + case Cheke_Holo = 'mrn'; + case Mru = 'mro'; + case Morouas = 'mrp'; + case North_Marquesan = 'mrq'; + case Maria_India = 'mrr'; + case Maragus = 'mrs'; + case Marghi_Central = 'mrt'; + case Mono_Cameroon = 'mru'; + case Mangareva = 'mrv'; + case Maranao = 'mrw'; + case Maremgi = 'mrx'; + case Mandaya = 'mry'; + case Marind = 'mrz'; + case Malay_macrolanguage = 'msa'; + case Masbatenyo = 'msb'; + case Sankaran_Maninka = 'msc'; + case Yucatec_Maya_Sign_Language = 'msd'; + case Musey = 'mse'; + case Mekwei = 'msf'; + case Moraid = 'msg'; + case Masikoro_Malagasy = 'msh'; + case Sabah_Malay = 'msi'; + case Ma_Democratic_Republic_of_Congo = 'msj'; + case Mansaka = 'msk'; + case Molof = 'msl'; + case Agusan_Manobo = 'msm'; + case Vures = 'msn'; + case Mombum = 'mso'; + case Maritsaua = 'msp'; + case Caac = 'msq'; + case Mongolian_Sign_Language = 'msr'; + case West_Masela = 'mss'; + case Musom = 'msu'; + case Maslam = 'msv'; + case Mansoanka = 'msw'; + case Moresada = 'msx'; + case Aruamu = 'msy'; + case Momare = 'msz'; + case Cotabato_Manobo = 'mta'; + case Anyin_Morofo = 'mtb'; + case Munit = 'mtc'; + case Mualang = 'mtd'; + case Mono_Solomon_Islands = 'mte'; + case Murik_Papua_New_Guinea = 'mtf'; + case Una = 'mtg'; + case Munggui = 'mth'; + case Maiwa_Papua_New_Guinea = 'mti'; + case Moskona = 'mtj'; + case Mbe_2 = 'mtk'; + case Montol = 'mtl'; + case Mator = 'mtm'; + case Matagalpa = 'mtn'; + case Totontepec_Mixe = 'mto'; + case Wichi_Lhamtes_Nocten = 'mtp'; + case Muong = 'mtq'; + case Mewari = 'mtr'; + case Yora = 'mts'; + case Mota = 'mtt'; + case Tututepec_Mixtec = 'mtu'; + case Asaro_o = 'mtv'; + case Southern_Binukidnon = 'mtw'; + case Tidaa_Mixtec = 'mtx'; + case Nabi = 'mty'; + case Mundang = 'mua'; + case Mubi = 'mub'; + case Ajumbu = 'muc'; + case Mednyj_Aleut = 'mud'; + case Media_Lengua = 'mue'; + case Musgu = 'mug'; + case Mundu = 'muh'; + case Musi = 'mui'; + case Mabire = 'muj'; + case Mugom = 'muk'; + case Multiple_languages = 'mul'; + case Maiwala = 'mum'; + case Nyong = 'muo'; + case Malvi = 'mup'; + case Eastern_Xiangxi_Miao = 'muq'; + case Murle = 'mur'; + case Creek = 'mus'; + case Western_Muria = 'mut'; + case Yaaku = 'muu'; + case Muthuvan = 'muv'; + case Bo_Ung = 'mux'; + case Muyang = 'muy'; + case Mursi = 'muz'; + case Manam = 'mva'; + case Mattole = 'mvb'; + case Mamboru = 'mvd'; + case Marwari_Pakistan = 'mve'; + case Peripheral_Mongolian = 'mvf'; + case Yucuane_Mixtec = 'mvg'; + case Mulgi = 'mvh'; + case Miyako = 'mvi'; + case Mekmek = 'mvk'; + case Mbara_Australia = 'mvl'; + case Minaveha = 'mvn'; + case Marovo = 'mvo'; + case Duri = 'mvp'; + case Moere = 'mvq'; + case Marau = 'mvr'; + case Massep = 'mvs'; + case Mpotovoro = 'mvt'; + case Marfa = 'mvu'; + case Tagal_Murut = 'mvv'; + case Machinga = 'mvw'; + case Meoswar = 'mvx'; + case Indus_Kohistani = 'mvy'; + case Mesqan = 'mvz'; + case Mwatebu = 'mwa'; + case Juwal = 'mwb'; + case Are = 'mwc'; + case Mwera_Chimwera = 'mwe'; + case Murrinh_Patha = 'mwf'; + case Aiklep = 'mwg'; + case Mouk_Aria = 'mwh'; + case Labo = 'mwi'; + case Kita_Maninkakan = 'mwk'; + case Mirandese = 'mwl'; + case Sar = 'mwm'; + case Nyamwanga = 'mwn'; + case Central_Maewo = 'mwo'; + case Kala_Lagaw_Ya = 'mwp'; + case Mun_Chin = 'mwq'; + case Marwari = 'mwr'; + case Mwimbi_Muthambi = 'mws'; + case Moken = 'mwt'; + case Mittu = 'mwu'; + case Mentawai = 'mwv'; + case Hmong_Daw = 'mww'; + case Moingi = 'mwz'; + case Northwest_Oaxaca_Mixtec = 'mxa'; + case Tezoatlan_Mixtec = 'mxb'; + case Manyika = 'mxc'; + case Modang = 'mxd'; + case Mele_Fila = 'mxe'; + case Malgbe = 'mxf'; + case Mbangala = 'mxg'; + case Mvuba = 'mxh'; + case Mozarabic = 'mxi'; + case Miju_Mishmi = 'mxj'; + case Monumbo = 'mxk'; + case Maxi_Gbe = 'mxl'; + case Meramera = 'mxm'; + case Moi_Indonesia = 'mxn'; + case Mbowe = 'mxo'; + case Tlahuitoltepec_Mixe = 'mxp'; + case Juquila_Mixe = 'mxq'; + case Murik_Malaysia = 'mxr'; + case Huitepec_Mixtec = 'mxs'; + case Jamiltepec_Mixtec = 'mxt'; + case Mada_Cameroon = 'mxu'; + case Metlatonoc_Mixtec = 'mxv'; + case Namo = 'mxw'; + case Mahou = 'mxx'; + case Southeastern_Nochixtlan_Mixtec = 'mxy'; + case Central_Masela = 'mxz'; + case Burmese = 'mya'; + case Mbay = 'myb'; + case Mayeka = 'myc'; + case Myene = 'mye'; + case Bambassi = 'myf'; + case Manta = 'myg'; + case Makah = 'myh'; + case Mangayat = 'myj'; + case Mamara_Senoufo = 'myk'; + case Moma = 'myl'; + case Me_en = 'mym'; + case Anfillo = 'myo'; + case Piraha = 'myp'; + case Muniche = 'myr'; + case Mesmes = 'mys'; + case Munduruku = 'myu'; + case Erzya = 'myv'; + case Muyuw = 'myw'; + case Masaaba = 'myx'; + case Macuna = 'myy'; + case Classical_Mandaic = 'myz'; + case Santa_Maria_Zacatepec_Mixtec = 'mza'; + case Tumzabt = 'mzb'; + case Madagascar_Sign_Language = 'mzc'; + case Malimba = 'mzd'; + case Morawa = 'mze'; + case Monastic_Sign_Language = 'mzg'; + case Wichi_Lhamtes_Guisnay = 'mzh'; + case Ixcatlan_Mazatec = 'mzi'; + case Manya = 'mzj'; + case Nigeria_Mambila = 'mzk'; + case Mazatlan_Mixe = 'mzl'; + case Mumuye = 'mzm'; + case Mazanderani = 'mzn'; + case Matipuhy = 'mzo'; + case Movima = 'mzp'; + case Mori_Atas = 'mzq'; + case Marubo = 'mzr'; + case Macanese = 'mzs'; + case Mintil = 'mzt'; + case Inapang = 'mzu'; + case Manza = 'mzv'; + case Deg = 'mzw'; + case Mawayana = 'mzx'; + case Mozambican_Sign_Language = 'mzy'; + case Maiadomu = 'mzz'; + case Namla = 'naa'; + case Southern_Nambikuara = 'nab'; + case Narak = 'nac'; + case Naka_ela = 'nae'; + case Nabak = 'naf'; + case Naga_Pidgin = 'nag'; + case Nalu = 'naj'; + case Nakanai = 'nak'; + case Nalik = 'nal'; + case Ngan_gityemerri = 'nam'; + case Min_Nan_Chinese = 'nan'; + case Naaba = 'nao'; + case Neapolitan = 'nap'; + case Khoekhoe = 'naq'; + case Iguta = 'nar'; + case Naasioi = 'nas'; + case Cahungwarya = 'nat'; + case Nauru = 'nau'; + case Navajo = 'nav'; + case Nawuri = 'naw'; + case Nakwi = 'nax'; + case Ngarrindjeri = 'nay'; + case Coatepec_Nahuatl = 'naz'; + case Nyemba = 'nba'; + case Ndoe = 'nbb'; + case Chang_Naga = 'nbc'; + case Ngbinda = 'nbd'; + case Konyak_Naga = 'nbe'; + case Nagarchal = 'nbg'; + case Ngamo = 'nbh'; + case Mao_Naga = 'nbi'; + case Ngarinyman = 'nbj'; + case Nake = 'nbk'; + case South_Ndebele = 'nbl'; + case Ngbaka_Ma_bo = 'nbm'; + case Kuri = 'nbn'; + case Nkukoli = 'nbo'; + case Nnam = 'nbp'; + case Nggem = 'nbq'; + case Numana = 'nbr'; + case Namibian_Sign_Language = 'nbs'; + case Na = 'nbt'; + case Rongmei_Naga = 'nbu'; + case Ngamambo = 'nbv'; + case Southern_Ngbandi = 'nbw'; + case Ningera = 'nby'; + case Iyo = 'nca'; + case Central_Nicobarese = 'ncb'; + case Ponam = 'ncc'; + case Nachering = 'ncd'; + case Yale = 'nce'; + case Notsi = 'ncf'; + case Nisga_a = 'ncg'; + case Central_Huasteca_Nahuatl = 'nch'; + case Classical_Nahuatl = 'nci'; + case Northern_Puebla_Nahuatl = 'ncj'; + case Na_kara = 'nck'; + case Michoacan_Nahuatl = 'ncl'; + case Nambo = 'ncm'; + case Nauna = 'ncn'; + case Sibe = 'nco'; + case Northern_Katang = 'ncq'; + case Ncane = 'ncr'; + case Nicaraguan_Sign_Language = 'ncs'; + case Chothe_Naga = 'nct'; + case Chumburung = 'ncu'; + case Central_Puebla_Nahuatl = 'ncx'; + case Natchez = 'ncz'; + case Ndasa = 'nda'; + case Kenswei_Nsei = 'ndb'; + case Ndau = 'ndc'; + case Nde_Nsele_Nta = 'ndd'; + case North_Ndebele = 'nde'; + case Nadruvian = 'ndf'; + case Ndengereko = 'ndg'; + case Ndali = 'ndh'; + case Samba_Leko = 'ndi'; + case Ndamba = 'ndj'; + case Ndaka = 'ndk'; + case Ndolo = 'ndl'; + case Ndam = 'ndm'; + case Ngundi = 'ndn'; + case Ndonga = 'ndo'; + case Ndo = 'ndp'; + case Ndombe = 'ndq'; + case Ndoola = 'ndr'; + case Low_German = 'nds'; + case Ndunga = 'ndt'; + case Dugun = 'ndu'; + case Ndut = 'ndv'; + case Ndobo = 'ndw'; + case Nduga = 'ndx'; + case Lutos = 'ndy'; + case Ndogo = 'ndz'; + case Eastern_Ngad_a = 'nea'; + case Toura_Cote_d_Ivoire = 'neb'; + case Nedebang = 'nec'; + case Nde_Gbite = 'ned'; + case Nelemwa_Nixumwak = 'nee'; + case Nefamese = 'nef'; + case Negidal = 'neg'; + case Nyenkha = 'neh'; + case Neo_Hittite = 'nei'; + case Neko = 'nej'; + case Neku = 'nek'; + case Nemi = 'nem'; + case Nengone = 'nen'; + case Na_Meo = 'neo'; + case Nepali_macrolanguage = 'nep'; + case North_Central_Mixe = 'neq'; + case Yahadian = 'ner'; + case Bhoti_Kinnauri = 'nes'; + case Nete = 'net'; + case Neo = 'neu'; + case Nyaheun = 'nev'; + case Newari = 'new'; + case Neme = 'nex'; + case Neyo = 'ney'; + case Nez_Perce = 'nez'; + case Dhao = 'nfa'; + case Ahwai = 'nfd'; + case Ayiwo = 'nfl'; + case Nafaanra = 'nfr'; + case Mfumte = 'nfu'; + case Ngbaka = 'nga'; + case Northern_Ngbandi = 'ngb'; + case Ngombe_Democratic_Republic_of_Congo = 'ngc'; + case Ngando_Central_African_Republic = 'ngd'; + case Ngemba = 'nge'; + case Ngbaka_Manza = 'ngg'; + case N_ng = 'ngh'; + case Ngizim = 'ngi'; + case Ngie = 'ngj'; + case Dalabon = 'ngk'; + case Lomwe = 'ngl'; + case Ngatik_Men_s_Creole = 'ngm'; + case Ngwo = 'ngn'; + case Ngulu = 'ngp'; + case Ngurimi = 'ngq'; + case Engdewu = 'ngr'; + case Gvoko = 'ngs'; + case Kriang = 'ngt'; + case Guerrero_Nahuatl = 'ngu'; + case Nagumi = 'ngv'; + case Ngwaba = 'ngw'; + case Nggwahyi = 'ngx'; + case Tibea = 'ngy'; + case Ngungwel = 'ngz'; + case Nhanda = 'nha'; + case Beng = 'nhb'; + case Tabasco_Nahuatl = 'nhc'; + case Chiripa = 'nhd'; + case Eastern_Huasteca_Nahuatl = 'nhe'; + case Nhuwala = 'nhf'; + case Tetelcingo_Nahuatl = 'nhg'; + case Nahari = 'nhh'; + case Zacatlan_Ahuacatlan_Tepetzintla_Nahuatl = 'nhi'; + case Isthmus_Cosoleacaque_Nahuatl = 'nhk'; + case Morelos_Nahuatl = 'nhm'; + case Central_Nahuatl = 'nhn'; + case Takuu = 'nho'; + case Isthmus_Pajapan_Nahuatl = 'nhp'; + case Huaxcaleca_Nahuatl = 'nhq'; + case Naro = 'nhr'; + case Ometepec_Nahuatl = 'nht'; + case Noone = 'nhu'; + case Temascaltepec_Nahuatl = 'nhv'; + case Western_Huasteca_Nahuatl = 'nhw'; + case Isthmus_Mecayapan_Nahuatl = 'nhx'; + case Northern_Oaxaca_Nahuatl = 'nhy'; + case Santa_Maria_La_Alta_Nahuatl = 'nhz'; + case Nias = 'nia'; + case Nakame = 'nib'; + case Ngandi = 'nid'; + case Niellim = 'nie'; + case Nek = 'nif'; + case Ngalakgan = 'nig'; + case Nyiha_Tanzania = 'nih'; + case Nii = 'nii'; + case Ngaju = 'nij'; + case Southern_Nicobarese = 'nik'; + case Nila = 'nil'; + case Nilamba = 'nim'; + case Ninzo = 'nin'; + case Nganasan = 'nio'; + case Nandi = 'niq'; + case Nimboran = 'nir'; + case Nimi = 'nis'; + case Southeastern_Kolami = 'nit'; + case Niuean = 'niu'; + case Gilyak = 'niv'; + case Nimo = 'niw'; + case Hema = 'nix'; + case Ngiti = 'niy'; + case Ningil = 'niz'; + case Nzanyi = 'nja'; + case Nocte_Naga = 'njb'; + case Ndonde_Hamba = 'njd'; + case Lotha_Naga = 'njh'; + case Gudanji = 'nji'; + case Njen = 'njj'; + case Njalgulgule = 'njl'; + case Angami_Naga = 'njm'; + case Liangmai_Naga = 'njn'; + case Ao_Naga = 'njo'; + case Njerep = 'njr'; + case Nisa = 'njs'; + case Ndyuka_Trio_Pidgin = 'njt'; + case Ngadjunmaya = 'nju'; + case Kunyi = 'njx'; + case Njyem = 'njy'; + case Nyishi = 'njz'; + case Nkoya = 'nka'; + case Khoibu_Naga = 'nkb'; + case Nkongho = 'nkc'; + case Koireng = 'nkd'; + case Duke = 'nke'; + case Inpui_Naga = 'nkf'; + case Nekgini = 'nkg'; + case Khezha_Naga = 'nkh'; + case Thangal_Naga = 'nki'; + case Nakai = 'nkj'; + case Nokuku = 'nkk'; + case Namat = 'nkm'; + case Nkangala = 'nkn'; + case Nkonya = 'nko'; + case Niuatoputapu = 'nkp'; + case Nkami = 'nkq'; + case Nukuoro = 'nkr'; + case North_Asmat = 'nks'; + case Nyika_Tanzania = 'nkt'; + case Bouna_Kulango = 'nku'; + case Nyika_Malawi_and_Zambia = 'nkv'; + case Nkutu = 'nkw'; + case Nkoroo = 'nkx'; + case Nkari = 'nkz'; + case Ngombale = 'nla'; + case Nalca = 'nlc'; + case Dutch = 'nld'; + case East_Nyala = 'nle'; + case Gela = 'nlg'; + case Grangali = 'nli'; + case Nyali = 'nlj'; + case Ninia_Yali = 'nlk'; + case Nihali = 'nll'; + case Mankiyali = 'nlm'; + case Ngul = 'nlo'; + case Lao_Naga = 'nlq'; + case Nchumbulu = 'nlu'; + case Orizaba_Nahuatl = 'nlv'; + case Walangama = 'nlw'; + case Nahali = 'nlx'; + case Nyamal = 'nly'; + case Nalogo = 'nlz'; + case Maram_Naga = 'nma'; + case Big_Nambas = 'nmb'; + case Ngam = 'nmc'; + case Ndumu = 'nmd'; + case Mzieme_Naga = 'nme'; + case Tangkhul_Naga_India = 'nmf'; + case Kwasio = 'nmg'; + case Monsang_Naga = 'nmh'; + case Nyam = 'nmi'; + case Ngombe_Central_African_Republic = 'nmj'; + case Namakura = 'nmk'; + case Ndemli = 'nml'; + case Manangba = 'nmm'; + case Xoo = 'nmn'; + case Moyon_Naga = 'nmo'; + case Nimanbur = 'nmp'; + case Nambya = 'nmq'; + case Nimbari = 'nmr'; + case Letemboi = 'nms'; + case Namonuito = 'nmt'; + case Northeast_Maidu = 'nmu'; + case Ngamini = 'nmv'; + case Nimoa = 'nmw'; + case Nama_Papua_New_Guinea = 'nmx'; + case Namuyi = 'nmy'; + case Nawdm = 'nmz'; + case Nyangumarta = 'nna'; + case Nande = 'nnb'; + case Nancere = 'nnc'; + case West_Ambae = 'nnd'; + case Ngandyera = 'nne'; + case Ngaing = 'nnf'; + case Maring_Naga = 'nng'; + case Ngiemboon = 'nnh'; + case North_Nuaulu = 'nni'; + case Nyangatom = 'nnj'; + case Nankina = 'nnk'; + case Northern_Rengma_Naga = 'nnl'; + case Namia = 'nnm'; + case Ngete = 'nnn'; + case Norwegian_Nynorsk = 'nno'; + case Wancho_Naga = 'nnp'; + case Ngindo = 'nnq'; + case Narungga = 'nnr'; + case Nanticoke = 'nnt'; + case Dwang = 'nnu'; + case Nugunu_Australia = 'nnv'; + case Southern_Nuni = 'nnw'; + case Nyangga = 'nny'; + case Nda_nda = 'nnz'; + case Woun_Meu = 'noa'; + case Norwegian_Bokmal = 'nob'; + case Nuk = 'noc'; + case Northern_Thai = 'nod'; + case Nimadi = 'noe'; + case Nomane = 'nof'; + case Nogai = 'nog'; + case Nomu = 'noh'; + case Noiri = 'noi'; + case Nonuya = 'noj'; + case Nooksack = 'nok'; + case Nomlaki = 'nol'; + case Old_Norse = 'non'; + case Numanggang = 'nop'; + case Ngongo = 'noq'; + case Norwegian = 'nor'; + case Eastern_Nisu = 'nos'; + case Nomatsiguenga = 'not'; + case Ewage_Notu = 'nou'; + case Novial = 'nov'; + case Nyambo = 'now'; + case Noy = 'noy'; + case Nayi = 'noz'; + case Nar_Phu = 'npa'; + case Nupbikha = 'npb'; + case Ponyo_Gongwang_Naga = 'npg'; + case Phom_Naga = 'nph'; + case Nepali_individual_language = 'npi'; + case Southeastern_Puebla_Nahuatl = 'npl'; + case Mondropolon = 'npn'; + case Pochuri_Naga = 'npo'; + case Nipsan = 'nps'; + case Puimei_Naga = 'npu'; + case Noipx = 'npx'; + case Napu = 'npy'; + case Southern_Nago = 'nqg'; + case Kura_Ede_Nago = 'nqk'; + case Ngendelengo = 'nql'; + case Ndom = 'nqm'; + case Nen = 'nqn'; + case N_Ko = 'nqo'; + case Kyan_Karyaw_Naga = 'nqq'; + case Nteng = 'nqt'; + case Akyaung_Ari_Naga = 'nqy'; + case Ngom = 'nra'; + case Nara = 'nrb'; + case Noric = 'nrc'; + case Southern_Rengma_Naga = 'nre'; + case Jerriais = 'nrf'; + case Narango = 'nrg'; + case Chokri_Naga = 'nri'; + case Ngarla = 'nrk'; + case Ngarluma = 'nrl'; + case Narom = 'nrm'; + case Norn = 'nrn'; + case North_Picene = 'nrp'; + case Norra = 'nrr'; + case Northern_Kalapuya = 'nrt'; + case Narua = 'nru'; + case Ngurmbur = 'nrx'; + case Lala = 'nrz'; + case Sangtam_Naga = 'nsa'; + case Lower_Nossob = 'nsb'; + case Nshi = 'nsc'; + case Southern_Nisu = 'nsd'; + case Nsenga = 'nse'; + case Northwestern_Nisu = 'nsf'; + case Ngasa = 'nsg'; + case Ngoshie = 'nsh'; + case Nigerian_Sign_Language = 'nsi'; + case Naskapi = 'nsk'; + case Norwegian_Sign_Language = 'nsl'; + case Sumi_Naga = 'nsm'; + case Nehan = 'nsn'; + case Pedi = 'nso'; + case Nepalese_Sign_Language = 'nsp'; + case Northern_Sierra_Miwok = 'nsq'; + case Maritime_Sign_Language = 'nsr'; + case Nali = 'nss'; + case Tase_Naga = 'nst'; + case Sierra_Negra_Nahuatl = 'nsu'; + case Southwestern_Nisu = 'nsv'; + case Navut = 'nsw'; + case Nsongo = 'nsx'; + case Nasal = 'nsy'; + case Nisenan = 'nsz'; + case Northern_Tidung = 'ntd'; + case Nathembo = 'nte'; + case Ngantangarra = 'ntg'; + case Natioro = 'nti'; + case Ngaanyatjarra = 'ntj'; + case Ikoma_Nata_Isenye = 'ntk'; + case Nateni = 'ntm'; + case Ntomba = 'nto'; + case Northern_Tepehuan = 'ntp'; + case Delo = 'ntr'; + case Natugu = 'ntu'; + case Nottoway = 'ntw'; + case Tangkhul_Naga_Myanmar = 'ntx'; + case Mantsi = 'nty'; + case Natanzi = 'ntz'; + case Yuanga = 'nua'; + case Nukuini = 'nuc'; + case Ngala = 'nud'; + case Ngundu = 'nue'; + case Nusu = 'nuf'; + case Nungali = 'nug'; + case Ndunda = 'nuh'; + case Ngumbi = 'nui'; + case Nyole = 'nuj'; + case Nuu_chah_nulth = 'nuk'; + case Nusa_Laut = 'nul'; + case Niuafo_ou = 'num'; + case Anong = 'nun'; + case Nguon = 'nuo'; + case Nupe_Nupe_Tako = 'nup'; + case Nukumanu = 'nuq'; + case Nukuria = 'nur'; + case Nuer = 'nus'; + case Nung_Viet_Nam = 'nut'; + case Ngbundu = 'nuu'; + case Northern_Nuni = 'nuv'; + case Nguluwan = 'nuw'; + case Mehek = 'nux'; + case Nunggubuyu = 'nuy'; + case Tlamacazapa_Nahuatl = 'nuz'; + case Nasarian = 'nvh'; + case Namiae = 'nvm'; + case Nyokon = 'nvo'; + case Nawathinehena = 'nwa'; + case Nyabwa = 'nwb'; + case Classical_Newari = 'nwc'; + case Ngwe = 'nwe'; + case Ngayawung = 'nwg'; + case Southwest_Tanna = 'nwi'; + case Nyamusa_Molo = 'nwm'; + case Nauo = 'nwo'; + case Nawaru = 'nwr'; + case Ndwewe = 'nww'; + case Middle_Newar = 'nwx'; + case Nottoway_Meherrin = 'nwy'; + case Nauete = 'nxa'; + case Ngando_Democratic_Republic_of_Congo = 'nxd'; + case Nage = 'nxe'; + case Ngad_a = 'nxg'; + case Nindi = 'nxi'; + case Koki_Naga = 'nxk'; + case South_Nuaulu = 'nxl'; + case Numidian = 'nxm'; + case Ngawun = 'nxn'; + case Ndambomo = 'nxo'; + case Naxi = 'nxq'; + case Ninggerum = 'nxr'; + case Nafri = 'nxx'; + case Nyanja = 'nya'; + case Nyangbo = 'nyb'; + case Nyanga_li = 'nyc'; + case Nyore = 'nyd'; + case Nyengo = 'nye'; + case Giryama = 'nyf'; + case Nyindu = 'nyg'; + case Nyikina = 'nyh'; + case Ama_Sudan = 'nyi'; + case Nyanga = 'nyj'; + case Nyaneka = 'nyk'; + case Nyeu = 'nyl'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nyang_i = 'nyp'; + case Nayini = 'nyq'; + case Nyiha_Malawi = 'nyr'; + case Nyungar = 'nys'; + case Nyawaygi = 'nyt'; + case Nyungwe = 'nyu'; + case Nyulnyul = 'nyv'; + case Nyaw = 'nyw'; + case Nganyaywana = 'nyx'; + case Nyakyusa_Ngonde = 'nyy'; + case Tigon_Mbembe = 'nza'; + case Njebi = 'nzb'; + case Nzadi = 'nzd'; + case Nzima = 'nzi'; + case Nzakara = 'nzk'; + case Zeme_Naga = 'nzm'; + case Dir_Nyamzak_Mbarimi = 'nzr'; + case New_Zealand_Sign_Language = 'nzs'; + case Teke_Nzikou = 'nzu'; + case Nzakambay = 'nzy'; + case Nanga_Dama_Dogon = 'nzz'; + case Orok = 'oaa'; + case Oroch = 'oac'; + case Old_Aramaic_up_to_700_BCE = 'oar'; + case Old_Avar = 'oav'; + case Obispeno = 'obi'; + case Southern_Bontok = 'obk'; + case Oblo = 'obl'; + case Moabite = 'obm'; + case Obo_Manobo = 'obo'; + case Old_Burmese = 'obr'; + case Old_Breton = 'obt'; + case Obulom = 'obu'; + case Ocaina = 'oca'; + case Old_Chinese = 'och'; + case Occitan_post_1500 = 'oci'; + case Old_Cham = 'ocm'; + case Old_Cornish = 'oco'; + case Atzingo_Matlatzinca = 'ocu'; + case Odut = 'oda'; + case Od = 'odk'; + case Old_Dutch = 'odt'; + case Odual = 'odu'; + case Ofo = 'ofo'; + case Old_Frisian = 'ofs'; + case Efutop = 'ofu'; + case Ogbia = 'ogb'; + case Ogbah = 'ogc'; + case Old_Georgian = 'oge'; + case Ogbogolo = 'ogg'; + case Khana = 'ogo'; + case Ogbronuagum = 'ogu'; + case Old_Hittite = 'oht'; + case Old_Hungarian = 'ohu'; + case Oirata = 'oia'; + case Okolie = 'oie'; + case Inebu_One = 'oin'; + case Northwestern_Ojibwa = 'ojb'; + case Central_Ojibwa = 'ojc'; + case Eastern_Ojibwa = 'ojg'; + case Ojibwa = 'oji'; + case Old_Japanese = 'ojp'; + case Severn_Ojibwa = 'ojs'; + case Ontong_Java = 'ojv'; + case Western_Ojibwa = 'ojw'; + case Okanagan = 'oka'; + case Okobo = 'okb'; + case Kobo = 'okc'; + case Okodia = 'okd'; + case Okpe_Southwestern_Edo = 'oke'; + case Koko_Babangk = 'okg'; + case Koresh_e_Rostam = 'okh'; + case Okiek = 'oki'; + case Oko_Juwoi = 'okj'; + case Kwamtim_One = 'okk'; + case Old_Kentish_Sign_Language = 'okl'; + case Middle_Korean_10th_16th_cent = 'okm'; + case Oki_No_Erabu = 'okn'; + case Old_Korean_3rd_9th_cent = 'oko'; + case Kirike = 'okr'; + case Oko_Eni_Osayen = 'oks'; + case Oku = 'oku'; + case Orokaiva = 'okv'; + case Okpe_Northwestern_Edo = 'okx'; + case Old_Khmer = 'okz'; + case Walungge = 'ola'; + case Mochi = 'old'; + case Olekha = 'ole'; + case Olkol = 'olk'; + case Oloma = 'olm'; + case Livvi = 'olo'; + case Olrat = 'olr'; + case Old_Lithuanian = 'olt'; + case Kuvale = 'olu'; + case Omaha_Ponca = 'oma'; + case East_Ambae = 'omb'; + case Mochica = 'omc'; + case Omagua = 'omg'; + case Omi = 'omi'; + case Omok = 'omk'; + case Ombo = 'oml'; + case Minoan = 'omn'; + case Utarmbung = 'omo'; + case Old_Manipuri = 'omp'; + case Old_Marathi = 'omr'; + case Omotik = 'omt'; + case Omurano = 'omu'; + case South_Tairora = 'omw'; + case Old_Mon = 'omx'; + case Old_Malay = 'omy'; + case Ona = 'ona'; + case Lingao = 'onb'; + case Oneida = 'one'; + case Olo = 'ong'; + case Onin = 'oni'; + case Onjob = 'onj'; + case Kabore_One = 'onk'; + case Onobasulu = 'onn'; + case Onondaga = 'ono'; + case Sartang = 'onp'; + case Northern_One = 'onr'; + case Ono = 'ons'; + case Ontenu = 'ont'; + case Unua = 'onu'; + case Old_Nubian = 'onw'; + case Onin_Based_Pidgin = 'onx'; + case Tohono_O_odham = 'ood'; + case Ong = 'oog'; + case Onge = 'oon'; + case Oorlams = 'oor'; + case Old_Ossetic = 'oos'; + case Okpamheri = 'opa'; + case Kopkaka = 'opk'; + case Oksapmin = 'opm'; + case Opao = 'opo'; + case Opata = 'opt'; + case Ofaye = 'opy'; + case Oroha = 'ora'; + case Orma = 'orc'; + case Orejon = 'ore'; + case Oring = 'org'; + case Oroqen = 'orh'; + case Oriya_macrolanguage = 'ori'; + case Oromo = 'orm'; + case Orang_Kanaq = 'orn'; + case Orokolo = 'oro'; + case Oruma = 'orr'; + case Orang_Seletar = 'ors'; + case Adivasi_Oriya = 'ort'; + case Ormuri = 'oru'; + case Old_Russian = 'orv'; + case Oro_Win = 'orw'; + case Oro = 'orx'; + case Odia = 'ory'; + case Ormu = 'orz'; + case Osage = 'osa'; + case Oscan = 'osc'; + case Osing = 'osi'; + case Old_Sundanese = 'osn'; + case Ososo = 'oso'; + case Old_Spanish = 'osp'; + case Ossetian = 'oss'; + case Osatu = 'ost'; + case Southern_One = 'osu'; + case Old_Saxon = 'osx'; + case Ottoman_Turkish_1500_1928 = 'ota'; + case Old_Tibetan = 'otb'; + case Ot_Danum = 'otd'; + case Mezquital_Otomi = 'ote'; + case Oti = 'oti'; + case Old_Turkish = 'otk'; + case Tilapa_Otomi = 'otl'; + case Eastern_Highland_Otomi = 'otm'; + case Tenango_Otomi = 'otn'; + case Queretaro_Otomi = 'otq'; + case Otoro = 'otr'; + case Estado_de_Mexico_Otomi = 'ots'; + case Temoaya_Otomi = 'ott'; + case Otuke = 'otu'; + case Ottawa = 'otw'; + case Texcatepec_Otomi = 'otx'; + case Old_Tamil = 'oty'; + case Ixtenco_Otomi = 'otz'; + case Tagargrent = 'oua'; + case Glio_Oubi = 'oub'; + case Oune = 'oue'; + case Old_Uighur = 'oui'; + case Ouma = 'oum'; + case Elfdalian = 'ovd'; + case Owiniga = 'owi'; + case Old_Welsh = 'owl'; + case Oy = 'oyb'; + case Oyda = 'oyd'; + case Wayampi = 'oym'; + case Oya_oya = 'oyy'; + case Koonzime = 'ozm'; + case Parecis = 'pab'; + case Pacoh = 'pac'; + case Paumari = 'pad'; + case Pagibete = 'pae'; + case Paranawat = 'paf'; + case Pangasinan = 'pag'; + case Tenharim = 'pah'; + case Pe = 'pai'; + case Parakana = 'pak'; + case Pahlavi = 'pal'; + case Pampanga = 'pam'; + case Panjabi = 'pan'; + case Northern_Paiute = 'pao'; + case Papiamento = 'pap'; + case Parya = 'paq'; + case Panamint = 'par'; + case Papasena = 'pas'; + case Palauan = 'pau'; + case Pakaasnovos = 'pav'; + case Pawnee = 'paw'; + case Pankarare = 'pax'; + case Pech = 'pay'; + case Pankararu = 'paz'; + case Paez = 'pbb'; + case Patamona = 'pbc'; + case Mezontla_Popoloca = 'pbe'; + case Coyotepec_Popoloca = 'pbf'; + case Paraujano = 'pbg'; + case E_napa_Woromaipu = 'pbh'; + case Parkwa = 'pbi'; + case Mak_Nigeria = 'pbl'; + case Puebla_Mazatec = 'pbm'; + case Kpasam = 'pbn'; + case Papel = 'pbo'; + case Badyara = 'pbp'; + case Pangwa = 'pbr'; + case Central_Pame = 'pbs'; + case Southern_Pashto = 'pbt'; + case Northern_Pashto = 'pbu'; + case Pnar = 'pbv'; + case Pyu_Papua_New_Guinea = 'pby'; + case Santa_Ines_Ahuatempan_Popoloca = 'pca'; + case Pear = 'pcb'; + case Bouyei = 'pcc'; + case Picard = 'pcd'; + case Ruching_Palaung = 'pce'; + case Paliyan = 'pcf'; + case Paniya = 'pcg'; + case Pardhan = 'pch'; + case Duruwa = 'pci'; + case Parenga = 'pcj'; + case Paite_Chin = 'pck'; + case Pardhi = 'pcl'; + case Nigerian_Pidgin = 'pcm'; + case Piti = 'pcn'; + case Pacahuara = 'pcp'; + case Pyapun = 'pcw'; + case Anam = 'pda'; + case Pennsylvania_German = 'pdc'; + case Pa_Di = 'pdi'; + case Podena = 'pdn'; + case Padoe = 'pdo'; + case Plautdietsch = 'pdt'; + case Kayan = 'pdu'; + case Peranakan_Indonesian = 'pea'; + case Eastern_Pomo = 'peb'; + case Mala_Papua_New_Guinea = 'ped'; + case Taje = 'pee'; + case Northeastern_Pomo = 'pef'; + case Pengo = 'peg'; + case Bonan = 'peh'; + case Chichimeca_Jonaz = 'pei'; + case Northern_Pomo = 'pej'; + case Penchal = 'pek'; + case Pekal = 'pel'; + case Phende = 'pem'; + case Old_Persian_ca_600_400_B_C = 'peo'; + case Kunja = 'pep'; + case Southern_Pomo = 'peq'; + case Iranian_Persian = 'pes'; + case Pemono = 'pev'; + case Petats = 'pex'; + case Petjo = 'pey'; + case Eastern_Penan = 'pez'; + case Paafang = 'pfa'; + case Pere = 'pfe'; + case Pfaelzisch = 'pfl'; + case Sudanese_Creole_Arabic = 'pga'; + case Gandhari = 'pgd'; + case Pangwali = 'pgg'; + case Pagi = 'pgi'; + case Rerep = 'pgk'; + case Primitive_Irish = 'pgl'; + case Paelignian = 'pgn'; + case Pangseng = 'pgs'; + case Pagu = 'pgu'; + case Papua_New_Guinean_Sign_Language = 'pgz'; + case Pa_Hng = 'pha'; + case Phudagi = 'phd'; + case Phuong = 'phg'; + case Phukha = 'phh'; + case Pahari = 'phj'; + case Phake = 'phk'; + case Phalura = 'phl'; + case Phimbi = 'phm'; + case Phoenician = 'phn'; + case Phunoi = 'pho'; + case Phana = 'phq'; + case Pahari_Potwari = 'phr'; + case Phu_Thai = 'pht'; + case Phuan = 'phu'; + case Pahlavani = 'phv'; + case Phangduwali = 'phw'; + case Pima_Bajo = 'pia'; + case Yine = 'pib'; + case Pinji = 'pic'; + case Piaroa = 'pid'; + case Piro = 'pie'; + case Pingelapese = 'pif'; + case Pisabo = 'pig'; + case Pitcairn_Norfolk = 'pih'; + case Pijao = 'pij'; + case Yom = 'pil'; + case Powhatan = 'pim'; + case Piame = 'pin'; + case Piapoco = 'pio'; + case Pero = 'pip'; + case Piratapuyo = 'pir'; + case Pijin = 'pis'; + case Pitta_Pitta = 'pit'; + case Pintupi_Luritja = 'piu'; + case Pileni = 'piv'; + case Pimbwe = 'piw'; + case Piu = 'pix'; + case Piya_Kwonci = 'piy'; + case Pije = 'piz'; + case Pitjantjatjara = 'pjt'; + case Ardhamagadhi_Prakrit = 'pka'; + case Pokomo = 'pkb'; + case Paekche = 'pkc'; + case Pak_Tong = 'pkg'; + case Pankhu = 'pkh'; + case Pakanha = 'pkn'; + case Pokoot = 'pko'; + case Pukapuka = 'pkp'; + case Attapady_Kurumba = 'pkr'; + case Pakistan_Sign_Language = 'pks'; + case Maleng = 'pkt'; + case Paku = 'pku'; + case Miani = 'pla'; + case Polonombauk = 'plb'; + case Central_Palawano = 'plc'; + case Polari = 'pld'; + case Palu_e = 'ple'; + case Pilaga = 'plg'; + case Paulohi = 'plh'; + case Pali = 'pli'; + case Kohistani_Shina = 'plk'; + case Shwe_Palaung = 'pll'; + case Palenquero = 'pln'; + case Oluta_Popoluca = 'plo'; + case Palaic = 'plq'; + case Palaka_Senoufo = 'plr'; + case San_Marcos_Tlacoyalco_Popoloca = 'pls'; + case Plateau_Malagasy = 'plt'; + case Palikur = 'plu'; + case Southwest_Palawano = 'plv'; + case Brooke_s_Point_Palawano = 'plw'; + case Bolyu = 'ply'; + case Paluan = 'plz'; + case Paama = 'pma'; + case Pambia = 'pmb'; + case Pallanganmiddang = 'pmd'; + case Pwaamei = 'pme'; + case Pamona = 'pmf'; + case Maharastri_Prakrit = 'pmh'; + case Northern_Pumi = 'pmi'; + case Southern_Pumi = 'pmj'; + case Lingua_Franca = 'pml'; + case Pomo = 'pmm'; + case Pam = 'pmn'; + case Pom = 'pmo'; + case Northern_Pame = 'pmq'; + case Paynamar = 'pmr'; + case Piemontese = 'pms'; + case Tuamotuan = 'pmt'; + case Plains_Miwok = 'pmw'; + case Poumei_Naga = 'pmx'; + case Papuan_Malay = 'pmy'; + case Southern_Pame = 'pmz'; + case Punan_Bah_Biau = 'pna'; + case Western_Panjabi = 'pnb'; + case Pannei = 'pnc'; + case Mpinda = 'pnd'; + case Western_Penan = 'pne'; + case Pangu = 'png'; + case Penrhyn = 'pnh'; + case Aoheng = 'pni'; + case Pinjarup = 'pnj'; + case Paunaka = 'pnk'; + case Paleni = 'pnl'; + case Punan_Batu_1 = 'pnm'; + case Pinai_Hagahai = 'pnn'; + case Panobo = 'pno'; + case Pancana = 'pnp'; + case Pana_Burkina_Faso = 'pnq'; + case Panim = 'pnr'; + case Ponosakan = 'pns'; + case Pontic = 'pnt'; + case Jiongnai_Bunu = 'pnu'; + case Pinigura = 'pnv'; + case Banyjima = 'pnw'; + case Phong_Kniang = 'pnx'; + case Pinyin = 'pny'; + case Pana_Central_African_Republic = 'pnz'; + case Poqomam = 'poc'; + case San_Juan_Atzingo_Popoloca = 'poe'; + case Poke = 'pof'; + case Potiguara = 'pog'; + case Poqomchi = 'poh'; + case Highland_Popoluca = 'poi'; + case Pokanga = 'pok'; + case Polish = 'pol'; + case Southeastern_Pomo = 'pom'; + case Pohnpeian = 'pon'; + case Central_Pomo = 'poo'; + case Pwapwa = 'pop'; + case Texistepec_Popoluca = 'poq'; + case Portuguese = 'por'; + case Sayula_Popoluca = 'pos'; + case Potawatomi = 'pot'; + case Upper_Guinea_Crioulo = 'pov'; + case San_Felipe_Otlaltepec_Popoloca = 'pow'; + case Polabian = 'pox'; + case Pogolo = 'poy'; + case Papi = 'ppe'; + case Paipai = 'ppi'; + case Uma = 'ppk'; + case Pipil = 'ppl'; + case Papuma = 'ppm'; + case Papapana = 'ppn'; + case Folopa = 'ppo'; + case Pelende = 'ppp'; + case Pei = 'ppq'; + case San_Luis_Temalacayuca_Popoloca = 'pps'; + case Pare = 'ppt'; + case Papora = 'ppu'; + case Pa_a = 'pqa'; + case Malecite_Passamaquoddy = 'pqm'; + case Parachi = 'prc'; + case Parsi_Dari = 'prd'; + case Principense = 'pre'; + case Paranan = 'prf'; + case Prussian = 'prg'; + case Porohanon = 'prh'; + case Paici = 'pri'; + case Parauk = 'prk'; + case Peruvian_Sign_Language = 'prl'; + case Kibiri = 'prm'; + case Prasuni = 'prn'; + case Old_Provencal_to_1500 = 'pro'; + case Asheninka_Perene = 'prq'; + case Puri = 'prr'; + case Dari = 'prs'; + case Phai = 'prt'; + case Puragi = 'pru'; + case Parawen = 'prw'; + case Purik = 'prx'; + case Providencia_Sign_Language = 'prz'; + case Asue_Awyu = 'psa'; + case Iranian_Sign_Language = 'psc'; + case Plains_Indian_Sign_Language = 'psd'; + case Central_Malay = 'pse'; + case Penang_Sign_Language = 'psg'; + case Southwest_Pashai = 'psh'; + case Southeast_Pashai = 'psi'; + case Puerto_Rican_Sign_Language = 'psl'; + case Pauserna = 'psm'; + case Panasuan = 'psn'; + case Polish_Sign_Language = 'pso'; + case Philippine_Sign_Language = 'psp'; + case Pasi = 'psq'; + case Portuguese_Sign_Language = 'psr'; + case Kaulong = 'pss'; + case Central_Pashto = 'pst'; + case Sauraseni_Prakrit = 'psu'; + case Port_Sandwich = 'psw'; + case Piscataway = 'psy'; + case Pai_Tavytera = 'pta'; + case Pataxo_Ha_Ha_Hae = 'pth'; + case Pindiini = 'pti'; + case Patani = 'ptn'; + case Zo_e = 'pto'; + case Patep = 'ptp'; + case Pattapu = 'ptq'; + case Piamatsina = 'ptr'; + case Enrekang = 'ptt'; + case Bambam = 'ptu'; + case Port_Vato = 'ptv'; + case Pentlatch = 'ptw'; + case Pathiya = 'pty'; + case Western_Highland_Purepecha = 'pua'; + case Purum = 'pub'; + case Punan_Merap = 'puc'; + case Punan_Aput = 'pud'; + case Puelche = 'pue'; + case Punan_Merah = 'puf'; + case Phuie = 'pug'; + case Puinave = 'pui'; + case Punan_Tubu = 'puj'; + case Puma = 'pum'; + case Puoc = 'puo'; + case Pulabu = 'pup'; + case Puquina = 'puq'; + case Purubora = 'pur'; + case Pushto = 'pus'; + case Putoh = 'put'; + case Punu = 'puu'; + case Puluwatese = 'puw'; + case Puare = 'pux'; + case Purisimeno = 'puy'; + case Pawaia = 'pwa'; + case Panawa = 'pwb'; + case Gapapaiwa = 'pwg'; + case Patwin = 'pwi'; + case Molbog = 'pwm'; + case Paiwan = 'pwn'; + case Pwo_Western_Karen = 'pwo'; + case Powari = 'pwr'; + case Pwo_Northern_Karen = 'pww'; + case Quetzaltepec_Mixe = 'pxm'; + case Pye_Krumen = 'pye'; + case Fyam = 'pym'; + case Poyanawa = 'pyn'; + case Paraguayan_Sign_Language = 'pys'; + case Puyuma = 'pyu'; + case Pyu_Myanmar = 'pyx'; + case Pyen = 'pyy'; + case Pesse = 'pze'; + case Pazeh = 'pzh'; + case Jejara_Naga = 'pzn'; + case Quapaw = 'qua'; + case Huallaga_Huanuco_Quechua = 'qub'; + case K_iche = 'quc'; + case Calderon_Highland_Quichua = 'qud'; + case Quechua = 'que'; + case Lambayeque_Quechua = 'quf'; + case Chimborazo_Highland_Quichua = 'qug'; + case South_Bolivian_Quechua = 'quh'; + case Quileute = 'qui'; + case Chachapoyas_Quechua = 'quk'; + case North_Bolivian_Quechua = 'qul'; + case Sipacapense = 'qum'; + case Quinault = 'qun'; + case Southern_Pastaza_Quechua = 'qup'; + case Quinqui = 'quq'; + case Yanahuanca_Pasco_Quechua = 'qur'; + case Santiago_del_Estero_Quichua = 'qus'; + case Sacapulteco = 'quv'; + case Tena_Lowland_Quichua = 'quw'; + case Yauyos_Quechua = 'qux'; + case Ayacucho_Quechua = 'quy'; + case Cusco_Quechua = 'quz'; + case Ambo_Pasco_Quechua = 'qva'; + case Cajamarca_Quechua = 'qvc'; + case Eastern_Apurimac_Quechua = 'qve'; + case Huamalies_Dos_de_Mayo_Huanuco_Quechua = 'qvh'; + case Imbabura_Highland_Quichua = 'qvi'; + case Loja_Highland_Quichua = 'qvj'; + case Cajatambo_North_Lima_Quechua = 'qvl'; + case Margos_Yarowilca_Lauricocha_Quechua = 'qvm'; + case North_Junin_Quechua = 'qvn'; + case Napo_Lowland_Quechua = 'qvo'; + case Pacaraos_Quechua = 'qvp'; + case San_Martin_Quechua = 'qvs'; + case Huaylla_Wanca_Quechua = 'qvw'; + case Queyu = 'qvy'; + case Northern_Pastaza_Quichua = 'qvz'; + case Corongo_Ancash_Quechua = 'qwa'; + case Classical_Quechua = 'qwc'; + case Huaylas_Ancash_Quechua = 'qwh'; + case Kuman_Russia = 'qwm'; + case Sihuas_Ancash_Quechua = 'qws'; + case Kwalhioqua_Tlatskanai = 'qwt'; + case Chiquian_Ancash_Quechua = 'qxa'; + case Chincha_Quechua = 'qxc'; + case Panao_Huanuco_Quechua = 'qxh'; + case Salasaca_Highland_Quichua = 'qxl'; + case Northern_Conchucos_Ancash_Quechua = 'qxn'; + case Southern_Conchucos_Ancash_Quechua = 'qxo'; + case Puno_Quechua = 'qxp'; + case Qashqa_i = 'qxq'; + case Canar_Highland_Quichua = 'qxr'; + case Southern_Qiang = 'qxs'; + case Santa_Ana_de_Tusi_Pasco_Quechua = 'qxt'; + case Arequipa_La_Union_Quechua = 'qxu'; + case Jauja_Wanca_Quechua = 'qxw'; + case Quenya = 'qya'; + case Quiripi = 'qyp'; + case Dungmali = 'raa'; + case Camling = 'rab'; + case Rasawa = 'rac'; + case Rade = 'rad'; + case Western_Meohang = 'raf'; + case Logooli = 'rag'; + case Rabha = 'rah'; + case Ramoaaina = 'rai'; + case Rajasthani = 'raj'; + case Tulu_Bohuai = 'rak'; + case Ralte = 'ral'; + case Canela = 'ram'; + case Riantana = 'ran'; + case Rao = 'rao'; + case Rapanui = 'rap'; + case Saam = 'raq'; + case Rarotongan = 'rar'; + case Tegali = 'ras'; + case Razajerdi = 'rat'; + case Raute = 'rau'; + case Sampang = 'rav'; + case Rawang = 'raw'; + case Rang = 'rax'; + case Rapa = 'ray'; + case Rahambuu = 'raz'; + case Rumai_Palaung = 'rbb'; + case Northern_Bontok = 'rbk'; + case Miraya_Bikol = 'rbl'; + case Barababaraba = 'rbp'; + case Reunion_Creole_French = 'rcf'; + case Rudbari = 'rdb'; + case Rerau = 'rea'; + case Rembong = 'reb'; + case Rejang_Kayan = 'ree'; + case Kara_Tanzania = 'reg'; + case Reli = 'rei'; + case Rejang = 'rej'; + case Rendille = 'rel'; + case Remo = 'rem'; + case Rengao = 'ren'; + case Rer_Bare = 'rer'; + case Reshe = 'res'; + case Retta = 'ret'; + case Reyesano = 'rey'; + case Roria = 'rga'; + case Romano_Greek = 'rge'; + case Rangkas = 'rgk'; + case Romagnol = 'rgn'; + case Resigaro = 'rgr'; + case Southern_Roglai = 'rgs'; + case Ringgou = 'rgu'; + case Rohingya = 'rhg'; + case Yahang = 'rhp'; + case Riang_India = 'ria'; + case Bribri_Sign_Language = 'rib'; + case Tarifit = 'rif'; + case Riang_Lang = 'ril'; + case Nyaturu = 'rim'; + case Nungu = 'rin'; + case Ribun = 'rir'; + case Ritharrngu = 'rit'; + case Riung = 'riu'; + case Rajong = 'rjg'; + case Raji = 'rji'; + case Rajbanshi = 'rjs'; + case Kraol = 'rka'; + case Rikbaktsa = 'rkb'; + case Rakahanga_Manihiki = 'rkh'; + case Rakhine = 'rki'; + case Marka = 'rkm'; + case Rangpuri = 'rkt'; + case Arakwal = 'rkw'; + case Rama = 'rma'; + case Rembarrnga = 'rmb'; + case Carpathian_Romani = 'rmc'; + case Traveller_Danish = 'rmd'; + case Angloromani = 'rme'; + case Kalo_Finnish_Romani = 'rmf'; + case Traveller_Norwegian = 'rmg'; + case Murkim = 'rmh'; + case Lomavren = 'rmi'; + case Romkun = 'rmk'; + case Baltic_Romani = 'rml'; + case Roma = 'rmm'; + case Balkan_Romani = 'rmn'; + case Sinte_Romani = 'rmo'; + case Rempi = 'rmp'; + case Calo = 'rmq'; + case Romanian_Sign_Language = 'rms'; + case Domari = 'rmt'; + case Tavringer_Romani = 'rmu'; + case Romanova = 'rmv'; + case Welsh_Romani = 'rmw'; + case Romam = 'rmx'; + case Vlax_Romani = 'rmy'; + case Marma = 'rmz'; + case Brunca_Sign_Language = 'rnb'; + case Ruund = 'rnd'; + case Ronga = 'rng'; + case Ranglong = 'rnl'; + case Roon = 'rnn'; + case Rongpo = 'rnp'; + case Nari_Nari = 'rnr'; + case Rungwa = 'rnw'; + case Tae = 'rob'; + case Cacgia_Roglai = 'roc'; + case Rogo = 'rod'; + case Ronji = 'roe'; + case Rombo = 'rof'; + case Northern_Roglai = 'rog'; + case Romansh = 'roh'; + case Romblomanon = 'rol'; + case Romany = 'rom'; + case Romanian = 'ron'; + case Rotokas = 'roo'; + case Kriol = 'rop'; + case Rongga = 'ror'; + case Runga = 'rou'; + case Dela_Oenale = 'row'; + case Repanbitip = 'rpn'; + case Rapting = 'rpt'; + case Ririo = 'rri'; + case Moriori = 'rrm'; + case Waima = 'rro'; + case Arritinngithigh = 'rrt'; + case Romano_Serbian = 'rsb'; + case Ruthenian = 'rsk'; + case Russian_Sign_Language = 'rsl'; + case Miriwoong_Sign_Language = 'rsm'; + case Rwandan_Sign_Language = 'rsn'; + case Rishiwa = 'rsw'; + case Rungtu_Chin = 'rtc'; + case Ratahan = 'rth'; + case Rotuman = 'rtm'; + case Yurats = 'rts'; + case Rathawi = 'rtw'; + case Gungu = 'rub'; + case Ruuli = 'ruc'; + case Rusyn = 'rue'; + case Luguru = 'ruf'; + case Roviana = 'rug'; + case Ruga = 'ruh'; + case Rufiji = 'rui'; + case Che = 'ruk'; + case Rundi = 'run'; + case Istro_Romanian = 'ruo'; + case Macedo_Romanian = 'rup'; + case Megleno_Romanian = 'ruq'; + case Russian = 'rus'; + case Rutul = 'rut'; + case Lanas_Lobu = 'ruu'; + case Mala_Nigeria = 'ruy'; + case Ruma = 'ruz'; + case Rawo = 'rwa'; + case Rwa = 'rwk'; + case Ruwila = 'rwl'; + case Amba_Uganda = 'rwm'; + case Rawa = 'rwo'; + case Marwari_India = 'rwr'; + case Ngardi = 'rxd'; + case Karuwali = 'rxw'; + case Northern_Amami_Oshima = 'ryn'; + case Yaeyama = 'rys'; + case Central_Okinawan = 'ryu'; + case Razihi = 'rzh'; + case Saba = 'saa'; + case Buglere = 'sab'; + case Meskwaki = 'sac'; + case Sandawe = 'sad'; + case Sabane = 'sae'; + case Safaliba = 'saf'; + case Sango = 'sag'; + case Yakut = 'sah'; + case Sahu = 'saj'; + case Sake = 'sak'; + case Samaritan_Aramaic = 'sam'; + case Sanskrit = 'san'; + case Sause = 'sao'; + case Samburu = 'saq'; + case Saraveca = 'sar'; + case Sasak = 'sas'; + case Santali = 'sat'; + case Saleman = 'sau'; + case Saafi_Saafi = 'sav'; + case Sawi = 'saw'; + case Sa = 'sax'; + case Saya = 'say'; + case Saurashtra = 'saz'; + case Ngambay = 'sba'; + case Simbo = 'sbb'; + case Kele_Papua_New_Guinea = 'sbc'; + case Southern_Samo = 'sbd'; + case Saliba = 'sbe'; + case Chabu = 'sbf'; + case Seget = 'sbg'; + case Sori_Harengan = 'sbh'; + case Seti = 'sbi'; + case Surbakhal = 'sbj'; + case Safwa = 'sbk'; + case Botolan_Sambal = 'sbl'; + case Sagala = 'sbm'; + case Sindhi_Bhil = 'sbn'; + case Sabum = 'sbo'; + case Sangu_Tanzania = 'sbp'; + case Sileibi = 'sbq'; + case Sembakung_Murut = 'sbr'; + case Subiya = 'sbs'; + case Kimki = 'sbt'; + case Stod_Bhoti = 'sbu'; + case Sabine = 'sbv'; + case Simba = 'sbw'; + case Seberuang = 'sbx'; + case Soli = 'sby'; + case Sara_Kaba = 'sbz'; + case Chut = 'scb'; + case Dongxiang = 'sce'; + case San_Miguel_Creole_French = 'scf'; + case Sanggau = 'scg'; + case Sakachep = 'sch'; + case Sri_Lankan_Creole_Malay = 'sci'; + case Sadri = 'sck'; + case Shina = 'scl'; + case Sicilian = 'scn'; + case Scots = 'sco'; + case Hyolmo = 'scp'; + case Sa_och = 'scq'; + case North_Slavey = 'scs'; + case Southern_Katang = 'sct'; + case Shumcho = 'scu'; + case Sheni = 'scv'; + case Sha = 'scw'; + case Sicel = 'scx'; + case Toraja_Sa_dan = 'sda'; + case Shabak = 'sdb'; + case Sassarese_Sardinian = 'sdc'; + case Surubu = 'sde'; + case Sarli = 'sdf'; + case Savi = 'sdg'; + case Southern_Kurdish = 'sdh'; + case Suundi = 'sdj'; + case Sos_Kundi = 'sdk'; + case Saudi_Arabian_Sign_Language = 'sdl'; + case Gallurese_Sardinian = 'sdn'; + case Bukar_Sadung_Bidayuh = 'sdo'; + case Sherdukpen = 'sdp'; + case Semandang = 'sdq'; + case Oraon_Sadri = 'sdr'; + case Sened = 'sds'; + case Shuadit = 'sdt'; + case Sarudu = 'sdu'; + case Sibu_Melanau = 'sdx'; + case Sallands = 'sdz'; + case Semai = 'sea'; + case Shempire_Senoufo = 'seb'; + case Sechelt = 'sec'; + case Sedang = 'sed'; + case Seneca = 'see'; + case Cebaara_Senoufo = 'sef'; + case Segeju = 'seg'; + case Sena = 'seh'; + case Seri = 'sei'; + case Sene = 'sej'; + case Sekani = 'sek'; + case Selkup = 'sel'; + case Nanerige_Senoufo = 'sen'; + case Suarmin = 'seo'; + case Sicite_Senoufo = 'sep'; + case Senara_Senoufo = 'seq'; + case Serrano = 'ser'; + case Koyraboro_Senni_Songhai = 'ses'; + case Sentani = 'set'; + case Serui_Laut = 'seu'; + case Nyarafolo_Senoufo = 'sev'; + case Sewa_Bay = 'sew'; + case Secoya = 'sey'; + case Senthang_Chin = 'sez'; + case Langue_des_signes_de_Belgique_Francophone = 'sfb'; + case Eastern_Subanen = 'sfe'; + case Small_Flowery_Miao = 'sfm'; + case South_African_Sign_Language = 'sfs'; + case Sehwi = 'sfw'; + case Old_Irish_to_900 = 'sga'; + case Mag_antsi_Ayta = 'sgb'; + case Kipsigis = 'sgc'; + case Surigaonon = 'sgd'; + case Segai = 'sge'; + case Swiss_German_Sign_Language = 'sgg'; + case Shughni = 'sgh'; + case Suga = 'sgi'; + case Surgujia = 'sgj'; + case Sangkong = 'sgk'; + case Singa = 'sgm'; + case Singpho = 'sgp'; + case Sangisari = 'sgr'; + case Samogitian = 'sgs'; + case Brokpake = 'sgt'; + case Salas = 'sgu'; + case Sebat_Bet_Gurage = 'sgw'; + case Sierra_Leone_Sign_Language = 'sgx'; + case Sanglechi = 'sgy'; + case Sursurunga = 'sgz'; + case Shall_Zwall = 'sha'; + case Ninam = 'shb'; + case Sonde = 'shc'; + case Kundal_Shahi = 'shd'; + case Sheko = 'she'; + case Shua = 'shg'; + case Shoshoni = 'shh'; + case Tachelhit = 'shi'; + case Shatt = 'shj'; + case Shilluk = 'shk'; + case Shendu = 'shl'; + case Shahrudi = 'shm'; + case Shan = 'shn'; + case Shanga = 'sho'; + case Shipibo_Conibo = 'shp'; + case Sala = 'shq'; + case Shi = 'shr'; + case Shuswap = 'shs'; + case Shasta = 'sht'; + case Chadian_Arabic = 'shu'; + case Shehri = 'shv'; + case Shwai = 'shw'; + case She = 'shx'; + case Tachawit = 'shy'; + case Syenara_Senoufo = 'shz'; + case Akkala_Sami = 'sia'; + case Sebop = 'sib'; + case Sidamo = 'sid'; + case Simaa = 'sie'; + case Siamou = 'sif'; + case Paasaal = 'sig'; + case Zire = 'sih'; + case Shom_Peng = 'sii'; + case Numbami = 'sij'; + case Sikiana = 'sik'; + case Tumulung_Sisaala = 'sil'; + case Mende_Papua_New_Guinea = 'sim'; + case Sinhala = 'sin'; + case Sikkimese = 'sip'; + case Sonia = 'siq'; + case Siri = 'sir'; + case Siuslaw = 'sis'; + case Sinagen = 'siu'; + case Sumariup = 'siv'; + case Siwai = 'siw'; + case Sumau = 'six'; + case Sivandi = 'siy'; + case Siwi = 'siz'; + case Epena = 'sja'; + case Sajau_Basap = 'sjb'; + case Kildin_Sami = 'sjd'; + case Pite_Sami = 'sje'; + case Assangori = 'sjg'; + case Kemi_Sami = 'sjk'; + case Sajalong = 'sjl'; + case Mapun = 'sjm'; + case Sindarin = 'sjn'; + case Xibe = 'sjo'; + case Surjapuri = 'sjp'; + case Siar_Lak = 'sjr'; + case Senhaja_De_Srair = 'sjs'; + case Ter_Sami = 'sjt'; + case Ume_Sami = 'sju'; + case Shawnee = 'sjw'; + case Skagit = 'ska'; + case Saek = 'skb'; + case Ma_Manda = 'skc'; + case Southern_Sierra_Miwok = 'skd'; + case Seke_Vanuatu = 'ske'; + case Sakirabia = 'skf'; + case Sakalava_Malagasy = 'skg'; + case Sikule = 'skh'; + case Sika = 'ski'; + case Seke_Nepal = 'skj'; + case Kutong = 'skm'; + case Kolibugan_Subanon = 'skn'; + case Seko_Tengah = 'sko'; + case Sekapan = 'skp'; + case Sininkere = 'skq'; + case Saraiki = 'skr'; + case Maia = 'sks'; + case Sakata = 'skt'; + case Sakao = 'sku'; + case Skou = 'skv'; + case Skepi_Creole_Dutch = 'skw'; + case Seko_Padang = 'skx'; + case Sikaiana = 'sky'; + case Sekar = 'skz'; + case Saliba_2 = 'slc'; + case Sissala = 'sld'; + case Sholaga = 'sle'; + case Swiss_Italian_Sign_Language = 'slf'; + case Selungai_Murut = 'slg'; + case Southern_Puget_Sound_Salish = 'slh'; + case Lower_Silesian = 'sli'; + case Saluma = 'slj'; + case Slovak = 'slk'; + case Salt_Yui = 'sll'; + case Pangutaran_Sama = 'slm'; + case Salinan = 'sln'; + case Lamaholot = 'slp'; + case Salar = 'slr'; + case Singapore_Sign_Language = 'sls'; + case Sila = 'slt'; + case Selaru = 'slu'; + case Slovenian = 'slv'; + case Sialum = 'slw'; + case Salampasu = 'slx'; + case Selayar = 'sly'; + case Ma_ya = 'slz'; + case Southern_Sami = 'sma'; + case Simbari = 'smb'; + case Som = 'smc'; + case Northern_Sami = 'sme'; + case Auwe = 'smf'; + case Simbali = 'smg'; + case Samei = 'smh'; + case Lule_Sami = 'smj'; + case Bolinao = 'smk'; + case Central_Sama = 'sml'; + case Musasa = 'smm'; + case Inari_Sami = 'smn'; + case Samoan = 'smo'; + case Samaritan = 'smp'; + case Samo = 'smq'; + case Simeulue = 'smr'; + case Skolt_Sami = 'sms'; + case Simte = 'smt'; + case Somray = 'smu'; + case Samvedi = 'smv'; + case Sumbawa = 'smw'; + case Samba = 'smx'; + case Semnani = 'smy'; + case Simeku = 'smz'; + case Shona = 'sna'; + case Sinaugoro = 'snc'; + case Sindhi = 'snd'; + case Bau_Bidayuh = 'sne'; + case Noon = 'snf'; + case Sanga_Democratic_Republic_of_Congo = 'sng'; + case Sensi = 'sni'; + case Riverain_Sango = 'snj'; + case Soninke = 'snk'; + case Sangil = 'snl'; + case Southern_Ma_di = 'snm'; + case Siona = 'snn'; + case Snohomish = 'sno'; + case Siane = 'snp'; + case Sangu_Gabon = 'snq'; + case Sihan = 'snr'; + case South_West_Bay = 'sns'; + case Senggi = 'snu'; + case Sa_ban = 'snv'; + case Selee = 'snw'; + case Sam = 'snx'; + case Saniyo_Hiyewe = 'sny'; + case Kou = 'snz'; + case Thai_Song = 'soa'; + case Sobei = 'sob'; + case So_Democratic_Republic_of_Congo = 'soc'; + case Songoora = 'sod'; + case Songomeno = 'soe'; + case Sogdian = 'sog'; + case Aka = 'soh'; + case Sonha = 'soi'; + case Soi = 'soj'; + case Sokoro = 'sok'; + case Solos = 'sol'; + case Somali = 'som'; + case Songo = 'soo'; + case Songe = 'sop'; + case Kanasi = 'soq'; + case Somrai = 'sor'; + case Seeku = 'sos'; + case Southern_Sotho = 'sot'; + case Southern_Thai = 'sou'; + case Sonsorol = 'sov'; + case Sowanda = 'sow'; + case Swo = 'sox'; + case Miyobe = 'soy'; + case Temi = 'soz'; + case Spanish = 'spa'; + case Sepa_Indonesia = 'spb'; + case Sape = 'spc'; + case Saep = 'spd'; + case Sepa_Papua_New_Guinea = 'spe'; + case Sian = 'spg'; + case Saponi = 'spi'; + case Sengo = 'spk'; + case Selepet = 'spl'; + case Akukem = 'spm'; + case Sanapana = 'spn'; + case Spokane = 'spo'; + case Supyire_Senoufo = 'spp'; + case Loreto_Ucayali_Spanish = 'spq'; + case Saparua = 'spr'; + case Saposa = 'sps'; + case Spiti_Bhoti = 'spt'; + case Sapuan = 'spu'; + case Sambalpuri = 'spv'; + case South_Picene = 'spx'; + case Sabaot = 'spy'; + case Shama_Sambuga = 'sqa'; + case Shau = 'sqh'; + case Albanian = 'sqi'; + case Albanian_Sign_Language = 'sqk'; + case Suma = 'sqm'; + case Susquehannock = 'sqn'; + case Sorkhei = 'sqo'; + case Sou = 'sqq'; + case Siculo_Arabic = 'sqr'; + case Sri_Lankan_Sign_Language = 'sqs'; + case Soqotri = 'sqt'; + case Squamish = 'squ'; + case Kufr_Qassem_Sign_Language_KQSL = 'sqx'; + case Saruga = 'sra'; + case Sora = 'srb'; + case Logudorese_Sardinian = 'src'; + case Sardinian = 'srd'; + case Sara = 'sre'; + case Nafi = 'srf'; + case Sulod = 'srg'; + case Sarikoli = 'srh'; + case Siriano = 'sri'; + case Serudung_Murut = 'srk'; + case Isirawa = 'srl'; + case Saramaccan = 'srm'; + case Sranan_Tongo = 'srn'; + case Campidanese_Sardinian = 'sro'; + case Serbian = 'srp'; + case Siriono = 'srq'; + case Serer = 'srr'; + case Sarsi = 'srs'; + case Sauri = 'srt'; + case Surui = 'sru'; + case Southern_Sorsoganon = 'srv'; + case Serua = 'srw'; + case Sirmauri = 'srx'; + case Sera = 'sry'; + case Shahmirzadi = 'srz'; + case Southern_Sama = 'ssb'; + case Suba_Simbiti = 'ssc'; + case Siroi = 'ssd'; + case Balangingi = 'sse'; + case Thao = 'ssf'; + case Seimat = 'ssg'; + case Shihhi_Arabic = 'ssh'; + case Sansi = 'ssi'; + case Sausi = 'ssj'; + case Sunam = 'ssk'; + case Western_Sisaala = 'ssl'; + case Semnam = 'ssm'; + case Waata = 'ssn'; + case Sissano = 'sso'; + case Spanish_Sign_Language = 'ssp'; + case So_a = 'ssq'; + case Swiss_French_Sign_Language = 'ssr'; + case So = 'sss'; + case Sinasina = 'sst'; + case Susuami = 'ssu'; + case Shark_Bay = 'ssv'; + case Swati = 'ssw'; + case Samberigi = 'ssx'; + case Saho = 'ssy'; + case Sengseng = 'ssz'; + case Settla = 'sta'; + case Northern_Subanen = 'stb'; + case Sentinel = 'std'; + case Liana_Seti = 'ste'; + case Seta = 'stf'; + case Trieng = 'stg'; + case Shelta = 'sth'; + case Bulo_Stieng = 'sti'; + case Matya_Samo = 'stj'; + case Arammba = 'stk'; + case Stellingwerfs = 'stl'; + case Setaman = 'stm'; + case Owa = 'stn'; + case Stoney = 'sto'; + case Southeastern_Tepehuan = 'stp'; + case Saterfriesisch = 'stq'; + case Straits_Salish = 'str'; + case Shumashti = 'sts'; + case Budeh_Stieng = 'stt'; + case Samtao = 'stu'; + case Silt_e = 'stv'; + case Satawalese = 'stw'; + case Siberian_Tatar = 'sty'; + case Sulka = 'sua'; + case Suku = 'sub'; + case Western_Subanon = 'suc'; + case Suena = 'sue'; + case Suganga = 'sug'; + case Suki = 'sui'; + case Shubi = 'suj'; + case Sukuma = 'suk'; + case Sundanese = 'sun'; + case Bouni = 'suo'; + case Tirmaga_Chai_Suri = 'suq'; + case Mwaghavul = 'sur'; + case Susu = 'sus'; + case Subtiaba = 'sut'; + case Puroik = 'suv'; + case Sumbwa = 'suw'; + case Sumerian = 'sux'; + case Suya = 'suy'; + case Sunwar = 'suz'; + case Svan = 'sva'; + case Ulau_Suain = 'svb'; + case Vincentian_Creole_English = 'svc'; + case Serili = 'sve'; + case Slovakian_Sign_Language = 'svk'; + case Slavomolisano = 'svm'; + case Savosavo = 'svs'; + case Skalvian = 'svx'; + case Swahili_macrolanguage = 'swa'; + case Maore_Comorian = 'swb'; + case Congo_Swahili = 'swc'; + case Swedish = 'swe'; + case Sere = 'swf'; + case Swabian = 'swg'; + case Swahili_individual_language = 'swh'; + case Sui = 'swi'; + case Sira = 'swj'; + case Malawi_Sena = 'swk'; + case Swedish_Sign_Language = 'swl'; + case Samosa = 'swm'; + case Sawknah = 'swn'; + case Shanenawa = 'swo'; + case Suau = 'swp'; + case Sharwa = 'swq'; + case Saweru = 'swr'; + case Seluwasan = 'sws'; + case Sawila = 'swt'; + case Suwawa = 'swu'; + case Shekhawati = 'swv'; + case Sowa = 'sww'; + case Suruaha = 'swx'; + case Sarua = 'swy'; + case Suba = 'sxb'; + case Sicanian = 'sxc'; + case Sighu = 'sxe'; + case Shuhi = 'sxg'; + case Southern_Kalapuya = 'sxk'; + case Selian = 'sxl'; + case Samre = 'sxm'; + case Sangir = 'sxn'; + case Sorothaptic = 'sxo'; + case Saaroa = 'sxr'; + case Sasaru = 'sxs'; + case Upper_Saxon = 'sxu'; + case Saxwe_Gbe = 'sxw'; + case Siang = 'sya'; + case Central_Subanen = 'syb'; + case Classical_Syriac = 'syc'; + case Seki = 'syi'; + case Sukur = 'syk'; + case Sylheti = 'syl'; + case Maya_Samo = 'sym'; + case Senaya = 'syn'; + case Suoy = 'syo'; + case Syriac = 'syr'; + case Sinyar = 'sys'; + case Kagate = 'syw'; + case Samay = 'syx'; + case Al_Sayyid_Bedouin_Sign_Language = 'syy'; + case Semelai = 'sza'; + case Ngalum = 'szb'; + case Semaq_Beri = 'szc'; + case Seze = 'sze'; + case Sengele = 'szg'; + case Silesian = 'szl'; + case Sula = 'szn'; + case Suabo = 'szp'; + case Solomon_Islands_Sign_Language = 'szs'; + case Isu_Fako_Division = 'szv'; + case Sawai = 'szw'; + case Sakizaya = 'szy'; + case Lower_Tanana = 'taa'; + case Tabassaran = 'tab'; + case Lowland_Tarahumara = 'tac'; + case Tause = 'tad'; + case Tariana = 'tae'; + case Tapirape = 'taf'; + case Tagoi = 'tag'; + case Tahitian = 'tah'; + case Eastern_Tamang = 'taj'; + case Tala = 'tak'; + case Tal = 'tal'; + case Tamil = 'tam'; + case Tangale = 'tan'; + case Yami = 'tao'; + case Taabwa = 'tap'; + case Tamasheq = 'taq'; + case Central_Tarahumara = 'tar'; + case Tay_Boi = 'tas'; + case Tatar = 'tat'; + case Upper_Tanana = 'tau'; + case Tatuyo = 'tav'; + case Tai = 'taw'; + case Tamki = 'tax'; + case Atayal = 'tay'; + case Tocho = 'taz'; + case Aikana = 'tba'; + case Takia = 'tbc'; + case Kaki_Ae = 'tbd'; + case Tanimbili = 'tbe'; + case Mandara = 'tbf'; + case North_Tairora = 'tbg'; + case Dharawal = 'tbh'; + case Gaam = 'tbi'; + case Tiang = 'tbj'; + case Calamian_Tagbanwa = 'tbk'; + case Tboli = 'tbl'; + case Tagbu = 'tbm'; + case Barro_Negro_Tunebo = 'tbn'; + case Tawala = 'tbo'; + case Taworta = 'tbp'; + case Tumtum = 'tbr'; + case Tanguat = 'tbs'; + case Tembo_Kitembo = 'tbt'; + case Tubar = 'tbu'; + case Tobo = 'tbv'; + case Tagbanwa = 'tbw'; + case Kapin = 'tbx'; + case Tabaru = 'tby'; + case Ditammari = 'tbz'; + case Ticuna = 'tca'; + case Tanacross = 'tcb'; + case Datooga = 'tcc'; + case Tafi = 'tcd'; + case Southern_Tutchone = 'tce'; + case Malinaltepec_Me_phaa = 'tcf'; + case Tamagario = 'tcg'; + case Turks_And_Caicos_Creole_English = 'tch'; + case Wara = 'tci'; + case Tchitchege = 'tck'; + case Taman_Myanmar = 'tcl'; + case Tanahmerah = 'tcm'; + case Tichurong = 'tcn'; + case Taungyo = 'tco'; + case Tawr_Chin = 'tcp'; + case Kaiy = 'tcq'; + case Torres_Strait_Creole = 'tcs'; + case T_en = 'tct'; + case Southeastern_Tarahumara = 'tcu'; + case Tecpatlan_Totonac = 'tcw'; + case Toda = 'tcx'; + case Tulu = 'tcy'; + case Thado_Chin = 'tcz'; + case Tagdal = 'tda'; + case Panchpargania = 'tdb'; + case Embera_Tado = 'tdc'; + case Tai_Nua = 'tdd'; + case Tiranige_Diga_Dogon = 'tde'; + case Talieng = 'tdf'; + case Western_Tamang = 'tdg'; + case Thulung = 'tdh'; + case Tomadino = 'tdi'; + case Tajio = 'tdj'; + case Tambas = 'tdk'; + case Sur = 'tdl'; + case Taruma = 'tdm'; + case Tondano = 'tdn'; + case Teme = 'tdo'; + case Tita = 'tdq'; + case Todrah = 'tdr'; + case Doutai = 'tds'; + case Tetun_Dili = 'tdt'; + case Toro = 'tdv'; + case Tandroy_Mahafaly_Malagasy = 'tdx'; + case Tadyawan = 'tdy'; + case Temiar = 'tea'; + case Tetete = 'teb'; + case Terik = 'tec'; + case Tepo_Krumen = 'ted'; + case Huehuetla_Tepehua = 'tee'; + case Teressa = 'tef'; + case Teke_Tege = 'teg'; + case Tehuelche = 'teh'; + case Torricelli = 'tei'; + case Ibali_Teke = 'tek'; + case Telugu = 'tel'; + case Timne = 'tem'; + case Tama_Colombia = 'ten'; + case Teso = 'teo'; + case Tepecano = 'tep'; + case Temein = 'teq'; + case Tereno = 'ter'; + case Tengger = 'tes'; + case Tetum = 'tet'; + case Soo = 'teu'; + case Teor = 'tev'; + case Tewa_USA = 'tew'; + case Tennet = 'tex'; + case Tulishi = 'tey'; + case Tetserret = 'tez'; + case Tofin_Gbe = 'tfi'; + case Tanaina = 'tfn'; + case Tefaro = 'tfo'; + case Teribe = 'tfr'; + case Ternate = 'tft'; + case Sagalla = 'tga'; + case Tobilung = 'tgb'; + case Tigak = 'tgc'; + case Ciwogai = 'tgd'; + case Eastern_Gorkha_Tamang = 'tge'; + case Chalikha = 'tgf'; + case Tobagonian_Creole_English = 'tgh'; + case Lawunuia = 'tgi'; + case Tagin = 'tgj'; + case Tajik = 'tgk'; + case Tagalog = 'tgl'; + case Tandaganon = 'tgn'; + case Sudest = 'tgo'; + case Tangoa = 'tgp'; + case Tring = 'tgq'; + case Tareng = 'tgr'; + case Nume = 'tgs'; + case Central_Tagbanwa = 'tgt'; + case Tanggu = 'tgu'; + case Tingui_Boto = 'tgv'; + case Tagwana_Senoufo = 'tgw'; + case Tagish = 'tgx'; + case Togoyo = 'tgy'; + case Tagalaka = 'tgz'; + case Thai = 'tha'; + case Kuuk_Thaayorre = 'thd'; + case Chitwania_Tharu = 'the'; + case Thangmi = 'thf'; + case Northern_Tarahumara = 'thh'; + case Tai_Long = 'thi'; + case Tharaka = 'thk'; + case Dangaura_Tharu = 'thl'; + case Aheu = 'thm'; + case Thachanadan = 'thn'; + case Thompson = 'thp'; + case Kochila_Tharu = 'thq'; + case Rana_Tharu = 'thr'; + case Thakali = 'ths'; + case Tahltan = 'tht'; + case Thuri = 'thu'; + case Tahaggart_Tamahaq = 'thv'; + case Tha = 'thy'; + case Tayart_Tamajeq = 'thz'; + case Tidikelt_Tamazight = 'tia'; + case Tira = 'tic'; + case Tifal = 'tif'; + case Tigre = 'tig'; + case Timugon_Murut = 'tih'; + case Tiene = 'tii'; + case Tilung = 'tij'; + case Tikar = 'tik'; + case Tillamook = 'til'; + case Timbe = 'tim'; + case Tindi = 'tin'; + case Teop = 'tio'; + case Trimuris = 'tip'; + case Tiefo = 'tiq'; + case Tigrinya = 'tir'; + case Masadiit_Itneg = 'tis'; + case Tinigua = 'tit'; + case Adasen = 'tiu'; + case Tiv = 'tiv'; + case Tiwi = 'tiw'; + case Southern_Tiwa = 'tix'; + case Tiruray = 'tiy'; + case Tai_Hongjin = 'tiz'; + case Tajuasohn = 'tja'; + case Tunjung = 'tjg'; + case Northern_Tujia = 'tji'; + case Tjungundji = 'tjj'; + case Tai_Laing = 'tjl'; + case Timucua = 'tjm'; + case Tonjon = 'tjn'; + case Temacine_Tamazight = 'tjo'; + case Tjupany = 'tjp'; + case Southern_Tujia = 'tjs'; + case Tjurruru = 'tju'; + case Djabwurrung = 'tjw'; + case Truka = 'tka'; + case Buksa = 'tkb'; + case Tukudede = 'tkd'; + case Takwane = 'tke'; + case Tukumanfed = 'tkf'; + case Tesaka_Malagasy = 'tkg'; + case Tokelau = 'tkl'; + case Takelma = 'tkm'; + case Toku_No_Shima = 'tkn'; + case Tikopia = 'tkp'; + case Tee = 'tkq'; + case Tsakhur = 'tkr'; + case Takestani = 'tks'; + case Kathoriya_Tharu = 'tkt'; + case Upper_Necaxa_Totonac = 'tku'; + case Mur_Pano = 'tkv'; + case Teanu = 'tkw'; + case Tangko = 'tkx'; + case Takua = 'tkz'; + case Southwestern_Tepehuan = 'tla'; + case Tobelo = 'tlb'; + case Yecuatla_Totonac = 'tlc'; + case Talaud = 'tld'; + case Telefol = 'tlf'; + case Tofanma = 'tlg'; + case Klingon = 'tlh'; + case Tlingit = 'tli'; + case Talinga_Bwisi = 'tlj'; + case Taloki = 'tlk'; + case Tetela = 'tll'; + case Tolomako = 'tlm'; + case Talondo = 'tln'; + case Talodi = 'tlo'; + case Filomena_Mata_Coahuitlan_Totonac = 'tlp'; + case Tai_Loi = 'tlq'; + case Talise = 'tlr'; + case Tambotalo = 'tls'; + case Sou_Nama = 'tlt'; + case Tulehu = 'tlu'; + case Taliabu = 'tlv'; + case Khehek = 'tlx'; + case Talysh = 'tly'; + case Tama_Chad = 'tma'; + case Katbol = 'tmb'; + case Tumak = 'tmc'; + case Haruai = 'tmd'; + case Tremembe = 'tme'; + case Toba_Maskoy = 'tmf'; + case Ternateno = 'tmg'; + case Tamashek = 'tmh'; + case Tutuba = 'tmi'; + case Samarokena = 'tmj'; + case Tamnim_Citak = 'tml'; + case Tai_Thanh = 'tmm'; + case Taman_Indonesia = 'tmn'; + case Temoq = 'tmo'; + case Tumleo = 'tmq'; + case Jewish_Babylonian_Aramaic_ca_200_1200_CE = 'tmr'; + case Tima = 'tms'; + case Tasmate = 'tmt'; + case Iau = 'tmu'; + case Tembo_Motembo = 'tmv'; + case Temuan = 'tmw'; + case Tami = 'tmy'; + case Tamanaku = 'tmz'; + case Tacana = 'tna'; + case Western_Tunebo = 'tnb'; + case Tanimuca_Retuara = 'tnc'; + case Angosturas_Tunebo = 'tnd'; + case Tobanga = 'tng'; + case Maiani = 'tnh'; + case Tandia = 'tni'; + case Kwamera = 'tnk'; + case Lenakel = 'tnl'; + case Tabla = 'tnm'; + case North_Tanna = 'tnn'; + case Toromono = 'tno'; + case Whitesands = 'tnp'; + case Taino = 'tnq'; + case Menik = 'tnr'; + case Tenis = 'tns'; + case Tontemboan = 'tnt'; + case Tay_Khang = 'tnu'; + case Tangchangya = 'tnv'; + case Tonsawang = 'tnw'; + case Tanema = 'tnx'; + case Tongwe = 'tny'; + case Ten_edn = 'tnz'; + case Toba = 'tob'; + case Coyutla_Totonac = 'toc'; + case Toma = 'tod'; + case Gizrra = 'tof'; + case Tonga_Nyasa = 'tog'; + case Gitonga = 'toh'; + case Tonga_Zambia = 'toi'; + case Tojolabal = 'toj'; + case Toki_Pona = 'tok'; + case Tolowa = 'tol'; + case Tombulu = 'tom'; + case Tonga_Tonga_Islands = 'ton'; + case Xicotepec_De_Juarez_Totonac = 'too'; + case Papantla_Totonac = 'top'; + case Toposa = 'toq'; + case Togbo_Vara_Banda = 'tor'; + case Highland_Totonac = 'tos'; + case Tho = 'tou'; + case Upper_Taromi = 'tov'; + case Jemez = 'tow'; + case Tobian = 'tox'; + case Topoiyo = 'toy'; + case To = 'toz'; + case Taupota = 'tpa'; + case Azoyu_Me_phaa = 'tpc'; + case Tippera = 'tpe'; + case Tarpia = 'tpf'; + case Kula = 'tpg'; + case Tok_Pisin = 'tpi'; + case Tapiete = 'tpj'; + case Tupinikin = 'tpk'; + case Tlacoapa_Me_phaa = 'tpl'; + case Tampulma = 'tpm'; + case Tupinamba = 'tpn'; + case Tai_Pao = 'tpo'; + case Pisaflores_Tepehua = 'tpp'; + case Tukpa = 'tpq'; + case Tupari = 'tpr'; + case Tlachichilco_Tepehua = 'tpt'; + case Tampuan = 'tpu'; + case Tanapag = 'tpv'; + case Acatepec_Me_phaa = 'tpx'; + case Trumai = 'tpy'; + case Tinputz = 'tpz'; + case Tembe = 'tqb'; + case Lehali = 'tql'; + case Turumsa = 'tqm'; + case Tenino = 'tqn'; + case Toaripi = 'tqo'; + case Tomoip = 'tqp'; + case Tunni = 'tqq'; + case Torona = 'tqr'; + case Western_Totonac = 'tqt'; + case Touo = 'tqu'; + case Tonkawa = 'tqw'; + case Tirahi = 'tra'; + case Terebu = 'trb'; + case Copala_Triqui = 'trc'; + case Turi = 'trd'; + case East_Tarangan = 'tre'; + case Trinidadian_Creole_English = 'trf'; + case Lishan_Didan = 'trg'; + case Turaka = 'trh'; + case Trio = 'tri'; + case Toram = 'trj'; + case Traveller_Scottish = 'trl'; + case Tregami = 'trm'; + case Trinitario = 'trn'; + case Tarao_Naga = 'tro'; + case Kok_Borok = 'trp'; + case San_Martin_Itunyoso_Triqui = 'trq'; + case Taushiro = 'trr'; + case Chicahuaxtla_Triqui = 'trs'; + case Tunggare = 'trt'; + case Turoyo = 'tru'; + case Sediq = 'trv'; + case Torwali = 'trw'; + case Tringgus_Sembaan_Bidayuh = 'trx'; + case Turung = 'try'; + case Tora = 'trz'; + case Tsaangi = 'tsa'; + case Tsamai = 'tsb'; + case Tswa = 'tsc'; + case Tsakonian = 'tsd'; + case Tunisian_Sign_Language = 'tse'; + case Tausug = 'tsg'; + case Tsuvan = 'tsh'; + case Tsimshian = 'tsi'; + case Tshangla = 'tsj'; + case Tseku = 'tsk'; + case Ts_un_Lao = 'tsl'; + case Turkish_Sign_Language = 'tsm'; + case Tswana = 'tsn'; + case Tsonga = 'tso'; + case Northern_Toussian = 'tsp'; + case Thai_Sign_Language = 'tsq'; + case Akei = 'tsr'; + case Taiwan_Sign_Language = 'tss'; + case Tondi_Songway_Kiini = 'tst'; + case Tsou = 'tsu'; + case Tsogo = 'tsv'; + case Tsishingini = 'tsw'; + case Mubami = 'tsx'; + case Tebul_Sign_Language = 'tsy'; + case Purepecha = 'tsz'; + case Tutelo = 'tta'; + case Gaa = 'ttb'; + case Tektiteko = 'ttc'; + case Tauade = 'ttd'; + case Bwanabwana = 'tte'; + case Tuotomb = 'ttf'; + case Tutong = 'ttg'; + case Upper_Ta_oih = 'tth'; + case Tobati = 'tti'; + case Tooro = 'ttj'; + case Totoro = 'ttk'; + case Totela = 'ttl'; + case Northern_Tutchone = 'ttm'; + case Towei = 'ttn'; + case Lower_Ta_oih = 'tto'; + case Tombelala = 'ttp'; + case Tawallammat_Tamajaq = 'ttq'; + case Tera = 'ttr'; + case Northeastern_Thai = 'tts'; + case Muslim_Tat = 'ttt'; + case Torau = 'ttu'; + case Titan = 'ttv'; + case Long_Wat = 'ttw'; + case Sikaritai = 'tty'; + case Tsum = 'ttz'; + case Wiarumus = 'tua'; + case Tubatulabal = 'tub'; + case Mutu = 'tuc'; + case Tuxa = 'tud'; + case Tuyuca = 'tue'; + case Central_Tunebo = 'tuf'; + case Tunia = 'tug'; + case Taulil = 'tuh'; + case Tupuri = 'tui'; + case Tugutil = 'tuj'; + case Turkmen = 'tuk'; + case Tula = 'tul'; + case Tumbuka = 'tum'; + case Tunica = 'tun'; + case Tucano = 'tuo'; + case Tedaga = 'tuq'; + case Turkish = 'tur'; + case Tuscarora = 'tus'; + case Tututni = 'tuu'; + case Turkana = 'tuv'; + case Tuxinawa = 'tux'; + case Tugen = 'tuy'; + case Turka = 'tuz'; + case Vaghua = 'tva'; + case Tsuvadi = 'tvd'; + case Te_un = 'tve'; + case Tulai = 'tvi'; + case Southeast_Ambrym = 'tvk'; + case Tuvalu = 'tvl'; + case Tela_Masbuar = 'tvm'; + case Tavoyan = 'tvn'; + case Tidore = 'tvo'; + case Taveta = 'tvs'; + case Tutsa_Naga = 'tvt'; + case Tunen = 'tvu'; + case Sedoa = 'tvw'; + case Taivoan = 'tvx'; + case Timor_Pidgin = 'tvy'; + case Twana = 'twa'; + case Western_Tawbuid = 'twb'; + case Teshenawa = 'twc'; + case Twents = 'twd'; + case Tewa_Indonesia = 'twe'; + case Northern_Tiwa = 'twf'; + case Tereweng = 'twg'; + case Tai_Don = 'twh'; + case Twi = 'twi'; + case Tawara = 'twl'; + case Tawang_Monpa = 'twm'; + case Twendi = 'twn'; + case Tswapong = 'two'; + case Ere = 'twp'; + case Tasawaq = 'twq'; + case Southwestern_Tarahumara = 'twr'; + case Turiwara = 'twt'; + case Termanu = 'twu'; + case Tuwari = 'tww'; + case Tewe = 'twx'; + case Tawoyan = 'twy'; + case Tombonuo = 'txa'; + case Tokharian_B = 'txb'; + case Tsetsaut = 'txc'; + case Totoli = 'txe'; + case Tangut = 'txg'; + case Thracian = 'txh'; + case Ikpeng = 'txi'; + case Tarjumo = 'txj'; + case Tomini = 'txm'; + case West_Tarangan = 'txn'; + case Toto = 'txo'; + case Tii = 'txq'; + case Tartessian = 'txr'; + case Tonsea = 'txs'; + case Citak = 'txt'; + case Kayapo = 'txu'; + case Tatana = 'txx'; + case Tanosy_Malagasy = 'txy'; + case Tauya = 'tya'; + case Kyanga = 'tye'; + case O_du = 'tyh'; + case Teke_Tsaayi = 'tyi'; + case Tai_Do = 'tyj'; + case Thu_Lao = 'tyl'; + case Kombai = 'tyn'; + case Thaypan = 'typ'; + case Tai_Daeng = 'tyr'; + case Tay_Sa_Pa = 'tys'; + case Tay_Tac = 'tyt'; + case Kua = 'tyu'; + case Tuvinian = 'tyv'; + case Teke_Tyee = 'tyx'; + case Tiyaa = 'tyy'; + case Tay = 'tyz'; + case Tanzanian_Sign_Language = 'tza'; + case Tzeltal = 'tzh'; + case Tz_utujil = 'tzj'; + case Talossan = 'tzl'; + case Central_Atlas_Tamazight = 'tzm'; + case Tugun = 'tzn'; + case Tzotzil = 'tzo'; + case Tabriak = 'tzx'; + case Uamue = 'uam'; + case Kuan = 'uan'; + case Tairuma = 'uar'; + case Ubang = 'uba'; + case Ubi = 'ubi'; + case Buhi_non_Bikol = 'ubl'; + case Ubir = 'ubr'; + case Umbu_Ungu = 'ubu'; + case Ubykh = 'uby'; + case Uda = 'uda'; + case Udihe = 'ude'; + case Muduga = 'udg'; + case Udi = 'udi'; + case Ujir = 'udj'; + case Wuzlam = 'udl'; + case Udmurt = 'udm'; + case Uduk = 'udu'; + case Kioko = 'ues'; + case Ufim = 'ufi'; + case Ugaritic = 'uga'; + case Kuku_Ugbanh = 'ugb'; + case Ughele = 'uge'; + case Kubachi = 'ugh'; + case Ugandan_Sign_Language = 'ugn'; + case Ugong = 'ugo'; + case Uruguayan_Sign_Language = 'ugy'; + case Uhami = 'uha'; + case Damal = 'uhn'; + case Uighur = 'uig'; + case Uisai = 'uis'; + case Iyive = 'uiv'; + case Tanjijili = 'uji'; + case Kaburi = 'uka'; + case Ukuriguma = 'ukg'; + case Ukhwejo = 'ukh'; + case Kui_India = 'uki'; + case Muak_Sa_aak = 'ukk'; + case Ukrainian_Sign_Language = 'ukl'; + case Ukpe_Bayobiri = 'ukp'; + case Ukwa = 'ukq'; + case Ukrainian = 'ukr'; + case Urubu_Kaapor_Sign_Language = 'uks'; + case Ukue = 'uku'; + case Kuku = 'ukv'; + case Ukwuani_Aboh_Ndoni = 'ukw'; + case Kuuk_Yak = 'uky'; + case Fungwa = 'ula'; + case Ulukwumi = 'ulb'; + case Ulch = 'ulc'; + case Lule = 'ule'; + case Usku = 'ulf'; + case Ulithian = 'uli'; + case Meriam_Mir = 'ulk'; + case Ullatan = 'ull'; + case Ulumanda = 'ulm'; + case Unserdeutsch = 'uln'; + case Uma_Lung = 'ulu'; + case Ulwa = 'ulw'; + case Buli = 'uly'; + case Umatilla = 'uma'; + case Umbundu = 'umb'; + case Marrucinian = 'umc'; + case Umbindhamu = 'umd'; + case Morrobalama = 'umg'; + case Ukit = 'umi'; + case Umon = 'umm'; + case Makyan_Naga = 'umn'; + case Umotina = 'umo'; + case Umpila = 'ump'; + case Umbugarla = 'umr'; + case Pendau = 'ums'; + case Munsee = 'umu'; + case North_Watut = 'una'; + case Undetermined = 'und'; + case Uneme = 'une'; + case Ngarinyin = 'ung'; + case Uni = 'uni'; + case Enawene_Nawe = 'unk'; + case Unami = 'unm'; + case Kurnai = 'unn'; + case Mundari = 'unr'; + case Unubahe = 'unu'; + case Munda = 'unx'; + case Unde_Kaili = 'unz'; + case Kulon = 'uon'; + case Umeda = 'upi'; + case Uripiv_Wala_Rano_Atchin = 'upv'; + case Urarina = 'ura'; + case Urubu_Kaapor = 'urb'; + case Urningangg = 'urc'; + case Urdu = 'urd'; + case Uru = 'ure'; + case Uradhi = 'urf'; + case Urigina = 'urg'; + case Urhobo = 'urh'; + case Urim = 'uri'; + case Urak_Lawoi = 'urk'; + case Urali = 'url'; + case Urapmin = 'urm'; + case Uruangnirin = 'urn'; + case Ura_Papua_New_Guinea = 'uro'; + case Uru_Pa_In = 'urp'; + case Lehalurup = 'urr'; + case Urat = 'urt'; + case Urumi = 'uru'; + case Uruava = 'urv'; + case Sop = 'urw'; + case Urimo = 'urx'; + case Orya = 'ury'; + case Uru_Eu_Wau_Wau = 'urz'; + case Usarufa = 'usa'; + case Ushojo = 'ush'; + case Usui = 'usi'; + case Usaghade = 'usk'; + case Uspanteco = 'usp'; + case us_Saare = 'uss'; + case Uya = 'usu'; + case Otank = 'uta'; + case Ute_Southern_Paiute = 'ute'; + case ut_Hun = 'uth'; + case Amba_Solomon_Islands = 'utp'; + case Etulo = 'utr'; + case Utu = 'utu'; + case Urum = 'uum'; + case Ura_Vanuatu = 'uur'; + case U = 'uuu'; + case West_Uvean = 'uve'; + case Uri = 'uvh'; + case Lote = 'uvl'; + case Kuku_Uwanh = 'uwa'; + case Doko_Uyanga = 'uya'; + case Uzbek = 'uzb'; + case Northern_Uzbek = 'uzn'; + case Southern_Uzbek = 'uzs'; + case Vaagri_Booli = 'vaa'; + case Vale = 'vae'; + case Vafsi = 'vaf'; + case Vagla = 'vag'; + case Varhadi_Nagpuri = 'vah'; + case Vai = 'vai'; + case Sekele = 'vaj'; + case Vehes = 'val'; + case Vanimo = 'vam'; + case Valman = 'van'; + case Vao = 'vao'; + case Vaiphei = 'vap'; + case Huarijio = 'var'; + case Vasavi = 'vas'; + case Vanuma = 'vau'; + case Varli = 'vav'; + case Wayu = 'vay'; + case Southeast_Babar = 'vbb'; + case Southwestern_Bontok = 'vbk'; + case Venetian = 'vec'; + case Veddah = 'ved'; + case Veluws = 'vel'; + case Vemgo_Mabas = 'vem'; + case Venda = 'ven'; + case Ventureno = 'veo'; + case Veps = 'vep'; + case Mom_Jango = 'ver'; + case Vaghri = 'vgr'; + case Vlaamse_Gebarentaal = 'vgt'; + case Virgin_Islands_Creole_English = 'vic'; + case Vidunda = 'vid'; + case Vietnamese = 'vie'; + case Vili = 'vif'; + case Viemo = 'vig'; + case Vilela = 'vil'; + case Vinza = 'vin'; + case Vishavan = 'vis'; + case Viti = 'vit'; + case Iduna = 'viv'; + case Bajjika = 'vjk'; + case Kariyarra = 'vka'; + case Kujarge = 'vkj'; + case Kaur = 'vkk'; + case Kulisusu = 'vkl'; + case Kamakan = 'vkm'; + case Koro_Nulu = 'vkn'; + case Kodeoha = 'vko'; + case Korlai_Creole_Portuguese = 'vkp'; + case Tenggarong_Kutai_Malay = 'vkt'; + case Kurrama = 'vku'; + case Koro_Zuba = 'vkz'; + case Valpei = 'vlp'; + case Vlaams = 'vls'; + case Martuyhunira = 'vma'; + case Barbaram = 'vmb'; + case Juxtlahuaca_Mixtec = 'vmc'; + case Mudu_Koraga = 'vmd'; + case East_Masela = 'vme'; + case Mainfrankisch = 'vmf'; + case Lungalunga = 'vmg'; + case Maraghei = 'vmh'; + case Miwa = 'vmi'; + case Ixtayutla_Mixtec = 'vmj'; + case Makhuwa_Shirima = 'vmk'; + case Malgana = 'vml'; + case Mitlatongo_Mixtec = 'vmm'; + case Soyaltepec_Mazatec = 'vmp'; + case Soyaltepec_Mixtec = 'vmq'; + case Marenje = 'vmr'; + case Moksela = 'vms'; + case Muluridyi = 'vmu'; + case Valley_Maidu = 'vmv'; + case Makhuwa = 'vmw'; + case Tamazola_Mixtec = 'vmx'; + case Ayautla_Mazatec = 'vmy'; + case Mazatlan_Mazatec = 'vmz'; + case Vano = 'vnk'; + case Vinmavis = 'vnm'; + case Vunapu = 'vnp'; + case Volapuk = 'vol'; + case Voro = 'vor'; + case Votic = 'vot'; + case Vera_a = 'vra'; + case Voro_2 = 'vro'; + case Varisi = 'vrs'; + case Burmbar = 'vrt'; + case Moldova_Sign_Language = 'vsi'; + case Venezuelan_Sign_Language = 'vsl'; + case Vedic_Sanskrit = 'vsn'; + case Valencian_Sign_Language = 'vsv'; + case Vitou = 'vto'; + case Vumbu = 'vum'; + case Vunjo = 'vun'; + case Vute = 'vut'; + case Awa_China = 'vwa'; + case Walla_Walla = 'waa'; + case Wab = 'wab'; + case Wasco_Wishram = 'wac'; + case Wamesa = 'wad'; + case Walser = 'wae'; + case Wakona = 'waf'; + case Wa_ema = 'wag'; + case Watubela = 'wah'; + case Wares = 'wai'; + case Waffa = 'waj'; + case Wolaytta = 'wal'; + case Wampanoag = 'wam'; + case Wan = 'wan'; + case Wappo = 'wao'; + case Wapishana = 'wap'; + case Wagiman = 'waq'; + case Waray_Philippines = 'war'; + case Washo = 'was'; + case Kaninuwa = 'wat'; + case Waura = 'wau'; + case Waka = 'wav'; + case Waiwai = 'waw'; + case Watam = 'wax'; + case Wayana = 'way'; + case Wampur = 'waz'; + case Warao = 'wba'; + case Wabo = 'wbb'; + case Waritai = 'wbe'; + case Wara_2 = 'wbf'; + case Wanda = 'wbh'; + case Vwanji = 'wbi'; + case Alagwa = 'wbj'; + case Waigali = 'wbk'; + case Wakhi = 'wbl'; + case Wa = 'wbm'; + case Warlpiri = 'wbp'; + case Waddar = 'wbq'; + case Wagdi = 'wbr'; + case West_Bengal_Sign_Language = 'wbs'; + case Warnman = 'wbt'; + case Wajarri = 'wbv'; + case Woi = 'wbw'; + case Yanomami = 'wca'; + case Waci_Gbe = 'wci'; + case Wandji = 'wdd'; + case Wadaginam = 'wdg'; + case Wadjiginy = 'wdj'; + case Wadikali = 'wdk'; + case Wendat = 'wdt'; + case Wadjigu = 'wdu'; + case Wadjabangayi = 'wdy'; + case Wewaw = 'wea'; + case We_Western = 'wec'; + case Wedau = 'wed'; + case Wergaia = 'weg'; + case Weh = 'weh'; + case Kiunum = 'wei'; + case Weme_Gbe = 'wem'; + case Wemale = 'weo'; + case Westphalien = 'wep'; + case Weri = 'wer'; + case Cameroon_Pidgin = 'wes'; + case Perai = 'wet'; + case Rawngtu_Chin = 'weu'; + case Wejewa = 'wew'; + case Yafi = 'wfg'; + case Wagaya = 'wga'; + case Wagawaga = 'wgb'; + case Wangkangurru = 'wgg'; + case Wahgi = 'wgi'; + case Waigeo = 'wgo'; + case Wirangu = 'wgu'; + case Warrgamay = 'wgy'; + case Sou_Upaa = 'wha'; + case North_Wahgi = 'whg'; + case Wahau_Kenyah = 'whk'; + case Wahau_Kayan = 'whu'; + case Southern_Toussian = 'wib'; + case Wichita = 'wic'; + case Wik_Epa = 'wie'; + case Wik_Keyangan = 'wif'; + case Wik_Ngathan = 'wig'; + case Wik_Me_anha = 'wih'; + case Minidien = 'wii'; + case Wik_Iiyanh = 'wij'; + case Wikalkan = 'wik'; + case Wilawila = 'wil'; + case Wik_Mungkan = 'wim'; + case Ho_Chunk = 'win'; + case Wirafed = 'wir'; + case Wiru = 'wiu'; + case Vitu = 'wiv'; + case Wiyot = 'wiy'; + case Waja = 'wja'; + case Warji = 'wji'; + case Kw_adza = 'wka'; + case Kumbaran = 'wkb'; + case Wakde = 'wkd'; + case Kalanadi = 'wkl'; + case Keerray_Woorroong = 'wkr'; + case Kunduvadi = 'wku'; + case Wakawaka = 'wkw'; + case Wangkayutyuru = 'wky'; + case Walio = 'wla'; + case Mwali_Comorian = 'wlc'; + case Wolane = 'wle'; + case Kunbarlang = 'wlg'; + case Welaun = 'wlh'; + case Waioli = 'wli'; + case Wailaki = 'wlk'; + case Wali_Sudan = 'wll'; + case Middle_Welsh = 'wlm'; + case Walloon = 'wln'; + case Wolio = 'wlo'; + case Wailapa = 'wlr'; + case Wallisian = 'wls'; + case Wuliwuli = 'wlu'; + case Wichi_Lhamtes_Vejoz = 'wlv'; + case Walak = 'wlw'; + case Wali_Ghana = 'wlx'; + case Waling = 'wly'; + case Mawa_Nigeria = 'wma'; + case Wambaya = 'wmb'; + case Wamas = 'wmc'; + case Mamainde = 'wmd'; + case Wambule = 'wme'; + case Western_Minyag = 'wmg'; + case Waima_a = 'wmh'; + case Wamin = 'wmi'; + case Maiwa_Indonesia = 'wmm'; + case Waamwang = 'wmn'; + case Wom_Papua_New_Guinea = 'wmo'; + case Wambon = 'wms'; + case Walmajarri = 'wmt'; + case Mwani = 'wmw'; + case Womo = 'wmx'; + case Mokati = 'wnb'; + case Wantoat = 'wnc'; + case Wandarang = 'wnd'; + case Waneci = 'wne'; + case Wanggom = 'wng'; + case Ndzwani_Comorian = 'wni'; + case Wanukaka = 'wnk'; + case Wanggamala = 'wnm'; + case Wunumara = 'wnn'; + case Wano = 'wno'; + case Wanap = 'wnp'; + case Usan = 'wnu'; + case Wintu = 'wnw'; + case Wanyi = 'wny'; + case Kuwema = 'woa'; + case We_Northern = 'wob'; + case Wogeo = 'woc'; + case Wolani = 'wod'; + case Woleaian = 'woe'; + case Gambian_Wolof = 'wof'; + case Wogamusin = 'wog'; + case Kamang = 'woi'; + case Longto = 'wok'; + case Wolof = 'wol'; + case Wom_Nigeria = 'wom'; + case Wongo = 'won'; + case Manombai = 'woo'; + case Woria = 'wor'; + case Hanga_Hundi = 'wos'; + case Wawonii = 'wow'; + case Weyto = 'woy'; + case Maco = 'wpc'; + case Waluwarra = 'wrb'; + case Warungu = 'wrg'; + case Wiradjuri = 'wrh'; + case Wariyangga = 'wri'; + case Garrwa = 'wrk'; + case Warlmanpa = 'wrl'; + case Warumungu = 'wrm'; + case Warnang = 'wrn'; + case Worrorra = 'wro'; + case Waropen = 'wrp'; + case Wardaman = 'wrr'; + case Waris = 'wrs'; + case Waru = 'wru'; + case Waruna = 'wrv'; + case Gugu_Warra = 'wrw'; + case Wae_Rana = 'wrx'; + case Merwari = 'wry'; + case Waray_Australia = 'wrz'; + case Warembori = 'wsa'; + case Adilabad_Gondi = 'wsg'; + case Wusi = 'wsi'; + case Waskia = 'wsk'; + case Owenia = 'wsr'; + case Wasa = 'wss'; + case Wasu = 'wsu'; + case Wotapuri_Katarqalai = 'wsv'; + case Matambwe = 'wtb'; + case Watiwa = 'wtf'; + case Wathawurrung = 'wth'; + case Berta = 'wti'; + case Watakataui = 'wtk'; + case Mewati = 'wtm'; + case Wotu = 'wtw'; + case Wikngenchera = 'wua'; + case Wunambal = 'wub'; + case Wudu = 'wud'; + case Wutunhua = 'wuh'; + case Silimo = 'wul'; + case Wumbvu = 'wum'; + case Bungu = 'wun'; + case Wurrugu = 'wur'; + case Wutung = 'wut'; + case Wu_Chinese = 'wuu'; + case Wuvulu_Aua = 'wuv'; + case Wulna = 'wux'; + case Wauyai = 'wuy'; + case Waama = 'wwa'; + case Wakabunga = 'wwb'; + case Wetamut = 'wwo'; + case Warrwa = 'wwr'; + case Wawa = 'www'; + case Waxianghua = 'wxa'; + case Wardandi = 'wxw'; + case Wangaaybuwan_Ngiyambaa = 'wyb'; + case Woiwurrung = 'wyi'; + case Wymysorys = 'wym'; + case Wyandot = 'wyn'; + case Wayoro = 'wyr'; + case Western_Fijian = 'wyy'; + case Andalusian_Arabic = 'xaa'; + case Sambe = 'xab'; + case Kachari = 'xac'; + case Adai = 'xad'; + case Aequian = 'xae'; + case Aghwan = 'xag'; + case Kaimbe = 'xai'; + case Ararandewara = 'xaj'; + case Maku = 'xak'; + case Kalmyk = 'xal'; + case Xam = 'xam'; + case Xamtanga = 'xan'; + case Khao = 'xao'; + case Apalachee = 'xap'; + case Aquitanian = 'xaq'; + case Karami = 'xar'; + case Kamas = 'xas'; + case Katawixi = 'xat'; + case Kauwera = 'xau'; + case Xavante = 'xav'; + case Kawaiisu = 'xaw'; + case Kayan_Mahakam = 'xay'; + case Lower_Burdekin = 'xbb'; + case Bactrian = 'xbc'; + case Bindal = 'xbd'; + case Bigambal = 'xbe'; + case Bunganditj = 'xbg'; + case Kombio = 'xbi'; + case Birrpayi = 'xbj'; + case Middle_Breton = 'xbm'; + case Kenaboi = 'xbn'; + case Bolgarian = 'xbo'; + case Bibbulman = 'xbp'; + case Kambera = 'xbr'; + case Kambiwa = 'xbw'; + case Batjala = 'xby'; + case Cumbric = 'xcb'; + case Camunic = 'xcc'; + case Celtiberian = 'xce'; + case Cisalpine_Gaulish = 'xcg'; + case Chemakum = 'xch'; + case Classical_Armenian = 'xcl'; + case Comecrudo = 'xcm'; + case Cotoname = 'xcn'; + case Chorasmian = 'xco'; + case Carian = 'xcr'; + case Classical_Tibetan = 'xct'; + case Curonian = 'xcu'; + case Chuvantsy = 'xcv'; + case Coahuilteco = 'xcw'; + case Cayuse = 'xcy'; + case Darkinyung = 'xda'; + case Dacian = 'xdc'; + case Dharuk = 'xdk'; + case Edomite = 'xdm'; + case Kwandu = 'xdo'; + case Kaitag = 'xdq'; + case Malayic_Dayak = 'xdy'; + case Eblan = 'xeb'; + case Hdi = 'xed'; + case Xegwi = 'xeg'; + case Kelo = 'xel'; + case Kembayan = 'xem'; + case Epi_Olmec = 'xep'; + case Xerente = 'xer'; + case Kesawai = 'xes'; + case Xeta = 'xet'; + case Keoru_Ahia = 'xeu'; + case Faliscan = 'xfa'; + case Galatian = 'xga'; + case Gbin = 'xgb'; + case Gudang = 'xgd'; + case Gabrielino_Fernandeno = 'xgf'; + case Goreng = 'xgg'; + case Garingbal = 'xgi'; + case Galindan = 'xgl'; + case Dharumbal = 'xgm'; + case Garza = 'xgr'; + case Unggumi = 'xgu'; + case Guwa = 'xgw'; + case Harami = 'xha'; + case Hunnic = 'xhc'; + case Hadrami = 'xhd'; + case Khetrani = 'xhe'; + case Middle_Khmer_1400_to_1850_CE = 'xhm'; + case Xhosa = 'xho'; + case Hernican = 'xhr'; + case Hattic = 'xht'; + case Hurrian = 'xhu'; + case Khua = 'xhv'; + case Iberian = 'xib'; + case Xiri = 'xii'; + case Illyrian = 'xil'; + case Xinca = 'xin'; + case Xiriana = 'xir'; + case Kisan = 'xis'; + case Indus_Valley_Language = 'xiv'; + case Xipaya = 'xiy'; + case Minjungbal = 'xjb'; + case Jaitmatang = 'xjt'; + case Kalkoti = 'xka'; + case Northern_Nago = 'xkb'; + case Kho_ini = 'xkc'; + case Mendalam_Kayan = 'xkd'; + case Kereho = 'xke'; + case Khengkha = 'xkf'; + case Kagoro = 'xkg'; + case Kenyan_Sign_Language = 'xki'; + case Kajali = 'xkj'; + case Kachok = 'xkk'; + case Mainstream_Kenyah = 'xkl'; + case Kayan_River_Kayan = 'xkn'; + case Kiorr = 'xko'; + case Kabatei = 'xkp'; + case Koroni = 'xkq'; + case Xakriaba = 'xkr'; + case Kumbewaha = 'xks'; + case Kantosi = 'xkt'; + case Kaamba = 'xku'; + case Kgalagadi = 'xkv'; + case Kembra = 'xkw'; + case Karore = 'xkx'; + case Uma_Lasan = 'xky'; + case Kurtokha = 'xkz'; + case Kamula = 'xla'; + case Loup_B = 'xlb'; + case Lycian = 'xlc'; + case Lydian = 'xld'; + case Lemnian = 'xle'; + case Ligurian_Ancient = 'xlg'; + case Liburnian = 'xli'; + case Alanic = 'xln'; + case Loup_A = 'xlo'; + case Lepontic = 'xlp'; + case Lusitanian = 'xls'; + case Cuneiform_Luwian = 'xlu'; + case Elymian = 'xly'; + case Mushungulu = 'xma'; + case Mbonga = 'xmb'; + case Makhuwa_Marrevone = 'xmc'; + case Mbudum = 'xmd'; + case Median = 'xme'; + case Mingrelian = 'xmf'; + case Mengaka = 'xmg'; + case Kugu_Muminh = 'xmh'; + case Majera = 'xmj'; + case Ancient_Macedonian = 'xmk'; + case Malaysian_Sign_Language = 'xml'; + case Manado_Malay = 'xmm'; + case Manichaean_Middle_Persian = 'xmn'; + case Morerebi = 'xmo'; + case Kuku_Mu_inh = 'xmp'; + case Kuku_Mangk = 'xmq'; + case Meroitic = 'xmr'; + case Moroccan_Sign_Language = 'xms'; + case Matbat = 'xmt'; + case Kamu = 'xmu'; + case Antankarana_Malagasy = 'xmv'; + case Tsimihety_Malagasy = 'xmw'; + case Salawati = 'xmx'; + case Mayaguduna = 'xmy'; + case Mori_Bawah = 'xmz'; + case Ancient_North_Arabian = 'xna'; + case Kanakanabu = 'xnb'; + case Middle_Mongolian = 'xng'; + case Kuanhua = 'xnh'; + case Ngarigu = 'xni'; + case Ngoni_Tanzania = 'xnj'; + case Nganakarti = 'xnk'; + case Ngumbarl = 'xnm'; + case Northern_Kankanay = 'xnn'; + case Anglo_Norman = 'xno'; + case Ngoni_Mozambique = 'xnq'; + case Kangri = 'xnr'; + case Kanashi = 'xns'; + case Narragansett = 'xnt'; + case Nukunul = 'xnu'; + case Nyiyaparli = 'xny'; + case Kenzi = 'xnz'; + case O_chi_chi = 'xoc'; + case Kokoda = 'xod'; + case Soga = 'xog'; + case Kominimung = 'xoi'; + case Xokleng = 'xok'; + case Komo_Sudan = 'xom'; + case Konkomba = 'xon'; + case Xukuru = 'xoo'; + case Kopar = 'xop'; + case Korubo = 'xor'; + case Kowaki = 'xow'; + case Pirriya = 'xpa'; + case Northeastern_Tasmanian = 'xpb'; + case Pecheneg = 'xpc'; + case Oyster_Bay_Tasmanian = 'xpd'; + case Liberia_Kpelle = 'xpe'; + case Southeast_Tasmanian = 'xpf'; + case Phrygian = 'xpg'; + case North_Midlands_Tasmanian = 'xph'; + case Pictish = 'xpi'; + case Mpalitjanh = 'xpj'; + case Kulina_Pano = 'xpk'; + case Port_Sorell_Tasmanian = 'xpl'; + case Pumpokol = 'xpm'; + case Kapinawa = 'xpn'; + case Pochutec = 'xpo'; + case Puyo_Paekche = 'xpp'; + case Mohegan_Pequot = 'xpq'; + case Parthian = 'xpr'; + case Pisidian = 'xps'; + case Punthamara = 'xpt'; + case Punic = 'xpu'; + case Northern_Tasmanian = 'xpv'; + case Northwestern_Tasmanian = 'xpw'; + case Southwestern_Tasmanian = 'xpx'; + case Puyo = 'xpy'; + case Bruny_Island_Tasmanian = 'xpz'; + case Karakhanid = 'xqa'; + case Qatabanian = 'xqt'; + case Kraho = 'xra'; + case Eastern_Karaboro = 'xrb'; + case Gundungurra = 'xrd'; + case Kreye = 'xre'; + case Minang = 'xrg'; + case Krikati_Timbira = 'xri'; + case Armazic = 'xrm'; + case Arin = 'xrn'; + case Raetic = 'xrr'; + case Aranama_Tamique = 'xrt'; + case Marriammu = 'xru'; + case Karawa = 'xrw'; + case Sabaean = 'xsa'; + case Sambal = 'xsb'; + case Scythian = 'xsc'; + case Sidetic = 'xsd'; + case Sempan = 'xse'; + case Shamang = 'xsh'; + case Sio = 'xsi'; + case Subi = 'xsj'; + case South_Slavey = 'xsl'; + case Kasem = 'xsm'; + case Sanga_Nigeria = 'xsn'; + case Solano = 'xso'; + case Silopi = 'xsp'; + case Makhuwa_Saka = 'xsq'; + case Sherpa = 'xsr'; + case Sanuma = 'xsu'; + case Sudovian = 'xsv'; + case Saisiyat = 'xsy'; + case Alcozauca_Mixtec = 'xta'; + case Chazumba_Mixtec = 'xtb'; + case Katcha_Kadugli_Miri = 'xtc'; + case Diuxi_Tilantongo_Mixtec = 'xtd'; + case Ketengban = 'xte'; + case Transalpine_Gaulish = 'xtg'; + case Yitha_Yitha = 'xth'; + case Sinicahua_Mixtec = 'xti'; + case San_Juan_Teita_Mixtec = 'xtj'; + case Tijaltepec_Mixtec = 'xtl'; + case Magdalena_Penasco_Mixtec = 'xtm'; + case Northern_Tlaxiaco_Mixtec = 'xtn'; + case Tokharian_A = 'xto'; + case San_Miguel_Piedras_Mixtec = 'xtp'; + case Tumshuqese = 'xtq'; + case Early_Tripuri = 'xtr'; + case Sindihui_Mixtec = 'xts'; + case Tacahua_Mixtec = 'xtt'; + case Cuyamecalco_Mixtec = 'xtu'; + case Thawa = 'xtv'; + case Tawande = 'xtw'; + case Yoloxochitl_Mixtec = 'xty'; + case Alu_Kurumba = 'xua'; + case Betta_Kurumba = 'xub'; + case Umiida = 'xud'; + case Kunigami = 'xug'; + case Jennu_Kurumba = 'xuj'; + case Ngunawal = 'xul'; + case Umbrian = 'xum'; + case Unggaranggu = 'xun'; + case Kuo = 'xuo'; + case Upper_Umpqua = 'xup'; + case Urartian = 'xur'; + case Kuthant = 'xut'; + case Kxoe = 'xuu'; + case Venetic = 'xve'; + case Kamviri = 'xvi'; + case Vandalic = 'xvn'; + case Volscian = 'xvo'; + case Vestinian = 'xvs'; + case Kwaza = 'xwa'; + case Woccon = 'xwc'; + case Wadi_Wadi = 'xwd'; + case Xwela_Gbe = 'xwe'; + case Kwegu = 'xwg'; + case Wajuk = 'xwj'; + case Wangkumara = 'xwk'; + case Western_Xwla_Gbe = 'xwl'; + case Written_Oirat = 'xwo'; + case Kwerba_Mamberamo = 'xwr'; + case Wotjobaluk = 'xwt'; + case Wemba_Wemba = 'xww'; + case Boro_Ghana = 'xxb'; + case Ke_o = 'xxk'; + case Minkin = 'xxm'; + case Koropo = 'xxr'; + case Tambora = 'xxt'; + case Yaygir = 'xya'; + case Yandjibara = 'xyb'; + case Mayi_Yapi = 'xyj'; + case Mayi_Kulan = 'xyk'; + case Yalakalore = 'xyl'; + case Mayi_Thakurti = 'xyt'; + case Yorta_Yorta = 'xyy'; + case Zhang_Zhung = 'xzh'; + case Zemgalian = 'xzm'; + case Ancient_Zapotec = 'xzp'; + case Yaminahua = 'yaa'; + case Yuhup = 'yab'; + case Pass_Valley_Yali = 'yac'; + case Yagua = 'yad'; + case Pume = 'yae'; + case Yaka_Democratic_Republic_of_Congo = 'yaf'; + case Yamana = 'yag'; + case Yazgulyam = 'yah'; + case Yagnobi = 'yai'; + case Banda_Yangere = 'yaj'; + case Yakama = 'yak'; + case Yalunka = 'yal'; + case Yamba = 'yam'; + case Mayangna = 'yan'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yaqui = 'yaq'; + case Yabarana = 'yar'; + case Nugunu_Cameroon = 'yas'; + case Yambeta = 'yat'; + case Yuwana = 'yau'; + case Yangben = 'yav'; + case Yawalapiti = 'yaw'; + case Yauma = 'yax'; + case Agwagwune = 'yay'; + case Lokaa = 'yaz'; + case Yala = 'yba'; + case Yemba = 'ybb'; + case West_Yugur = 'ybe'; + case Yakha = 'ybh'; + case Yamphu = 'ybi'; + case Hasha = 'ybj'; + case Bokha = 'ybk'; + case Yukuben = 'ybl'; + case Yaben = 'ybm'; + case Yabaana = 'ybn'; + case Yabong = 'ybo'; + case Yawiyo = 'ybx'; + case Yaweyuha = 'yby'; + case Chesu = 'ych'; + case Lolopo = 'ycl'; + case Yucuna = 'ycn'; + case Chepya = 'ycp'; + case Yilan_Creole = 'ycr'; + case Yanda = 'yda'; + case Eastern_Yiddish = 'ydd'; + case Yangum_Dey = 'yde'; + case Yidgha = 'ydg'; + case Yoidik = 'ydk'; + case Ravula = 'yea'; + case Yeniche = 'yec'; + case Yimas = 'yee'; + case Yeni = 'yei'; + case Yevanic = 'yej'; + case Yela = 'yel'; + case Tarok = 'yer'; + case Nyankpa = 'yes'; + case Yetfa = 'yet'; + case Yerukula = 'yeu'; + case Yapunda = 'yev'; + case Yeyi = 'yey'; + case Malyangapa = 'yga'; + case Yiningayi = 'ygi'; + case Yangum_Gel = 'ygl'; + case Yagomi = 'ygm'; + case Gepo = 'ygp'; + case Yagaria = 'ygr'; + case Yol_u_Sign_Language = 'ygs'; + case Yugul = 'ygu'; + case Yagwoia = 'ygw'; + case Baha_Buyang = 'yha'; + case Judeo_Iraqi_Arabic = 'yhd'; + case Hlepho_Phowa = 'yhl'; + case Yan_nhangu_Sign_Language = 'yhs'; + case Yinggarda = 'yia'; + case Yiddish = 'yid'; + case Ache_2 = 'yif'; + case Wusa_Nasu = 'yig'; + case Western_Yiddish = 'yih'; + case Yidiny = 'yii'; + case Yindjibarndi = 'yij'; + case Dongshanba_Lalo = 'yik'; + case Yindjilandji = 'yil'; + case Yimchungru_Naga = 'yim'; + case Riang_Lai = 'yin'; + case Pholo = 'yip'; + case Miqie = 'yiq'; + case North_Awyu = 'yir'; + case Yis = 'yis'; + case Eastern_Lalu = 'yit'; + case Awu = 'yiu'; + case Northern_Nisu = 'yiv'; + case Axi_Yi = 'yix'; + case Azhe = 'yiz'; + case Yakan = 'yka'; + case Northern_Yukaghir = 'ykg'; + case Khamnigan_Mongol = 'ykh'; + case Yoke = 'yki'; + case Yakaikeke = 'ykk'; + case Khlula = 'ykl'; + case Kap = 'ykm'; + case Kua_nsi = 'ykn'; + case Yasa = 'yko'; + case Yekora = 'ykr'; + case Kathu = 'ykt'; + case Kuamasi = 'yku'; + case Yakoma = 'yky'; + case Yaul = 'yla'; + case Yaleba = 'ylb'; + case Yele = 'yle'; + case Yelogu = 'ylg'; + case Angguruk_Yali = 'yli'; + case Yil = 'yll'; + case Limi = 'ylm'; + case Langnian_Buyang = 'yln'; + case Naluo_Yi = 'ylo'; + case Yalarnnga = 'ylr'; + case Aribwaung = 'ylu'; + case Nyalayu = 'yly'; + case Yambes = 'ymb'; + case Southern_Muji = 'ymc'; + case Muda = 'ymd'; + case Yameo = 'yme'; + case Yamongeri = 'ymg'; + case Mili = 'ymh'; + case Moji = 'ymi'; + case Makwe = 'ymk'; + case Iamalele = 'yml'; + case Maay = 'ymm'; + case Yamna = 'ymn'; + case Yangum_Mon = 'ymo'; + case Yamap = 'ymp'; + case Qila_Muji = 'ymq'; + case Malasar = 'ymr'; + case Mysian = 'yms'; + case Northern_Muji = 'ymx'; + case Muzi = 'ymz'; + case Aluo = 'yna'; + case Yandruwandha = 'ynd'; + case Lang_e = 'yne'; + case Yango = 'yng'; + case Naukan_Yupik = 'ynk'; + case Yangulam = 'ynl'; + case Yana = 'ynn'; + case Yong = 'yno'; + case Yendang = 'ynq'; + case Yansi = 'yns'; + case Yahuna = 'ynu'; + case Yoba = 'yob'; + case Yogad = 'yog'; + case Yonaguni = 'yoi'; + case Yokuts = 'yok'; + case Yola = 'yol'; + case Yombe = 'yom'; + case Yongkom = 'yon'; + case Yoruba = 'yor'; + case Yotti = 'yot'; + case Yoron = 'yox'; + case Yoy = 'yoy'; + case Phala = 'ypa'; + case Labo_Phowa = 'ypb'; + case Phola = 'ypg'; + case Phupha = 'yph'; + case Phuma = 'ypm'; + case Ani_Phowa = 'ypn'; + case Alo_Phola = 'ypo'; + case Phupa = 'ypp'; + case Phuza = 'ypz'; + case Yerakai = 'yra'; + case Yareba = 'yrb'; + case Yaoure = 'yre'; + case Nenets = 'yrk'; + case Nhengatu = 'yrl'; + case Yirrk_Mel = 'yrm'; + case Yerong = 'yrn'; + case Yaroame = 'yro'; + case Yarsun = 'yrs'; + case Yarawata = 'yrw'; + case Yarluyandi = 'yry'; + case Yassic = 'ysc'; + case Samatao = 'ysd'; + case Sonaga = 'ysg'; + case Yugoslavian_Sign_Language = 'ysl'; + case Myanmar_Sign_Language = 'ysm'; + case Sani = 'ysn'; + case Nisi_China = 'yso'; + case Southern_Lolopo = 'ysp'; + case Sirenik_Yupik = 'ysr'; + case Yessan_Mayo = 'yss'; + case Sanie = 'ysy'; + case Talu = 'yta'; + case Tanglang = 'ytl'; + case Thopho = 'ytp'; + case Yout_Wam = 'ytw'; + case Yatay = 'yty'; + case Yucateco = 'yua'; + case Yugambal = 'yub'; + case Yuchi = 'yuc'; + case Judeo_Tripolitanian_Arabic = 'yud'; + case Yue_Chinese = 'yue'; + case Havasupai_Walapai_Yavapai = 'yuf'; + case Yug = 'yug'; + case Yuruti = 'yui'; + case Karkar_Yuri = 'yuj'; + case Yuki = 'yuk'; + case Yulu = 'yul'; + case Quechan = 'yum'; + case Bena_Nigeria = 'yun'; + case Yukpa = 'yup'; + case Yuqui = 'yuq'; + case Yurok = 'yur'; + case Yopno = 'yut'; + case Yau_Morobe_Province = 'yuw'; + case Southern_Yukaghir = 'yux'; + case East_Yugur = 'yuy'; + case Yuracare = 'yuz'; + case Yawa = 'yva'; + case Yavitero = 'yvt'; + case Kalou = 'ywa'; + case Yinhawangka = 'ywg'; + case Western_Lalu = 'ywl'; + case Yawanawa = 'ywn'; + case Wuding_Luquan_Yi = 'ywq'; + case Yawuru = 'ywr'; + case Xishanba_Lalo = 'ywt'; + case Wumeng_Nasu = 'ywu'; + case Yawarawarga = 'yww'; + case Mayawali = 'yxa'; + case Yagara = 'yxg'; + case Yardliyawarra = 'yxl'; + case Yinwum = 'yxm'; + case Yuyu = 'yxu'; + case Yabula_Yabula = 'yxy'; + case Yir_Yoront = 'yyr'; + case Yau_Sandaun_Province = 'yyu'; + case Ayizi = 'yyz'; + case E_ma_Buyang = 'yzg'; + case Zokhuo = 'yzk'; + case Sierra_de_Juarez_Zapotec = 'zaa'; + case Western_Tlacolula_Valley_Zapotec = 'zab'; + case Ocotlan_Zapotec = 'zac'; + case Cajonos_Zapotec = 'zad'; + case Yareni_Zapotec = 'zae'; + case Ayoquesco_Zapotec = 'zaf'; + case Zaghawa = 'zag'; + case Zangwal = 'zah'; + case Isthmus_Zapotec = 'zai'; + case Zaramo = 'zaj'; + case Zanaki = 'zak'; + case Zauzou = 'zal'; + case Miahuatlan_Zapotec = 'zam'; + case Ozolotepec_Zapotec = 'zao'; + case Zapotec = 'zap'; + case Aloapam_Zapotec = 'zaq'; + case Rincon_Zapotec = 'zar'; + case Santo_Domingo_Albarradas_Zapotec = 'zas'; + case Tabaa_Zapotec = 'zat'; + case Zangskari = 'zau'; + case Yatzachi_Zapotec = 'zav'; + case Mitla_Zapotec = 'zaw'; + case Xadani_Zapotec = 'zax'; + case Zayse_Zergulla = 'zay'; + case Zari = 'zaz'; + case Balaibalan = 'zba'; + case Central_Berawan = 'zbc'; + case East_Berawan = 'zbe'; + case Blissymbols = 'zbl'; + case Batui = 'zbt'; + case Bu_Bauchi_State = 'zbu'; + case West_Berawan = 'zbw'; + case Coatecas_Altas_Zapotec = 'zca'; + case Las_Delicias_Zapotec = 'zcd'; + case Central_Hongshuihe_Zhuang = 'zch'; + case Ngazidja_Comorian = 'zdj'; + case Zeeuws = 'zea'; + case Zenag = 'zeg'; + case Eastern_Hongshuihe_Zhuang = 'zeh'; + case Zeem = 'zem'; + case Zenaga = 'zen'; + case Kinga = 'zga'; + case Guibei_Zhuang = 'zgb'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Minz_Zhuang = 'zgm'; + case Guibian_Zhuang = 'zgn'; + case Magori = 'zgr'; + case Zhuang = 'zha'; + case Zhaba = 'zhb'; + case Dai_Zhuang = 'zhd'; + case Zhire = 'zhi'; + case Nong_Zhuang = 'zhn'; + case Chinese = 'zho'; + case Zhoa = 'zhw'; + case Zia = 'zia'; + case Zimbabwe_Sign_Language = 'zib'; + case Zimakani = 'zik'; + case Zialo = 'zil'; + case Mesme = 'zim'; + case Zinza = 'zin'; + case Zigula = 'ziw'; + case Zizilivakan = 'ziz'; + case Kaimbulawa = 'zka'; + case Kadu = 'zkd'; + case Koguryo = 'zkg'; + case Khorezmian = 'zkh'; + case Karankawa = 'zkk'; + case Kanan = 'zkn'; + case Kott = 'zko'; + case Sao_Paulo_Kaingang = 'zkp'; + case Zakhring = 'zkr'; + case Kitan = 'zkt'; + case Kaurna = 'zku'; + case Krevinian = 'zkv'; + case Khazar = 'zkz'; + case Zula = 'zla'; + case Liujiang_Zhuang = 'zlj'; + case Malay_individual_language = 'zlm'; + case Lianshan_Zhuang = 'zln'; + case Liuqian_Zhuang = 'zlq'; + case Zul = 'zlu'; + case Manda_Australia = 'zma'; + case Zimba = 'zmb'; + case Margany = 'zmc'; + case Maridan = 'zmd'; + case Mangerr = 'zme'; + case Mfinu = 'zmf'; + case Marti_Ke = 'zmg'; + case Makolkol = 'zmh'; + case Negeri_Sembilan_Malay = 'zmi'; + case Maridjabin = 'zmj'; + case Mandandanyi = 'zmk'; + case Matngala = 'zml'; + case Marimanindji = 'zmm'; + case Mbangwe = 'zmn'; + case Molo = 'zmo'; + case Mpuono = 'zmp'; + case Mituku = 'zmq'; + case Maranunggu = 'zmr'; + case Mbesa = 'zms'; + case Maringarr = 'zmt'; + case Muruwari = 'zmu'; + case Mbariman_Gudhinma = 'zmv'; + case Mbo_Democratic_Republic_of_Congo = 'zmw'; + case Bomitaba = 'zmx'; + case Mariyedi = 'zmy'; + case Mbandja = 'zmz'; + case Zan_Gula = 'zna'; + case Zande_individual_language = 'zne'; + case Mang = 'zng'; + case Manangkari = 'znk'; + case Mangas = 'zns'; + case Copainala_Zoque = 'zoc'; + case Chimalapa_Zoque = 'zoh'; + case Zou = 'zom'; + case Asuncion_Mixtepec_Zapotec = 'zoo'; + case Tabasco_Zoque = 'zoq'; + case Rayon_Zoque = 'zor'; + case Francisco_Leon_Zoque = 'zos'; + case Lachiguiri_Zapotec = 'zpa'; + case Yautepec_Zapotec = 'zpb'; + case Choapan_Zapotec = 'zpc'; + case Southeastern_Ixtlan_Zapotec = 'zpd'; + case Petapa_Zapotec = 'zpe'; + case San_Pedro_Quiatoni_Zapotec = 'zpf'; + case Guevea_De_Humboldt_Zapotec = 'zpg'; + case Totomachapan_Zapotec = 'zph'; + case Santa_Maria_Quiegolani_Zapotec = 'zpi'; + case Quiavicuzas_Zapotec = 'zpj'; + case Tlacolulita_Zapotec = 'zpk'; + case Lachixio_Zapotec = 'zpl'; + case Mixtepec_Zapotec = 'zpm'; + case Santa_Ines_Yatzechi_Zapotec = 'zpn'; + case Amatlan_Zapotec = 'zpo'; + case El_Alto_Zapotec = 'zpp'; + case Zoogocho_Zapotec = 'zpq'; + case Santiago_Xanica_Zapotec = 'zpr'; + case Coatlan_Zapotec = 'zps'; + case San_Vicente_Coatlan_Zapotec = 'zpt'; + case Yalalag_Zapotec = 'zpu'; + case Chichicapan_Zapotec = 'zpv'; + case Zaniza_Zapotec = 'zpw'; + case San_Baltazar_Loxicha_Zapotec = 'zpx'; + case Mazaltepec_Zapotec = 'zpy'; + case Texmelucan_Zapotec = 'zpz'; + case Qiubei_Zhuang = 'zqe'; + case Kara_Korea = 'zra'; + case Mirgan = 'zrg'; + case Zerenkel = 'zrn'; + case Zaparo = 'zro'; + case Zarphatic = 'zrp'; + case Mairasi = 'zrs'; + case Sarasira = 'zsa'; + case Kaskean = 'zsk'; + case Zambian_Sign_Language = 'zsl'; + case Standard_Malay = 'zsm'; + case Southern_Rincon_Zapotec = 'zsr'; + case Sukurum = 'zsu'; + case Elotepec_Zapotec = 'zte'; + case Xanaguia_Zapotec = 'ztg'; + case Lapaguia_Guivini_Zapotec = 'ztl'; + case San_Agustin_Mixtepec_Zapotec = 'ztm'; + case Santa_Catarina_Albarradas_Zapotec = 'ztn'; + case Loxicha_Zapotec = 'ztp'; + case Quioquitani_Quieri_Zapotec = 'ztq'; + case Tilquiapan_Zapotec = 'zts'; + case Tejalapan_Zapotec = 'ztt'; + case Guila_Zapotec = 'ztu'; + case Zaachila_Zapotec = 'ztx'; + case Yatee_Zapotec = 'zty'; + case Tokano = 'zuh'; + case Zulu = 'zul'; + case Kumzari = 'zum'; + case Zuni = 'zun'; + case Zumaya = 'zuy'; + case Zay = 'zwa'; + case No_linguistic_content = 'zxx'; + case Yongbei_Zhuang = 'zyb'; + case Yang_Zhuang = 'zyg'; + case Youjiang_Zhuang = 'zyj'; + case Yongnan_Zhuang = 'zyn'; + case Zyphe_Chin = 'zyp'; + case Zaza = 'zza'; + case Zuojiang_Zhuang = 'zzj'; + +} + +class LanguageName {} + +class BackedEnum { + static public function fromName(string $s, string $t):mixed { + return null; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10867.php b/tests/PHPStan/Analyser/data/bug-10867.php new file mode 100644 index 0000000000..82620c277c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10867.php @@ -0,0 +1,10 @@ + + +

+ $array */ + public function sayHello(array $array): void + { + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("array", $array); + } + + /** @param array $array */ + public function sayHello2(array $array): void + { + if (count($array) > 0) { + return; + } + + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("array{}", $array); + } + + /** @param array $array */ + public function sayHello3(array $array): void + { + if (count($array) === 0) { + return; + } + + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("non-empty-array", $array); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10952c.php b/tests/PHPStan/Analyser/data/bug-10952c.php new file mode 100644 index 0000000000..87d28e3827 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10952c.php @@ -0,0 +1,30 @@ +getString(); + + if ((strlen($string) > 1) === true) { + assertType('non-empty-string', $string); + } else { + assertType("string", $string); + } + + match (true) { + (strlen($string) > 1) => assertType('non-empty-string', $string), + default => assertType("string", $string), + }; + + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10979.php b/tests/PHPStan/Analyser/data/bug-10979.php new file mode 100644 index 0000000000..562d7b4eeb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10979.php @@ -0,0 +1,802 @@ + 'Guzzle', + self::PHPUnit => 'PHPUnit', + self::Monolog => 'Monolog', + self::PChart => 'pChart', + self::PHPStan => 'PHPStan', + self::PHPMailer => 'PHPMailer', + self::RespectValidation => 'RespectValidation', + self::Stripe => 'Stripe', + self::Ratchet => 'Ratchet', + self::Sentinel => 'Sentinel', + // Python + self::Matplotlib => 'Matplotlib', + self::Seaborn => 'Seaborn', + self::Selenium => 'Selenium', + self::OpenCV => 'OpenCV', + self::Keras => 'Keras', + self::PyTorch => 'PyTorch', + self::NumPy => 'NumPy', + self::Pandas => 'Pandas', + self::Plotly => 'Plotly', + // C++ + self::Cmake => 'CMake', + // Node.js + self::Playwright => 'Playwright', + + /** + * サーバーサイド(フレームワーク) + */ + // Laravel + self::LaravelScout => 'Laravel Scout', + self::LaravelCashier => 'Laravel Cashier', + self::LaravelJetstream => 'Laravel Jetstream', + self::LaravelSanctum => 'Laravel Sanctum', + // Java + self::SLF4J => 'SLF4J', + self::Mockito => 'Mockito', + self::OpenCSV => 'OpenCSV', + // Rails + self::Devise => 'Devise', + self::Capybara => 'Capybara', + + /** + * フロントエンド + */ + // JavaScript・TypeScript + self::JQuery => 'jQuery', + self::D3js => 'D3.js', + self::Lodash => 'Lodash', + self::Underscorejs => 'Underscore.js', + self::Animejs => 'Anime.js', + self::AnimateOnScroll => 'Animate On Scroll', + self::Videojs => 'Video.js', + self::Chartjs => 'Chart.js', + self::Cleavejs => 'Cleave.js', + self::FullPagejs => 'FullPage.js', + self::Leaflet => 'Leaflet', + self::Threejs => 'Three.js', + self::Screenfulljs => 'Screenfull.js', + self::Axios => 'Axios', + self::SocketIO => 'Socket.io', + self::TanStackQuery => 'TanStack Query', + self::Htmx => 'htmx', + self::GSAP => 'GSAP', + self::Swiper => 'Swiper', + self::EmblaCarousel => 'Embla Carousel', + self::Husky => 'husky', + self::MilionJs => 'Milion.js', + self::Biome => 'Biome', + self::Prettier => 'Prettier', + self::ESLint => 'ESLint', + self::SolidJS => 'SolidJS', + self::NextAuth => 'NextAuth', + self::InertiaJS => 'Inertia.js', + self::DrizzleORM => 'Drizzle ORM', // TS専用 + self::Zod => 'Zod', // TS専用 + self::TypeORM => 'TypeORM', // TS専用 + // Vue + self::VueChartjs => 'Vue Chart.js', + self::VeeValidate => 'VeeValidate', + self::VueDraggable => 'Vue Draggable', + self::Vuelidate => 'Vuelidate', + self::VueMultiselect => 'Vue Multiselect', + self::Vuex => 'Vuex', + self::Vuetify => 'Vuetify', + self::ElementUI => 'Element UI', + self::VueMaterial => 'Vue Material', + self::BootstrapVue => 'Bootstrap Vue', + self::Pinia => 'Pinia', + // React + self::Redux => 'Redux', + self::Tldraw => 'tldraw', + self::ShadcnUi => 'shadcn/ui', + self::MUI => 'MUI', + self::ChakraUI => 'Chakra UI', + self::Recoil => 'Recoil', + self::Jotai => 'Jotai', + self::Zustand => 'Zustand', + self::SWR => 'SWR', + self::ReactHookForm => 'React Hook Form', + self::RadixUI => 'Radix UI', + // Tailwind CSS + self::DaisyUI => 'DaisyUI', + + /** + * インフラ + */ + + /** + * モバイルアプリ + */ + + /** + * データベース + */ + self::DBeaver => 'DBeaver', + self::SequelPro => 'Sequel Pro', + self::SequelAce => 'Sequel Ace', + self::TablePlus => 'TablePlus', + self::Navicat => 'Navicat', + self::MySQLWorkbench => 'MySQL Workbench', + self::PHPMyAdmin => 'phpMyAdmin', + }; + } + + /** + * 関連する分野のIDを配列で返す + * ※複数の分野に関連する場合は複数のIDを返す + * + * @return array + */ + public function getFieldIds(): array + { + return match ($this) { + /** + * 複数に関連するライブラリ + */ + self::InertiaJS => [ + FieldMasterEnum::FrontEnd->value, + FieldMasterEnum::ServerSide->value, + ], + + /** + * サーバーサイド + */ + self::Guzzle, + self::PHPUnit, + self::Monolog, + self::PChart, + self::PHPStan, + self::PHPMailer, + self::RespectValidation, + self::Stripe, + self::Ratchet, + self::Sentinel, + self::Matplotlib, + self::Seaborn, + self::Selenium, + self::OpenCV, + self::Keras, + self::PyTorch, + self::NumPy, + self::Pandas, + self::Plotly, + self::Cmake, + self::LaravelScout, + self::LaravelCashier, + self::LaravelJetstream, + self::LaravelSanctum, + self::SLF4J, + self::Mockito, + self::OpenCSV, + self::Devise, + self::Capybara + => [ + FieldMasterEnum::ServerSide->value, + ], + + /** + * フロントエンド + */ + self::JQuery, + self::D3js, + self::Lodash, + self::Underscorejs, + self::Animejs, + self::AnimateOnScroll, + self::Videojs, + self::Chartjs, + self::Cleavejs, + self::FullPagejs, + self::Leaflet, + self::Threejs, + self::Screenfulljs, + self::Axios, + self::TypeORM, + self::VueChartjs, + self::VeeValidate, + self::VueDraggable, + self::Vuelidate, + self::VueMultiselect, + self::Vuex, + self::Vuetify, + self::ElementUI, + self::VueMaterial, + self::BootstrapVue, + self::SocketIO, + self::TanStackQuery, + self::Htmx, + self::Zod, + self::Redux, + self::Tldraw, + self::ShadcnUi, + self::MUI, + self::ChakraUI, + self::Recoil, + self::Jotai, + self::Zustand, + self::SWR, + self::ReactHookForm, + self::RadixUI, + self::GSAP, + self::Swiper, + self::EmblaCarousel, + self::Husky, + self::DrizzleORM, + self::MilionJs, + self::Biome, + self::Prettier, + self::ESLint, + self::DaisyUI, + self::Playwright, + self::Pinia, + self::SolidJS, + self::NextAuth + => [ + FieldMasterEnum::FrontEnd->value, + ], + + /** + * インフラ + */ + + /** + * モバイルアプリ + */ + + /** + * データベース + */ + self::DBeaver, + self::SequelPro, + self::SequelAce, + self::TablePlus, + self::Navicat, + self::MySQLWorkbench, + self::PHPMyAdmin + => [ + FieldMasterEnum::Database->value, + ], + }; + } + + /** + * 関連する言語・ツール、もしくはフレームワークのIDを配列で返す + * ※複数に関連する場合は複数のIDを返す + * + * @return array + */ + public function getMasterIds(): array + { + return match ($this) { + /** + * 複数に関連するライブラリ + */ + self::Stripe => [ + LanguageToolMasterEnum::PHP->value, + LanguageToolMasterEnum::Ruby->value, + LanguageToolMasterEnum::Java->value, + LanguageToolMasterEnum::Python->value, + LanguageToolMasterEnum::Go->value, + LanguageToolMasterEnum::NodeJS->value, + LanguageToolMasterEnum::JavaScript->value, + LanguageToolMasterEnum::TypeScript->value, + FrameworkMasterEnum::ReactNative->value, + ], + self::PyTorch => [ + LanguageToolMasterEnum::Python->value, + LanguageToolMasterEnum::CPlusPlus->value, + ], + self::OpenCV => [ + LanguageToolMasterEnum::Python->value, + LanguageToolMasterEnum::CPlusPlus->value, + LanguageToolMasterEnum::Java->value, + ], + self::InertiaJS => [ + FrameworkMasterEnum::Vue->value, + FrameworkMasterEnum::React->value, + FrameworkMasterEnum::Svelte->value, + FrameworkMasterEnum::Laravel->value, + FrameworkMasterEnum::RubyOnRails->value, + ], + + /** + * サーバーサイド(言語・ツール) + */ + // PHP + self::Guzzle, + self::PHPUnit, + self::Monolog, + self::PChart, + self::PHPStan, + self::PHPMailer, + self::RespectValidation, + self::Ratchet, + self::Sentinel => [ + LanguageToolMasterEnum::PHP->value, + ], + // C++ + self::Cmake => [ + LanguageToolMasterEnum::CPlusPlus->value, + ], + // Python + self::Matplotlib, + self::Seaborn, + self::Selenium, + self::Keras, + self::NumPy, + self::Pandas, + self::Plotly => [ + LanguageToolMasterEnum::Python->value, + ], + // Java + self::SLF4J, + self::Mockito, + self::OpenCSV => [ + LanguageToolMasterEnum::Java->value, + ], + // Node.js + self::Playwright => [ + LanguageToolMasterEnum::NodeJS->value, + ], + + /** + * サーバーサイド(フレームワーク) + */ + // Laravel + self::LaravelScout, + self::LaravelCashier, + self::LaravelJetstream, + self::LaravelSanctum => [ + FrameworkMasterEnum::Laravel->value, + ], + // Rails + self::Devise, + self::Capybara => [ + FrameworkMasterEnum::RubyOnRails->value, + ], + + /** + * フロントエンド + */ + // JavaScript・TypeScript + self::JQuery, + self::D3js, + self::Lodash, + self::Underscorejs, + self::Animejs, + self::AnimateOnScroll, + self::Videojs, + self::Chartjs, + self::Cleavejs, + self::FullPagejs, + self::Leaflet, + self::Threejs, + self::Screenfulljs, + self::Axios, + self::SocketIO, + self::TanStackQuery, + self::Htmx, + self::GSAP, + self::Swiper, + self::EmblaCarousel, + self::Husky, + self::Biome, + self::Prettier, + self::ESLint, + self::SolidJS, + self::NextAuth + => [ + LanguageToolMasterEnum::JavaScript->value, + LanguageToolMasterEnum::TypeScript->value, + ], + // TypeScript + self::Zod, + self::TypeORM, + self::DrizzleORM + => [ + LanguageToolMasterEnum::TypeScript->value, + ], + // Vue + self::VueChartjs, + self::VeeValidate, + self::VueDraggable, + self::Vuelidate, + self::VueMultiselect, + self::Vuex, + self::Vuetify, + self::ElementUI, + self::VueMaterial, + self::BootstrapVue, + self::Pinia + => [ + FrameworkMasterEnum::Vue->value, + ], + // React + self::Redux, + self::Tldraw, + self::ShadcnUi, + self::MUI, + self::ChakraUI, + self::Recoil, + self::Jotai, + self::Zustand, + self::SWR, + self::ReactHookForm, + self::RadixUI, + self::MilionJs + => [ + FrameworkMasterEnum::React->value, + ], + // Tailwind CSS + self::DaisyUI => [ + FrameworkMasterEnum::TailwindCSS->value, + ], + + /** + * インフラ + */ + + /** + * モバイルアプリ + */ + + /** + * データベース + */ + self::DBeaver => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::PostgreSQL->value, + LanguageToolMasterEnum::SQLite->value, + LanguageToolMasterEnum::OracleDatabase->value, + LanguageToolMasterEnum::Db2->value, + LanguageToolMasterEnum::SQLServer->value, + LanguageToolMasterEnum::FirebirdSQL->value, + LanguageToolMasterEnum::MongoDB->value, + LanguageToolMasterEnum::ApacheCassandra->value, + LanguageToolMasterEnum::Redis->value, + LanguageToolMasterEnum::BigQuery->value, + LanguageToolMasterEnum::AmazonDynamoDB->value, + ], + self::SequelPro, + self::SequelAce => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + ], + self::TablePlus => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::PostgreSQL->value, + LanguageToolMasterEnum::SQLite->value, + LanguageToolMasterEnum::OracleDatabase->value, + LanguageToolMasterEnum::Redis->value, + LanguageToolMasterEnum::ApacheCassandra->value, + LanguageToolMasterEnum::MongoDB->value, + LanguageToolMasterEnum::MariaDB->value, + LanguageToolMasterEnum::SQLServer->value, + LanguageToolMasterEnum::BigQuery->value, + LanguageToolMasterEnum::CockroachDB->value, + ], + self::Navicat => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::PostgreSQL->value, + LanguageToolMasterEnum::SQLite->value, + LanguageToolMasterEnum::OracleDatabase->value, + LanguageToolMasterEnum::SQLServer->value, + LanguageToolMasterEnum::MariaDB->value, + ], + self::MySQLWorkbench => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + ], + self::PHPMyAdmin => [ + LanguageToolMasterEnum::SQL->value, + LanguageToolMasterEnum::MySQL->value, + LanguageToolMasterEnum::MariaDB->value, + ], + }; + } + + /** + * `language_tool` or `framework`いずれかのtypeを返す + * + * @return LibraryRelationTypeEnum + */ + public function getType(): LibraryRelationTypeEnum + { + return match ($this) { + /** + * 言語・ツール&フレームワークどちらにも関連するライブラリ + */ + + /** + * 言語・ツール + */ + self::Guzzle, + self::PHPUnit, + self::Monolog, + self::PChart, + self::PHPStan, + self::PHPMailer, + self::RespectValidation, + self::Stripe, + self::Ratchet, + self::Sentinel, + self::Matplotlib, + self::Seaborn, + self::Selenium, + self::OpenCV, + self::Keras, + self::PyTorch, + self::NumPy, + self::Pandas, + self::Plotly, + self::Cmake, + self::SLF4J, + self::Mockito, + self::OpenCSV, + self::JQuery, + self::D3js, + self::Lodash, + self::Underscorejs, + self::Animejs, + self::AnimateOnScroll, + self::Videojs, + self::Chartjs, + self::Cleavejs, + self::FullPagejs, + self::Leaflet, + self::Threejs, + self::Screenfulljs, + self::Axios, + self::SocketIO, + self::Htmx, + self::TanStackQuery, + self::Zod, + self::TypeORM, + self::DBeaver, + self::SequelPro, + self::SequelAce, + self::TablePlus, + self::Navicat, + self::MySQLWorkbench, + self::PHPMyAdmin, + self::GSAP, + self::Swiper, + self::EmblaCarousel, + self::Husky, + self::DrizzleORM, + self::MilionJs, + self::Biome, + self::Prettier, + self::ESLint, + self::DaisyUI, + self::Playwright, + self::SolidJS, + self::NextAuth + => LibraryRelationTypeEnum::LanguageTool, + + /** + * フレームワーク + */ + self::LaravelScout, + self::LaravelCashier, + self::LaravelJetstream, + self::LaravelSanctum, + self::VueChartjs, + self::VeeValidate, + self::VueDraggable, + self::Vuelidate, + self::VueMultiselect, + self::Vuex, + self::Vuetify, + self::ElementUI, + self::VueMaterial, + self::BootstrapVue, + self::Redux, + self::Tldraw, + self::Devise, + self::Capybara, + self::ShadcnUi, + self::MUI, + self::ChakraUI, + self::Recoil, + self::Jotai, + self::Zustand, + self::SWR, + self::ReactHookForm, + self::RadixUI, + self::Pinia, + self::InertiaJS + => LibraryRelationTypeEnum::Framework, + }; + } + + /** + * カテゴリIDがライブラリのIDかどうか判定 + * + * @param int $categoryId + * @return bool + */ + public static function isLibraryCategoryId(int $categoryId): bool + { + foreach (self::cases() as $case) { + if ($categoryId === $case->value) { + return true; + } + } + return false; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10980.php b/tests/PHPStan/Analyser/data/bug-10980.php new file mode 100644 index 0000000000..97e04e83e5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10980.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug10985; + +use function PHPStan\Testing\assertType; + +enum Test { + case ORIGINAL; +} + +function (): void { + $item = Test::class; + $result = ($item)::ORIGINAL; + assertType('Bug10985\\Test::ORIGINAL', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-11009.php b/tests/PHPStan/Analyser/data/bug-11009.php new file mode 100644 index 0000000000..1eea19fe18 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11009.php @@ -0,0 +1,45 @@ +returnStatic()); + assertType(B::class, $b->returnSelf()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-11009.stub b/tests/PHPStan/Analyser/data/bug-11009.stub new file mode 100644 index 0000000000..dce4347bb4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11009.stub @@ -0,0 +1,21 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug11263; + +enum FirstEnum: string +{ + + case XyzSaturdayStopDomestic = 'XYZ_DOMESTIC_300'; + case XyzSaturdayDeliveryAir = 'XYZ_AIR_300'; + case XyzAdditionalHandling = 'XYZ_100'; + case XyzCommercialDomesticAirDeliveryArea = 'XYZ_COMMERCIAL_AIR_376'; + case XyzCommercialDomesticAirExtendedDeliveryArea = 'XYZ_COMMERCIAL_AIR_EXTENDED_376'; + case XyzCommercialDomesticGroundDeliveryArea = 'XYZ_COMMERCIAL_GROUND_376'; + case XyzCommercialDomesticGroundExtendedDeliveryArea = 'XYZ_COMMERCIAL_GROUND_EXTENDED_376'; + case XyzResidentialDomesticAirDeliveryArea = 'XYZ_RESIDENTIAL_AIR_376'; + case XyzResidentialDomesticAirExtendedDeliveryArea = 'XYZ_RESIDENTIAL_AIR_EXTENDED_376'; + case XyzResidentialDomesticGroundDeliveryArea = 'XYZ_RESIDENTIAL_GROUND_376'; + case XyzResidentialDomesticGroundExtendedDeliveryArea = 'XYZ_RESIDENTIAL_GROUND_EXTENDED_376'; + case XyzDeliveryAreaSurchargeSurePost = 'XYZ_SURE_POST_376'; + case XyzDeliveryAreaSurchargeSurePostExtended = 'XYZ_SURE_POST_EXTENDED_376'; + case XyzDeliveryAreaSurchargeOther = 'XYZ_DELIVERY_AREA_OTHER_376'; + case XyzResidentialSurchargeAir = 'XYZ_RESIDENTIAL_SURCHARGE_AIR_270'; + case XyzResidentialSurchargeGround = 'XYZ_RESIDENTIAL_SURCHARGE_GROUND_270'; + case XyzSurchargeCodeFuel = 'XYZ_375'; + case XyzCod = 'XYZ_COD_110'; + case XyzDeliveryConfirmation = 'XYZ_DELIVERY_CONFIRMATION_120'; + case XyzShipDeliveryConfirmation = 'XYZ_SHIP_DELIVERY_CONFIRMATION_121'; + case XyzExtendedArea = 'XYZ_EXTENDED_AREA_190'; + case XyzHazMat = 'XYZ_HAZ_MAT_199'; + case XyzDryIce = 'XYZ_DRY_ICE_200'; + case XyzIscSeeds = 'XYZ_ISC_SEEDS_201'; + case XyzIscPerishables = 'XYZ_ISC_PERISHABLES_202'; + case XyzIscTobacco = 'XYZ_ISC_TOBACCO_203'; + case XyzIscPlants = 'XYZ_ISC_PLANTS_204'; + case XyzIscAlcoholicBeverages = 'XYZ_ISC_ALCOHOLIC_BEVERAGES_205'; + case XyzIscBiologicalSubstances = 'XYZ_ISC_BIOLOGICAL_SUBSTANCES_206'; + case XyzIscSpecialExceptions = 'XYZ_ISC_SPECIAL_EXCEPTIONS_207'; + case XyzHoldForPickup = 'XYZ_HOLD_FOR_PICKUP_220'; + case XyzOriginCertificate = 'XYZ_ORIGIN_CERTIFICATE_240'; + case XyzPrintReturnLabel = 'XYZ_PRINT_RETURN_LABEL_250'; + case XyzExportLicenseVerification = 'XYZ_EXPORT_LICENSE_VERIFICATION_258'; + case XyzPrintNMail = 'XYZ_PRINT_N_MAIL_260'; + case XyzReturnService1attempt = 'XYZ_RETURN_SERVICE_1ATTEMPT_280'; + case XyzReturnService3attempt = 'XYZ_RETURN_SERVICE_3ATTEMPT_290'; + case XyzSaturdayInternationalProcessingFee = 'XYZ_SATURDAY_INTERNATIONAL_PROCESSING_FEE_310'; + case XyzElectronicReturnLabel = 'XYZ_ELECTRONIC_RETURN_LABEL_350'; + case XyzPreparedSedForm = 'XYZ_PREPARED_SED_FORM_374'; + case XyzLargePackage = 'XYZ_LARGE_PACKAGE_377'; + case XyzShipperPaysDutyTax = 'XYZ_SHIPPER_PAYS_DUTY_TAX_378'; + case XyzShipperPaysDutyTaxUnpaid = 'XYZ_SHIPPER_PAYS_DUTY_TAX_UNPAID_379'; + case XyzExpressPlusSurcharge = 'XYZ_EXPRESS_PLUS_SURCHARGE_380'; + case XyzInsurance = 'XYZ_INSURANCE_400'; + case XyzShipAdditionalHandling = 'XYZ_SHIP_ADDITIONAL_HANDLING_401'; + case XyzShipperRelease = 'XYZ_SHIPPER_RELEASE_402'; + case XyzCheckToShipper = 'XYZ_CHECK_TO_SHIPPER_403'; + case XyzProactiveResponse = 'XYZ_PROACTIVE_RESPONSE_404'; + case XyzGermanPickup = 'XYZ_GERMAN_PICKUP_405'; + case XyzGermanRoadTax = 'XYZ_GERMAN_ROAD_TAX_406'; + case XyzExtendedAreaPickup = 'XYZ_EXTENDED_AREA_PICKUP_407'; + case XyzReturnOfDocument = 'XYZ_RETURN_OF_DOCUMENT_410'; + case XyzPeakSeason = 'XYZ_PEAK_SEASON_430'; + case XyzLargePackageSeasonalSurcharge = 'XYZ_LARGE_PACKAGE_SEASONAL_SURCHARGE_431'; + case XyzAdditionalHandlingSeasonalSurchargeDiscontinued = 'XYZ_ADDITIONAL_HANDLING_SEASONAL_SURCHARGE_432'; + case XyzShipLargePackage = 'XYZ_SHIP_LARGE_PACKAGE_440'; + case XyzCarbonNeutral = 'XYZ_CARBON_NEUTRAL_441'; + case XyzImportControl = 'XYZ_IMPORT_CONTROL_444'; + case XyzCommercialInvoiceRemoval = 'XYZ_COMMERCIAL_INVOICE_REMOVAL_445'; + case XyzImportControlElectronicLabel = 'XYZ_IMPORT_CONTROL_ELECTRONIC_LABEL_446'; + case XyzImportControlPrintLabel = 'XYZ_IMPORT_CONTROL_PRINT_LABEL_447'; + case XyzImportControlPrintAndMailLabel = 'XYZ_IMPORT_CONTROL_PRINT_AND_MAIL_LABEL_448'; + case XyzImportControlOnePickupAttemptLabel = 'XYZ_IMPORT_CONTROL_ONE_PICKUP_ATTEMPT_LABEL_449'; + case XyzImportControlThreePickUpAttemptLabel = 'XYZ_IMPORT_CONTROL_THREE_PICK_UP_ATTEMPT_LABEL_450'; + case XyzRefrigeration = 'XYZ_REFRIGERATION_452'; + case XyzExchangePrintReturnLabel = 'XYZ_EXCHANGE_PRINT_RETURN_LABEL_464'; + case XyzCommittedDeliveryWindow = 'XYZ_COMMITTED_DELIVERY_WINDOW_470'; + case XyzSecuritySurcharge = 'XYZ_SECURITY_SURCHARGE_480'; + case XyzNonMachinableCharge = 'XYZ_NON_MACHINABLE_CHARGE_490'; + case XyzCustomerTransactionFee = 'XYZ_CUSTOMER_TRANSACTION_FEE_492'; + case XyzSurePostNonStandardLength = 'XYZ_493'; + case XyzSurePostNonStandardExtraLength = 'XYZ_494'; + case XyzSurePostNonStandardCube = 'XYZ_NON_STANDARD_CUBE_CHARGE_495'; + case XyzShipmentCod = 'XYZ_SHIPMENT_COD_500'; + case XyzLiftGateForPickup = 'XYZ_LIFT_GATE_FOR_PICKUP_510'; + case XyzLiftGateForDelivery = 'XYZ_LIFT_GATE_FOR_DELIVERY_511'; + case XyzDropOffAtXyzFacility = 'XYZ_DROP_OFF_AT_XYZ_FACILITY_512'; + case XyzPremiumCare = 'XYZ_PREMIUM_CARE_515'; + case XyzOversizePallet = 'XYZ_OVERSIZE_PALLET_520'; + case XyzFreightDeliverySurcharge = 'XYZ_FREIGHT_DELIVERY_SURCHARGE_530'; + case XyzFreightPickxyzurcharge = 'XYZ_FREIGHT_PICKUP_SURCHARGE_531'; + case XyzDirectToRetail = 'XYZ_DIRECT_TO_RETAIL_540'; + case XyzDirectDeliveryOnly = 'XYZ_DIRECT_DELIVERY_ONLY_541'; + case XyzNoAccessPoint = 'XYZ_NO_ACCESS_POINT_541'; + case XyzDeliverToAddresseeOnly = 'XYZ_DELIVER_TO_ADDRESSEE_ONLY_542'; + case XyzDirectToRetailCod = 'XYZ_DIRECT_TO_RETAIL_COD_543'; + case XyzRetailAccessPoint = 'XYZ_RETAIL_ACCESS_POINT_544'; + case XyzElectronicPackageReleaseAuthentication = 'XYZ_ELECTRONIC_PACKAGE_RELEASE_AUTHENTICATION_546'; + case XyzPayAtStore = 'XYZ_PAY_AT_STORE_547'; + case XyzInsideDelivery = 'XYZ_INSIDE_DELIVERY_549'; + case XyzItemDisposal = 'XYZ_ITEM_DISPOSAL_550'; + case XyzAddressCorrections = 'XYZ_ADDRESS_CORRECTIONS'; + case XyzNotPreviouslyBilledFee = 'XYZ_NOT_PREVIOUSLY_BILLED_FEE'; + case XyzPickxyzurcharge = 'XYZ_PICKUP_SURCHARGE'; + case XyzChargeback = 'XYZ_CHARGEBACK'; + case XyzAdditionalHandlingPeakDemand = 'XYZ_ADDITIONAL_HANDLING_PEAK_DEMAND'; + case XyzOtherSurcharge = 'XYZ_OTHER_SURCHARGE'; + + case XyzRemoteAreaSurcharge = 'XYZ_REMOTE_AREA_SURCHARGE'; + case XyzRemoteAreaOtherSurcharge = 'XYZ_REMOTE_AREA_OTHER_SURCHARGE'; + + case CompanyEconomyResidentialSurchargeLightweight = 'COMPANY_ECONOMY_RESIDENTIAL_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyDeliverySurchargeLightweight = 'COMPANY_ECONOMY_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyExtendedDeliverySurchargeLightweight = 'COMPANY_ECONOMY_EXTENDED_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardResidentialSurchargeLightweight = 'COMPANY_STANDARD_RESIDENTIAL_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardDeliverySurchargeLightweight = 'COMPANY_STANDARD_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardExtendedDeliverySurchargeLightweight = 'COMPANY_STANDARD_EXTENDED_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyResidentialSurchargePlus = 'COMPANY_ECONOMY_RESIDENTIAL_SURCHARGE_PLUS'; + case CompanyEconomyDeliverySurchargePlus = 'COMPANY_ECONOMY_DELIVERY_SURCHARGE_PLUS'; + case CompanyEconomyExtendedDeliverySurchargePlus = 'COMPANY_ECONOMY_EXTENDED_DELIVERY_SURCHARGE_PLUS'; + case CompanyEconomyPeakSurchargeLightweight = 'COMPANY_ECONOMY_PEAK_SURCHARGE_LIGHTWEIGHT'; + case CompanyEconomyPeakSurchargePlus = 'COMPANY_ECONOMY_PEAK_SURCHARGE_PLUS'; + case CompanyEconomyPeakSurchargeOver5Lbs = 'COMPANY_ECONOMY_PEAK_SURCHARGE_OVER_5_LBS'; + case CompanyStandardResidentialSurchargePlus = 'COMPANY_STANDARD_RESIDENTIAL_SURCHARGE_PLUS'; + case CompanyStandardDeliverySurchargePlus = 'COMPANY_STANDARD_DELIVERY_SURCHARGE_PLUS'; + case CompanyStandardExtendedDeliverySurchargePlus = 'COMPANY_STANDARD_EXTENDED_DELIVERY_SURCHARGE_PLUS'; + case CompanyStandardPeakSurchargeLightweight = 'COMPANY_STANDARD_PEAK_SURCHARGE_LIGHTWEIGHT'; + case CompanyStandardPeakSurchargePlus = 'COMPANY_STANDARD_PEAK_SURCHARGE_PLUS'; + case CompanyStandardPeakSurchargeOver5Lbs = 'COMPANY_STANDARD_PEAK_SURCHARGE_OVER_5_LBS'; + + case Company2DayResidentialSurcharge = 'COMPANY_2_DAY_RESIDENTIAL_SURCHARGE'; + case Company2DayDeliverySurcharge = 'COMPANY_2_DAY_DELIVERY_SURCHARGE'; + case Company2DayExtendedDeliverySurcharge = 'COMPANY_2_DAY_EXTENDED_DELIVERY_SURCHARGE'; + case Company2DayPeakSurcharge = 'COMPANY_2_DAY_PEAK_SURCHARGE'; + + case CompanyHazmatResidentialSurcharge = 'COMPANY_HAZMAT_RESIDENTIAL_SURCHARGE'; + case CompanyHazmatResidentialSurchargeLightweight = 'COMPANY_HAZMAT_RESIDENTIAL_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatDeliverySurcharge = 'COMPANY_HAZMAT_DELIVERY_SURCHARGE'; + case CompanyHazmatDeliverySurchargeLightweight = 'COMPANY_HAZMAT_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatExtendedDeliverySurcharge = 'COMPANY_HAZMAT_EXTENDED_DELIVERY_SURCHARGE'; + case CompanyHazmatExtendedDeliverySurchargeLightweight = 'COMPANY_HAZMAT_EXTENDED_DELIVERY_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatPeakSurchargeLightweight = 'COMPANY_HAZMAT_PEAK_SURCHARGE_LIGHTWEIGHT'; + case CompanyHazmatPeakSurcharge = 'COMPANY_HAZMAT_PEAK_SURCHARGE'; + case CompanyHazmatPeakSurchargePlus = 'COMPANY_HAZMAT_PEAK_SURCHARGE_PLUS'; + case CompanyHazmatPeakSurchargeOver5Lbs = 'COMPANY_HAZMAT_PEAK_SURCHARGE_OVER_5_LBS'; + + case CompanyFuelSurcharge = 'COMPANY_FUEL_SURCHARGE'; + + case Company2DayAdditionalHandlingSurchargeDimensions = 'COMPANY_2DAY_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case Company2DayAdditionalHandlingSurchargeWeight = 'COMPANY_2DAY_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case CompanyStandardAdditionalHandlingSurchargeDimensions = 'COMPANY_STANDARD_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case CompanyStandardAdditionalHandlingSurchargeWeight = 'COMPANY_STANDARD_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case CompanyEconomyAdditionalHandlingSurchargeDimensions = 'COMPANY_ECONOMY_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case CompanyEconomyAdditionalHandlingSurchargeWeight = 'COMPANY_ECONOMY_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case CompanyHazmatAdditionalHandlingSurchargeDimensions = 'COMPANY_HAZMAT_ADDITIONAL_HANDLING_SURCHARGE_DIMENSIONS'; + case CompanyHazmatAdditionalHandlingSurchargeWeight = 'COMPANY_HAZMAT_ADDITIONAL_HANDLING_SURCHARGE_WEIGHT'; + case Company2DayLargePackageSurcharge = 'COMPANY_2DAY_LARGE_PACKAGE_SURCHARGE'; + case CompanyStandardLargePackageSurcharge = 'COMPANY_STANDARD_LARGE_PACKAGE_SURCHARGE'; + case CompanyEconomyLargePackageSurcharge = 'COMPANY_ECONOMY_LARGE_PACKAGE_SURCHARGE'; + case CompanyHazmatLargePackageSurcharge = 'COMPANY_HAZMAT_LARGE_PACKAGE_SURCHARGE'; + case CompanyLargePackagePeakSurcharge = 'COMPANY_LARGE_PACKAGE_PEAK_SURCHARGE'; + + case CompanyUkSignatureSurcharge = 'COMPANY_UK_SIGNATURE_SURCHARGE'; + + case AciFuelSurcharge = 'ACI_FUEL_SURCHARGE'; + case AciUnmanifestedSurcharge = 'ACI_UNMANIFESTED_SURCHARGE'; + case AciPeakSurcharge = 'ACI_PEAK_SURCHARGE'; + case AciOversizeSurcharge = 'ACI_OVERSIZE_SURCHARGE'; + case AciUltraUrbanSurcharge = 'ACI_ULTRA_URBAN_SURCHARGE'; + case AciNonStandardSurcharge = 'ACI_NON_STANDARD_SURCHARGE'; + + case XyzmiFuelSurcharge = 'XYZMI_FUEL_SURCHARGE'; + case XyzmiNonStandardSurcharge = 'XYZMI_NON_STANDARD_SURCHARGE'; + + case FooEcommerceFuelSurcharge = 'FOO_ECOMMERCE_FUEL_SURCHARGE'; + case FooEcommercePeakSurcharge = 'FOO_ECOMMERCE_PEAK_SURCHARGE'; + case FooEcommerceOversizeSurcharge = 'FOO_ECOMMERCE_OVERSIZE_SURCHARGE'; + case FooEcommerceFutureUseSurcharge = 'FOO_ECOMMERCE_FUTURE_USE_SURCHARGE'; + case FooEcommerceDimLengthSurcharge = 'FOO_ECOMMERCE_DIM_LENGTH_SURCHARGE'; + + case HooFuelSurcharge = 'LASER_SHIP_FUEL_SURCHARGE'; + case HooResidentialSurcharge = 'LASER_SHIP_RESIDENTIAL_SURCHARGE'; + case HooDeliverySurcharge = 'LASER_SHIP_DELIVERY_SURCHARGE'; + case HooExtendedDeliverySurcharge = 'LASER_SHIP_EXTENDED_DELIVERY_SURCHARGE'; + + case MooSmartPostFuelSurcharge = 'MOO_SMART_POST_FUEL_SURCHARGE'; + case MooSmartPostDeliverySurcharge = 'MOO_SMART_POST_DELIVERY_SURCHARGE'; + case MooSmartPostExtendedDeliverySurcharge = 'MOO_SMART_POST_EXTENDED_DELIVERY_SURCHARGE'; + case MooSmartPostNonMachinableSurcharge = 'MOO_SMART_POST_NON_MACHINABLE_SURCHARGE'; + + case MooAdditionalHandlingDomesticDimensionSurcharge = 'MOO_ADDITIONAL_HANDLING_DOMESTIC_DIMENSION_SURCHARGE'; + case MooOversizeSurcharge = 'MOO_OVERSIZE_SURCHARGE'; + + case MooAdditionalHandling = 'MOO_ADDITIONAL_HANDLING'; + case MooAdditionalHandlingChargeDimensions = 'MOO_ADDITIONAL_HANDLING_CHARGE_DIMENSIONS'; + case MooAdditionalHandlingChargePackage = 'MOO_ADDITIONAL_HANDLING_CHARGE_PACKAGE'; + case MooAdditionalHandlingChargeWeight = 'MOO_ADDITIONAL_HANDLING_CHARGE_WEIGHT'; + case MooAdditionalWeightCharge = 'MOO_ADDITIONAL_WEIGHT_CHARGE'; + case MooAdvancementFee = 'MOO_ADVANCEMENT_FEE'; + case MooAhsDimensions = 'MOO_AHS_DIMENSIONS'; + case MooAhsWeight = 'MOO_AHS_WEIGHT'; + case MooAlaskaOrHawaiiOrPuertoRicoPkupOrDel = 'MOO_ALASKA/HAWAII/PUERTO_RICO_PKUP/DEL'; + case MooAppointment = 'MOO_APPOINTMENT'; + case MooBox24X24X18DblWalledProductQuantity2 = 'MOO_BOX_24_X_24_X_18_DBL_WALLED_PRODUCT_QUANTITY_2'; + case MooBox28X28X28DblWalledProductQuantity3 = 'MOO_BOX_28_X_28_X_28_DBL_WALLED_PRODUCT_QUANTITY_3'; + case MooBoxMultiDepth22X22X22ProductQuantity7 = 'MOO_BOX_MULTI_DEPTH_22_X_22_X_22_PRODUCT_QUANTITY_7'; + case MooBrokerDocumentTransferFee = 'MOO_BROKER_DOCUMENT_TRANSFER_FEE'; + case MooCallTag = 'MOO_CALL_TAG'; + case MooCustomsOvertimeFee = 'MOO_CUSTOMS_OVERTIME_FEE'; + case MooDasAlaskaComm = 'MOO_DAS_ALASKA_COMM'; + case MooDasAlaskaResi = 'MOO_DAS_ALASKA_RESI'; + case MooDasComm = 'MOO_DAS_COMM'; + case MooDasExtendedComm = 'MOO_DAS_EXTENDED_COMM'; + case MooDasExtendedResi = 'MOO_DAS_EXTENDED_RESI'; + case MooDasHawaiiComm = 'MOO_DAS_HAWAII_COMM'; + case MooDasHawaiiResi = 'MOO_DAS_HAWAII_RESI'; + case MooDasRemoteComm = 'MOO_DAS_REMOTE_COMM'; + case MooDasRemoteResi = 'MOO_DAS_REMOTE_RESI'; + case MooDasResi = 'MOO_DAS_RESI'; + case MooDateCertain = 'MOO_DATE_CERTAIN'; + case MooDeclaredValue = 'MOO_DECLARED_VALUE'; + case MooDeclaredValueCharge = 'MOO_DECLARED_VALUE_CHARGE'; + case MooDeliveryAndReturns = 'MOO_DELIVERY_AND_RETURNS'; + case MooDeliveryAreaSurcharge = 'MOO_DELIVERY_AREA_SURCHARGE'; + case MooDeliveryAreaSurchargeAlaska = 'MOO_DELIVERY_AREA_SURCHARGE_ALASKA'; + case MooDeliveryAreaSurchargeExtended = 'MOO_DELIVERY_AREA_SURCHARGE_EXTENDED'; + case MooDeliveryAreaSurchargeHawaii = 'MOO_DELIVERY_AREA_SURCHARGE_HAWAII'; + case MooElectronicEntryForFormalEntry = 'MOO_ELECTRONIC_ENTRY_FOR_FORMAL_ENTRY'; + case MooEvening = 'MOO_EVENING'; + case MooExtendedDeliveryArea = 'MOO_EXTENDED_DELIVERY_AREA'; + case MooFoodAndDrugAdministrationClearance = 'MOO_FOOD_AND_DRUG_ADMINISTRATION_CLEARANCE'; + case MooFragileLarge20X20X12ProductQuantity2 = 'MOO_FRAGILE_LARGE_20_X_20_X_12_PRODUCT_QUANTITY_2'; + case MooFragileLarge23X17X12ProductQuantity1 = 'MOO_FRAGILE_LARGE_23_X_17_X_12_PRODUCT_QUANTITY_1'; + case MooFreeTradeZone = 'MOO_FREE_TRADE_ZONE'; + case MooFuelSurcharge = 'MOO_FUEL_SURCHARGE'; + case MooHandlingFee = 'MOO_HANDLING_FEE'; + case MooHoldForPickup = 'MOO_HOLD_FOR_PICKUP'; + case MooImportPermitsAndLicensesFee = 'MOO_IMPORT_PERMITS_AND_LICENSES_FEE'; + case MooPeakAhsCharge = 'MOO_PEAK_AHS_CHARGE'; + case MooResidential = 'MOO_RESIDENTIAL'; + case MooOversizeCharge = 'MOO_OVERSIZE_CHARGE'; + case MooPeakOversizeSurcharge = 'MOO_PEAK_OVERSIZE_CHARGE'; + case MooAdditionalVat = 'MOO_ADDITIONAL_VAT'; + case MooGstOnDisbOrAncillaryServiceFees = 'MOO_GST_ON_DISB_OR_ANCILLARY_SERVICE_FEES'; + case MooHstOnAdvOrAncillaryServiceFees = 'MOO_HST_ON_ADV_OR_ANCILLARY_SERVICE_FEES'; + case MooHstOnDisbOrAncillaryServiceFees = 'MOO_HST_ON_DISB_OR_ANCILLARY_SERVICE_FEES'; + case MooIndiaCgst = 'MOO_INDIA_CGST'; + case MooIndiaSgst = 'MOO_INDIA_SGST'; + case MooMooAdditionalVat = 'MOO_MOO_ADDITIONAL_VAT'; + case MooMooAdditionalDuty = 'MOO_MOO_ADDITIONAL_DUTY'; + case MooEgyptVatOnFreight = 'MOO_EGYPT_VAT_ON_FREIGHT'; + case MooDutyAndTaxAmendmentFee = 'MOO_DUTY_AND_TAX_AMENDMENT_FEE'; + case MooCustomsDuty = 'MOO_CUSTOMS_DUTY'; + case MooCstAdditionalDuty = 'MOO_CST_ADDITIONAL_DUTY'; + case MooChinaVatDutyOrTax = 'MOO_CHINA_VAT_DUTY_OR_TAX'; + case MooArgentinaExportDuty = 'MOO_ARGENTINA_EXPORT_DUTY'; + case MooAustraliaGst = 'MOO_AUSTRALIA_GST'; + case MooBhFreightVat = 'MOO_BH_FREIGHT_VAT'; + case MooCanadaGst = 'MOO_CANADA_GST'; + case MooCanadaHst = 'MOO_CANADA_HST'; + case MooCanadaHstNb = 'MOO_CANADA_HST_NB'; + case MooCanadaHstOn = 'MOO_CANADA_HST_ON'; + case MooGstSingapore = 'MOO_GST_SINGAPORE'; + case MooBritishColumbiaPst = 'MOO_BRITISH_COLUMBIA_PST'; + case MooDisbursementFee = 'MOO_DISBURSEMENT_FEE'; + case MooVatOnDisbursementFee = 'MOO_VAT_ON_DISBURSEMENT_FEE'; + case MooResidentialRuralZone = 'MOO_RESIDENTIAL_RURAL_ZONE'; + case MooOriginalVat = 'MOO_ORIGINAL_VAT'; + case MooMexicoIvaFreight = 'MOO_MEXICO_IVA_FREIGHT'; + case MooOtherGovernmentAgencyFee = 'MOO_OTHER_GOVERNMENT_AGENCY_FEE'; + case MooCustodyFee = 'MOO_CUSTODY_FEE'; + case MooProcessingFee = 'MOO_PROCESSING_FEE'; + case MooStorageFee = 'MOO_STORAGE_FEE'; + case MooIndividualFormalEntry = 'MOO_INDIVIDUAL_FORMAL_ENTRY'; + case MooRebillDuty = 'MOO_REBILL_DUTY'; + case MooClearanceEntryFee = 'MOO_CLEARANCE_ENTRY_FEE'; + case MooCustomsClearanceFee = 'MOO_CUSTOMS_CLEARANCE_FEE'; + case MooRebillVAT = 'MOO_REBILL_VAT'; + case MooIdVatOnAncillaries = 'MOO_ID_VAT_ON_ANCILLARIES'; + + case MooPeakSurcharge = 'MOO_PEAK_CHARGE'; + case MooOutOfDeliveryAreaTier = 'MOO_OUT_OF_DELIVERY_AREA_TIER'; + case MooMerchandiseProcessingFee = 'MOO_MERCHANDISE_PROCESSING_FEE'; + case MooReturnOnCallSurcharge = 'MOO_RETURN_ON_CALL_SURCHARGE'; + case MooUnauthorizedOSSurcharge = 'MOO_UNAUTHORIZED_OS'; + case MooPeakUnauthCharge = 'MOO_PEAK_UNAUTH_CHARGE'; + case MooMissingAccountNumber = 'MOO_MISSING_ACCOUNT_NUMBER'; + case MooHazardousMaterial = 'MOO_HAZARDOUS_MATERIAL'; + case MooReturnPickupFee = 'MOO_RETURN_PICKUP_FEE'; + case MooPeakResiCharge = 'MOO_PEAK_RESI_CHARGE'; + case MooSalesTax = 'MOO_SALES_TAX'; + case MooOther = 'MOO_OTHER'; + + case ZooFuelSurcharge = 'CANADA_POST_FUEL_SURCHARGE'; + + case ZooGoodsAndServicesTaxSurcharge = 'CANADA_POST_GST_SURCHARGE'; + + case ZooHarmonizedSalesTaxSurcharge = 'CANADA_POST_HST_SURCHARGE'; + + case ZooProvincialSalesTaxSurcharge = 'CANADA_POST_PST_SURCHARGE'; + + case ZooPackageRedirectionSurcharge = 'CANADA_POST_REDIRECTION_SURCHARGE'; + + case ZooDeliveryConfirmationSurcharge = 'CANADA_POST_DELIVERY_CONFIRMATION'; + + case ZooSignatureOptionSurcharge = 'CANADA_POST_SIGNATURE_OPTION_SURCHARGE'; + + case ZooOnDemandPickxyzurcharge = 'CANADA_POST_ON_DEMAND_PICKUP'; + + case ZooOutOfSpecSurcharge = 'CANADA_POST_OUT_OF_SPEC_SURCHARGE'; + + case ZooAutoBillingSurcharge = 'CANADA_POST_AUTO_BILLING_SURCHARGE'; + + case ZooOversizeNotPackagedSurcharge = 'CANADA_POST_OVERSIZE_NOT_PACKAGED_SURCHARGE'; + + case EvriRelabellingSurcharge = 'EVRI_RELABELLING_SURCHARGE'; + + case EvriNetworkUndeliveredSurcharge = 'EVRI_NETWORK_UNDELIVERED_SURCHARGE'; + + case GooInvalidAddressCorrection = 'GOO_INVALID_ADDRESS_CORRECTION'; + case GooIncorrectAddressCorrection = 'GOO_INCORRECT_ADDRESS_CORRECTION'; + case GooGroupCZip = 'GOO_GROUP_CZIP'; + case GooDeliveryAreaSurcharge = 'GOO_DELIVERY_AREA_SURCHARGE'; + case GooExtendedDeliveryAreaSurcharge = 'GOO_EXTENDED_DELIVERY_AREA_SURCHARGE'; + case GooEnergySurcharge = 'GOO_ENERGY_SURCHARGE'; + case GooExtraPiece = 'GOO_EXTRA_PIECE'; + case GooResidentialCharge = 'GOO_RESIDENTIAL_CHARGE'; + case GooRelabelCharge = 'GOO_RELABEL_CHARGE'; + case GooWeekendPerPiece = 'GOO_WEEKEND_PER_PIECE'; + case GooExtraWeight = 'GOO_EXTRA_WEIGHT'; + case GooReturnBaseCharge = 'GOO_RETURN_BASE_CHARGE'; + case GooReturnExtraWeight = 'GOO_RETURN_EXTRA_WEIGHT'; + case GooPeakSurcharge = 'GOO_PEAK_SURCHARGE'; + case GooAdditionalHandling = 'GOO_ADDITIONAL_HANDLING'; + case GooVolumeRebate = 'GOO_VOLUME_REBATE'; + case GooOverMaxLimit = 'GOO_OVER_MAX_LIMIT'; + case GooRemoteDeliveryAreaSurcharge = 'GOO_REMOTE_DELIVERY_AREA_SURCHARGE'; + case GooOffHour = 'GOO_OFF_HOUR'; + case GooVolumeRebateBase = 'GOO_VOLUME_REBATE_BASE'; + case GooResidentialSignature = 'GOO_RESIDENTIAL_SIGNATURE'; + case GooAHDemandSurcharge = 'GOO_AHDEMAND_SURCHARGE'; + case GooOversizeDemandSurcharge = 'GOO_OVERSIZE_DEMAND_SURCHARGE'; + case GooAuditFee = 'GOO_AUDIT_FEE'; + case GooVolumeRebate2 = 'GOO_VOLUME_REBATE_2'; + case GooVolumeRebate3 = 'GOO_VOLUME_REBATE_3'; + case GooUnmappedSurcharge = 'GOO_UNMAPPED_SURCHARGE'; + + case LooDeliveryAreaSurcharge = 'PITNEY_BOWES_DELIVERY_AREA_SURCHARGE'; + case LooFuelSurcharge = 'PITNEY_BOWES_FUEL_SURCHARGE'; + + case FooExpressSaturdayDelivery = 'FOO_EXPRESS_SATURDAY_DELIVERY'; + case FooExpressElevatedRisk = 'FOO_EXPRESS_ELEVATED_RISK'; + case FooExpressEmergencySituation = 'FOO_EXPRESS_EMERGENCY_SITUATION'; + case FooExpressDutiesTaxesPaid = 'FOO_EXPRESS_DUTIES_TAXES_PAID'; + case FooExpressDutyTaxPaid = 'FOO_EXPRESS_DUTY_TAX_PAID'; + case FooExpressFuelSurcharge = 'FOO_EXPRESS_FUEL_SURCHARGE'; + case FooExpressShipmentValueProtection = 'FOO_EXPRESS_SHIPMENT_VALUE_PROTECTION'; + case FooExpressAddressCorrection = 'FOO_EXPRESS_ADDRESS_CORRECTION'; + case FooExpressNeutralDelivery = 'FOO_EXPRESS_NEUTRAL_DELIVERY'; + case FooExpressRemoteAreaPickup = 'FOO_EXPRESS_REMOTE_AREA_PICKUP'; + case FooExpressRemoteAreaDelivery = 'FOO_EXPRESS_REMOTE_AREA_DELIVERY'; + case FooExpressShipmentPreparation = 'FOO_EXPRESS_SHIPMENT_PREPARATION'; + case FooExpressStandardPickup = 'FOO_EXPRESS_STANDARD_PICKUP'; + case FooExpressNonStandardPickup = 'FOO_EXPRESS_NON_STANDARD_PICKUP'; + case FooExpressMonthlyPickxyzervice = 'FOO_EXPRESS_MONTHLY_PICKUP_SERVICE'; + case FooExpressResidentialAddress = 'FOO_EXPRESS_RESIDENTIAL_ADDRESS'; + case FooExpressResidentialDelivery = 'FOO_EXPRESS_RESIDENTIAL_DELIVERY'; + case FooExpressSingleClearance = 'FOO_EXPRESS_SINGLE_CLEARANCE'; + case FooExpressUnderBondGuarantee = 'FOO_EXPRESS_UNDER_BOND_GUARANTEE'; + case FooExpressFormalClearance = 'FOO_EXPRESS_FORMAL_CLEARANCE'; + case FooExpressNonRoutineEntry = 'FOO_EXPRESS_NON_ROUTINE_ENTRY'; + case FooExpressDisbursements = 'FOO_EXPRESS_DISBURSEMENTS'; + case FooExpressDutyTaxImporter = 'FOO_EXPRESS_DUTY_TAX_IMPORTER'; + case FooExpressDutyTaxProcessing = 'FOO_EXPRESS_DUTY_TAX_PROCESSING'; + case FooExpressMultilineEntry = 'FOO_EXPRESS_MULTILINE_ENTRY'; + case FooExpressOtherGovtAgcyBorderControls = 'FOO_EXPRESS_OTHER_GOVT_AGCY_BORDER_CONTROLS'; + case FooExpressPrintedInvoice = 'FOO_EXPRESS_PRINTED_INVOICE'; + case FooExpressObtainingPermitsLicenses = 'FOO_EXPRESS_OBTAINING_PERMITS_LICENSES'; + case FooExpressPermitsLicences = 'FOO_EXPRESS_PERMITS_LICENCES'; + case FooExpressBondedStorage = 'FOO_EXPRESS_BONDED_STORAGE'; + case FooExpressExportDeclaration = 'FOO_EXPRESS_EXPORT_DECLARATION'; + case FooExpressExporterValidation = 'FOO_EXPRESS_EXPORTER_VALIDATION'; + case FooExpressRestrictedDestination = 'FOO_EXPRESS_RESTRICTED_DESTINATION'; + case FooExpressAdditionalDuty = 'FOO_EXPRESS_ADDITIONAL_DUTY'; + case FooExpressImportExportTaxes = 'FOO_EXPRESS_IMPORT_EXPORT_TAXES'; + case FooExpressQuarantineInspection = 'FOO_EXPRESS_QUARANTINE_INSPECTION'; + case FooExpressMerchandiseProcessing = 'FOO_EXPRESS_MERCHANDISE_PROCESSING'; + case FooExpressMerchandiseProcess = 'FOO_EXPRESS_MERCHANDISE_PROCESS'; + case FooExpressImportPenalty = 'FOO_EXPRESS_IMPORT_PENALTY'; + case FooExpressTradeZoneProcess = 'FOO_EXPRESS_TRADE_ZONE_PROCESS'; + case FooExpressRegulatoryCharge = 'FOO_EXPRESS_REGULATORY_CHARGE'; + case FooExpressRegulatoryCharges = 'FOO_EXPRESS_REGULATORY_CHARGES'; + case FooExpressVatOnNonRevenueItem = 'FOO_EXPRESS_VAT_ON_NON_REVENUE_ITEM'; + case FooExpressExciseTax = 'FOO_EXPRESS_EXCISE_TAX'; + case FooExpressImportExportDuties = 'FOO_EXPRESS_IMPORT_EXPORT_DUTIES'; + case FooExpressOversizePieceDimension = 'FOO_EXPRESS_OVERSIZE_PIECE_DIMENSION'; + case FooExpressOversizePiece = 'FOO_EXPRESS_OVERSIZE_PIECE'; + case FooExpressNonStackablePallet = 'FOO_EXPRESS_NON_STACKABLE_PALLET'; + case FooExpressPremium900 = 'FOO_EXPRESS_PREMIUM_9_00'; + case FooExpressPremium1200 = 'FOO_EXPRESS_PREMIUM_12_00'; + case FooExpressOverweightPiece = 'FOO_EXPRESS_OVERWEIGHT_PIECE'; + case FooExpressCommercialGesture = 'FOO_EXPRESS_COMMERCIAL_GESTURE'; + + case PassportTaxes = 'PASSPORT_TAXES'; + case PassportDuties = 'PASSPORT_DUTIES'; + case PassportClearanceFee = 'PASSPORT_CLEARANCE_FEE'; + + case IooProvincialTax = 'IOO_PST'; + case IooGoodsAndServicesTax = 'IOO_GST'; + case IooHarmonizedTax = 'IOO_HST'; + case IooTaxes = 'IOO_TAXES'; + case IooDuties = 'IOO_DUTIES'; + + case FooExpressEuFuel = 'FOO_EXPRESS_EU_FUEL'; + case FooExpressEuRemoteAreaDelivery = 'FOO_EXPRESS_EU_REMOTE_AREA_DELIVERY'; + case FooExpressEuOverWeight = 'FOO_EXPRESS_EU_OVER_WEIGHT'; + +} + +enum SecondEnum: string +{ + + case Duties = 'duties'; + case ProcessingFees = 'processing_fees'; + case Taxes = 'taxes'; + + public function getLabel(): string + { + return match ($this) { + self::Duties => 'duties', + self::ProcessingFees => 'processing fees', + self::Taxes => 'taxes', + }; + } + + public static function fromFirstEnum(FirstEnum $FirstEnum): ?self + { + return match ($FirstEnum) { + FirstEnum::FooExpressExciseTax, + FirstEnum::FooExpressVatOnNonRevenueItem, + FirstEnum::FooExpressImportExportTaxes, + FirstEnum::IooTaxes, + FirstEnum::IooProvincialTax, + FirstEnum::IooHarmonizedTax, + FirstEnum::IooGoodsAndServicesTax, + FirstEnum::PassportTaxes => self::Taxes, + FirstEnum::FooExpressRegulatoryCharges, + FirstEnum::FooExpressRegulatoryCharge, + FirstEnum::FooExpressTradeZoneProcess, + FirstEnum::FooExpressImportPenalty, + FirstEnum::FooExpressMerchandiseProcess, + FirstEnum::FooExpressMerchandiseProcessing, + FirstEnum::FooExpressQuarantineInspection, + FirstEnum::FooExpressBondedStorage, + FirstEnum::FooExpressPermitsLicences, + FirstEnum::FooExpressObtainingPermitsLicenses, + FirstEnum::FooExpressPrintedInvoice, + FirstEnum::FooExpressOtherGovtAgcyBorderControls, + FirstEnum::FooExpressMultilineEntry, + FirstEnum::FooExpressDutyTaxProcessing, + FirstEnum::FooExpressDutyTaxImporter, + FirstEnum::FooExpressDisbursements, + FirstEnum::FooExpressNonRoutineEntry, + FirstEnum::FooExpressFormalClearance, + FirstEnum::FooExpressUnderBondGuarantee, + FirstEnum::FooExpressSingleClearance, + FirstEnum::FooExpressDutyTaxPaid, + FirstEnum::FooExpressDutiesTaxesPaid, + FirstEnum::PassportClearanceFee => self::ProcessingFees, + FirstEnum::FooExpressAdditionalDuty, + FirstEnum::FooExpressImportExportDuties, + FirstEnum::IooDuties, + FirstEnum::PassportDuties => self::Duties, + FirstEnum::XyzSaturdayStopDomestic, + FirstEnum::XyzSaturdayDeliveryAir, + FirstEnum::XyzAdditionalHandling, + FirstEnum::XyzCommercialDomesticAirDeliveryArea, + FirstEnum::XyzCommercialDomesticAirExtendedDeliveryArea, + FirstEnum::XyzCommercialDomesticGroundDeliveryArea, + FirstEnum::XyzCommercialDomesticGroundExtendedDeliveryArea, + FirstEnum::XyzResidentialDomesticAirDeliveryArea, + FirstEnum::XyzResidentialDomesticAirExtendedDeliveryArea, + FirstEnum::XyzResidentialDomesticGroundDeliveryArea, + FirstEnum::XyzResidentialDomesticGroundExtendedDeliveryArea, + FirstEnum::XyzDeliveryAreaSurchargeSurePost, + FirstEnum::XyzDeliveryAreaSurchargeSurePostExtended, + FirstEnum::XyzDeliveryAreaSurchargeOther, + FirstEnum::XyzResidentialSurchargeAir, + FirstEnum::XyzResidentialSurchargeGround, + FirstEnum::XyzSurchargeCodeFuel, + FirstEnum::XyzCod, + FirstEnum::XyzDeliveryConfirmation, + FirstEnum::XyzShipDeliveryConfirmation, + FirstEnum::XyzExtendedArea, + FirstEnum::XyzHazMat, + FirstEnum::XyzDryIce, + FirstEnum::XyzIscSeeds, + FirstEnum::XyzIscPerishables, + FirstEnum::XyzIscTobacco, + FirstEnum::XyzIscPlants, + FirstEnum::XyzIscAlcoholicBeverages, + FirstEnum::XyzIscBiologicalSubstances, + FirstEnum::XyzIscSpecialExceptions, + FirstEnum::XyzHoldForPickup, + FirstEnum::XyzOriginCertificate, + FirstEnum::XyzPrintReturnLabel, + FirstEnum::XyzExportLicenseVerification, + FirstEnum::XyzPrintNMail, + FirstEnum::XyzReturnService1attempt, + FirstEnum::XyzReturnService3attempt, + FirstEnum::XyzSaturdayInternationalProcessingFee, + FirstEnum::XyzElectronicReturnLabel, + FirstEnum::XyzPreparedSedForm, + FirstEnum::XyzLargePackage, + FirstEnum::XyzShipperPaysDutyTax, + FirstEnum::XyzShipperPaysDutyTaxUnpaid, + FirstEnum::XyzExpressPlusSurcharge, + FirstEnum::XyzInsurance, + FirstEnum::XyzShipAdditionalHandling, + FirstEnum::XyzShipperRelease, + FirstEnum::XyzCheckToShipper, + FirstEnum::XyzProactiveResponse, + FirstEnum::XyzGermanPickup, + FirstEnum::XyzGermanRoadTax, + FirstEnum::XyzExtendedAreaPickup, + FirstEnum::XyzReturnOfDocument, + FirstEnum::XyzPeakSeason, + FirstEnum::XyzLargePackageSeasonalSurcharge, + FirstEnum::XyzAdditionalHandlingSeasonalSurchargeDiscontinued, + FirstEnum::XyzShipLargePackage, + FirstEnum::XyzCarbonNeutral, + FirstEnum::XyzImportControl, + FirstEnum::XyzCommercialInvoiceRemoval, + FirstEnum::XyzImportControlElectronicLabel, + FirstEnum::XyzImportControlPrintLabel, + FirstEnum::XyzImportControlPrintAndMailLabel, + FirstEnum::XyzImportControlOnePickupAttemptLabel, + FirstEnum::XyzImportControlThreePickUpAttemptLabel, + FirstEnum::XyzRefrigeration, + FirstEnum::XyzExchangePrintReturnLabel, + FirstEnum::XyzCommittedDeliveryWindow, + FirstEnum::XyzSecuritySurcharge, + FirstEnum::XyzNonMachinableCharge, + FirstEnum::XyzCustomerTransactionFee, + FirstEnum::XyzSurePostNonStandardLength, + FirstEnum::XyzSurePostNonStandardExtraLength, + FirstEnum::XyzSurePostNonStandardCube, + FirstEnum::XyzShipmentCod, + FirstEnum::XyzLiftGateForPickup, + FirstEnum::XyzLiftGateForDelivery, + FirstEnum::XyzDropOffAtXyzFacility, + FirstEnum::XyzPremiumCare, + FirstEnum::XyzOversizePallet, + FirstEnum::XyzFreightDeliverySurcharge, + FirstEnum::XyzFreightPickxyzurcharge, + FirstEnum::XyzDirectToRetail, + FirstEnum::XyzDirectDeliveryOnly, + FirstEnum::XyzNoAccessPoint, + FirstEnum::XyzDeliverToAddresseeOnly, + FirstEnum::XyzDirectToRetailCod, + FirstEnum::XyzRetailAccessPoint, + FirstEnum::XyzElectronicPackageReleaseAuthentication, + FirstEnum::XyzPayAtStore, + FirstEnum::XyzInsideDelivery, + FirstEnum::XyzItemDisposal, + FirstEnum::XyzAddressCorrections, + FirstEnum::XyzNotPreviouslyBilledFee, + FirstEnum::XyzPickxyzurcharge, + FirstEnum::XyzChargeback, + FirstEnum::XyzAdditionalHandlingPeakDemand, + FirstEnum::XyzOtherSurcharge, + FirstEnum::XyzRemoteAreaSurcharge, + FirstEnum::XyzRemoteAreaOtherSurcharge, + FirstEnum::CompanyEconomyResidentialSurchargeLightweight, + FirstEnum::CompanyEconomyDeliverySurchargeLightweight, + FirstEnum::CompanyEconomyExtendedDeliverySurchargeLightweight, + FirstEnum::CompanyStandardResidentialSurchargeLightweight, + FirstEnum::CompanyStandardDeliverySurchargeLightweight, + FirstEnum::CompanyStandardExtendedDeliverySurchargeLightweight, + FirstEnum::CompanyEconomyResidentialSurchargePlus, + FirstEnum::CompanyEconomyDeliverySurchargePlus, + FirstEnum::CompanyEconomyExtendedDeliverySurchargePlus, + FirstEnum::CompanyEconomyPeakSurchargeLightweight, + FirstEnum::CompanyEconomyPeakSurchargePlus, + FirstEnum::CompanyEconomyPeakSurchargeOver5Lbs, + FirstEnum::CompanyStandardResidentialSurchargePlus, + FirstEnum::CompanyStandardDeliverySurchargePlus, + FirstEnum::CompanyStandardExtendedDeliverySurchargePlus, + FirstEnum::CompanyStandardPeakSurchargeLightweight, + FirstEnum::CompanyStandardPeakSurchargePlus, + FirstEnum::CompanyStandardPeakSurchargeOver5Lbs, + FirstEnum::Company2DayResidentialSurcharge, + FirstEnum::Company2DayDeliverySurcharge, + FirstEnum::Company2DayExtendedDeliverySurcharge, + FirstEnum::Company2DayPeakSurcharge, + FirstEnum::CompanyHazmatResidentialSurcharge, + FirstEnum::CompanyHazmatResidentialSurchargeLightweight, + FirstEnum::CompanyHazmatDeliverySurcharge, + FirstEnum::CompanyHazmatDeliverySurchargeLightweight, + FirstEnum::CompanyHazmatExtendedDeliverySurcharge, + FirstEnum::CompanyHazmatExtendedDeliverySurchargeLightweight, + FirstEnum::CompanyHazmatPeakSurchargeLightweight, + FirstEnum::CompanyHazmatPeakSurcharge, + FirstEnum::CompanyHazmatPeakSurchargePlus, + FirstEnum::CompanyHazmatPeakSurchargeOver5Lbs, + FirstEnum::CompanyFuelSurcharge, + FirstEnum::Company2DayAdditionalHandlingSurchargeDimensions, + FirstEnum::Company2DayAdditionalHandlingSurchargeWeight, + FirstEnum::CompanyStandardAdditionalHandlingSurchargeDimensions, + FirstEnum::CompanyStandardAdditionalHandlingSurchargeWeight, + FirstEnum::CompanyEconomyAdditionalHandlingSurchargeDimensions, + FirstEnum::CompanyEconomyAdditionalHandlingSurchargeWeight, + FirstEnum::CompanyHazmatAdditionalHandlingSurchargeDimensions, + FirstEnum::CompanyHazmatAdditionalHandlingSurchargeWeight, + FirstEnum::Company2DayLargePackageSurcharge, + FirstEnum::CompanyStandardLargePackageSurcharge, + FirstEnum::CompanyEconomyLargePackageSurcharge, + FirstEnum::CompanyHazmatLargePackageSurcharge, + FirstEnum::CompanyLargePackagePeakSurcharge, + FirstEnum::CompanyUkSignatureSurcharge, + FirstEnum::AciFuelSurcharge, + FirstEnum::AciUnmanifestedSurcharge, + FirstEnum::AciPeakSurcharge, + FirstEnum::AciOversizeSurcharge, + FirstEnum::AciUltraUrbanSurcharge, + FirstEnum::AciNonStandardSurcharge, + FirstEnum::XyzmiFuelSurcharge, + FirstEnum::XyzmiNonStandardSurcharge, + FirstEnum::FooEcommerceFuelSurcharge, + FirstEnum::FooEcommercePeakSurcharge, + FirstEnum::FooEcommerceOversizeSurcharge, + FirstEnum::FooEcommerceFutureUseSurcharge, + FirstEnum::FooEcommerceDimLengthSurcharge, + FirstEnum::HooFuelSurcharge, + FirstEnum::HooResidentialSurcharge, + FirstEnum::HooDeliverySurcharge, + FirstEnum::HooExtendedDeliverySurcharge, + FirstEnum::MooSmartPostFuelSurcharge, + FirstEnum::MooSmartPostDeliverySurcharge, + FirstEnum::MooSmartPostExtendedDeliverySurcharge, + FirstEnum::MooSmartPostNonMachinableSurcharge, + FirstEnum::MooAdditionalHandlingDomesticDimensionSurcharge, + FirstEnum::MooOversizeSurcharge, + FirstEnum::MooAdditionalHandling, + FirstEnum::MooAdditionalHandlingChargeDimensions, + FirstEnum::MooAdditionalHandlingChargePackage, + FirstEnum::MooAdditionalHandlingChargeWeight, + FirstEnum::MooAdditionalWeightCharge, + FirstEnum::MooAdvancementFee, + FirstEnum::MooAhsDimensions, + FirstEnum::MooAhsWeight, + FirstEnum::MooAlaskaOrHawaiiOrPuertoRicoPkupOrDel, + FirstEnum::MooAppointment, + FirstEnum::MooBox24X24X18DblWalledProductQuantity2, + FirstEnum::MooBox28X28X28DblWalledProductQuantity3, + FirstEnum::MooBoxMultiDepth22X22X22ProductQuantity7, + FirstEnum::MooBrokerDocumentTransferFee, + FirstEnum::MooCallTag, + FirstEnum::MooCustomsOvertimeFee, + FirstEnum::MooDasAlaskaComm, + FirstEnum::MooDasAlaskaResi, + FirstEnum::MooDasComm, + FirstEnum::MooDasExtendedComm, + FirstEnum::MooDasExtendedResi, + FirstEnum::MooDasHawaiiComm, + FirstEnum::MooDasHawaiiResi, + FirstEnum::MooDasRemoteComm, + FirstEnum::MooDasRemoteResi, + FirstEnum::MooDasResi, + FirstEnum::MooDateCertain, + FirstEnum::MooDeclaredValue, + FirstEnum::MooDeclaredValueCharge, + FirstEnum::MooDeliveryAndReturns, + FirstEnum::MooDeliveryAreaSurcharge, + FirstEnum::MooDeliveryAreaSurchargeAlaska, + FirstEnum::MooDeliveryAreaSurchargeExtended, + FirstEnum::MooDeliveryAreaSurchargeHawaii, + FirstEnum::MooElectronicEntryForFormalEntry, + FirstEnum::MooEvening, + FirstEnum::MooExtendedDeliveryArea, + FirstEnum::MooFoodAndDrugAdministrationClearance, + FirstEnum::MooFragileLarge20X20X12ProductQuantity2, + FirstEnum::MooFragileLarge23X17X12ProductQuantity1, + FirstEnum::MooFreeTradeZone, + FirstEnum::MooFuelSurcharge, + FirstEnum::MooHandlingFee, + FirstEnum::MooHoldForPickup, + FirstEnum::MooImportPermitsAndLicensesFee, + FirstEnum::MooPeakAhsCharge, + FirstEnum::MooResidential, + FirstEnum::MooOversizeCharge, + FirstEnum::MooPeakOversizeSurcharge, + FirstEnum::MooAdditionalVat, + FirstEnum::MooGstOnDisbOrAncillaryServiceFees, + FirstEnum::MooHstOnAdvOrAncillaryServiceFees, + FirstEnum::MooHstOnDisbOrAncillaryServiceFees, + FirstEnum::MooIndiaCgst, + FirstEnum::MooIndiaSgst, + FirstEnum::MooMooAdditionalVat, + FirstEnum::MooMooAdditionalDuty, + FirstEnum::MooEgyptVatOnFreight, + FirstEnum::MooDutyAndTaxAmendmentFee, + FirstEnum::MooCustomsDuty, + FirstEnum::MooCstAdditionalDuty, + FirstEnum::MooChinaVatDutyOrTax, + FirstEnum::MooArgentinaExportDuty, + FirstEnum::MooAustraliaGst, + FirstEnum::MooBhFreightVat, + FirstEnum::MooCanadaGst, + FirstEnum::MooCanadaHst, + FirstEnum::MooCanadaHstNb, + FirstEnum::MooCanadaHstOn, + FirstEnum::MooGstSingapore, + FirstEnum::MooBritishColumbiaPst, + FirstEnum::MooDisbursementFee, + FirstEnum::MooVatOnDisbursementFee, + FirstEnum::MooResidentialRuralZone, + FirstEnum::MooOriginalVat, + FirstEnum::MooMexicoIvaFreight, + FirstEnum::MooOtherGovernmentAgencyFee, + FirstEnum::MooCustodyFee, + FirstEnum::MooProcessingFee, + FirstEnum::MooStorageFee, + FirstEnum::MooIndividualFormalEntry, + FirstEnum::MooRebillDuty, + FirstEnum::MooClearanceEntryFee, + FirstEnum::MooCustomsClearanceFee, + FirstEnum::MooRebillVAT, + FirstEnum::MooIdVatOnAncillaries, + FirstEnum::MooPeakSurcharge, + FirstEnum::MooOutOfDeliveryAreaTier, + FirstEnum::MooMerchandiseProcessingFee, + FirstEnum::MooReturnOnCallSurcharge, + FirstEnum::MooUnauthorizedOSSurcharge, + FirstEnum::MooPeakUnauthCharge, + FirstEnum::MooMissingAccountNumber, + FirstEnum::MooHazardousMaterial, + FirstEnum::MooReturnPickupFee, + FirstEnum::MooPeakResiCharge, + FirstEnum::MooSalesTax, + FirstEnum::MooOther, + FirstEnum::ZooFuelSurcharge, + FirstEnum::ZooGoodsAndServicesTaxSurcharge, + FirstEnum::ZooHarmonizedSalesTaxSurcharge, + FirstEnum::ZooProvincialSalesTaxSurcharge, + FirstEnum::ZooPackageRedirectionSurcharge, + FirstEnum::ZooDeliveryConfirmationSurcharge, + FirstEnum::ZooSignatureOptionSurcharge, + FirstEnum::ZooOnDemandPickxyzurcharge, + FirstEnum::ZooOutOfSpecSurcharge, + FirstEnum::ZooAutoBillingSurcharge, + FirstEnum::ZooOversizeNotPackagedSurcharge, + FirstEnum::EvriRelabellingSurcharge, + FirstEnum::EvriNetworkUndeliveredSurcharge, + FirstEnum::GooInvalidAddressCorrection, + FirstEnum::GooIncorrectAddressCorrection, + FirstEnum::GooGroupCZip, + FirstEnum::GooDeliveryAreaSurcharge, + FirstEnum::GooExtendedDeliveryAreaSurcharge, + FirstEnum::GooEnergySurcharge, + FirstEnum::GooExtraPiece, + FirstEnum::GooResidentialCharge, + FirstEnum::GooRelabelCharge, + FirstEnum::GooWeekendPerPiece, + FirstEnum::GooExtraWeight, + FirstEnum::GooReturnBaseCharge, + FirstEnum::GooReturnExtraWeight, + FirstEnum::GooPeakSurcharge, + FirstEnum::GooAdditionalHandling, + FirstEnum::GooVolumeRebate, + FirstEnum::GooOverMaxLimit, + FirstEnum::GooRemoteDeliveryAreaSurcharge, + FirstEnum::GooOffHour, + FirstEnum::GooVolumeRebateBase, + FirstEnum::GooResidentialSignature, + FirstEnum::GooAHDemandSurcharge, + FirstEnum::GooOversizeDemandSurcharge, + FirstEnum::GooAuditFee, + FirstEnum::GooVolumeRebate2, + FirstEnum::GooVolumeRebate3, + FirstEnum::GooUnmappedSurcharge, + FirstEnum::LooDeliveryAreaSurcharge, + FirstEnum::LooFuelSurcharge, + FirstEnum::FooExpressSaturdayDelivery, + FirstEnum::FooExpressElevatedRisk, + FirstEnum::FooExpressEmergencySituation, + FirstEnum::FooExpressFuelSurcharge, + FirstEnum::FooExpressShipmentValueProtection, + FirstEnum::FooExpressAddressCorrection, + FirstEnum::FooExpressNeutralDelivery, + FirstEnum::FooExpressRemoteAreaPickup, + FirstEnum::FooExpressRemoteAreaDelivery, + FirstEnum::FooExpressShipmentPreparation, + FirstEnum::FooExpressStandardPickup, + FirstEnum::FooExpressNonStandardPickup, + FirstEnum::FooExpressMonthlyPickxyzervice, + FirstEnum::FooExpressResidentialAddress, + FirstEnum::FooExpressResidentialDelivery, + FirstEnum::FooExpressExportDeclaration, + FirstEnum::FooExpressExporterValidation, + FirstEnum::FooExpressRestrictedDestination, + FirstEnum::FooExpressOversizePieceDimension, + FirstEnum::FooExpressOversizePiece, + FirstEnum::FooExpressNonStackablePallet, + FirstEnum::FooExpressPremium900, + FirstEnum::FooExpressPremium1200, + FirstEnum::FooExpressOverweightPiece, + FirstEnum::FooExpressEuFuel, + FirstEnum::FooExpressEuOverWeight, + FirstEnum::FooExpressEuRemoteAreaDelivery, + FirstEnum::FooExpressCommercialGesture => null, + }; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-11283.php b/tests/PHPStan/Analyser/data/bug-11283.php new file mode 100644 index 0000000000..f0c4b3fc17 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11283.php @@ -0,0 +1,168 @@ +|TFulfilled)) $onFulfilled + * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected + * @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))> + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; + + /** + * @template TThrowable of \Throwable + * @template TRejected + * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected + * @return PromiseInterface + */ + public function catch(callable $onRejected): PromiseInterface; + + /** + * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected + * @return PromiseInterface + */ + public function finally(callable $onFulfilledOrRejected): PromiseInterface; +} + +/** + * @template T + * @param PromiseInterface|T $promiseOrValue + * @return PromiseInterface + */ +function resolve($promiseOrValue): PromiseInterface +{ + return returnMixed(); +} + +class Demonstration +{ + public function parseMessage(): void + { + $params = []; + $packet = []; + $promise = resolve(null); + + $promise->then(function () use (&$packet, &$params) { + if (mt_rand(0, 1)) { + resolve(null)->then( + function () use ($packet, &$params) { + $packet['payload']['type'] = 0; + $this->groupNotify( + $packet, + function () use ($packet, &$params) { + $this->save($packet, $params)->then(function () use ($packet, &$params) { + if ($params['links']) { + $this->handle($params)->then(function ($result) use ($packet) { + if ($result) { + $packet['payload']['preview'] = $result; + } + }); + } + }); + } + ); + } + ); + } else { + $this->call(function () use (&$params) { + $packet['target'] = []; + + $this->asyncAction()->then(function ($value) use (&$packet, &$params) { + if (!$value) { + $packet['payload']['type'] = 0; + $packet['payload']['message'] = ''; + $this->selfNotify($packet); + return; + } + + $packet['payload']['type'] = 0; + $this->groupNotify( + $packet, + function () use ($packet, &$params) { + $this->save($packet, $params)->then(function () use ($packet, &$params) { + if ($params) { + $this->handle($params)->then(function ($result) use ($packet) { + if ($result) { + $packet['payload']['preview'] = $result; + $this->selfNotify($packet); + } + }); + } + }); + } + ); + }); + }); + } + }); + } + + /** + * @return PromiseInterface + */ + private function handle(mixed $params): PromiseInterface + { + return resolve(null); + } + + /** + * @param array $packet + * @param array $params + * @return PromiseInterface + */ + private function save(array $packet, array &$params): PromiseInterface + { + return resolve(0); + } + + /** + * @param array $packet + * @param (callable():void) $callback + * @return bool + */ + public function groupNotify(array $packet, callable $callback): bool + { + return true; + } + + /** + * @param array $packet + * @return bool + */ + public function selfNotify(array $packet): bool + { + return true; + } + + /** + * @return PromiseInterface + */ + private function asyncAction(): PromiseInterface + { + return resolve(''); + } + + /** + * @param callable():void $callback + */ + private function call(callable $callback): void + { + } +} + diff --git a/tests/PHPStan/Analyser/data/bug-11292.php b/tests/PHPStan/Analyser/data/bug-11292.php new file mode 100644 index 0000000000..7d317e9e6c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11292.php @@ -0,0 +1,13 @@ +[\\x7f-\\xff]{1,64})(:[^]\\\\\\x00-\\x20\\"(),:-<>[\\x7f-\\xff]{1,64})?@)?((?:[-a-zA-Z0-9\\x7f-\\xff]{1,63}\\.)+[a-zA-Z\\x7f-\\xff][-a-zA-Z0-9\\x7f-\\xff]{1,62})((:[0-9]{1,5})?(/[!$-/0-9:;=@_~\':;!a-zA-Z\\x7f-\\xff]*?)?(\\?[!$-/0-9:;=@_\':;!a-zA-Z\\x7f-\\xff]+?)?(#[!$-/0-9?:;=@_\':;!a-zA-Z\\x7f-\\xff]+?)?)(?=[)\'?.!,;:]*(' . $nonUrl . '|$))}'; + if (preg_match($pattern, $s, $matches, PREG_OFFSET_CAPTURE, 0)) { + assertType('array}>', $matches); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-11297.php b/tests/PHPStan/Analyser/data/bug-11297.php new file mode 100644 index 0000000000..5bc767581c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11297.php @@ -0,0 +1,251 @@ += 8.1 + +namespace Bug11297; + +class ClassA { + /** @param array $array */ + public static function doSomething(string $string, array $array): void + { + } +} + +enum Icon: string +{ + case CASE1 = 'case1'; + case CASE2 = 'case2'; + case CASE3 = 'case3'; + case CASE4 = 'case4'; + case CASE5 = 'case5'; + case CASE6 = 'case6'; + case CASE7 = 'case7'; + case CASE8 = 'case8'; + case CASE9 = 'case9'; + case CASE10 = 'case10'; + case CASE11 = 'case11'; + case CASE12 = 'case12'; + case CASE13 = 'case13'; + case CASE14 = 'case14'; + case CASE15 = 'case15'; + case CASE16 = 'case16'; + case CASE17 = 'case17'; + case CASE18 = 'case18'; + case CASE19 = 'case19'; + case CASE20 = 'case20'; + case CASE21 = 'case21'; + case CASE22 = 'case22'; + case CASE23 = 'case23'; + case CASE24 = 'case24'; + case CASE25 = 'case25'; + case CASE26 = 'case26'; + case CASE27 = 'case27'; + case CASE28 = 'case28'; + case CASE29 = 'case29'; + case CASE30 = 'case30'; + case CASE31 = 'case31'; + case CASE32 = 'case32'; + case CASE33 = 'case33'; + case CASE34 = 'case34'; + case CASE35 = 'case35'; + case CASE36 = 'case36'; + case CASE37 = 'case37'; + case CASE38 = 'case38'; + case CASE39 = 'case39'; + case CASE40 = 'case40'; + case CASE41 = 'case41'; + case CASE42 = 'case42'; + case CASE43 = 'case43'; + case CASE44 = 'case44'; + case CASE45 = 'case45'; + case CASE46 = 'case46'; + case CASE47 = 'case47'; + case CASE48 = 'case48'; + case CASE49 = 'case49'; + case CASE50 = 'case50'; + case CASE51 = 'case51'; + case CASE52 = 'case52'; + case CASE53 = 'case53'; + case CASE54 = 'case54'; + case CASE55 = 'case55'; + case CASE56 = 'case56'; + case CASE57 = 'case57'; + case CASE58 = 'case58'; + case CASE59 = 'case59'; + case CASE60 = 'case60'; + case CASE61 = 'case61'; + case CASE62 = 'case62'; + case CASE63 = 'case63'; + case CASE64 = 'case64'; + case CASE65 = 'case65'; + case CASE66 = 'case66'; + case CASE67 = 'case67'; + case CASE68 = 'case68'; + case CASE69 = 'case69'; + case CASE70 = 'case70'; + case CASE71 = 'case71'; + case CASE72 = 'case72'; + case CASE73 = 'case73'; + case CASE74 = 'case74'; + case CASE75 = 'case75'; + case CASE76 = 'case76'; + case CASE77 = 'case77'; + case CASE78 = 'case78'; + case CASE79 = 'case79'; + case CASE80 = 'case80'; + case CASE81 = 'case81'; + case CASE82 = 'case82'; + case CASE83 = 'case83'; + case CASE84 = 'case84'; + case CASE85 = 'case85'; + case CASE86 = 'case86'; + case CASE87 = 'case87'; + case CASE88 = 'case88'; + case CASE89 = 'case89'; + case CASE90 = 'case90'; + case CASE91 = 'case91'; + case CASE92 = 'case92'; + case CASE93 = 'case93'; + case CASE94 = 'case94'; + case CASE95 = 'case95'; + case CASE96 = 'case96'; + case CASE97 = 'case97'; + case CASE98 = 'case98'; + case CASE99 = 'case99'; + case CASE100 = 'case100'; + case CASE101 = 'case101'; + case CASE102 = 'case102'; + case CASE103 = 'case103'; + case CASE104 = 'case104'; + case CASE105 = 'case105'; + case CASE106 = 'case106'; + case CASE107 = 'case107'; + case CASE108 = 'case108'; + case CASE109 = 'case109'; + case CASE110 = 'case110'; + case CASE111 = 'case111'; + case CASE112 = 'case112'; + case CASE113 = 'case113'; + case CASE114 = 'case114'; + case CASE115 = 'case115'; + case CASE116 = 'case116'; + case CASE117 = 'case117'; + case CASE118 = 'case118'; + case CASE119 = 'case119'; + case CASE120 = 'case120'; + case CASE121 = 'case121'; + case CASE122 = 'case122'; + case CASE123 = 'case123'; + case CASE124 = 'case124'; + case CASE125 = 'case125'; + case CASE126 = 'case126'; + case CASE127 = 'case127'; + case CASE128 = 'case128'; + case CASE129 = 'case129'; + case CASE130 = 'case130'; + case CASE131 = 'case131'; + case CASE132 = 'case132'; + case CASE133 = 'case133'; + case CASE134 = 'case134'; + case CASE135 = 'case135'; + case CASE136 = 'case136'; + case CASE137 = 'case137'; + case CASE138 = 'case138'; + case CASE139 = 'case139'; + case CASE140 = 'case140'; + case CASE141 = 'case141'; + case CASE142 = 'case142'; + case CASE143 = 'case143'; + case CASE144 = 'case144'; + case CASE145 = 'case145'; + case CASE146 = 'case146'; + case CASE147 = 'case147'; + case CASE148 = 'case148'; + case CASE149 = 'case149'; + case CASE150 = 'case150'; + case CASE151 = 'case151'; + case CASE152 = 'case152'; + case CASE153 = 'case153'; + case CASE154 = 'case154'; + case CASE155 = 'case155'; + case CASE156 = 'case156'; + case CASE157 = 'case157'; + case CASE158 = 'case158'; + case CASE159 = 'case159'; + case CASE160 = 'case160'; + case CASE161 = 'case161'; + case CASE162 = 'case162'; + case CASE163 = 'case163'; + case CASE164 = 'case164'; + case CASE165 = 'case165'; + case CASE166 = 'case166'; + case CASE167 = 'case167'; + case CASE168 = 'case168'; + case CASE169 = 'case169'; + case CASE170 = 'case170'; + case CASE171 = 'case171'; + case CASE172 = 'case172'; + case CASE173 = 'case173'; + case CASE174 = 'case174'; + case CASE175 = 'case175'; + case CASE176 = 'case176'; + case CASE177 = 'case177'; + case CASE178 = 'case178'; + case CASE179 = 'case179'; + case CASE180 = 'case180'; + case CASE181 = 'case181'; + case CASE182 = 'case182'; + case CASE183 = 'case183'; + case CASE184 = 'case184'; + case CASE185 = 'case185'; + case CASE186 = 'case186'; + case CASE187 = 'case187'; + case CASE188 = 'case188'; + case CASE189 = 'case189'; + case CASE190 = 'case190'; + case CASE191 = 'case191'; + case CASE192 = 'case192'; + case CASE193 = 'case193'; + case CASE194 = 'case194'; + case CASE195 = 'case195'; + case CASE196 = 'case196'; + case CASE197 = 'case197'; + case CASE198 = 'case198'; + case CASE199 = 'case199'; + case CASE200 = 'case200'; + + public function getFileIdentifier(): string + { + return match ($this) { + default => $this->value + }; + } + + public function getBackendLabelIdentifier(): string + { + return 'foo:' . $this->getLocallangIdentifier(); + } + + private function getLocallangIdentifier(): string + { + return 'foo.icon.' . $this->value; + } +} + +(static function (string $table): void { + /** + * Add TCA for top bar field in pages. + */ + ClassA::doSomething($table, [ + 'foo' => [ + 'config' => [ + 'items' => [['', ''], ...array_map( + static fn (Icon $icon): array => [ + $icon->getBackendLabelIdentifier(), + $icon->value, + 'foo/' . $icon->getFileIdentifier() . '.svg', + ], + Icon::cases(), + )], + ], + ], + ]); +})('foo'); diff --git a/tests/PHPStan/Analyser/data/bug-11511.php b/tests/PHPStan/Analyser/data/bug-11511.php new file mode 100644 index 0000000000..7af5066cc1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-11511.php @@ -0,0 +1,10 @@ += 8.0 + +namespace Bug11511; + +$myObject = new class (new class { public string $bar = 'test'; }) { + public function __construct(public object $foo) + { + } +}; +echo $myObject->foo->bar; diff --git a/tests/PHPStan/Analyser/data/bug-3300.php b/tests/PHPStan/Analyser/data/bug-3300.php new file mode 100644 index 0000000000..c5614b868a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3300.php @@ -0,0 +1,44 @@ + 'TextType::class', + 'group' => 'EntityManagerFormType::class', + 'number' => 'IntegerType::class', + 'select' => 'ChoiceType::class', + 'radio' => 'ChoiceType::class', + 'checkbox' => 'ChoiceType::class', + 'bool' => 'CheckboxType::class', + ]; + + /** + * @param string $class + * + * @return string + * + * @throws \Exception + */ + public static function getTypeFromClass(string $class): string + { + $type = array_keys(self::TYPE_TO_CLASS_MAP, $class, true); + + if (0 === count($type)) { + throw new \Exception(sprintf('No type matched class %s', $class)); + } + if (1 < count($type)) { + throw new \Exception( + sprintf('Multiple types found, did you mean any of %s', implode(', ', $type)) + ); + } + + return $type[0]; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3981.php b/tests/PHPStan/Analyser/data/bug-3981.php deleted file mode 100644 index 5656b80018..0000000000 --- a/tests/PHPStan/Analyser/data/bug-3981.php +++ /dev/null @@ -1,25 +0,0 @@ -= 8.0 + +namespace Bug5597; + +interface InterfaceA {} + +class ClassA implements InterfaceA {} + +class ClassB +{ + public function __construct( + private InterfaceA $parameterA, + ) { + } + + public function test() : InterfaceA + { + return $this->parameterA; + } +} + +$classA = new class() extends ClassA {}; +$thisWorks = new class($classA) extends ClassB {}; + +$thisFailsWithTwoErrors = new class(new class() extends ClassA {}) extends ClassB {}; diff --git a/tests/PHPStan/Analyser/data/bug-6160.php b/tests/PHPStan/Analyser/data/bug-6160.php index 9470109e79..b0ac5850d1 100644 --- a/tests/PHPStan/Analyser/data/bug-6160.php +++ b/tests/PHPStan/Analyser/data/bug-6160.php @@ -16,10 +16,10 @@ public static function split($flags = 0){ public static function test(): void { - self::split(94561); // should error - self::split(PREG_SPLIT_NO_EMPTY); // should work - self::split(PREG_SPLIT_DELIM_CAPTURE); // should work - self::split(PREG_SPLIT_NO_EMPTY_COPY); // should work - self::split("sdf"); // should error + $a = self::split(94561); // should error + $a = self::split(PREG_SPLIT_NO_EMPTY); // should work + $a = self::split(PREG_SPLIT_DELIM_CAPTURE); // should work + $a = self::split(PREG_SPLIT_NO_EMPTY_COPY); // should work + $a = self::split("sdf"); // should error } } diff --git a/tests/PHPStan/Analyser/data/bug-6442.php b/tests/PHPStan/Analyser/data/bug-6442.php index 413826daa7..2bcd2309ff 100644 --- a/tests/PHPStan/Analyser/data/bug-6442.php +++ b/tests/PHPStan/Analyser/data/bug-6442.php @@ -17,7 +17,7 @@ class B extends A use T; } -new class() extends B +$a = new class() extends B { use T; }; diff --git a/tests/PHPStan/Analyser/data/bug-7012.php b/tests/PHPStan/Analyser/data/bug-7012.php index 536c646980..26ad352969 100644 --- a/tests/PHPStan/Analyser/data/bug-7012.php +++ b/tests/PHPStan/Analyser/data/bug-7012.php @@ -9,6 +9,7 @@ enum Foo function test(Foo $f = Foo::BAR): void { + echo 'test'; } function test2(): void diff --git a/tests/PHPStan/Analyser/data/bug-7110.php b/tests/PHPStan/Analyser/data/bug-7110.php index b1d9be2a0c..9a81a94852 100644 --- a/tests/PHPStan/Analyser/data/bug-7110.php +++ b/tests/PHPStan/Analyser/data/bug-7110.php @@ -16,8 +16,12 @@ function validateStringArray(array $arr) : void { } } -function takesString(string $s) : void {} -function takesInt(int $s) : void {} +function takesString(string $s) : void { + echo $s; +} +function takesInt(int $s) : void { + echo (string) $s; +} /** * @param mixed[] $arr diff --git a/tests/PHPStan/Analyser/data/bug-7140.php b/tests/PHPStan/Analyser/data/bug-7140.php index 211516c042..ede86286ad 100644 --- a/tests/PHPStan/Analyser/data/bug-7140.php +++ b/tests/PHPStan/Analyser/data/bug-7140.php @@ -41,5 +41,6 @@ function foo(array $arr): void if (isset($arr['k_' . $i])) { } + echo 'test'; } } diff --git a/tests/PHPStan/Analyser/data/bug-7581.php b/tests/PHPStan/Analyser/data/bug-7581.php index 73423fed8e..53b18b1252 100644 --- a/tests/PHPStan/Analyser/data/bug-7581.php +++ b/tests/PHPStan/Analyser/data/bug-7581.php @@ -110,4 +110,6 @@ function parse(array $parsed): void break; endswitch; endforeach; + + echo 'test'; } diff --git a/tests/PHPStan/Analyser/data/bug-9307.php b/tests/PHPStan/Analyser/data/bug-9307.php new file mode 100644 index 0000000000..f5b2cb7906 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9307.php @@ -0,0 +1,56 @@ +getIds() as $id) { + if (! array_key_exists($id, $itemCache)) { + $items = $this->getObjects(); + $itemCache[$id] = $items; + } else { + $items = $itemCache[$id]; + } + + // It works when the following line is uncommented. + //$items = $this->getObjects(); + + foreach ($items as $item) { + $objects[$item->id] = $item; + } + } + + assertType('array', $objects); + + $this->acceptObjects($objects); + } + + /** @return array */ + public function getIds(): array + { + return []; + } + + /** @return array */ + public function getObjects(): array + { + return []; + } + + /** @param array $objects */ + public function acceptObjects(array $objects): void + { + + } +} diff --git a/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php b/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php index 8500424864..9dda0d65b1 100644 --- a/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php +++ b/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php @@ -5,7 +5,7 @@ class test { public function test2(bool $isFoo, bool $isBar): void { - match (true) { + $a = match (true) { $isFoo && $isBar => $foo = 1, $isFoo || $isBar => $foo = 2, default => $foo = null, diff --git a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php index f79aea7dd3..420d5e089c 100644 --- a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php +++ b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php @@ -2,5 +2,5 @@ function bcompiler_write_file(): void { - + echo 'test'; } diff --git a/tests/PHPStan/Analyser/data/dynamic-sprintf.php b/tests/PHPStan/Analyser/data/dynamic-sprintf.php deleted file mode 100644 index 17bff757cd..0000000000 --- a/tests/PHPStan/Analyser/data/dynamic-sprintf.php +++ /dev/null @@ -1,21 +0,0 @@ -doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } +} diff --git a/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.neon b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.neon new file mode 100644 index 0000000000..790db39dd6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.neon @@ -0,0 +1,3 @@ +parameters: + exceptions: + implicitThrows: false diff --git a/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.php b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.php new file mode 100644 index 0000000000..6b2c380923 --- /dev/null +++ b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.php @@ -0,0 +1,30 @@ +noThrow(...), []); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/impure-method.php b/tests/PHPStan/Analyser/data/impure-method.php deleted file mode 100644 index d471c0d54a..0000000000 --- a/tests/PHPStan/Analyser/data/impure-method.php +++ /dev/null @@ -1,81 +0,0 @@ -fooProp = rand(0, 1); - } - - public function ordinaryMethod(): int - { - return 1; - } - - /** - * @phpstan-impure - * @return int - */ - public function impureMethod(): int - { - $this->fooProp = rand(0, 1); - - return $this->fooProp; - } - - /** - * @impure - * @return int - */ - public function impureMethod2(): int - { - $this->fooProp = rand(0, 1); - - return $this->fooProp; - } - - public function doFoo(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->voidMethod(); - assertType('int', $this->fooProp); - } - - public function doBar(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->ordinaryMethod(); - assertType('1', $this->fooProp); - } - - public function doBaz(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->impureMethod(); - assertType('int', $this->fooProp); - } - - public function doLorem(): void - { - $this->fooProp = 1; - assertType('1', $this->fooProp); - - $this->impureMethod2(); - assertType('int', $this->fooProp); - } - -} diff --git a/tests/PHPStan/Analyser/data/is-resource-specified.php b/tests/PHPStan/Analyser/data/is-resource-specified.php new file mode 100644 index 0000000000..c78d5ad75a --- /dev/null +++ b/tests/PHPStan/Analyser/data/is-resource-specified.php @@ -0,0 +1,11 @@ + $items - */ -function foo(array $items) { - assertType('list', $items); - if (count($items) === 3) { - assertType('array{int, int, int}', $items); - array_shift($items); - assertType('array{int, int}', $items); - } elseif (count($items) === 0) { - assertType('array{}', $items); - } elseif (count($items) === 5) { - assertType('array{int, int, int, int, int}', $items); - } else { - assertType('non-empty-list', $items); - } - assertType('list', $items); -} diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php72.php b/tests/PHPStan/Analyser/data/mb-strlen-php72.php index 4e9c20f4cd..86069cb774 100644 --- a/tests/PHPStan/Analyser/data/mb-strlen-php72.php +++ b/tests/PHPStan/Analyser/data/mb-strlen-php72.php @@ -1,4 +1,4 @@ -= 7.2 namespace MbStrlenPhp72; diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php73.php b/tests/PHPStan/Analyser/data/mb-strlen-php73.php index 8132b9e8ca..45fad0364b 100644 --- a/tests/PHPStan/Analyser/data/mb-strlen-php73.php +++ b/tests/PHPStan/Analyser/data/mb-strlen-php73.php @@ -1,4 +1,4 @@ -= 7.3 namespace MbStrlenPhp73; diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php8.php b/tests/PHPStan/Analyser/data/mb-strlen-php8.php index 70092b04d6..3fb7f73706 100644 --- a/tests/PHPStan/Analyser/data/mb-strlen-php8.php +++ b/tests/PHPStan/Analyser/data/mb-strlen-php8.php @@ -1,4 +1,4 @@ -= 8.0 namespace MbStrlenPhp8; diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php82.php b/tests/PHPStan/Analyser/data/mb-strlen-php82.php index bd2dbf3632..7424e938a4 100644 --- a/tests/PHPStan/Analyser/data/mb-strlen-php82.php +++ b/tests/PHPStan/Analyser/data/mb-strlen-php82.php @@ -1,4 +1,6 @@ -= 8.2 + +declare(strict_types=1); namespace MbStrlenPhp82; diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php new file mode 100644 index 0000000000..5a44bfa784 --- /dev/null +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php @@ -0,0 +1,174 @@ +doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } + } + + /** + * @phan-return self[] + */ + public function doBar(): array + { + + } + + public function returnParent(): parent + { + + } + + /** + * @phan-return parent + */ + public function returnPhpDocParent() + { + + } + + /** + * @phan-return NULL[] + */ + public function returnNulls(): array + { + + } + + public function returnObject(): object + { + + } + + public function phpDocVoidMethod(): self + { + + } + + public function phpDocVoidMethodFromInterface(): self + { + + } + + public function phpDocVoidParentMethod(): self + { + + } + + public function phpDocWithoutCurlyBracesVoidParentMethod(): self + { + + } + + /** + * @phan-return string[] + */ + public function returnsStringArray(): array + { + + } + + private function privateMethodWithPhpDoc() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/non-empty-string-substr.php b/tests/PHPStan/Analyser/data/non-empty-string-substr.php deleted file mode 100644 index 5d8491224d..0000000000 --- a/tests/PHPStan/Analyser/data/non-empty-string-substr.php +++ /dev/null @@ -1,34 +0,0 @@ -transactional(function () { + assertType(EntityManagerParamClosureThis::class, $this); + }); + } + + public function doFoo2(): void + { + \MyFunctionClosureThis\doFoo(function () { + assertType(\MyFunctionClosureThis\Foo::class, $this); + }); + } + + public function doFoo3(array $a): void + { + uksort($a, function () { + assertType(\stdClass::class, $this); + }); + } + + /** + * @param \Ds\Deque $deque + */ + public function doFoo4(\Ds\Deque $deque): void + { + $deque->filter(function () { + assertType('Ds\Deque', $this); + }); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub b/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub new file mode 100644 index 0000000000..ec35c140a2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub @@ -0,0 +1,54 @@ + + * @param-closure-this $this $callback + */ + public function filter(callable $callback = null): Deque + { + } + } +} diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index 232eb60ebc..1f4fc69bd3 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -250,7 +250,8 @@ function fooShuffle() { function fooSort() { $array = ["foo" => 123, "bar" => 456]; sort($array); - assertType('array{foo: 123, bar: 456}', $array); + assertType('non-empty-list<123|456>', $array); + assertType('true', array_is_list($array)); $emptyArray = []; sort($emptyArray); @@ -282,17 +283,6 @@ function fooScanf(): void assertType('float|int|string|null', $p2); } -function fooMatch(string $input): void { - preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_PATTERN_ORDER); - assertType('array>', $matches); - - preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_SET_ORDER); - assertType('list>', $matches); - - preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array", $matches); -} - function fooParams(ExtendsFooBar $subX, float $x1, float $y1) { $subX->renamedParams($x1, $y1); @@ -315,11 +305,6 @@ function fooDateTime(\SplFileObject $splFileObject, ?string $wouldBlock) { assertType('string', $wouldBlock); } -function testMatch() { - preg_match('#.*#', 'foo', $matches); - assertType('array', $matches); -} - function testParseStr() { $str="first=value&arr[]=foo+bar&arr[]=baz"; parse_str($str, $output); @@ -445,3 +430,75 @@ function fooIsCallable($x, bool $b) is_callable($x, $b, $name); assertType('callable-string', $name); } + +function noParamOut(string &$s): void +{ + +} + +function noParamOutVariadic(string &...$s): void +{ + +} + +function ($s): void { + assertType('mixed', $s); + noParamOut($s); + assertType('string', $s); +}; + +function ($s, $t): void { + assertType('mixed', $s); + assertType('mixed', $t); + noParamOutVariadic($s, $t); + assertType('string', $s); + assertType('string', $t); +}; + +class NoParamOutClass +{ + + function doFoo(string &$s): void + { + + } + + function doFooVariadic(string &...$s): void + { + + } + +} + +function ($s): void { + assertType('mixed', $s); + $c = new NoParamOutClass(); + $c->doFoo($s); + assertType('string', $s); +}; + +function ($s, $t): void { + assertType('mixed', $s); + assertType('mixed', $t); + $c = new NoParamOutClass(); + $c->doFooVariadic($s, $t); + assertType('string', $s); + assertType('string', $t); +}; + +function fooMatch(string $input): void { + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_PATTERN_ORDER); + assertType('array{list}', $matches); + + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_SET_ORDER); + assertType('list', $matches); + + preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); + assertType("array{0?: string}", $matches); +} + +function testMatch() { + preg_match('#.*#', 'foo', $matches); + assertType('array{0?: string}', $matches); +} + diff --git a/tests/PHPStan/Analyser/data/param-out/ParamOutFunctionExtension.php b/tests/PHPStan/Analyser/data/param-out/ParamOutFunctionExtension.php new file mode 100644 index 0000000000..abeb8cb533 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/ParamOutFunctionExtension.php @@ -0,0 +1,26 @@ +getName() === 'ParameterOutTests\callWithOut' && $parameter->getName() === 'outParam'; + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new StringType(); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out/ParamOutMethodExtension.php b/tests/PHPStan/Analyser/data/param-out/ParamOutMethodExtension.php new file mode 100644 index 0000000000..087eb5ac46 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/ParamOutMethodExtension.php @@ -0,0 +1,31 @@ +getDeclaringClass()->getName() === FooClass::class + && $methodReflection->getName() === 'callWithOut' + && $parameter->getName() === 'outParam' + ; + } + + public function getParameterOutTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new IntegerType(); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out/ParamOutStaticMethodExtension.php b/tests/PHPStan/Analyser/data/param-out/ParamOutStaticMethodExtension.php new file mode 100644 index 0000000000..4c7b628bd7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/ParamOutStaticMethodExtension.php @@ -0,0 +1,32 @@ +getDeclaringClass()->getName() === FooClass::class + && $methodReflection->getName() === 'staticCallWithOut' + && $parameter->getName() === 'outParam' + ; + } + + public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new BooleanType(); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out/parameter-out-types.php b/tests/PHPStan/Analyser/data/param-out/parameter-out-types.php new file mode 100644 index 0000000000..57296b7d03 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out/parameter-out-types.php @@ -0,0 +1,39 @@ +callWithOut(12, $methodOut, $anotherOut); + assertType('int', $methodOut); + assertType('mixed', $anotherOut); +} + diff --git a/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php b/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php new file mode 100644 index 0000000000..b3876d7408 --- /dev/null +++ b/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php @@ -0,0 +1,199 @@ +getName() === 'ParameterClosureTypeExtensionArrowFunction\functionWithCallable'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $functionCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class MethodParameterClosureTypeExtension implements \PHPStan\Type\MethodParameterClosureTypeExtension +{ + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && + $parameter->getName() === 'callback' && + $methodReflection->getName() === 'methodWithCallable'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class StaticMethodParameterClosureTypeExtension implements \PHPStan\Type\StaticMethodParameterClosureTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithCallable'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + return new CallableType( + [ + new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class Foo +{ + + /** + * @param int $foo + * @param callable(Generic) $callback + * + * @return void + */ + public function methodWithCallable(int $foo, callable $callback) + { + + } + + /** + * @return void + */ + public static function staticMethodWithCallable(callable $callback) + { + + } + +} + +/** + * @template T + */ +class Generic +{ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function getValue() + { + return $this->value; + } +} + +/** + * @param int $foo + * @param callable(Generic) $callback + * + * @return void + */ +function functionWithCallable(int $foo, callable $callback) +{ + +} + +function test(Foo $foo): void +{ + + $foo->methodWithCallable(1, fn ($i) => assertType('int', $i->getValue())); + + (new Foo)->methodWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); + + Foo::staticMethodWithCallable(fn ($i) => assertType('float', $i)); +} + +functionWithCallable(1, fn ($i) => assertType('int', $i->getValue())); + +functionWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); diff --git a/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php b/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php new file mode 100644 index 0000000000..e081621ff1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php @@ -0,0 +1,220 @@ +getName() === 'ParameterClosureTypeExtension\functionWithClosure'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $functionCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), new GenericObjectType(Generic::class, [new IntegerType()]), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new VoidType() + ); + } + + return new ClosureType( + [ + new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), new GenericObjectType(Generic::class, [new StringType()]), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), + ], + new VoidType() + ); + } +} + +class MethodParameterClosureTypeExtension implements \PHPStan\Type\MethodParameterClosureTypeExtension +{ + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && + $parameter->getName() === 'callback' && + $methodReflection->getName() === 'methodWithClosure'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new ClosureType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } elseif ($integer === 5) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new ClosureType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } +} + +class StaticMethodParameterClosureTypeExtension implements \PHPStan\Type\StaticMethodParameterClosureTypeExtension +{ + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithClosure'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + ParameterReflection $parameter, + Scope $scope + ): ?Type { + return new ClosureType( + [ + new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } +} + +class Foo +{ + + /** + * @param int $foo + * @param Closure(Generic): void $callback + * + * @return void + */ + public function methodWithClosure(int $foo, Closure $callback) + { + + } + + /** + * @param Closure(): void $callback + * + * @return void + */ + public static function staticMethodWithClosure(Closure $callback) + { + + } + +} + +/** + * @template T + */ +class Generic +{ + private $value; + + /** + * @param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function getValue() + { + return $this->value; + } +} + +/** + * @param int $foo + * @param Closure(Generic): void $callback + * + * @return void + */ +function functionWithClosure(int $foo, Closure $callback) +{ + +} + +function test(Foo $foo): void +{ + $foo->methodWithClosure(1, function ($i) { + assertType('int', $i->getValue()); + }); + + (new Foo)->methodWithClosure(2, function (Generic $i) { + assertType('string', $i->getValue()); + }); + + Foo::staticMethodWithClosure(function ($i) { + assertType('float', $i); + }); +} + +functionWithClosure(1, function ($i) { + assertType('int', $i->getValue()); +}); + +functionWithClosure(2, function (Generic $i) { + assertType('string', $i->getValue()); +}); diff --git a/tests/PHPStan/Analyser/data/pathConstants-win.php b/tests/PHPStan/Analyser/data/pathConstants-win.php new file mode 100644 index 0000000000..a21b9fb49e --- /dev/null +++ b/tests/PHPStan/Analyser/data/pathConstants-win.php @@ -0,0 +1,6 @@ +value); } + /** @param \Closure(T|null): T $callback */ + public function onResolve2(\Closure $callback) : void{ + $r = $callback($this->value); + assertType('TValue (class ProcessCalledMethodInfiniteLoop\\Promise, argument)', $r); + + $callback($this->value); + } } class HelloWorld { diff --git a/tests/PHPStan/Analyser/data/str-caseing-php84.php b/tests/PHPStan/Analyser/data/str-caseing-php84.php new file mode 100644 index 0000000000..ed6e8dab73 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-caseing-php84.php @@ -0,0 +1,23 @@ += 8.3 + +declare(strict_types = 1); + +namespace DateTimeModifyReturnTypes83; + +use DateTime; +use DateTimeImmutable; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function modify(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'+2 day' $modify + */ + public function modifyWithValidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + + /** + * @param 'kewk'|'koko' $modify + */ + public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('*NEVER*', $datetime->modify($modify)); + assertType('*NEVER*', $dateTimeImmutable->modify($modify)); + } + + /** + * @param '+1 day'|'koko' $modify + */ + public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { + assertType('DateTime', $datetime->modify($modify)); + assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify)); + } + +} diff --git a/tests/PHPStan/Analyser/data/PDOStatement.php b/tests/PHPStan/Analyser/nsrt/PDOStatement.php similarity index 100% rename from tests/PHPStan/Analyser/data/PDOStatement.php rename to tests/PHPStan/Analyser/nsrt/PDOStatement.php diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php new file mode 100644 index 0000000000..eb644eb4bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -0,0 +1,215 @@ +', abs($int)); + + /** @var positive-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var negative-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var non-negative-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var non-positive-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, max> $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<-123, 0> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<123, max> $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<123, 456> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<-456, -123> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int<-123, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + } + + public function multipleIntegerRanges(int $int): void + { + /** @var non-zero-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int|int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<-20, -10>|int<5, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -5>|int<10, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-25, -10>|int<5, 20> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -10>|int<20, 30> $int */ + assertType('int<10, 30>', abs($int)); + } + + public function constantInteger(int $int): void + { + /** @var 0 $int */ + assertType('0', abs($int)); + + /** @var 1 $int */ + assertType('1', abs($int)); + + /** @var -1 $int */ + assertType('1', abs($int)); + + assertType('123', abs(123)); + + assertType('123', abs(-123)); + } + + public function mixedIntegerUnion(int $int): void + { + /** @var 123|int<456, max> $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var int|-123 $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var -123|int<124, 125> $int */ + assertType('int<123, 125>', abs($int)); + + /** @var int<124, 125>|-123 $int */ + assertType('int<123, 125>', abs($int)); + } + + public function constantFloat(float $float): void + { + /** @var 0.0 $float */ + assertType('0.0', abs($float)); + + /** @var 1.0 $float */ + assertType('1.0', abs($float)); + + /** @var -1.0 $float */ + assertType('1.0', abs($float)); + + assertType('123.4', abs(123.4)); + + assertType('123.4', abs(-123.4)); + } + + public function string(string $string): void + { + /** @var string $string */ + assertType('float|int<0, max>', abs($string)); + + /** @var numeric-string $string */ + assertType('float|int<0, max>', abs($string)); + + /** @var '-1' $string */ + assertType('1', abs($string)); + + /** @var '-1'|'-2.0'|'3.0'|'4' $string */ + assertType('1|2.0|3.0|4', abs($string)); + + /** @var literal-string $string */ + assertType('float|int<0, max>', abs($string)); + + assertType('123', abs('123')); + + assertType('123', abs('-123')); + + assertType('123.0', abs('123.0')); + + assertType('123.0', abs('-123.0')); + + assertType('float|int<0, max>', abs('foo')); + } + + public function mixedUnion(mixed $value): void + { + /** @var 1.0|int<2, 3> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var -1.0|int<-3, -2> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var 2.0|int<1, 3> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -2.0|int<-3, -1> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -1.0|int<2, 3>|numeric-string $value */ + assertType('float|int<0, max>', abs($value)); + } + + public function intersection(mixed $value): void + { + /** @var int&int<-10, 10> $value */ + assertType('int<0, 10>', abs($value)); + } + + public function invalidType(mixed $nonInt): void + { + /** @var string $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var string|positive-int $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var 'foo' $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var array $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var non-empty-list $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var object $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var \DateTime $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var null $nonInt */ + assertType('0', abs($nonInt)); + + assertType('float|int<0, max>', abs('foo')); + + assertType('0', abs(null)); + } + +} diff --git a/tests/PHPStan/Analyser/data/allowed-subtypes-datetime.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php similarity index 100% rename from tests/PHPStan/Analyser/data/allowed-subtypes-datetime.php rename to tests/PHPStan/Analyser/nsrt/allowed-subtypes-datetime.php diff --git a/tests/PHPStan/Analyser/data/allowed-subtypes-enum.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-enum.php similarity index 95% rename from tests/PHPStan/Analyser/data/allowed-subtypes-enum.php rename to tests/PHPStan/Analyser/nsrt/allowed-subtypes-enum.php index c66759043f..2cfe7640ab 100644 --- a/tests/PHPStan/Analyser/data/allowed-subtypes-enum.php +++ b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-enum.php @@ -1,4 +1,4 @@ -= 8.1 namespace AllowedSubtypesEnum; diff --git a/tests/PHPStan/Analyser/data/allowed-subtypes-throwable.php b/tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php similarity index 100% rename from tests/PHPStan/Analyser/data/allowed-subtypes-throwable.php rename to tests/PHPStan/Analyser/nsrt/allowed-subtypes-throwable.php diff --git a/tests/PHPStan/Analyser/data/always-true-elseif.php b/tests/PHPStan/Analyser/nsrt/always-true-elseif.php similarity index 100% rename from tests/PHPStan/Analyser/data/always-true-elseif.php rename to tests/PHPStan/Analyser/nsrt/always-true-elseif.php diff --git a/tests/PHPStan/Analyser/data/array-chunk-php8.php b/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php similarity index 88% rename from tests/PHPStan/Analyser/data/array-chunk-php8.php rename to tests/PHPStan/Analyser/nsrt/array-chunk-php8.php index 056695fe89..5c36290a8f 100644 --- a/tests/PHPStan/Analyser/data/array-chunk-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-chunk-php8.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace ArrayChunkPhp8; diff --git a/tests/PHPStan/Analyser/data/array-chunk-php81.php b/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php similarity index 92% rename from tests/PHPStan/Analyser/data/array-chunk-php81.php rename to tests/PHPStan/Analyser/nsrt/array-chunk-php81.php index 5009d428b1..d65050061d 100644 --- a/tests/PHPStan/Analyser/data/array-chunk-php81.php +++ b/tests/PHPStan/Analyser/nsrt/array-chunk-php81.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types = 1); namespace ArrayChunkPhp81; diff --git a/tests/PHPStan/Analyser/data/array-chunk.php b/tests/PHPStan/Analyser/nsrt/array-chunk.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-chunk.php rename to tests/PHPStan/Analyser/nsrt/array-chunk.php diff --git a/tests/PHPStan/Analyser/data/array-column-php7.php b/tests/PHPStan/Analyser/nsrt/array-column-php7.php similarity index 96% rename from tests/PHPStan/Analyser/data/array-column-php7.php rename to tests/PHPStan/Analyser/nsrt/array-column-php7.php index 5b634d5146..5d8018f599 100644 --- a/tests/PHPStan/Analyser/data/array-column-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayColumn\Php8; diff --git a/tests/PHPStan/Analyser/data/array-column-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php similarity index 99% rename from tests/PHPStan/Analyser/data/array-column-php82.php rename to tests/PHPStan/Analyser/nsrt/array-column-php82.php index b700980a37..62350f5992 100644 --- a/tests/PHPStan/Analyser/data/array-column-php82.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -1,4 +1,4 @@ -= 8.2 namespace ArrayColumn82; diff --git a/tests/PHPStan/Analyser/data/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php similarity index 99% rename from tests/PHPStan/Analyser/data/array-column.php rename to tests/PHPStan/Analyser/nsrt/array-column.php index 2bf6c929ba..2455d6ace9 100644 --- a/tests/PHPStan/Analyser/data/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayCombinePHP8; diff --git a/tests/PHPStan/Analyser/data/array-destructuring-types.php b/tests/PHPStan/Analyser/nsrt/array-destructuring-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-destructuring-types.php rename to tests/PHPStan/Analyser/nsrt/array-destructuring-types.php diff --git a/tests/PHPStan/Analyser/data/array-fill-keys-php7.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php similarity index 93% rename from tests/PHPStan/Analyser/data/array-fill-keys-php7.php rename to tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php index 80f3f43f7f..7bbc485b5a 100644 --- a/tests/PHPStan/Analyser/data/array-fill-keys-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayFillKeysPhp8; diff --git a/tests/PHPStan/Analyser/data/array-fill-keys.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-fill-keys.php rename to tests/PHPStan/Analyser/nsrt/array-fill-keys.php diff --git a/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php b/tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-filter-arrow-functions.php rename to tests/PHPStan/Analyser/nsrt/array-filter-arrow-functions.php diff --git a/tests/PHPStan/Analyser/data/array-filter-callables.php b/tests/PHPStan/Analyser/nsrt/array-filter-callables.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-filter-callables.php rename to tests/PHPStan/Analyser/nsrt/array-filter-callables.php diff --git a/tests/PHPStan/Analyser/data/array-filter-constant.php b/tests/PHPStan/Analyser/nsrt/array-filter-constant.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-filter-constant.php rename to tests/PHPStan/Analyser/nsrt/array-filter-constant.php diff --git a/tests/PHPStan/Analyser/data/array-filter-string-callables.php b/tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-filter-string-callables.php rename to tests/PHPStan/Analyser/nsrt/array-filter-string-callables.php diff --git a/tests/PHPStan/Analyser/data/array-filter.php b/tests/PHPStan/Analyser/nsrt/array-filter.php similarity index 85% rename from tests/PHPStan/Analyser/data/array-filter.php rename to tests/PHPStan/Analyser/nsrt/array-filter.php index def7caf588..682e117812 100644 --- a/tests/PHPStan/Analyser/data/array-filter.php +++ b/tests/PHPStan/Analyser/nsrt/array-filter.php @@ -11,7 +11,7 @@ function withoutAnyArgs(): void } /** - * @param $var1 $mixed + * @param mixed $var1 */ function withMixedInsteadOfArray($var1): void { @@ -35,3 +35,8 @@ function withoutCallback(array $map1, array $map2, array $map3): void $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/data/array-flip-constant.php b/tests/PHPStan/Analyser/nsrt/array-flip-constant.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-flip-constant.php rename to tests/PHPStan/Analyser/nsrt/array-flip-constant.php diff --git a/tests/PHPStan/Analyser/data/array-flip-php7.php b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php similarity index 93% rename from tests/PHPStan/Analyser/data/array-flip-php7.php rename to tests/PHPStan/Analyser/nsrt/array-flip-php7.php index 9802d817b0..0b7058de01 100644 --- a/tests/PHPStan/Analyser/data/array-flip-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayFlipPhp8; diff --git a/tests/PHPStan/Analyser/data/array-flip.php b/tests/PHPStan/Analyser/nsrt/array-flip.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-flip.php rename to tests/PHPStan/Analyser/nsrt/array-flip.php diff --git a/tests/PHPStan/Analyser/data/array-intersect-key-constant.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-constant.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-intersect-key-constant.php rename to tests/PHPStan/Analyser/nsrt/array-intersect-key-constant.php diff --git a/tests/PHPStan/Analyser/data/array-intersect-key-php7.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php similarity index 97% rename from tests/PHPStan/Analyser/data/array-intersect-key-php7.php rename to tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php index 002db78bb0..79b7af7b18 100644 --- a/tests/PHPStan/Analyser/data/array-intersect-key-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayIntersectKeyPhp8; diff --git a/tests/PHPStan/Analyser/data/array-intersect-key.php b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php similarity index 95% rename from tests/PHPStan/Analyser/data/array-intersect-key.php rename to tests/PHPStan/Analyser/nsrt/array-intersect-key.php index 5d17736266..bf620b508b 100644 --- a/tests/PHPStan/Analyser/data/array-intersect-key.php +++ b/tests/PHPStan/Analyser/nsrt/array-intersect-key.php @@ -15,9 +15,9 @@ class Foo public function nonEmpty(array $arr, array $arr2): void { assertType('non-empty-array', array_intersect_key($arr)); - assertType('non-empty-array', array_intersect_key($arr, $arr)); + assertType('array', array_intersect_key($arr, $arr)); assertType('array', array_intersect_key($arr, $arr2)); - assertType('non-empty-array', array_intersect_key($arr2, $arr)); + assertType('array', array_intersect_key($arr2, $arr)); assertType('array{}', array_intersect_key($arr, [])); assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17])); } diff --git a/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php b/tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-is-list-type-specifying.php rename to tests/PHPStan/Analyser/nsrt/array-is-list-type-specifying.php diff --git a/tests/PHPStan/Analyser/data/array-is-list-unset.php b/tests/PHPStan/Analyser/nsrt/array-is-list-unset.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-is-list-unset.php rename to tests/PHPStan/Analyser/nsrt/array-is-list-unset.php diff --git a/tests/PHPStan/Analyser/data/array-key-exists.php b/tests/PHPStan/Analyser/nsrt/array-key-exists.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-key-exists.php rename to tests/PHPStan/Analyser/nsrt/array-key-exists.php diff --git a/tests/PHPStan/Analyser/data/array-key.php b/tests/PHPStan/Analyser/nsrt/array-key.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-key.php rename to tests/PHPStan/Analyser/nsrt/array-key.php diff --git a/tests/PHPStan/Analyser/data/array-map-closure.php b/tests/PHPStan/Analyser/nsrt/array-map-closure.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-map-closure.php rename to tests/PHPStan/Analyser/nsrt/array-map-closure.php diff --git a/tests/PHPStan/Analyser/data/array-map.php b/tests/PHPStan/Analyser/nsrt/array-map.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-map.php rename to tests/PHPStan/Analyser/nsrt/array-map.php diff --git a/tests/PHPStan/Analyser/data/array-merge.php b/tests/PHPStan/Analyser/nsrt/array-merge.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-merge.php rename to tests/PHPStan/Analyser/nsrt/array-merge.php diff --git a/tests/PHPStan/Analyser/data/array-merge2.php b/tests/PHPStan/Analyser/nsrt/array-merge2.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-merge2.php rename to tests/PHPStan/Analyser/nsrt/array-merge2.php diff --git a/tests/PHPStan/Analyser/data/array-next.php b/tests/PHPStan/Analyser/nsrt/array-next.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-next.php rename to tests/PHPStan/Analyser/nsrt/array-next.php diff --git a/tests/PHPStan/Analyser/data/array-offset-unset.php b/tests/PHPStan/Analyser/nsrt/array-offset-unset.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-offset-unset.php rename to tests/PHPStan/Analyser/nsrt/array-offset-unset.php diff --git a/tests/PHPStan/Analyser/data/array-plus.php b/tests/PHPStan/Analyser/nsrt/array-plus.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-plus.php rename to tests/PHPStan/Analyser/nsrt/array-plus.php diff --git a/tests/PHPStan/Analyser/data/array-pop.php b/tests/PHPStan/Analyser/nsrt/array-pop.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-pop.php rename to tests/PHPStan/Analyser/nsrt/array-pop.php diff --git a/tests/PHPStan/Analyser/data/array-push.php b/tests/PHPStan/Analyser/nsrt/array-push.php similarity index 96% rename from tests/PHPStan/Analyser/data/array-push.php rename to tests/PHPStan/Analyser/nsrt/array-push.php index 8bef636a1b..4e2e235530 100644 --- a/tests/PHPStan/Analyser/data/array-push.php +++ b/tests/PHPStan/Analyser/nsrt/array-push.php @@ -66,7 +66,7 @@ function arrayPushConstantArray(): void /** @var array $f1 */ $f1 = []; array_push($f, ...$f1); - assertType('non-empty-array, 17|bool|null>', $f); + assertType('non-empty-list<17|bool|null>', $f); $g = [new stdClass()]; array_push($g, ...[new stdClass(), new stdClass()]); diff --git a/tests/PHPStan/Analyser/data/array-replace.php b/tests/PHPStan/Analyser/nsrt/array-replace.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-replace.php rename to tests/PHPStan/Analyser/nsrt/array-replace.php diff --git a/tests/PHPStan/Analyser/data/array-reverse.php b/tests/PHPStan/Analyser/nsrt/array-reverse.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-reverse.php rename to tests/PHPStan/Analyser/nsrt/array-reverse.php diff --git a/tests/PHPStan/Analyser/data/array-search-php7.php b/tests/PHPStan/Analyser/nsrt/array-search-php7.php similarity index 92% rename from tests/PHPStan/Analyser/data/array-search-php7.php rename to tests/PHPStan/Analyser/nsrt/array-search-php7.php index b6f9bbae4d..2cd24b7c9d 100644 --- a/tests/PHPStan/Analyser/data/array-search-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-search-php7.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace ArraySearchPhp8; diff --git a/tests/PHPStan/Analyser/data/array-search-type-specifying.php b/tests/PHPStan/Analyser/nsrt/array-search-type-specifying.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-search-type-specifying.php rename to tests/PHPStan/Analyser/nsrt/array-search-type-specifying.php diff --git a/tests/PHPStan/Analyser/data/array-search.php b/tests/PHPStan/Analyser/nsrt/array-search.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-search.php rename to tests/PHPStan/Analyser/nsrt/array-search.php diff --git a/tests/PHPStan/Analyser/data/array-shape-list-optional.php b/tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-shape-list-optional.php rename to tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php diff --git a/tests/PHPStan/Analyser/data/array-shapes-keys-strings.php b/tests/PHPStan/Analyser/nsrt/array-shapes-keys-strings.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-shapes-keys-strings.php rename to tests/PHPStan/Analyser/nsrt/array-shapes-keys-strings.php diff --git a/tests/PHPStan/Analyser/data/array-shift.php b/tests/PHPStan/Analyser/nsrt/array-shift.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-shift.php rename to tests/PHPStan/Analyser/nsrt/array-shift.php diff --git a/tests/PHPStan/Analyser/data/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-slice.php rename to tests/PHPStan/Analyser/nsrt/array-slice.php diff --git a/tests/PHPStan/Analyser/data/array-sum.php b/tests/PHPStan/Analyser/nsrt/array-sum.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-sum.php rename to tests/PHPStan/Analyser/nsrt/array-sum.php diff --git a/tests/PHPStan/Analyser/data/array-typehint-without-null-in-phpdoc.php b/tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php similarity index 100% rename from tests/PHPStan/Analyser/data/array-typehint-without-null-in-phpdoc.php rename to tests/PHPStan/Analyser/nsrt/array-typehint-without-null-in-phpdoc.php diff --git a/tests/PHPStan/Analyser/data/array-unpacking-string-keys.php b/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php similarity index 98% rename from tests/PHPStan/Analyser/data/array-unpacking-string-keys.php rename to tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php index ccb2e6da0b..a03dbb75c9 100644 --- a/tests/PHPStan/Analyser/data/array-unpacking-string-keys.php +++ b/tests/PHPStan/Analyser/nsrt/array-unpacking-string-keys.php @@ -1,4 +1,4 @@ -= 8.1 namespace ArrayUnpackingWithStringKeys; diff --git a/tests/PHPStan/Analyser/data/array-unshift.php b/tests/PHPStan/Analyser/nsrt/array-unshift.php similarity index 96% rename from tests/PHPStan/Analyser/data/array-unshift.php rename to tests/PHPStan/Analyser/nsrt/array-unshift.php index 899b011cfa..933aad522d 100644 --- a/tests/PHPStan/Analyser/data/array-unshift.php +++ b/tests/PHPStan/Analyser/nsrt/array-unshift.php @@ -66,7 +66,7 @@ function arrayUnshiftConstantArray(): void /** @var array $f1 */ $f1 = []; array_unshift($f, ...$f1); - assertType('non-empty-array, 17|bool|null>', $f); + assertType('non-empty-list<17|bool|null>', $f); $g = [new stdClass()]; array_unshift($g, ...[new stdClass(), new stdClass()]); diff --git a/tests/PHPStan/Analyser/data/array_keys-php7.php b/tests/PHPStan/Analyser/nsrt/array_keys-php7.php similarity index 94% rename from tests/PHPStan/Analyser/data/array_keys-php7.php rename to tests/PHPStan/Analyser/nsrt/array_keys-php7.php index bf83b41e99..103cb0a003 100644 --- a/tests/PHPStan/Analyser/data/array_keys-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array_keys-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayKeys; diff --git a/tests/PHPStan/Analyser/data/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php similarity index 100% rename from tests/PHPStan/Analyser/data/array_map_multiple.php rename to tests/PHPStan/Analyser/nsrt/array_map_multiple.php diff --git a/tests/PHPStan/Analyser/data/array_splice.php b/tests/PHPStan/Analyser/nsrt/array_splice.php similarity index 100% rename from tests/PHPStan/Analyser/data/array_splice.php rename to tests/PHPStan/Analyser/nsrt/array_splice.php diff --git a/tests/PHPStan/Analyser/data/array_values-php7.php b/tests/PHPStan/Analyser/nsrt/array_values-php7.php similarity index 93% rename from tests/PHPStan/Analyser/data/array_values-php7.php rename to tests/PHPStan/Analyser/nsrt/array_values-php7.php index 4e6e93d33f..10de823980 100644 --- a/tests/PHPStan/Analyser/data/array_values-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array_values-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace ArrayValues; diff --git a/tests/PHPStan/Analyser/data/arrow-function-argument-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/arrow-function-argument-type.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-argument-type.php diff --git a/tests/PHPStan/Analyser/data/arrow-function-return-type.php b/tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/arrow-function-return-type.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-return-type.php diff --git a/tests/PHPStan/Analyser/data/arrow-function-types.php b/tests/PHPStan/Analyser/nsrt/arrow-function-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/arrow-function-types.php rename to tests/PHPStan/Analyser/nsrt/arrow-function-types.php diff --git a/tests/PHPStan/Analyser/data/assert-class-type.php b/tests/PHPStan/Analyser/nsrt/assert-class-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-class-type.php rename to tests/PHPStan/Analyser/nsrt/assert-class-type.php diff --git a/tests/PHPStan/Analyser/data/assert-conditional.php b/tests/PHPStan/Analyser/nsrt/assert-conditional.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-conditional.php rename to tests/PHPStan/Analyser/nsrt/assert-conditional.php diff --git a/tests/PHPStan/Analyser/nsrt/assert-constructor.php b/tests/PHPStan/Analyser/nsrt/assert-constructor.php new file mode 100644 index 0000000000..2d133d8c6c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assert-constructor.php @@ -0,0 +1,22 @@ + + */ +class IntWrapper implements WrapperInterface +{ + public function assert(mixed $param): void + { + } + + public function supports(mixed $param): bool + { + return is_int($param); + } + + public function notSupports(mixed $param): bool + { + return !is_int($param); + } +} + +/** + * @template T of object + * @implements WrapperInterface + */ +abstract class ObjectWrapper implements WrapperInterface +{ +} + +/** + * @extends ObjectWrapper<\DateTimeInterface> + */ +class DateTimeInterfaceWrapper extends ObjectWrapper +{ + public function assert(mixed $param): void + { + } + + public function supports(mixed $param): bool + { + return $param instanceof \DateTimeInterface; + } + + public function notSupports(mixed $param): bool + { + return !$param instanceof \DateTimeInterface; + } +} + +function (IntWrapper $test, $val) { + if ($test->supports($val)) { + assertType('int', $val); + } else { + assertType('mixed~int', $val); + } + + if ($test->notSupports($val)) { + assertType('mixed~int', $val); + } else { + assertType('int', $val); + } + + assertType('mixed', $val); + $test->assert($val); + assertType('int', $val); +}; + +function (DateTimeInterfaceWrapper $test, $val) { + if ($test->supports($val)) { + assertType('DateTimeInterface', $val); + } else { + assertType('mixed~DateTimeInterface', $val); + } + + if ($test->notSupports($val)) { + assertType('mixed~DateTimeInterface', $val); + } else { + assertType('DateTimeInterface', $val); + } + + assertType('mixed', $val); + $test->assert($val); + assertType('DateTimeInterface', $val); +}; diff --git a/tests/PHPStan/Analyser/data/assert-intersected.php b/tests/PHPStan/Analyser/nsrt/assert-intersected.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-intersected.php rename to tests/PHPStan/Analyser/nsrt/assert-intersected.php diff --git a/tests/PHPStan/Analyser/data/assert-invariant.php b/tests/PHPStan/Analyser/nsrt/assert-invariant.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-invariant.php rename to tests/PHPStan/Analyser/nsrt/assert-invariant.php diff --git a/tests/PHPStan/Analyser/data/assert-method.php b/tests/PHPStan/Analyser/nsrt/assert-method.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-method.php rename to tests/PHPStan/Analyser/nsrt/assert-method.php diff --git a/tests/PHPStan/Analyser/data/assert-methods.php b/tests/PHPStan/Analyser/nsrt/assert-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-methods.php rename to tests/PHPStan/Analyser/nsrt/assert-methods.php diff --git a/tests/PHPStan/Analyser/data/assert-property.php b/tests/PHPStan/Analyser/nsrt/assert-property.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-property.php rename to tests/PHPStan/Analyser/nsrt/assert-property.php diff --git a/tests/PHPStan/Analyser/data/assert-this.php b/tests/PHPStan/Analyser/nsrt/assert-this.php similarity index 100% rename from tests/PHPStan/Analyser/data/assert-this.php rename to tests/PHPStan/Analyser/nsrt/assert-this.php diff --git a/tests/PHPStan/Analyser/data/assign-nested-arrays.php b/tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php similarity index 100% rename from tests/PHPStan/Analyser/data/assign-nested-arrays.php rename to tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php diff --git a/tests/PHPStan/Analyser/data/asymmetric-properties.php b/tests/PHPStan/Analyser/nsrt/asymmetric-properties.php similarity index 100% rename from tests/PHPStan/Analyser/data/asymmetric-properties.php rename to tests/PHPStan/Analyser/nsrt/asymmetric-properties.php diff --git a/tests/PHPStan/Analyser/data/base64_decode.php b/tests/PHPStan/Analyser/nsrt/base64_decode.php similarity index 100% rename from tests/PHPStan/Analyser/data/base64_decode.php rename to tests/PHPStan/Analyser/nsrt/base64_decode.php diff --git a/tests/PHPStan/Analyser/data/bcmath-dynamic-return-php7.php b/tests/PHPStan/Analyser/nsrt/bcmath-dynamic-return-php7.php similarity index 99% rename from tests/PHPStan/Analyser/data/bcmath-dynamic-return-php7.php rename to tests/PHPStan/Analyser/nsrt/bcmath-dynamic-return-php7.php index e7be5836a4..810ffd54e8 100644 --- a/tests/PHPStan/Analyser/data/bcmath-dynamic-return-php7.php +++ b/tests/PHPStan/Analyser/nsrt/bcmath-dynamic-return-php7.php @@ -1,4 +1,4 @@ -= 8.0 // Verification for constant types: https://3v4l.org/96GSj diff --git a/tests/PHPStan/Analyser/data/benevolent-union-math.php b/tests/PHPStan/Analyser/nsrt/benevolent-union-math.php similarity index 100% rename from tests/PHPStan/Analyser/data/benevolent-union-math.php rename to tests/PHPStan/Analyser/nsrt/benevolent-union-math.php diff --git a/tests/PHPStan/Analyser/data/bitwise-not.php b/tests/PHPStan/Analyser/nsrt/bitwise-not.php similarity index 100% rename from tests/PHPStan/Analyser/data/bitwise-not.php rename to tests/PHPStan/Analyser/nsrt/bitwise-not.php diff --git a/tests/PHPStan/Analyser/data/bug-10002.php b/tests/PHPStan/Analyser/nsrt/bug-10002.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-10002.php rename to tests/PHPStan/Analyser/nsrt/bug-10002.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10037.php b/tests/PHPStan/Analyser/nsrt/bug-10037.php new file mode 100644 index 0000000000..58adb961c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10037.php @@ -0,0 +1,96 @@ + */ +final readonly class PostFetcher implements Fetcher +{ + public function supports(Identifier $identifier): bool + { + return $identifier instanceof PostIdentifier; + } + + public function fetch(Identifier $identifier): Document + { + // SA knows $identifier is instance of PostIdentifier here + return $identifier->foo(); + } +} + +class PostIdentifier implements Identifier +{ + public function foo(): Document + { + return new class implements Document{}; + } +} + +function (Identifier $i): void { + $fetcher = new PostFetcher(); + \PHPStan\Testing\assertType('Bug10037\Identifier', $i); + if ($fetcher->supports($i)) { + \PHPStan\Testing\assertType('Bug10037\PostIdentifier', $i); + $fetcher->fetch($i); + } else { + $fetcher->fetch($i); + } +}; + +class Post +{ +} + +/** @template T */ +abstract class Voter +{ + + /** @phpstan-assert-if-true T $subject */ + abstract function supports(string $attribute, mixed $subject): bool; + + /** @param T $subject */ + abstract function voteOnAttribute(string $attribute, mixed $subject): bool; + +} + +/** @extends Voter */ +class PostVoter extends Voter +{ + + /** @phpstan-assert-if-true Post $subject */ + function supports(string $attribute, mixed $subject): bool + { + + } + + function voteOnAttribute(string $attribute, mixed $subject): bool + { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + } +} + +function ($subject): void { + $voter = new PostVoter(); + \PHPStan\Testing\assertType('mixed', $subject); + if ($voter->supports('aaa', $subject)) { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + $voter->voteOnAttribute('aaa', $subject); + } else { + $voter->voteOnAttribute('aaa', $subject); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-10071.php b/tests/PHPStan/Analyser/nsrt/bug-10071.php similarity index 86% rename from tests/PHPStan/Analyser/data/bug-10071.php rename to tests/PHPStan/Analyser/nsrt/bug-10071.php index ecb298adb3..ef25a1d61d 100644 --- a/tests/PHPStan/Analyser/data/bug-10071.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10071.php @@ -1,4 +1,6 @@ -= 8.0 += 8.0 + +declare(strict_types=1); namespace Bug10071; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10080.php b/tests/PHPStan/Analyser/nsrt/bug-10080.php new file mode 100644 index 0000000000..1875d50dfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10080.php @@ -0,0 +1,76 @@ + + */ + public static function some($value): self + { + return new self($value); + } + + /** + * @template Tu + * + * @param (Closure(T): Tu) $closure + * + * @return Option + */ + public function map(Closure $closure): self + { + return new self($closure($this->unwrap())); + } + + /** + * @return T + */ + public function unwrap() + { + if ($this->value === null) { + throw new RuntimeException(); + } + + return $this->value; + } + + /** + * @template To + * @param self $other + * @return self + */ + public function zip(self $other) + { + return new self([ + $this->unwrap(), + $other->unwrap() + ]); + } +} + + +function (): void { + $value = Option::some(1) + ->zip(Option::some(2)); + + assertType('Bug10254\\Option', $value); + + $value1 = $value->map(function ($value) { + assertType('int', $value[0]); + assertType('int', $value[1]); + return $value[0] + $value[1]; + }); + + assertType('Bug10254\\Option', $value1); + + $value2 = $value->map(function ($value): int { + assertType('int', $value[0]); + assertType('int', $value[1]); + return $value[0] + $value[1]; + }); + + assertType('Bug10254\\Option', $value2); + +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-10264.php b/tests/PHPStan/Analyser/nsrt/bug-10264.php new file mode 100644 index 0000000000..20b1361a25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10264.php @@ -0,0 +1,80 @@ + $list */ + $list = []; + + assertType('list', $list); + + assert((count($list) <= 1) === true); + assertType('list', $list); + } + + function doFoo2() { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + assert((count($list, COUNT_NORMAL) <= 1) === true); + assertType('list', $list); + } + + /** @param list $c */ + public function sayHello(array $c): void + { + assertType('list', $c); + if (count($c) > 0) { + $c = array_map(fn() => new stdClass(), $c); + assertType('non-empty-list', $c); + } else { + assertType('array{}', $c); + } + + assertType('list', $c); + } + + function doBar() { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + assert((count($list, COUNT_RECURSIVE) <= 1) === true); + assertType('list', $list); + } + + function doIf():void { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + if( count($list, COUNT_RECURSIVE) >= 1) { + assertType('non-empty-list', $list); + } else { + assertType('array{}', $list); + } + } + + function countModeInt(int $i):void { + /** @var list $list */ + $list = []; + + assertType('list', $list); + + if( count($list, $i) >= 1) { + assertType('non-empty-list', $list); + } else { + assertType('array{}', $list); + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10283.php b/tests/PHPStan/Analyser/nsrt/bug-10283.php new file mode 100644 index 0000000000..e2cb63e31e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10283.php @@ -0,0 +1,25 @@ + + */ +class Rows +{ + + /** + * @param list $rowsData + */ + public function __construct(private array $rowsData) + {} + + /** + * @return Row|NULL + */ + public function getByIndex(int $index): ?Row + { + return isset($this->rowsData[$index]) + ? new Row($this->rowsData[$index]) + : NULL; + } +} + +/** + * @template TRow of array + * @implements ArrayAccess, value-of> + */ +class Row implements ArrayAccess +{ + + /** + * @param TRow $data + */ + public function __construct(private array $data) + {} + + /** + * @param key-of $key + */ + public function offsetExists($key): bool + { + return isset($this->data[$key]); + } + + /** + * @template TKey of key-of + * @param TKey $key + * @return TRow[TKey] + */ + public function offsetGet($key): mixed + { + return $this->data[$key]; + } + + public function offsetSet($key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function offsetUnset($key): void + { + unset($this->data[$key]); + } + + /** + * @return TRow + */ + public function toArray(): array + { + return $this->data; + } + +} + +class Foo +{ + + /** @param Rows}> $rows */ + public function doFoo(Rows $rows): void + { + assertType('Bug10473\Rows}>', $rows); + + $row = $rows->getByIndex(0); + + if ($row !== NULL) { + assertType('Bug10473\Row}>', $row); + $fooFromRow = $row['foo']; + + assertType('int<0, max>', $fooFromRow); + assertType('array{foo: int<0, max>}', $row->toArray()); + } + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10477.php b/tests/PHPStan/Analyser/nsrt/bug-10477.php new file mode 100644 index 0000000000..c97672bf98 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10477.php @@ -0,0 +1,28 @@ +foo); + assertType('$this(Bug10477\A)', $this); + (new B())->foo($this); + assertType('mixed', $this->foo); + assertType('$this(Bug10477\A)', $this); + if (isset($this->data['test'])) { + $this->foo = $this->data['test']; + } + } +} + +class B +{ + public function foo(mixed &$var): void {} +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10566.php b/tests/PHPStan/Analyser/nsrt/bug-10566.php new file mode 100644 index 0000000000..2fb46c5a7f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10566.php @@ -0,0 +1,171 @@ +running = true; + + while ($this->running) { + assertType('true', $this->running); + call_user_func(function () { + $this->stop(); + }); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function stop(): void + { + $this->running = false; + } + + public function run2(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run3(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () { + $s = new self(); + $s->stop(); + }); + assertType('true', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run4(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + $cb = function () use ($s) { + $s = new self(); + $s->stop(); + }; + assertType('true', $s->running); + call_user_func($cb); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run5(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + $cb = function () { + $this->stop(); + }; + assertType('true', $this->running); + call_user_func($cb); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function run6(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + (function () use ($s) { + $s = new self(); + $s->stop(); + })(); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run7(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + (function () { + $this->stop(); + })(); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + function run8(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(static function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10627.php b/tests/PHPStan/Analyser/nsrt/bug-10627.php new file mode 100644 index 0000000000..17579ec52c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10627.php @@ -0,0 +1,95 @@ + $list + * @return void + */ + public function sayHello9(array $list): void + { + krsort($list); + assertType("array, string>", $list); + } + + public function sayHello10(): void + { + $list = ['a' => 'A', 'c' => 'C', 'b' => 'B']; + krsort($list); + assertType("array{a: 'A', c: 'C', b: 'B'}", $list); + assertType('false', array_is_list($list)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10699.php b/tests/PHPStan/Analyser/nsrt/bug-10699.php new file mode 100644 index 0000000000..de06986e90 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10699.php @@ -0,0 +1,49 @@ + + */ + public function retrieve(?int $limit = 20): array + { + $list = [ + 'zib', + 'zib 2', + 'zeit im bild', + 'soko', + 'landkrimi', + 'tatort', + ]; + + assertType("array{'zib', 'zib 2', 'zeit im bild', 'soko', 'landkrimi', 'tatort'}", $list); + shuffle($list); + assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", $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))); + return array_slice($list, 0, max($limit, 1)); + } + + public function listVariants(): void + { + $arr = [ + 2 => 'zib', + 4 => 'zib 2', + ]; + + assertType("array{2: 'zib', 4: 'zib 2'}", $arr); + shuffle($arr); + assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $arr); + + $list = [ + 'zib', + 'zib 2', + ]; + + 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)); + } + + /** + * @param array $strings + * @param 0|1 $maybeZero + */ + 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)); + + 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)); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10834.php b/tests/PHPStan/Analyser/nsrt/bug-10834.php new file mode 100644 index 0000000000..69efb18635 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10834.php @@ -0,0 +1,21 @@ + $b + */ + public function doFoo($b): void + { + assertType('non-falsy-string', '@' . $b); + } + + /** + * @param int|false $b + */ + public function doFoo2($b): void + { + assertType('non-falsy-string', '@' . $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10893.php b/tests/PHPStan/Analyser/nsrt/bug-10893.php new file mode 100644 index 0000000000..469c8956bd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10893.php @@ -0,0 +1,20 @@ +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); + + return (int) $value->format('u') !== 0; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10952.php b/tests/PHPStan/Analyser/nsrt/bug-10952.php new file mode 100644 index 0000000000..d25c03b1fe --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10952.php @@ -0,0 +1,32 @@ + + */ + public function getArray(): array + { + return array_fill(0, random_int(0, 10), 'test'); + } + + public function test(): void + { + $array = $this->getArray(); + + if (count($array) > 1) { + assertType('non-empty-array', $array); + } else { + assertType('array', $array); + } + + match (true) { + count($array) > 1 => assertType('non-empty-array', $array), + default => assertType('array', $array), + }; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10952b.php b/tests/PHPStan/Analyser/nsrt/bug-10952b.php new file mode 100644 index 0000000000..f8f70e07d0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10952b.php @@ -0,0 +1,41 @@ +getString(); + + if (1 < mb_strlen($string)) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + if (mb_strlen($string) > 1) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + if (2 < mb_strlen($string)) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + match (true) { + mb_strlen($string) > 0 => assertType('non-empty-string', $string), + default => assertType("''", $string), + }; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11035.php b/tests/PHPStan/Analyser/nsrt/bug-11035.php new file mode 100644 index 0000000000..dabb834965 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11035.php @@ -0,0 +1,41 @@ + $maybeOne + * @param int<2,10> $neverOne + */ +function lengthTypes(string $phone, int $maybeOne, int $neverOne): string +{ + if ( + 10 === strlen($phone) + ) { + assertType('non-falsy-string', $phone); + + assertType('non-empty-string', substr($phone, 0, 1)); + assertType('bool', '0' === substr($phone, 0, 1)); + + assertType('non-empty-string', substr($phone, 0, $maybeOne)); + assertType('bool', '0' === substr($phone, 0, $maybeOne)); + + assertType('non-falsy-string', substr($phone, 0, $neverOne)); + assertType('false', '0' === substr($phone, 0, $neverOne)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11129.php b/tests/PHPStan/Analyser/nsrt/bug-11129.php new file mode 100644 index 0000000000..8637ad7c4a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11129.php @@ -0,0 +1,83 @@ +eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgetss(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + } +} \ No newline at end of file diff --git a/tests/PHPStan/Analyser/nsrt/bug-11200.php b/tests/PHPStan/Analyser/nsrt/bug-11200.php new file mode 100644 index 0000000000..588298c7d6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11200.php @@ -0,0 +1,180 @@ +eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fflush(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fgetc() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgetc(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fgetcsv() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgetcsv(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fgets() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fgets(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fpassthru() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fpassthru(); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fputcsv() : void + { + // places file pointer at the start of the file + $file = new \SplFileObject('php://memory', 'rw+'); + assertType('int|false', $file->ftell()); + if ($file->ftell() !== 0) + { + return; + } + assertType('0', $file->ftell()); + // This file is not empty. + // call method that has side effects + $file->fputcsv(['a']); + // the value of ftell may have changed + assertType('int|false', $file->ftell()); + } + + public function fread() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fread(1); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fscanf() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->fscanf('%f'); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fseek() : void + { + // places file pointer at the start of the file + $file = new \SplFileObject('php://memory', 'rw+'); + assertType('int|false', $file->ftell()); + if ($file->ftell() !== 0) + { + return; + } + assertType('0', $file->ftell()); + // This file is not empty. + // call method that has side effects + $file->fseek(1,\SEEK_SET); + // the value of ftell may have changed + assertType('int|false', $file->ftell()); + } + + public function ftruncate() : void + { + $file = new \SplFileObject('php://memory', 'r'); + assertType('bool', $file->eof()); + if ( $file->eof() ) + { + return; + } + assertType('false', $file->eof()); + // call method that has side effects + $file->ftruncate(0); + // the value of eof may have changed + assertType('bool', $file->eof()); + } + + public function fwrite() : void + { + // places file pointer at the start of the file + $file = new \SplFileObject('php://memory', 'rw+'); + assertType('int|false', $file->ftell()); + if ($file->ftell() !== 0) + { + return; + } + assertType('0', $file->ftell()); + // This file is not empty. + // call method that has side effects + $file->fwrite('a'); + // the value of ftell may have changed + assertType('int|false', $file->ftell()); + } + +} \ No newline at end of file diff --git a/tests/PHPStan/Analyser/nsrt/bug-11201.php b/tests/PHPStan/Analyser/nsrt/bug-11201.php new file mode 100644 index 0000000000..74a41fa235 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11201.php @@ -0,0 +1,56 @@ + */ +function returnsArray(){ + return []; +} + +/** @return non-empty-string */ +function returnsNonEmptyString(): string +{ + return 'a'; +} + +/** @return non-falsy-string */ +function returnsNonFalsyString(): string +{ + return '1'; +} + +/** @return string */ +function returnsJustString(): string +{ + return rand(0,1) === 1 ? 'foo' : ''; +} + +function returnsBool(): bool { + return true; +} + +$s = sprintf("%s", returnsNonEmptyString()); +assertType('non-empty-string', $s); + +$s = sprintf("%s", returnsNonFalsyString()); +assertType('non-falsy-string', $s); + +$s = sprintf("%s", returnsJustString()); +assertType('string', $s); + +$s = sprintf("%s", implode(', ', array_map('intval', returnsArray()))); +assertType('string', $s); + +$s = sprintf('%2$s', 1234, returnsNonFalsyString()); +assertType('non-falsy-string', $s); + +$s = sprintf('%20s', 'abc'); +assertType("' abc'", $s); + +$s = sprintf('%20s', true); +assertType("' 1'", $s); + +$s = sprintf('%20s', returnsBool()); +assertType("non-falsy-string", $s); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11233.php b/tests/PHPStan/Analyser/nsrt/bug-11233.php new file mode 100644 index 0000000000..e8191d37a5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11233.php @@ -0,0 +1,30 @@ += 8.1 + +namespace Bug11233; + +use function PHPStan\Testing\assertType; + +class EnumExtension +{ + /** + * @template T of \UnitEnum + * + * @param class-string $enum + */ + public static function getEnumCases(string $enum): void + { + assertType('list', $enum::cases()); + } + + /** + * @template T of \BackedEnum + * + * @param class-string $enum + * + * @return list + */ + public static function getEnumCases2(string $enum): void + { + assertType('list', $enum::cases()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php b/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php new file mode 100644 index 0000000000..40e6d99d9f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11311-php72.php @@ -0,0 +1,30 @@ +\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 new file mode 100644 index 0000000000..3a01594ded --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -0,0 +1,226 @@ += 7.4 + +namespace Bug11311; + +use function PHPStan\Testing\assertType; +use InvalidArgumentException; + +function doFoo(string $s) { + if (1 === preg_match('/(?\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); + } +} + +function doUnmatchedAsNull(string $s): void { + if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); + } + assertType("array{}|array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); +} + +// see https://3v4l.org/VeDob +function unmatchedAsNullWithOptionalGroup(string $s): void { + if (preg_match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + // with PREG_UNMATCHED_AS_NULL the offset 1 will always exist. It is correct that it's nullable because it's optional though + assertType('array{string, non-empty-string|null}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, non-empty-string|null}', $matches); +} + +function bug11331a(string $url):void { + // group a is actually optional as the entire (?:...) around it is optional + if (preg_match('{^ + (?: + (?.+) + )? + (?.+)}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{0: string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string, 2: non-empty-string}', $matches); + } +} + +function bug11331b(string $url):void { + if (preg_match('{^ + (?: + (?.+) + )? + (?.+)?}mix', $url, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{0: string, a: non-empty-string|null, 1: non-empty-string|null, b: non-empty-string|null, 2: non-empty-string|null}', $matches); + } +} + +function bug11331c(string $url):void { + if (preg_match('{^ + (?: + (?:https?|git)://([^/]+)/ (?# group 1 here can be null if group 2 matches) + | (?# the alternation making it so that only either should match) + git@([^:]+):/? (?# group 2 here can be null if group 1 matches) + ) + ([^/]+) + / + ([^/]+?) + (?:\.git|/)? +$}x', $url, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{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 + } + } + + 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 + } + } +} + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +function (string $s): void { + if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{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); + } +}; + +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); + } +}; + +function (string $s): void { + if (preg_match('/(?\s*)(?.*)/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType('array{0: string, whitespace: string, 1: string, value: string, 2: string}', $matches); + } +}; + +function (string $s): void { + preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); + assertType("array{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} +}; + +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); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{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); + } +}; + +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); + } +}; + +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); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11518-types.php b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php new file mode 100644 index 0000000000..4f66f4f0af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11518-types.php @@ -0,0 +1,23 @@ += 8.0 namespace Bug2600Php8; diff --git a/tests/PHPStan/Analyser/data/bug-2600.php b/tests/PHPStan/Analyser/nsrt/bug-2600.php similarity index 98% rename from tests/PHPStan/Analyser/data/bug-2600.php rename to tests/PHPStan/Analyser/nsrt/bug-2600.php index 11c341582b..9ab5e49598 100644 --- a/tests/PHPStan/Analyser/data/bug-2600.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2600.php @@ -1,4 +1,4 @@ - 'een', 'two' => 'twee', 'three' => 'drie']; + usort($arr, 'strcmp'); + assertType("non-empty-list<'drie'|'een'|'twee'>", $arr); +} diff --git a/tests/PHPStan/Analyser/data/bug-3321.php b/tests/PHPStan/Analyser/nsrt/bug-3321.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3321.php rename to tests/PHPStan/Analyser/nsrt/bug-3321.php diff --git a/tests/PHPStan/Analyser/data/bug-3331.php b/tests/PHPStan/Analyser/nsrt/bug-3331.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3331.php rename to tests/PHPStan/Analyser/nsrt/bug-3331.php diff --git a/tests/PHPStan/Analyser/data/bug-3336.php b/tests/PHPStan/Analyser/nsrt/bug-3336.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-3336.php rename to tests/PHPStan/Analyser/nsrt/bug-3336.php index a0f7127237..a6712e6f69 100644 --- a/tests/PHPStan/Analyser/data/bug-3336.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3336.php @@ -1,4 +1,4 @@ - 1){ - assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}|array{1}', $idGroups); + assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); } }; diff --git a/tests/PHPStan/Analyser/data/bug-3617.php b/tests/PHPStan/Analyser/nsrt/bug-3617.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3617.php rename to tests/PHPStan/Analyser/nsrt/bug-3617.php diff --git a/tests/PHPStan/Analyser/data/bug-3677.php b/tests/PHPStan/Analyser/nsrt/bug-3677.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3677.php rename to tests/PHPStan/Analyser/nsrt/bug-3677.php diff --git a/tests/PHPStan/Analyser/data/bug-3710.php b/tests/PHPStan/Analyser/nsrt/bug-3710.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3710.php rename to tests/PHPStan/Analyser/nsrt/bug-3710.php diff --git a/tests/PHPStan/Analyser/data/bug-3760.php b/tests/PHPStan/Analyser/nsrt/bug-3760.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3760.php rename to tests/PHPStan/Analyser/nsrt/bug-3760.php diff --git a/tests/PHPStan/Analyser/data/bug-3789.php b/tests/PHPStan/Analyser/nsrt/bug-3789.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3789.php rename to tests/PHPStan/Analyser/nsrt/bug-3789.php diff --git a/tests/PHPStan/Analyser/data/bug-3822.php b/tests/PHPStan/Analyser/nsrt/bug-3822.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3822.php rename to tests/PHPStan/Analyser/nsrt/bug-3822.php diff --git a/tests/PHPStan/Analyser/data/bug-3853.php b/tests/PHPStan/Analyser/nsrt/bug-3853.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3853.php rename to tests/PHPStan/Analyser/nsrt/bug-3853.php diff --git a/tests/PHPStan/Analyser/data/bug-3858.php b/tests/PHPStan/Analyser/nsrt/bug-3858.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3858.php rename to tests/PHPStan/Analyser/nsrt/bug-3858.php diff --git a/tests/PHPStan/Analyser/data/bug-3866.php b/tests/PHPStan/Analyser/nsrt/bug-3866.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3866.php rename to tests/PHPStan/Analyser/nsrt/bug-3866.php diff --git a/tests/PHPStan/Analyser/data/bug-3875.php b/tests/PHPStan/Analyser/nsrt/bug-3875.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3875.php rename to tests/PHPStan/Analyser/nsrt/bug-3875.php diff --git a/tests/PHPStan/Analyser/data/bug-3880.php b/tests/PHPStan/Analyser/nsrt/bug-3880.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3880.php rename to tests/PHPStan/Analyser/nsrt/bug-3880.php diff --git a/tests/PHPStan/Analyser/data/bug-3915.php b/tests/PHPStan/Analyser/nsrt/bug-3915.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3915.php rename to tests/PHPStan/Analyser/nsrt/bug-3915.php diff --git a/tests/PHPStan/Analyser/data/bug-3922.php b/tests/PHPStan/Analyser/nsrt/bug-3922.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-3922.php rename to tests/PHPStan/Analyser/nsrt/bug-3922.php diff --git a/tests/PHPStan/Analyser/data/bug-3961-php8.php b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php similarity index 75% rename from tests/PHPStan/Analyser/data/bug-3961-php8.php rename to tests/PHPStan/Analyser/nsrt/bug-3961-php8.php index fadfcd7209..9eaeff4a72 100644 --- a/tests/PHPStan/Analyser/data/bug-3961-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3961-php8.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug3961Php8; @@ -14,8 +14,8 @@ public function doFoo(string $v, string $d, $m): void assertType('list', explode('.', $v, -2)); assertType('non-empty-list', explode('.', $v, 0)); assertType('non-empty-list', explode('.', $v, 1)); - assertType('list', explode($d, $v)); - assertType('list', explode($m, $v)); + assertType('non-empty-list', explode($d, $v)); + assertType('non-empty-list', explode($m, $v)); } } diff --git a/tests/PHPStan/Analyser/data/bug-3961.php b/tests/PHPStan/Analyser/nsrt/bug-3961.php similarity index 73% rename from tests/PHPStan/Analyser/data/bug-3961.php rename to tests/PHPStan/Analyser/nsrt/bug-3961.php index 5482601dd4..b4725ec070 100644 --- a/tests/PHPStan/Analyser/data/bug-3961.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3961.php @@ -1,4 +1,4 @@ -', explode('.', $v, -2)); assertType('non-empty-list', explode('.', $v, 0)); assertType('non-empty-list', explode('.', $v, 1)); - assertType('list|false', explode($d, $v)); - assertType('(list|false)', explode($m, $v)); + assertType('non-empty-list|false', explode($d, $v)); + assertType('(non-empty-list|false)', explode($m, $v)); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3981.php b/tests/PHPStan/Analyser/nsrt/bug-3981.php new file mode 100644 index 0000000000..c962983a3e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3981.php @@ -0,0 +1,54 @@ += 8.1 namespace Bug4213; diff --git a/tests/PHPStan/Analyser/data/bug-4215.php b/tests/PHPStan/Analyser/nsrt/bug-4215.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4215.php rename to tests/PHPStan/Analyser/nsrt/bug-4215.php diff --git a/tests/PHPStan/Analyser/data/bug-4231.php b/tests/PHPStan/Analyser/nsrt/bug-4231.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4231.php rename to tests/PHPStan/Analyser/nsrt/bug-4231.php diff --git a/tests/PHPStan/Analyser/data/bug-4247.php b/tests/PHPStan/Analyser/nsrt/bug-4247.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4247.php rename to tests/PHPStan/Analyser/nsrt/bug-4247.php diff --git a/tests/PHPStan/Analyser/data/bug-4267.php b/tests/PHPStan/Analyser/nsrt/bug-4267.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4267.php rename to tests/PHPStan/Analyser/nsrt/bug-4267.php diff --git a/tests/PHPStan/Analyser/data/bug-4287.php b/tests/PHPStan/Analyser/nsrt/bug-4287.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4287.php rename to tests/PHPStan/Analyser/nsrt/bug-4287.php diff --git a/tests/PHPStan/Analyser/data/bug-4302b.php b/tests/PHPStan/Analyser/nsrt/bug-4302b.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4302b.php rename to tests/PHPStan/Analyser/nsrt/bug-4302b.php diff --git a/tests/PHPStan/Analyser/data/bug-4326.php b/tests/PHPStan/Analyser/nsrt/bug-4326.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4326.php rename to tests/PHPStan/Analyser/nsrt/bug-4326.php diff --git a/tests/PHPStan/Analyser/data/bug-4339.php b/tests/PHPStan/Analyser/nsrt/bug-4339.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4339.php rename to tests/PHPStan/Analyser/nsrt/bug-4339.php diff --git a/tests/PHPStan/Analyser/data/bug-4343.php b/tests/PHPStan/Analyser/nsrt/bug-4343.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4343.php rename to tests/PHPStan/Analyser/nsrt/bug-4343.php diff --git a/tests/PHPStan/Analyser/data/bug-4351.php b/tests/PHPStan/Analyser/nsrt/bug-4351.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4351.php rename to tests/PHPStan/Analyser/nsrt/bug-4351.php diff --git a/tests/PHPStan/Analyser/data/bug-4357.php b/tests/PHPStan/Analyser/nsrt/bug-4357.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4357.php rename to tests/PHPStan/Analyser/nsrt/bug-4357.php diff --git a/tests/PHPStan/Analyser/data/bug-4371.php b/tests/PHPStan/Analyser/nsrt/bug-4371.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4371.php rename to tests/PHPStan/Analyser/nsrt/bug-4371.php diff --git a/tests/PHPStan/Analyser/data/bug-4398.php b/tests/PHPStan/Analyser/nsrt/bug-4398.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4398.php rename to tests/PHPStan/Analyser/nsrt/bug-4398.php diff --git a/tests/PHPStan/Analyser/data/bug-4415.php b/tests/PHPStan/Analyser/nsrt/bug-4415.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4415.php rename to tests/PHPStan/Analyser/nsrt/bug-4415.php diff --git a/tests/PHPStan/Analyser/data/bug-4423.php b/tests/PHPStan/Analyser/nsrt/bug-4423.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4423.php rename to tests/PHPStan/Analyser/nsrt/bug-4423.php diff --git a/tests/PHPStan/Analyser/data/bug-4434.php b/tests/PHPStan/Analyser/nsrt/bug-4434.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4434.php rename to tests/PHPStan/Analyser/nsrt/bug-4434.php diff --git a/tests/PHPStan/Analyser/data/bug-4436.php b/tests/PHPStan/Analyser/nsrt/bug-4436.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4436.php rename to tests/PHPStan/Analyser/nsrt/bug-4436.php diff --git a/tests/PHPStan/Analyser/data/bug-4498.php b/tests/PHPStan/Analyser/nsrt/bug-4498.php similarity index 84% rename from tests/PHPStan/Analyser/data/bug-4498.php rename to tests/PHPStan/Analyser/nsrt/bug-4498.php index 19e878c763..ad07baa3db 100644 --- a/tests/PHPStan/Analyser/data/bug-4498.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4498.php @@ -38,7 +38,7 @@ public function fcn(iterable $iterable): iterable public function bar(iterable $iterable): iterable { if (is_array($iterable)) { - assertType('array', $iterable); + assertType('array<((int&TKey (method Bug4498\Foo::bar(), argument))|(string&TKey (method Bug4498\Foo::bar(), argument))), TValue (method Bug4498\Foo::bar(), argument)>', $iterable); return $iterable; } diff --git a/tests/PHPStan/Analyser/data/bug-4499.php b/tests/PHPStan/Analyser/nsrt/bug-4499.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4499.php rename to tests/PHPStan/Analyser/nsrt/bug-4499.php diff --git a/tests/PHPStan/Analyser/data/bug-4500.php b/tests/PHPStan/Analyser/nsrt/bug-4500.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4500.php rename to tests/PHPStan/Analyser/nsrt/bug-4500.php diff --git a/tests/PHPStan/Analyser/data/bug-4504.php b/tests/PHPStan/Analyser/nsrt/bug-4504.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4504.php rename to tests/PHPStan/Analyser/nsrt/bug-4504.php diff --git a/tests/PHPStan/Analyser/data/bug-4538.php b/tests/PHPStan/Analyser/nsrt/bug-4538.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4538.php rename to tests/PHPStan/Analyser/nsrt/bug-4538.php diff --git a/tests/PHPStan/Analyser/data/bug-4545.php b/tests/PHPStan/Analyser/nsrt/bug-4545.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4545.php rename to tests/PHPStan/Analyser/nsrt/bug-4545.php diff --git a/tests/PHPStan/Analyser/data/bug-4557.php b/tests/PHPStan/Analyser/nsrt/bug-4557.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4557.php rename to tests/PHPStan/Analyser/nsrt/bug-4557.php diff --git a/tests/PHPStan/Analyser/data/bug-4558.php b/tests/PHPStan/Analyser/nsrt/bug-4558.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4558.php rename to tests/PHPStan/Analyser/nsrt/bug-4558.php diff --git a/tests/PHPStan/Analyser/data/bug-4565.php b/tests/PHPStan/Analyser/nsrt/bug-4565.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4565.php rename to tests/PHPStan/Analyser/nsrt/bug-4565.php diff --git a/tests/PHPStan/Analyser/data/bug-4573.php b/tests/PHPStan/Analyser/nsrt/bug-4573.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4573.php rename to tests/PHPStan/Analyser/nsrt/bug-4573.php diff --git a/tests/PHPStan/Analyser/data/bug-4577.php b/tests/PHPStan/Analyser/nsrt/bug-4577.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4577.php rename to tests/PHPStan/Analyser/nsrt/bug-4577.php diff --git a/tests/PHPStan/Analyser/data/bug-4579.php b/tests/PHPStan/Analyser/nsrt/bug-4579.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4579.php rename to tests/PHPStan/Analyser/nsrt/bug-4579.php diff --git a/tests/PHPStan/Analyser/data/bug-4586.php b/tests/PHPStan/Analyser/nsrt/bug-4586.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4586.php rename to tests/PHPStan/Analyser/nsrt/bug-4586.php diff --git a/tests/PHPStan/Analyser/data/bug-4587.php b/tests/PHPStan/Analyser/nsrt/bug-4587.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4587.php rename to tests/PHPStan/Analyser/nsrt/bug-4587.php diff --git a/tests/PHPStan/Analyser/data/bug-4588.php b/tests/PHPStan/Analyser/nsrt/bug-4588.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4588.php rename to tests/PHPStan/Analyser/nsrt/bug-4588.php diff --git a/tests/PHPStan/Analyser/data/bug-4592.php b/tests/PHPStan/Analyser/nsrt/bug-4592.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4592.php rename to tests/PHPStan/Analyser/nsrt/bug-4592.php diff --git a/tests/PHPStan/Analyser/data/bug-4602.php b/tests/PHPStan/Analyser/nsrt/bug-4602.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4602.php rename to tests/PHPStan/Analyser/nsrt/bug-4602.php diff --git a/tests/PHPStan/Analyser/data/bug-4606.php b/tests/PHPStan/Analyser/nsrt/bug-4606.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4606.php rename to tests/PHPStan/Analyser/nsrt/bug-4606.php diff --git a/tests/PHPStan/Analyser/data/bug-4642.php b/tests/PHPStan/Analyser/nsrt/bug-4642.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4642.php rename to tests/PHPStan/Analyser/nsrt/bug-4642.php diff --git a/tests/PHPStan/Analyser/data/bug-4650.php b/tests/PHPStan/Analyser/nsrt/bug-4650.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4650.php rename to tests/PHPStan/Analyser/nsrt/bug-4650.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4657.php b/tests/PHPStan/Analyser/nsrt/bug-4657.php new file mode 100644 index 0000000000..db175d8a6d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4657.php @@ -0,0 +1,58 @@ +', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('int<0, 5>', count($a)); - assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + assertType('0', count($a)); + assertType('array{}', $a); } }; @@ -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<2, 5>', count($a)); + assertType('int<1, 5>', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('int<0, 5>', count($a)); - assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + assertType('0', count($a)); + assertType('array{}', $a); } }; diff --git a/tests/PHPStan/Analyser/data/bug-4707.php b/tests/PHPStan/Analyser/nsrt/bug-4707.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4707.php rename to tests/PHPStan/Analyser/nsrt/bug-4707.php diff --git a/tests/PHPStan/Analyser/data/bug-4708.php b/tests/PHPStan/Analyser/nsrt/bug-4708.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4708.php rename to tests/PHPStan/Analyser/nsrt/bug-4708.php diff --git a/tests/PHPStan/Analyser/data/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4711.php rename to tests/PHPStan/Analyser/nsrt/bug-4711.php diff --git a/tests/PHPStan/Analyser/data/bug-4714.php b/tests/PHPStan/Analyser/nsrt/bug-4714.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4714.php rename to tests/PHPStan/Analyser/nsrt/bug-4714.php diff --git a/tests/PHPStan/Analyser/data/bug-4725.php b/tests/PHPStan/Analyser/nsrt/bug-4725.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4725.php rename to tests/PHPStan/Analyser/nsrt/bug-4725.php diff --git a/tests/PHPStan/Analyser/data/bug-4733.php b/tests/PHPStan/Analyser/nsrt/bug-4733.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4733.php rename to tests/PHPStan/Analyser/nsrt/bug-4733.php diff --git a/tests/PHPStan/Analyser/data/bug-4741.php b/tests/PHPStan/Analyser/nsrt/bug-4741.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4741.php rename to tests/PHPStan/Analyser/nsrt/bug-4741.php diff --git a/tests/PHPStan/Analyser/data/bug-4743.php b/tests/PHPStan/Analyser/nsrt/bug-4743.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4743.php rename to tests/PHPStan/Analyser/nsrt/bug-4743.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4754.php b/tests/PHPStan/Analyser/nsrt/bug-4754.php new file mode 100644 index 0000000000..ffa98ee0f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4754.php @@ -0,0 +1,42 @@ +, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedComponentNotSpecified); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|int<0, 65535>|string|false|null', $parsedNotConstant); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedAllConstant); + assertType('string|false|null', $parsedSchemeConstant); + assertType('string|false|null', $parsedHostConstant); + assertType('int<0, 65535>|false|null', $parsedPortConstant); + assertType('string|false|null', $parsedUserConstant); + assertType('string|false|null', $parsedPassConstant); + assertType('string|false|null', $parsedPathConstant); + assertType('string|false|null', $parsedQueryConstant); + assertType('string|false|null', $parsedFragmentConstant); +} diff --git a/tests/PHPStan/Analyser/data/bug-4757.php b/tests/PHPStan/Analyser/nsrt/bug-4757.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4757.php rename to tests/PHPStan/Analyser/nsrt/bug-4757.php diff --git a/tests/PHPStan/Analyser/data/bug-4761.php b/tests/PHPStan/Analyser/nsrt/bug-4761.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4761.php rename to tests/PHPStan/Analyser/nsrt/bug-4761.php diff --git a/tests/PHPStan/Analyser/data/bug-4803.php b/tests/PHPStan/Analyser/nsrt/bug-4803.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4803.php rename to tests/PHPStan/Analyser/nsrt/bug-4803.php diff --git a/tests/PHPStan/Analyser/data/bug-4814.php b/tests/PHPStan/Analyser/nsrt/bug-4814.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4814.php rename to tests/PHPStan/Analyser/nsrt/bug-4814.php diff --git a/tests/PHPStan/Analyser/data/bug-4816.php b/tests/PHPStan/Analyser/nsrt/bug-4816.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4816.php rename to tests/PHPStan/Analyser/nsrt/bug-4816.php diff --git a/tests/PHPStan/Analyser/data/bug-4820.php b/tests/PHPStan/Analyser/nsrt/bug-4820.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4820.php rename to tests/PHPStan/Analyser/nsrt/bug-4820.php diff --git a/tests/PHPStan/Analyser/data/bug-4821.php b/tests/PHPStan/Analyser/nsrt/bug-4821.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4821.php rename to tests/PHPStan/Analyser/nsrt/bug-4821.php diff --git a/tests/PHPStan/Analyser/data/bug-4822.php b/tests/PHPStan/Analyser/nsrt/bug-4822.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4822.php rename to tests/PHPStan/Analyser/nsrt/bug-4822.php diff --git a/tests/PHPStan/Analyser/data/bug-4838.php b/tests/PHPStan/Analyser/nsrt/bug-4838.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4838.php rename to tests/PHPStan/Analyser/nsrt/bug-4838.php diff --git a/tests/PHPStan/Analyser/data/bug-4843.php b/tests/PHPStan/Analyser/nsrt/bug-4843.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4843.php rename to tests/PHPStan/Analyser/nsrt/bug-4843.php diff --git a/tests/PHPStan/Analyser/data/bug-4875.php b/tests/PHPStan/Analyser/nsrt/bug-4875.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4875.php rename to tests/PHPStan/Analyser/nsrt/bug-4875.php diff --git a/tests/PHPStan/Analyser/data/bug-4879.php b/tests/PHPStan/Analyser/nsrt/bug-4879.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4879.php rename to tests/PHPStan/Analyser/nsrt/bug-4879.php diff --git a/tests/PHPStan/Analyser/data/bug-4885.php b/tests/PHPStan/Analyser/nsrt/bug-4885.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4885.php rename to tests/PHPStan/Analyser/nsrt/bug-4885.php diff --git a/tests/PHPStan/Analyser/data/bug-4887.php b/tests/PHPStan/Analyser/nsrt/bug-4887.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4887.php rename to tests/PHPStan/Analyser/nsrt/bug-4887.php diff --git a/tests/PHPStan/Analyser/data/bug-4896.php b/tests/PHPStan/Analyser/nsrt/bug-4896.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4896.php rename to tests/PHPStan/Analyser/nsrt/bug-4896.php diff --git a/tests/PHPStan/Analyser/data/bug-4902-php8.php b/tests/PHPStan/Analyser/nsrt/bug-4902-php8.php similarity index 98% rename from tests/PHPStan/Analyser/data/bug-4902-php8.php rename to tests/PHPStan/Analyser/nsrt/bug-4902-php8.php index 046f2ac338..760070dd83 100644 --- a/tests/PHPStan/Analyser/data/bug-4902-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4902-php8.php @@ -1,4 +1,4 @@ -= 7.4 += 8.0 namespace Bug4902Php8; diff --git a/tests/PHPStan/Analyser/data/bug-4903.php b/tests/PHPStan/Analyser/nsrt/bug-4903.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4903.php rename to tests/PHPStan/Analyser/nsrt/bug-4903.php diff --git a/tests/PHPStan/Analyser/data/bug-4907.php b/tests/PHPStan/Analyser/nsrt/bug-4907.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4907.php rename to tests/PHPStan/Analyser/nsrt/bug-4907.php diff --git a/tests/PHPStan/Analyser/data/bug-4950.php b/tests/PHPStan/Analyser/nsrt/bug-4950.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4950.php rename to tests/PHPStan/Analyser/nsrt/bug-4950.php diff --git a/tests/PHPStan/Analyser/data/bug-4970.php b/tests/PHPStan/Analyser/nsrt/bug-4970.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4970.php rename to tests/PHPStan/Analyser/nsrt/bug-4970.php diff --git a/tests/PHPStan/Analyser/data/bug-4982.php b/tests/PHPStan/Analyser/nsrt/bug-4982.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4982.php rename to tests/PHPStan/Analyser/nsrt/bug-4982.php diff --git a/tests/PHPStan/Analyser/data/bug-4985.php b/tests/PHPStan/Analyser/nsrt/bug-4985.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-4985.php rename to tests/PHPStan/Analyser/nsrt/bug-4985.php diff --git a/tests/PHPStan/Analyser/data/bug-5000.php b/tests/PHPStan/Analyser/nsrt/bug-5000.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5000.php rename to tests/PHPStan/Analyser/nsrt/bug-5000.php diff --git a/tests/PHPStan/Analyser/data/bug-5017.php b/tests/PHPStan/Analyser/nsrt/bug-5017.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5017.php rename to tests/PHPStan/Analyser/nsrt/bug-5017.php diff --git a/tests/PHPStan/Analyser/data/bug-505.php b/tests/PHPStan/Analyser/nsrt/bug-505.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-505.php rename to tests/PHPStan/Analyser/nsrt/bug-505.php diff --git a/tests/PHPStan/Analyser/data/bug-5072.php b/tests/PHPStan/Analyser/nsrt/bug-5072.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5072.php rename to tests/PHPStan/Analyser/nsrt/bug-5072.php diff --git a/tests/PHPStan/Analyser/data/bug-5086.php b/tests/PHPStan/Analyser/nsrt/bug-5086.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5086.php rename to tests/PHPStan/Analyser/nsrt/bug-5086.php diff --git a/tests/PHPStan/Analyser/data/bug-5129.php b/tests/PHPStan/Analyser/nsrt/bug-5129.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5129.php rename to tests/PHPStan/Analyser/nsrt/bug-5129.php diff --git a/tests/PHPStan/Analyser/data/bug-5140.php b/tests/PHPStan/Analyser/nsrt/bug-5140.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5140.php rename to tests/PHPStan/Analyser/nsrt/bug-5140.php diff --git a/tests/PHPStan/Analyser/data/bug-5172.php b/tests/PHPStan/Analyser/nsrt/bug-5172.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5172.php rename to tests/PHPStan/Analyser/nsrt/bug-5172.php diff --git a/tests/PHPStan/Analyser/data/bug-5219.php b/tests/PHPStan/Analyser/nsrt/bug-5219.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5219.php rename to tests/PHPStan/Analyser/nsrt/bug-5219.php diff --git a/tests/PHPStan/Analyser/data/bug-5223.php b/tests/PHPStan/Analyser/nsrt/bug-5223.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5223.php rename to tests/PHPStan/Analyser/nsrt/bug-5223.php diff --git a/tests/PHPStan/Analyser/data/bug-5259.php b/tests/PHPStan/Analyser/nsrt/bug-5259.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5259.php rename to tests/PHPStan/Analyser/nsrt/bug-5259.php diff --git a/tests/PHPStan/Analyser/data/bug-5262.php b/tests/PHPStan/Analyser/nsrt/bug-5262.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5262.php rename to tests/PHPStan/Analyser/nsrt/bug-5262.php diff --git a/tests/PHPStan/Analyser/data/bug-5287-php81.php b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-5287-php81.php rename to tests/PHPStan/Analyser/nsrt/bug-5287-php81.php index c0eef41b30..7d57aea2c0 100644 --- a/tests/PHPStan/Analyser/data/bug-5287-php81.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5287-php81.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types=1); namespace Bug5287Php81; diff --git a/tests/PHPStan/Analyser/data/bug-5287.php b/tests/PHPStan/Analyser/nsrt/bug-5287.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-5287.php rename to tests/PHPStan/Analyser/nsrt/bug-5287.php index cf1cda0791..83bbd544d2 100644 --- a/tests/PHPStan/Analyser/data/bug-5287.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5287.php @@ -1,4 +1,6 @@ - 0) { + $x += 1; + } + assertType('0.0|1.0', $x); + if($x > 0) { + assertType('1.0', $x); + return 5 / $x; + } + assertType('0.0|1.0', $x); // could be '0.0' when we support float-ranges + + return 1.0; +} + +function greaterEqual(float $y): float { + $x = 0.0; + if($y > 0) { + $x += 1; + } + assertType('0.0|1.0', $x); + if($x >= 0) { + assertType('0.0|1.0', $x); + return 5 / $x; + } + assertType('0.0|1.0', $x); // could be '*NEVER*' when we support float-ranges + + return 1.0; +} + +function smaller(float $y): float { + $x = 0.0; + if($y > 0) { + $x -= 1; + } + assertType('-1.0|0.0', $x); + if($x < 0) { + assertType('-1.0', $x); + return 5 / $x; + } + assertType('-1.0|0.0', $x); // could be '0.0' when we support float-ranges + + return 1.0; +} + +function smallerEqual(float $y): float { + $x = 0.0; + if($y > 0) { + $x -= 1; + } + assertType('-1.0|0.0', $x); + if($x <= 0) { + assertType('-1.0|0.0', $x); + return 5 / $x; + } + assertType('*NEVER*', $x); + + return 1.0; +} diff --git a/tests/PHPStan/Analyser/data/bug-5316.php b/tests/PHPStan/Analyser/nsrt/bug-5316.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5316.php rename to tests/PHPStan/Analyser/nsrt/bug-5316.php diff --git a/tests/PHPStan/Analyser/data/bug-5322.php b/tests/PHPStan/Analyser/nsrt/bug-5322.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5322.php rename to tests/PHPStan/Analyser/nsrt/bug-5322.php diff --git a/tests/PHPStan/Analyser/data/bug-5328.php b/tests/PHPStan/Analyser/nsrt/bug-5328.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5328.php rename to tests/PHPStan/Analyser/nsrt/bug-5328.php diff --git a/tests/PHPStan/Analyser/data/bug-5336.php b/tests/PHPStan/Analyser/nsrt/bug-5336.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5336.php rename to tests/PHPStan/Analyser/nsrt/bug-5336.php diff --git a/tests/PHPStan/Analyser/data/bug-5351.php b/tests/PHPStan/Analyser/nsrt/bug-5351.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5351.php rename to tests/PHPStan/Analyser/nsrt/bug-5351.php diff --git a/tests/PHPStan/Analyser/data/bug-5458.php b/tests/PHPStan/Analyser/nsrt/bug-5458.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5458.php rename to tests/PHPStan/Analyser/nsrt/bug-5458.php diff --git a/tests/PHPStan/Analyser/data/bug-5501.php b/tests/PHPStan/Analyser/nsrt/bug-5501.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5501.php rename to tests/PHPStan/Analyser/nsrt/bug-5501.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-5508.php b/tests/PHPStan/Analyser/nsrt/bug-5508.php new file mode 100644 index 0000000000..89af5b4b98 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5508.php @@ -0,0 +1,57 @@ + + */ + protected $items = []; + + /** + * @param array $items + * @return void + */ + public function __construct($items) + { + $this->items = $items; + } + + /** + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return self + */ + public function map(callable $callback) + { + $keys = array_keys($this->items); + + $items = array_map($callback, $this->items, $keys); + + return new self(array_combine($keys, $items)); + } + + /** + * @return array + */ + public function all() + { + return $this->items; + } +} + +function (): void { + $result = (new Collection(['book', 'cars']))->map(function($category) { + return $category; + })->all(); + + assertType('array', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-5529.php b/tests/PHPStan/Analyser/nsrt/bug-5529.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5529.php rename to tests/PHPStan/Analyser/nsrt/bug-5529.php diff --git a/tests/PHPStan/Analyser/data/bug-5530.php b/tests/PHPStan/Analyser/nsrt/bug-5530.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5530.php rename to tests/PHPStan/Analyser/nsrt/bug-5530.php diff --git a/tests/PHPStan/Analyser/data/bug-5552.php b/tests/PHPStan/Analyser/nsrt/bug-5552.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5552.php rename to tests/PHPStan/Analyser/nsrt/bug-5552.php diff --git a/tests/PHPStan/Analyser/data/bug-5584.php b/tests/PHPStan/Analyser/nsrt/bug-5584.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5584.php rename to tests/PHPStan/Analyser/nsrt/bug-5584.php diff --git a/tests/PHPStan/Analyser/data/bug-560.php b/tests/PHPStan/Analyser/nsrt/bug-560.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-560.php rename to tests/PHPStan/Analyser/nsrt/bug-560.php diff --git a/tests/PHPStan/Analyser/data/bug-5615.php b/tests/PHPStan/Analyser/nsrt/bug-5615.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5615.php rename to tests/PHPStan/Analyser/nsrt/bug-5615.php diff --git a/tests/PHPStan/Analyser/data/bug-5628.php b/tests/PHPStan/Analyser/nsrt/bug-5628.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5628.php rename to tests/PHPStan/Analyser/nsrt/bug-5628.php diff --git a/tests/PHPStan/Analyser/data/bug-5668.php b/tests/PHPStan/Analyser/nsrt/bug-5668.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5668.php rename to tests/PHPStan/Analyser/nsrt/bug-5668.php diff --git a/tests/PHPStan/Analyser/data/bug-5675.php b/tests/PHPStan/Analyser/nsrt/bug-5675.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5675.php rename to tests/PHPStan/Analyser/nsrt/bug-5675.php diff --git a/tests/PHPStan/Analyser/data/bug-5698-php7.php b/tests/PHPStan/Analyser/nsrt/bug-5698-php7.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-5698-php7.php rename to tests/PHPStan/Analyser/nsrt/bug-5698-php7.php index 84deaabdcb..76ac881bc6 100644 --- a/tests/PHPStan/Analyser/data/bug-5698-php7.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5698-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug5698; diff --git a/tests/PHPStan/Analyser/data/bug-5759.php b/tests/PHPStan/Analyser/nsrt/bug-5759.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5759.php rename to tests/PHPStan/Analyser/nsrt/bug-5759.php diff --git a/tests/PHPStan/Analyser/data/bug-5782b-php7.php b/tests/PHPStan/Analyser/nsrt/bug-5782b-php7.php similarity index 97% rename from tests/PHPStan/Analyser/data/bug-5782b-php7.php rename to tests/PHPStan/Analyser/nsrt/bug-5782b-php7.php index c25319df95..46677a9005 100644 --- a/tests/PHPStan/Analyser/data/bug-5782b-php7.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5782b-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug5782bPhp8; diff --git a/tests/PHPStan/Analyser/data/bug-5783.php b/tests/PHPStan/Analyser/nsrt/bug-5783.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5783.php rename to tests/PHPStan/Analyser/nsrt/bug-5783.php diff --git a/tests/PHPStan/Analyser/data/bug-5785.php b/tests/PHPStan/Analyser/nsrt/bug-5785.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5785.php rename to tests/PHPStan/Analyser/nsrt/bug-5785.php diff --git a/tests/PHPStan/Analyser/data/bug-5817.php b/tests/PHPStan/Analyser/nsrt/bug-5817.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5817.php rename to tests/PHPStan/Analyser/nsrt/bug-5817.php diff --git a/tests/PHPStan/Analyser/data/bug-5843.php b/tests/PHPStan/Analyser/nsrt/bug-5843.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5843.php rename to tests/PHPStan/Analyser/nsrt/bug-5843.php diff --git a/tests/PHPStan/Analyser/data/bug-5845.php b/tests/PHPStan/Analyser/nsrt/bug-5845.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5845.php rename to tests/PHPStan/Analyser/nsrt/bug-5845.php diff --git a/tests/PHPStan/Analyser/data/bug-5846.php b/tests/PHPStan/Analyser/nsrt/bug-5846.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5846.php rename to tests/PHPStan/Analyser/nsrt/bug-5846.php diff --git a/tests/PHPStan/Analyser/data/bug-5896.php b/tests/PHPStan/Analyser/nsrt/bug-5896.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5896.php rename to tests/PHPStan/Analyser/nsrt/bug-5896.php diff --git a/tests/PHPStan/Analyser/data/bug-5920.php b/tests/PHPStan/Analyser/nsrt/bug-5920.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5920.php rename to tests/PHPStan/Analyser/nsrt/bug-5920.php diff --git a/tests/PHPStan/Analyser/data/bug-5961.php b/tests/PHPStan/Analyser/nsrt/bug-5961.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5961.php rename to tests/PHPStan/Analyser/nsrt/bug-5961.php diff --git a/tests/PHPStan/Analyser/data/bug-5992.php b/tests/PHPStan/Analyser/nsrt/bug-5992.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5992.php rename to tests/PHPStan/Analyser/nsrt/bug-5992.php diff --git a/tests/PHPStan/Analyser/data/bug-5998.php b/tests/PHPStan/Analyser/nsrt/bug-5998.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-5998.php rename to tests/PHPStan/Analyser/nsrt/bug-5998.php diff --git a/tests/PHPStan/Analyser/data/bug-6001.php b/tests/PHPStan/Analyser/nsrt/bug-6001.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6001.php rename to tests/PHPStan/Analyser/nsrt/bug-6001.php diff --git a/tests/PHPStan/Analyser/data/bug-6070.php b/tests/PHPStan/Analyser/nsrt/bug-6070.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6070.php rename to tests/PHPStan/Analyser/nsrt/bug-6070.php diff --git a/tests/PHPStan/Analyser/data/bug-6108.php b/tests/PHPStan/Analyser/nsrt/bug-6108.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6108.php rename to tests/PHPStan/Analyser/nsrt/bug-6108.php diff --git a/tests/PHPStan/Analyser/data/bug-6138.php b/tests/PHPStan/Analyser/nsrt/bug-6138.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6138.php rename to tests/PHPStan/Analyser/nsrt/bug-6138.php diff --git a/tests/PHPStan/Analyser/data/bug-6170.php b/tests/PHPStan/Analyser/nsrt/bug-6170.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6170.php rename to tests/PHPStan/Analyser/nsrt/bug-6170.php diff --git a/tests/PHPStan/Analyser/data/bug-6174.php b/tests/PHPStan/Analyser/nsrt/bug-6174.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6174.php rename to tests/PHPStan/Analyser/nsrt/bug-6174.php diff --git a/tests/PHPStan/Analyser/data/bug-6196.php b/tests/PHPStan/Analyser/nsrt/bug-6196.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6196.php rename to tests/PHPStan/Analyser/nsrt/bug-6196.php diff --git a/tests/PHPStan/Analyser/data/bug-6251.php b/tests/PHPStan/Analyser/nsrt/bug-6251.php similarity index 94% rename from tests/PHPStan/Analyser/data/bug-6251.php rename to tests/PHPStan/Analyser/nsrt/bug-6251.php index bb7bda8df5..6909623930 100644 --- a/tests/PHPStan/Analyser/data/bug-6251.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6251.php @@ -1,4 +1,6 @@ -= 8.0 += 8.0 + +declare(strict_types = 1); namespace Bug6251; diff --git a/tests/PHPStan/Analyser/data/bug-6293.php b/tests/PHPStan/Analyser/nsrt/bug-6293.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6293.php rename to tests/PHPStan/Analyser/nsrt/bug-6293.php diff --git a/tests/PHPStan/Analyser/data/bug-6294.php b/tests/PHPStan/Analyser/nsrt/bug-6294.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6294.php rename to tests/PHPStan/Analyser/nsrt/bug-6294.php diff --git a/tests/PHPStan/Analyser/data/bug-6305.php b/tests/PHPStan/Analyser/nsrt/bug-6305.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6305.php rename to tests/PHPStan/Analyser/nsrt/bug-6305.php diff --git a/tests/PHPStan/Analyser/data/bug-6308.php b/tests/PHPStan/Analyser/nsrt/bug-6308.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6308.php rename to tests/PHPStan/Analyser/nsrt/bug-6308.php diff --git a/tests/PHPStan/Analyser/data/bug-6329.php b/tests/PHPStan/Analyser/nsrt/bug-6329.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6329.php rename to tests/PHPStan/Analyser/nsrt/bug-6329.php diff --git a/tests/PHPStan/Analyser/data/bug-6383.php b/tests/PHPStan/Analyser/nsrt/bug-6383.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6383.php rename to tests/PHPStan/Analyser/nsrt/bug-6383.php diff --git a/tests/PHPStan/Analyser/data/bug-6399.php b/tests/PHPStan/Analyser/nsrt/bug-6399.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6399.php rename to tests/PHPStan/Analyser/nsrt/bug-6399.php diff --git a/tests/PHPStan/Analyser/data/bug-6404.php b/tests/PHPStan/Analyser/nsrt/bug-6404.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6404.php rename to tests/PHPStan/Analyser/nsrt/bug-6404.php diff --git a/tests/PHPStan/Analyser/data/bug-6433.php b/tests/PHPStan/Analyser/nsrt/bug-6433.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6433.php rename to tests/PHPStan/Analyser/nsrt/bug-6433.php diff --git a/tests/PHPStan/Analyser/data/bug-6462.php b/tests/PHPStan/Analyser/nsrt/bug-6462.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6462.php rename to tests/PHPStan/Analyser/nsrt/bug-6462.php diff --git a/tests/PHPStan/Analyser/data/bug-6488.php b/tests/PHPStan/Analyser/nsrt/bug-6488.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6488.php rename to tests/PHPStan/Analyser/nsrt/bug-6488.php diff --git a/tests/PHPStan/Analyser/data/bug-6497.php b/tests/PHPStan/Analyser/nsrt/bug-6497.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6497.php rename to tests/PHPStan/Analyser/nsrt/bug-6497.php diff --git a/tests/PHPStan/Analyser/data/bug-6500.php b/tests/PHPStan/Analyser/nsrt/bug-6500.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6500.php rename to tests/PHPStan/Analyser/nsrt/bug-6500.php diff --git a/tests/PHPStan/Analyser/data/bug-6505.php b/tests/PHPStan/Analyser/nsrt/bug-6505.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6505.php rename to tests/PHPStan/Analyser/nsrt/bug-6505.php diff --git a/tests/PHPStan/Analyser/data/bug-651.php b/tests/PHPStan/Analyser/nsrt/bug-651.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-651.php rename to tests/PHPStan/Analyser/nsrt/bug-651.php diff --git a/tests/PHPStan/Analyser/data/bug-6556.php b/tests/PHPStan/Analyser/nsrt/bug-6556.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6556.php rename to tests/PHPStan/Analyser/nsrt/bug-6556.php diff --git a/tests/PHPStan/Analyser/data/bug-6566-types.php b/tests/PHPStan/Analyser/nsrt/bug-6566-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6566-types.php rename to tests/PHPStan/Analyser/nsrt/bug-6566-types.php diff --git a/tests/PHPStan/Analyser/data/bug-6576.php b/tests/PHPStan/Analyser/nsrt/bug-6576.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6576.php rename to tests/PHPStan/Analyser/nsrt/bug-6576.php diff --git a/tests/PHPStan/Analyser/data/bug-6584.php b/tests/PHPStan/Analyser/nsrt/bug-6584.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6584.php rename to tests/PHPStan/Analyser/nsrt/bug-6584.php diff --git a/tests/PHPStan/Analyser/data/bug-6591.php b/tests/PHPStan/Analyser/nsrt/bug-6591.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6591.php rename to tests/PHPStan/Analyser/nsrt/bug-6591.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6609-83.php b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php new file mode 100644 index 0000000000..65d7f22f1d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6609-83.php @@ -0,0 +1,58 @@ += 8.3 + +namespace Bug6609Php83; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify(\DateTimeInterface $date) { + $date = $date->modify('+1 day'); + assertType('T of DateTime|DateTimeImmutable (method Bug6609Php83\Foo::modify(), argument)', $date); + + return $date; + } + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify2(\DateTimeInterface $date) { + $date = $date->modify('invalidd'); + assertType('*NEVER*', $date); + + return $date; + } + + /** + * This method should return the same type as a parameter passed. + * + * @template T of \DateTime|\DateTimeImmutable + * + * @param T $date + * + * @return T + */ + function modify3(\DateTimeInterface $date, string $s) { + $date = $date->modify($s); + assertType('DateTime|DateTimeImmutable', $date); + + return $date; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6609.php b/tests/PHPStan/Analyser/nsrt/bug-6609.php similarity index 98% rename from tests/PHPStan/Analyser/data/bug-6609.php rename to tests/PHPStan/Analyser/nsrt/bug-6609.php index 74241e97a0..046f9c8403 100644 --- a/tests/PHPStan/Analyser/data/bug-6609.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6609.php @@ -1,4 +1,4 @@ -format('u')); +}; diff --git a/tests/PHPStan/Analyser/data/bug-6624.php b/tests/PHPStan/Analyser/nsrt/bug-6624.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6624.php rename to tests/PHPStan/Analyser/nsrt/bug-6624.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6633.php b/tests/PHPStan/Analyser/nsrt/bug-6633.php new file mode 100644 index 0000000000..3689f53ee9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6633.php @@ -0,0 +1,75 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); + + assertType('Bug6633\ServiceRedis|Bug6633\ServiceSolr', $service); +} diff --git a/tests/PHPStan/Analyser/data/bug-6654.php b/tests/PHPStan/Analyser/nsrt/bug-6654.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6654.php rename to tests/PHPStan/Analyser/nsrt/bug-6654.php diff --git a/tests/PHPStan/Analyser/data/bug-6672.php b/tests/PHPStan/Analyser/nsrt/bug-6672.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6672.php rename to tests/PHPStan/Analyser/nsrt/bug-6672.php diff --git a/tests/PHPStan/Analyser/data/bug-6682.php b/tests/PHPStan/Analyser/nsrt/bug-6682.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6682.php rename to tests/PHPStan/Analyser/nsrt/bug-6682.php diff --git a/tests/PHPStan/Analyser/data/bug-6687.php b/tests/PHPStan/Analyser/nsrt/bug-6687.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6687.php rename to tests/PHPStan/Analyser/nsrt/bug-6687.php diff --git a/tests/PHPStan/Analyser/data/bug-6695.php b/tests/PHPStan/Analyser/nsrt/bug-6695.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6695.php rename to tests/PHPStan/Analyser/nsrt/bug-6695.php diff --git a/tests/PHPStan/Analyser/data/bug-6696.php b/tests/PHPStan/Analyser/nsrt/bug-6696.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6696.php rename to tests/PHPStan/Analyser/nsrt/bug-6696.php diff --git a/tests/PHPStan/Analyser/data/bug-6698.php b/tests/PHPStan/Analyser/nsrt/bug-6698.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6698.php rename to tests/PHPStan/Analyser/nsrt/bug-6698.php diff --git a/tests/PHPStan/Analyser/data/bug-6699.php b/tests/PHPStan/Analyser/nsrt/bug-6699.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6699.php rename to tests/PHPStan/Analyser/nsrt/bug-6699.php diff --git a/tests/PHPStan/Analyser/data/bug-6704.php b/tests/PHPStan/Analyser/nsrt/bug-6704.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6704.php rename to tests/PHPStan/Analyser/nsrt/bug-6704.php diff --git a/tests/PHPStan/Analyser/data/bug-6715.php b/tests/PHPStan/Analyser/nsrt/bug-6715.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6715.php rename to tests/PHPStan/Analyser/nsrt/bug-6715.php diff --git a/tests/PHPStan/Analyser/data/bug-6728.php b/tests/PHPStan/Analyser/nsrt/bug-6728.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6728.php rename to tests/PHPStan/Analyser/nsrt/bug-6728.php diff --git a/tests/PHPStan/Analyser/data/bug-6748.php b/tests/PHPStan/Analyser/nsrt/bug-6748.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6748.php rename to tests/PHPStan/Analyser/nsrt/bug-6748.php diff --git a/tests/PHPStan/Analyser/data/bug-6790.php b/tests/PHPStan/Analyser/nsrt/bug-6790.php similarity index 94% rename from tests/PHPStan/Analyser/data/bug-6790.php rename to tests/PHPStan/Analyser/nsrt/bug-6790.php index 68aa284b3b..b9ea9ee82b 100644 --- a/tests/PHPStan/Analyser/data/bug-6790.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6790.php @@ -1,4 +1,6 @@ -= 8.0 += 8.0 + +declare(strict_types = 1); namespace Bug6790; diff --git a/tests/PHPStan/Analyser/data/bug-6845.php b/tests/PHPStan/Analyser/nsrt/bug-6845.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6845.php rename to tests/PHPStan/Analyser/nsrt/bug-6845.php diff --git a/tests/PHPStan/Analyser/data/bug-6859.php b/tests/PHPStan/Analyser/nsrt/bug-6859.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6859.php rename to tests/PHPStan/Analyser/nsrt/bug-6859.php diff --git a/tests/PHPStan/Analyser/data/bug-6864.php b/tests/PHPStan/Analyser/nsrt/bug-6864.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6864.php rename to tests/PHPStan/Analyser/nsrt/bug-6864.php diff --git a/tests/PHPStan/Analyser/data/bug-6870.php b/tests/PHPStan/Analyser/nsrt/bug-6870.php similarity index 95% rename from tests/PHPStan/Analyser/data/bug-6870.php rename to tests/PHPStan/Analyser/nsrt/bug-6870.php index 8aaa2a5f90..73d6a6dc04 100644 --- a/tests/PHPStan/Analyser/data/bug-6870.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6870.php @@ -1,4 +1,6 @@ -= 8.0 += 8.0 + +declare(strict_types = 1); namespace Bug6870; diff --git a/tests/PHPStan/Analyser/data/bug-6889.php b/tests/PHPStan/Analyser/nsrt/bug-6889.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6889.php rename to tests/PHPStan/Analyser/nsrt/bug-6889.php diff --git a/tests/PHPStan/Analyser/data/bug-6891.php b/tests/PHPStan/Analyser/nsrt/bug-6891.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6891.php rename to tests/PHPStan/Analyser/nsrt/bug-6891.php diff --git a/tests/PHPStan/Analyser/data/bug-6901.php b/tests/PHPStan/Analyser/nsrt/bug-6901.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6901.php rename to tests/PHPStan/Analyser/nsrt/bug-6901.php diff --git a/tests/PHPStan/Analyser/data/bug-6904.php b/tests/PHPStan/Analyser/nsrt/bug-6904.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6904.php rename to tests/PHPStan/Analyser/nsrt/bug-6904.php diff --git a/tests/PHPStan/Analyser/data/bug-6917.php b/tests/PHPStan/Analyser/nsrt/bug-6917.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6917.php rename to tests/PHPStan/Analyser/nsrt/bug-6917.php diff --git a/tests/PHPStan/Analyser/data/bug-6927.php b/tests/PHPStan/Analyser/nsrt/bug-6927.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6927.php rename to tests/PHPStan/Analyser/nsrt/bug-6927.php diff --git a/tests/PHPStan/Analyser/data/bug-6936-limit.php b/tests/PHPStan/Analyser/nsrt/bug-6936-limit.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6936-limit.php rename to tests/PHPStan/Analyser/nsrt/bug-6936-limit.php diff --git a/tests/PHPStan/Analyser/data/bug-6993.php b/tests/PHPStan/Analyser/nsrt/bug-6993.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-6993.php rename to tests/PHPStan/Analyser/nsrt/bug-6993.php diff --git a/tests/PHPStan/Analyser/data/bug-7000.php b/tests/PHPStan/Analyser/nsrt/bug-7000.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7000.php rename to tests/PHPStan/Analyser/nsrt/bug-7000.php diff --git a/tests/PHPStan/Analyser/data/bug-7031.php b/tests/PHPStan/Analyser/nsrt/bug-7031.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7031.php rename to tests/PHPStan/Analyser/nsrt/bug-7031.php diff --git a/tests/PHPStan/Analyser/data/bug-7056.php b/tests/PHPStan/Analyser/nsrt/bug-7056.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7056.php rename to tests/PHPStan/Analyser/nsrt/bug-7056.php diff --git a/tests/PHPStan/Analyser/data/bug-7068.php b/tests/PHPStan/Analyser/nsrt/bug-7068.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7068.php rename to tests/PHPStan/Analyser/nsrt/bug-7068.php diff --git a/tests/PHPStan/Analyser/data/bug-7078.php b/tests/PHPStan/Analyser/nsrt/bug-7078.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7078.php rename to tests/PHPStan/Analyser/nsrt/bug-7078.php diff --git a/tests/PHPStan/Analyser/data/bug-7096.php b/tests/PHPStan/Analyser/nsrt/bug-7096.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7096.php rename to tests/PHPStan/Analyser/nsrt/bug-7096.php diff --git a/tests/PHPStan/Analyser/data/bug-7106.php b/tests/PHPStan/Analyser/nsrt/bug-7106.php similarity index 90% rename from tests/PHPStan/Analyser/data/bug-7106.php rename to tests/PHPStan/Analyser/nsrt/bug-7106.php index b61a844ef6..50e6c0e86f 100644 --- a/tests/PHPStan/Analyser/data/bug-7106.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7106.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 1); namespace Bug7106; diff --git a/tests/PHPStan/Analyser/data/bug-7115.php b/tests/PHPStan/Analyser/nsrt/bug-7115.php similarity index 89% rename from tests/PHPStan/Analyser/data/bug-7115.php rename to tests/PHPStan/Analyser/nsrt/bug-7115.php index 9377457745..40263db3fa 100644 --- a/tests/PHPStan/Analyser/data/bug-7115.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7115.php @@ -27,7 +27,7 @@ public function doFoo(): void array_push($d, $thing); } - assertType('array, array{a: int, b: string}>', $b); + assertType('list', $b); assertType('list', $c); assertType('list', $d); } diff --git a/tests/PHPStan/Analyser/data/bug-7141.php b/tests/PHPStan/Analyser/nsrt/bug-7141.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7141.php rename to tests/PHPStan/Analyser/nsrt/bug-7141.php diff --git a/tests/PHPStan/Analyser/data/bug-7144-composer-integration.php b/tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7144-composer-integration.php rename to tests/PHPStan/Analyser/nsrt/bug-7144-composer-integration.php diff --git a/tests/PHPStan/Analyser/data/bug-7144.php b/tests/PHPStan/Analyser/nsrt/bug-7144.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7144.php rename to tests/PHPStan/Analyser/nsrt/bug-7144.php diff --git a/tests/PHPStan/Analyser/data/bug-7153.php b/tests/PHPStan/Analyser/nsrt/bug-7153.php similarity index 97% rename from tests/PHPStan/Analyser/data/bug-7153.php rename to tests/PHPStan/Analyser/nsrt/bug-7153.php index 973beecf24..902764f977 100644 --- a/tests/PHPStan/Analyser/data/bug-7153.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7153.php @@ -17,6 +17,7 @@ function bleh(): ?string function blih(string $blah, string $bleh): void { + echo 'test'; } function () { diff --git a/tests/PHPStan/Analyser/data/bug-7162.php b/tests/PHPStan/Analyser/nsrt/bug-7162.php similarity index 79% rename from tests/PHPStan/Analyser/data/bug-7162.php rename to tests/PHPStan/Analyser/nsrt/bug-7162.php index 87815e4acb..9b1fb4f54b 100644 --- a/tests/PHPStan/Analyser/data/bug-7162.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7162.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types=1); namespace Bug7162; @@ -27,7 +29,7 @@ enum Test{ * @phpstan-param TEnum $case */ function dumpCases(\UnitEnum $case) : void{ - assertType('array', $case::cases()); + assertType('list', $case::cases()); } function dumpCases2(Test $case) : void{ diff --git a/tests/PHPStan/Analyser/data/bug-7167.php b/tests/PHPStan/Analyser/nsrt/bug-7167.php similarity index 75% rename from tests/PHPStan/Analyser/data/bug-7167.php rename to tests/PHPStan/Analyser/nsrt/bug-7167.php index 18ffcf9ccd..b62b834988 100644 --- a/tests/PHPStan/Analyser/data/bug-7167.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7167.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 1); namespace Bug7167; diff --git a/tests/PHPStan/Analyser/data/bug-7176.php b/tests/PHPStan/Analyser/nsrt/bug-7176.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-7176.php rename to tests/PHPStan/Analyser/nsrt/bug-7176.php index 05d1de958c..6be67ffaba 100644 --- a/tests/PHPStan/Analyser/data/bug-7176.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7176.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 1); namespace Bug7176Types; diff --git a/tests/PHPStan/Analyser/data/bug-7210.php b/tests/PHPStan/Analyser/nsrt/bug-7210.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7210.php rename to tests/PHPStan/Analyser/nsrt/bug-7210.php diff --git a/tests/PHPStan/Analyser/data/bug-7224.php b/tests/PHPStan/Analyser/nsrt/bug-7224.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7224.php rename to tests/PHPStan/Analyser/nsrt/bug-7224.php diff --git a/tests/PHPStan/Analyser/data/bug-7239-php8.php b/tests/PHPStan/Analyser/nsrt/bug-7239-php8.php similarity index 93% rename from tests/PHPStan/Analyser/data/bug-7239-php8.php rename to tests/PHPStan/Analyser/nsrt/bug-7239-php8.php index 11103c7ded..24564d4233 100644 --- a/tests/PHPStan/Analyser/data/bug-7239-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7239-php8.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug7239php8; diff --git a/tests/PHPStan/Analyser/data/bug-7239.php b/tests/PHPStan/Analyser/nsrt/bug-7239.php similarity index 93% rename from tests/PHPStan/Analyser/data/bug-7239.php rename to tests/PHPStan/Analyser/nsrt/bug-7239.php index b31a69e785..62bf97a119 100644 --- a/tests/PHPStan/Analyser/data/bug-7239.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7239.php @@ -1,4 +1,6 @@ - $array + * @param (callable(T, K): U) $fn + * + * @return array + */ +function map(array $array, callable $fn): array +{ + /** @phpstan-ignore-next-line */ + return array_map($fn, $array); +} + +function (): void { + /** + * @var array> $timelines + */ + $timelines = []; + + assertType('array>', map( + $timelines, + static function (Timeline $timeline): Timeline { + return $timeline; + }, + )); + assertType('array>', map( + $timelines, + static function ($timeline) { + return $timeline; + }, + )); + + assertType('array>', map( + $timelines, + static fn (Timeline $timeline): Timeline => $timeline, + )); + assertType('array>', map( + $timelines, + static fn ($timeline) => $timeline, + )); + + assertType('array>', array_map( + static function (Timeline $timeline): Timeline { + return $timeline; + }, + $timelines, + )); + assertType('array>', array_map( + static function ($timeline) { + return $timeline; + }, + $timelines, + )); + + assertType('array>', array_map( + static fn (Timeline $timeline): Timeline => $timeline, + $timelines, + )); + assertType('array>', array_map( + static fn ($timeline) => $timeline, + $timelines, + )); + + assertType('array>', array_map( + static function (Timeline $timeline) { + return $timeline; + }, + $timelines, + )); + assertType('array>', array_map( + static function ($timeline): Timeline { + return $timeline; + }, + $timelines, + )); + + assertType('array>', array_map( + static fn (Timeline $timeline) => $timeline, + $timelines, + )); + assertType('array>', array_map( + static fn ($timeline): Timeline => $timeline, + $timelines, + )); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7291.php b/tests/PHPStan/Analyser/nsrt/bug-7291.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7291.php rename to tests/PHPStan/Analyser/nsrt/bug-7291.php diff --git a/tests/PHPStan/Analyser/data/bug-7301.php b/tests/PHPStan/Analyser/nsrt/bug-7301.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7301.php rename to tests/PHPStan/Analyser/nsrt/bug-7301.php diff --git a/tests/PHPStan/Analyser/data/bug-7341.php b/tests/PHPStan/Analyser/nsrt/bug-7341.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7341.php rename to tests/PHPStan/Analyser/nsrt/bug-7341.php diff --git a/tests/PHPStan/Analyser/data/bug-7353.php b/tests/PHPStan/Analyser/nsrt/bug-7353.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7353.php rename to tests/PHPStan/Analyser/nsrt/bug-7353.php diff --git a/tests/PHPStan/Analyser/data/bug-7374.php b/tests/PHPStan/Analyser/nsrt/bug-7374.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7374.php rename to tests/PHPStan/Analyser/nsrt/bug-7374.php diff --git a/tests/PHPStan/Analyser/data/bug-7387.php b/tests/PHPStan/Analyser/nsrt/bug-7387.php similarity index 78% rename from tests/PHPStan/Analyser/data/bug-7387.php rename to tests/PHPStan/Analyser/nsrt/bug-7387.php index 5f9a4e4879..4623719b4d 100644 --- a/tests/PHPStan/Analyser/data/bug-7387.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7387.php @@ -23,7 +23,7 @@ public function inputTypes(int $i, float $f, string $s) { public function specifiers(int $i) { // https://3v4l.org/fmVIg - assertType('non-falsy-string', sprintf('%14s', $i)); + assertType('numeric-string', sprintf('%14s', $i)); assertType('numeric-string', sprintf('%d', $i)); @@ -45,9 +45,19 @@ public function specifiers(int $i) { } - public function positionalArgs($mixed, int $i, float $f, string $s) { + /** + * @param positive-int $posInt + * @param negative-int $negInt + * @param int<1, 5> $nonZeroIntRange + * @param int<-1, 5> $intRange + */ + public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) { // https://3v4l.org/vVL0c - assertType('non-falsy-string', sprintf('%2$14s', $mixed, $i)); + 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('numeric-string', sprintf('%2$.14F', $mixed, $i)); assertType('numeric-string', sprintf('%2$.14F', $mixed, $f)); @@ -61,7 +71,7 @@ public function positionalArgs($mixed, int $i, float $f, string $s) { assertType('numeric-string', sprintf('%2$14F', $mixed, $f)); assertType('numeric-string', sprintf('%2$14F', $mixed, $s)); - assertType('numeric-string', sprintf('%10$14F', $mixed, $s)); + assertType('string', sprintf('%10$14F', $mixed, $s)); } public function invalidPositionalArgFormat($mixed, string $s) { @@ -78,7 +88,7 @@ public function vsprintf(array $array) assertType('numeric-string', vsprintf("%4d", explode('-', '1988-8-1'))); assertType('numeric-string', vsprintf("%4d", $array)); assertType('numeric-string', vsprintf("%4d", ['123'])); - assertType('non-falsy-string', vsprintf("%s", ['123'])); + assertType('string', vsprintf("%s", ['123'])); // could be '123' // too many arguments.. php silently allows it assertType('numeric-string', vsprintf("%4d", ['123', '456'])); } diff --git a/tests/PHPStan/Analyser/data/bug-7391.php b/tests/PHPStan/Analyser/nsrt/bug-7391.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7391.php rename to tests/PHPStan/Analyser/nsrt/bug-7391.php diff --git a/tests/PHPStan/Analyser/data/bug-7483.php b/tests/PHPStan/Analyser/nsrt/bug-7483.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7483.php rename to tests/PHPStan/Analyser/nsrt/bug-7483.php diff --git a/tests/PHPStan/Analyser/data/bug-7490.php b/tests/PHPStan/Analyser/nsrt/bug-7490.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7490.php rename to tests/PHPStan/Analyser/nsrt/bug-7490.php diff --git a/tests/PHPStan/Analyser/data/bug-7492.php b/tests/PHPStan/Analyser/nsrt/bug-7492.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7492.php rename to tests/PHPStan/Analyser/nsrt/bug-7492.php diff --git a/tests/PHPStan/Analyser/data/bug-7501.php b/tests/PHPStan/Analyser/nsrt/bug-7501.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7501.php rename to tests/PHPStan/Analyser/nsrt/bug-7501.php diff --git a/tests/PHPStan/Analyser/data/bug-7519.php b/tests/PHPStan/Analyser/nsrt/bug-7519.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7519.php rename to tests/PHPStan/Analyser/nsrt/bug-7519.php diff --git a/tests/PHPStan/Analyser/data/bug-7547.php b/tests/PHPStan/Analyser/nsrt/bug-7547.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7547.php rename to tests/PHPStan/Analyser/nsrt/bug-7547.php diff --git a/tests/PHPStan/Analyser/data/bug-7550.php b/tests/PHPStan/Analyser/nsrt/bug-7550.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7550.php rename to tests/PHPStan/Analyser/nsrt/bug-7550.php diff --git a/tests/PHPStan/Analyser/data/bug-7563.php b/tests/PHPStan/Analyser/nsrt/bug-7563.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7563.php rename to tests/PHPStan/Analyser/nsrt/bug-7563.php diff --git a/tests/PHPStan/Analyser/data/bug-7580.php b/tests/PHPStan/Analyser/nsrt/bug-7580.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7580.php rename to tests/PHPStan/Analyser/nsrt/bug-7580.php diff --git a/tests/PHPStan/Analyser/data/bug-7607.php b/tests/PHPStan/Analyser/nsrt/bug-7607.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7607.php rename to tests/PHPStan/Analyser/nsrt/bug-7607.php diff --git a/tests/PHPStan/Analyser/data/bug-7621-1.php b/tests/PHPStan/Analyser/nsrt/bug-7621-1.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7621-1.php rename to tests/PHPStan/Analyser/nsrt/bug-7621-1.php diff --git a/tests/PHPStan/Analyser/data/bug-7621-2.php b/tests/PHPStan/Analyser/nsrt/bug-7621-2.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7621-2.php rename to tests/PHPStan/Analyser/nsrt/bug-7621-2.php diff --git a/tests/PHPStan/Analyser/data/bug-7621-3.php b/tests/PHPStan/Analyser/nsrt/bug-7621-3.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7621-3.php rename to tests/PHPStan/Analyser/nsrt/bug-7621-3.php diff --git a/tests/PHPStan/Analyser/data/bug-7639.php b/tests/PHPStan/Analyser/nsrt/bug-7639.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7639.php rename to tests/PHPStan/Analyser/nsrt/bug-7639.php diff --git a/tests/PHPStan/Analyser/data/bug-7663-php7.php b/tests/PHPStan/Analyser/nsrt/bug-7663-php7.php similarity index 94% rename from tests/PHPStan/Analyser/data/bug-7663-php7.php rename to tests/PHPStan/Analyser/nsrt/bug-7663-php7.php index 774ab2c37e..2b62e963ed 100644 --- a/tests/PHPStan/Analyser/data/bug-7663-php7.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7663-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug7663; diff --git a/tests/PHPStan/Analyser/data/bug-7663.php b/tests/PHPStan/Analyser/nsrt/bug-7663.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7663.php rename to tests/PHPStan/Analyser/nsrt/bug-7663.php diff --git a/tests/PHPStan/Analyser/data/bug-7688.php b/tests/PHPStan/Analyser/nsrt/bug-7688.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7688.php rename to tests/PHPStan/Analyser/nsrt/bug-7688.php diff --git a/tests/PHPStan/Analyser/data/bug-7689.php b/tests/PHPStan/Analyser/nsrt/bug-7689.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7689.php rename to tests/PHPStan/Analyser/nsrt/bug-7689.php diff --git a/tests/PHPStan/Analyser/data/bug-7698.php b/tests/PHPStan/Analyser/nsrt/bug-7698.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7698.php rename to tests/PHPStan/Analyser/nsrt/bug-7698.php diff --git a/tests/PHPStan/Analyser/data/bug-7764.php b/tests/PHPStan/Analyser/nsrt/bug-7764.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7764.php rename to tests/PHPStan/Analyser/nsrt/bug-7764.php diff --git a/tests/PHPStan/Analyser/data/bug-7776.php b/tests/PHPStan/Analyser/nsrt/bug-7776.php similarity index 92% rename from tests/PHPStan/Analyser/data/bug-7776.php rename to tests/PHPStan/Analyser/nsrt/bug-7776.php index 6bea7de1a2..e01fa5f841 100644 --- a/tests/PHPStan/Analyser/data/bug-7776.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7776.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types = 1); namespace Bug7776; diff --git a/tests/PHPStan/Analyser/data/bug-778.php b/tests/PHPStan/Analyser/nsrt/bug-778.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-778.php rename to tests/PHPStan/Analyser/nsrt/bug-778.php diff --git a/tests/PHPStan/Analyser/data/bug-7788.php b/tests/PHPStan/Analyser/nsrt/bug-7788.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7788.php rename to tests/PHPStan/Analyser/nsrt/bug-7788.php diff --git a/tests/PHPStan/Analyser/data/bug-7805.php b/tests/PHPStan/Analyser/nsrt/bug-7805.php similarity index 86% rename from tests/PHPStan/Analyser/data/bug-7805.php rename to tests/PHPStan/Analyser/nsrt/bug-7805.php index 8d59d97184..cf96fe46d5 100644 --- a/tests/PHPStan/Analyser/data/bug-7805.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7805.php @@ -21,10 +21,10 @@ function foo(array $params) assertNativeType("array", $params); $params = $params === [] ? ['list'] : $params; assertType("array{'list'}", $params); - assertNativeType("non-empty-array", $params); + assertNativeType("non-empty-array", $params); array_unshift($params, 'help'); assertType("array{'help', 'list'}", $params); - assertNativeType("non-empty-array", $params); + assertNativeType("non-empty-array", $params); } assertType("array{}|array{'help', 'list'}", $params); assertNativeType('array', $params); diff --git a/tests/PHPStan/Analyser/data/bug-7809.php b/tests/PHPStan/Analyser/nsrt/bug-7809.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7809.php rename to tests/PHPStan/Analyser/nsrt/bug-7809.php diff --git a/tests/PHPStan/Analyser/data/bug-7877.php b/tests/PHPStan/Analyser/nsrt/bug-7877.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7877.php rename to tests/PHPStan/Analyser/nsrt/bug-7877.php diff --git a/tests/PHPStan/Analyser/data/bug-7909.php b/tests/PHPStan/Analyser/nsrt/bug-7909.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7909.php rename to tests/PHPStan/Analyser/nsrt/bug-7909.php diff --git a/tests/PHPStan/Analyser/data/bug-7913.php b/tests/PHPStan/Analyser/nsrt/bug-7913.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7913.php rename to tests/PHPStan/Analyser/nsrt/bug-7913.php diff --git a/tests/PHPStan/Analyser/data/bug-7915.php b/tests/PHPStan/Analyser/nsrt/bug-7915.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7915.php rename to tests/PHPStan/Analyser/nsrt/bug-7915.php diff --git a/tests/PHPStan/Analyser/data/bug-7921.php b/tests/PHPStan/Analyser/nsrt/bug-7921.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7921.php rename to tests/PHPStan/Analyser/nsrt/bug-7921.php diff --git a/tests/PHPStan/Analyser/data/bug-7928.php b/tests/PHPStan/Analyser/nsrt/bug-7928.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7928.php rename to tests/PHPStan/Analyser/nsrt/bug-7928.php diff --git a/tests/PHPStan/Analyser/data/bug-7944.php b/tests/PHPStan/Analyser/nsrt/bug-7944.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7944.php rename to tests/PHPStan/Analyser/nsrt/bug-7944.php diff --git a/tests/PHPStan/Analyser/data/bug-7949.php b/tests/PHPStan/Analyser/nsrt/bug-7949.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7949.php rename to tests/PHPStan/Analyser/nsrt/bug-7949.php diff --git a/tests/PHPStan/Analyser/data/bug-7963-three.php b/tests/PHPStan/Analyser/nsrt/bug-7963-three.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7963-three.php rename to tests/PHPStan/Analyser/nsrt/bug-7963-three.php diff --git a/tests/PHPStan/Analyser/data/bug-7987.php b/tests/PHPStan/Analyser/nsrt/bug-7987.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7987.php rename to tests/PHPStan/Analyser/nsrt/bug-7987.php diff --git a/tests/PHPStan/Analyser/data/bug-7993.php b/tests/PHPStan/Analyser/nsrt/bug-7993.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7993.php rename to tests/PHPStan/Analyser/nsrt/bug-7993.php diff --git a/tests/PHPStan/Analyser/data/bug-7996.php b/tests/PHPStan/Analyser/nsrt/bug-7996.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-7996.php rename to tests/PHPStan/Analyser/nsrt/bug-7996.php diff --git a/tests/PHPStan/Analyser/data/bug-8008.php b/tests/PHPStan/Analyser/nsrt/bug-8008.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8008.php rename to tests/PHPStan/Analyser/nsrt/bug-8008.php diff --git a/tests/PHPStan/Analyser/data/bug-801.php b/tests/PHPStan/Analyser/nsrt/bug-801.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-801.php rename to tests/PHPStan/Analyser/nsrt/bug-801.php diff --git a/tests/PHPStan/Analyser/data/bug-8015.php b/tests/PHPStan/Analyser/nsrt/bug-8015.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8015.php rename to tests/PHPStan/Analyser/nsrt/bug-8015.php diff --git a/tests/PHPStan/Analyser/data/bug-8017.php b/tests/PHPStan/Analyser/nsrt/bug-8017.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8017.php rename to tests/PHPStan/Analyser/nsrt/bug-8017.php diff --git a/tests/PHPStan/Analyser/data/bug-8033.php b/tests/PHPStan/Analyser/nsrt/bug-8033.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8033.php rename to tests/PHPStan/Analyser/nsrt/bug-8033.php diff --git a/tests/PHPStan/Analyser/data/bug-8084.php b/tests/PHPStan/Analyser/nsrt/bug-8084.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8084.php rename to tests/PHPStan/Analyser/nsrt/bug-8084.php diff --git a/tests/PHPStan/Analyser/data/bug-8087.php b/tests/PHPStan/Analyser/nsrt/bug-8087.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8087.php rename to tests/PHPStan/Analyser/nsrt/bug-8087.php diff --git a/tests/PHPStan/Analyser/data/bug-8092.php b/tests/PHPStan/Analyser/nsrt/bug-8092.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8092.php rename to tests/PHPStan/Analyser/nsrt/bug-8092.php diff --git a/tests/PHPStan/Analyser/data/bug-8127.php b/tests/PHPStan/Analyser/nsrt/bug-8127.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8127.php rename to tests/PHPStan/Analyser/nsrt/bug-8127.php diff --git a/tests/PHPStan/Analyser/data/bug-82.php b/tests/PHPStan/Analyser/nsrt/bug-82.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-82.php rename to tests/PHPStan/Analyser/nsrt/bug-82.php diff --git a/tests/PHPStan/Analyser/data/bug-8225.php b/tests/PHPStan/Analyser/nsrt/bug-8225.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8225.php rename to tests/PHPStan/Analyser/nsrt/bug-8225.php diff --git a/tests/PHPStan/Analyser/data/bug-8242.php b/tests/PHPStan/Analyser/nsrt/bug-8242.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8242.php rename to tests/PHPStan/Analyser/nsrt/bug-8242.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-8249.php b/tests/PHPStan/Analyser/nsrt/bug-8249.php new file mode 100644 index 0000000000..960126723d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8249.php @@ -0,0 +1,36 @@ + foo()]; + + if (is_int($x['x'])) { + assertType('array{x: int}', $x); + assertType('int', $x['x']); + assertType('true', is_int($x['x'])); + } else { + assertType('array{x: mixed~int}', $x); + assertType('mixed~int', $x['x']); + assertType('false', is_int($x['x'])); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-8272.php b/tests/PHPStan/Analyser/nsrt/bug-8272.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8272.php rename to tests/PHPStan/Analyser/nsrt/bug-8272.php diff --git a/tests/PHPStan/Analyser/data/bug-8361.php b/tests/PHPStan/Analyser/nsrt/bug-8361.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8361.php rename to tests/PHPStan/Analyser/nsrt/bug-8361.php diff --git a/tests/PHPStan/Analyser/data/bug-8366.php b/tests/PHPStan/Analyser/nsrt/bug-8366.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8366.php rename to tests/PHPStan/Analyser/nsrt/bug-8366.php diff --git a/tests/PHPStan/Analyser/data/bug-8373.php b/tests/PHPStan/Analyser/nsrt/bug-8373.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8373.php rename to tests/PHPStan/Analyser/nsrt/bug-8373.php diff --git a/tests/PHPStan/Analyser/data/bug-8421.php b/tests/PHPStan/Analyser/nsrt/bug-8421.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8421.php rename to tests/PHPStan/Analyser/nsrt/bug-8421.php diff --git a/tests/PHPStan/Analyser/data/bug-8442.php b/tests/PHPStan/Analyser/nsrt/bug-8442.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8442.php rename to tests/PHPStan/Analyser/nsrt/bug-8442.php diff --git a/tests/PHPStan/Analyser/data/bug-8467b.php b/tests/PHPStan/Analyser/nsrt/bug-8467b.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8467b.php rename to tests/PHPStan/Analyser/nsrt/bug-8467b.php diff --git a/tests/PHPStan/Analyser/data/bug-8486.php b/tests/PHPStan/Analyser/nsrt/bug-8486.php similarity index 87% rename from tests/PHPStan/Analyser/data/bug-8486.php rename to tests/PHPStan/Analyser/nsrt/bug-8486.php index 1f2025276a..e15c8a6544 100644 --- a/tests/PHPStan/Analyser/data/bug-8486.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8486.php @@ -24,7 +24,7 @@ public function typeInference(): void { match ($this) { self::None => 'baz', - default => assertType('$this(Bug8486\Operator~Bug8486\Operator::None)', $this), + default => assertType('($this(Bug8486\Operator)&Bug8486\Operator::Foo)|($this(Bug8486\Operator)&Bug8486\Operator::Bar)', $this), }; } @@ -56,7 +56,7 @@ public function typeInference(Operator $operator): void { match ($operator) { Operator::None => 'baz', - default => assertType('Bug8486\Operator~Bug8486\Operator::None', $operator), + default => assertType('Bug8486\Operator::Bar|Bug8486\Operator::Foo', $operator), }; } diff --git a/tests/PHPStan/Analyser/data/bug-8517.php b/tests/PHPStan/Analyser/nsrt/bug-8517.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8517.php rename to tests/PHPStan/Analyser/nsrt/bug-8517.php diff --git a/tests/PHPStan/Analyser/data/bug-8520.php b/tests/PHPStan/Analyser/nsrt/bug-8520.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8520.php rename to tests/PHPStan/Analyser/nsrt/bug-8520.php diff --git a/tests/PHPStan/Analyser/data/bug-8543.php b/tests/PHPStan/Analyser/nsrt/bug-8543.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8543.php rename to tests/PHPStan/Analyser/nsrt/bug-8543.php diff --git a/tests/PHPStan/Analyser/data/bug-8568.php b/tests/PHPStan/Analyser/nsrt/bug-8568.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8568.php rename to tests/PHPStan/Analyser/nsrt/bug-8568.php diff --git a/tests/PHPStan/Analyser/data/bug-8609.php b/tests/PHPStan/Analyser/nsrt/bug-8609.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8609.php rename to tests/PHPStan/Analyser/nsrt/bug-8609.php diff --git a/tests/PHPStan/Analyser/data/bug-8621.php b/tests/PHPStan/Analyser/nsrt/bug-8621.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8621.php rename to tests/PHPStan/Analyser/nsrt/bug-8621.php diff --git a/tests/PHPStan/Analyser/data/bug-8625.php b/tests/PHPStan/Analyser/nsrt/bug-8625.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8625.php rename to tests/PHPStan/Analyser/nsrt/bug-8625.php diff --git a/tests/PHPStan/Analyser/data/bug-8635.php b/tests/PHPStan/Analyser/nsrt/bug-8635.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8635.php rename to tests/PHPStan/Analyser/nsrt/bug-8635.php diff --git a/tests/PHPStan/Analyser/data/bug-8752.php b/tests/PHPStan/Analyser/nsrt/bug-8752.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8752.php rename to tests/PHPStan/Analyser/nsrt/bug-8752.php diff --git a/tests/PHPStan/Analyser/data/bug-8775.php b/tests/PHPStan/Analyser/nsrt/bug-8775.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8775.php rename to tests/PHPStan/Analyser/nsrt/bug-8775.php diff --git a/tests/PHPStan/Analyser/data/bug-8803.php b/tests/PHPStan/Analyser/nsrt/bug-8803.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8803.php rename to tests/PHPStan/Analyser/nsrt/bug-8803.php diff --git a/tests/PHPStan/Analyser/data/bug-8827.php b/tests/PHPStan/Analyser/nsrt/bug-8827.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8827.php rename to tests/PHPStan/Analyser/nsrt/bug-8827.php diff --git a/tests/PHPStan/Analyser/data/bug-8917.php b/tests/PHPStan/Analyser/nsrt/bug-8917.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8917.php rename to tests/PHPStan/Analyser/nsrt/bug-8917.php diff --git a/tests/PHPStan/Analyser/data/bug-8924.php b/tests/PHPStan/Analyser/nsrt/bug-8924.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8924.php rename to tests/PHPStan/Analyser/nsrt/bug-8924.php diff --git a/tests/PHPStan/Analyser/data/bug-8956.php b/tests/PHPStan/Analyser/nsrt/bug-8956.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8956.php rename to tests/PHPStan/Analyser/nsrt/bug-8956.php diff --git a/tests/PHPStan/Analyser/data/bug-9000.php b/tests/PHPStan/Analyser/nsrt/bug-9000.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9000.php rename to tests/PHPStan/Analyser/nsrt/bug-9000.php diff --git a/tests/PHPStan/Analyser/data/bug-9062.php b/tests/PHPStan/Analyser/nsrt/bug-9062.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9062.php rename to tests/PHPStan/Analyser/nsrt/bug-9062.php diff --git a/tests/PHPStan/Analyser/data/bug-9084.php b/tests/PHPStan/Analyser/nsrt/bug-9084.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9084.php rename to tests/PHPStan/Analyser/nsrt/bug-9084.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9086.php b/tests/PHPStan/Analyser/nsrt/bug-9086.php new file mode 100644 index 0000000000..db0110f2f4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9086.php @@ -0,0 +1,36 @@ + + */ +function getObject(): ArrayObject +{ + return new ArrayObject; +} + +function (): void { + $result = pipe(getObject(), function(ArrayObject $i) { + assertType('ArrayObject', $i); + return $i; + }); + + assertType('ArrayObject', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9105.php b/tests/PHPStan/Analyser/nsrt/bug-9105.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9105.php rename to tests/PHPStan/Analyser/nsrt/bug-9105.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9123.php b/tests/PHPStan/Analyser/nsrt/bug-9123.php new file mode 100644 index 0000000000..d1d307fff1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9123.php @@ -0,0 +1,53 @@ + */ +final class Implementation implements EventListener +{ + public function canBeListen(Event $event): bool + { + return $event instanceof MyEvent; + } + + public function listen(Event $event): void + { + if (! $this->canBeListen($event)) { + return; + } + + \PHPStan\Testing\assertType('Bug9123\MyEvent', $event); + } +} + +/** @implements EventListener */ +final class Implementation2 implements EventListener +{ + /** @phpstan-assert-if-true MyEvent $event */ + public function canBeListen(Event $event): bool + { + return $event instanceof MyEvent; + } + + public function listen(Event $event): void + { + if (! $this->canBeListen($event)) { + return; + } + + \PHPStan\Testing\assertType('Bug9123\MyEvent', $event); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9131.php b/tests/PHPStan/Analyser/nsrt/bug-9131.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9131.php rename to tests/PHPStan/Analyser/nsrt/bug-9131.php diff --git a/tests/PHPStan/Analyser/data/bug-9208.php b/tests/PHPStan/Analyser/nsrt/bug-9208.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9208.php rename to tests/PHPStan/Analyser/nsrt/bug-9208.php diff --git a/tests/PHPStan/Analyser/data/bug-9274.php b/tests/PHPStan/Analyser/nsrt/bug-9274.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9274.php rename to tests/PHPStan/Analyser/nsrt/bug-9274.php diff --git a/tests/PHPStan/Analyser/data/bug-9293.php b/tests/PHPStan/Analyser/nsrt/bug-9293.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9293.php rename to tests/PHPStan/Analyser/nsrt/bug-9293.php diff --git a/tests/PHPStan/Analyser/data/bug-9341.php b/tests/PHPStan/Analyser/nsrt/bug-9341.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9341.php rename to tests/PHPStan/Analyser/nsrt/bug-9341.php diff --git a/tests/PHPStan/Analyser/data/bug-9394.php b/tests/PHPStan/Analyser/nsrt/bug-9394.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9394.php rename to tests/PHPStan/Analyser/nsrt/bug-9394.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9397.php b/tests/PHPStan/Analyser/nsrt/bug-9397.php new file mode 100644 index 0000000000..f197e3b438 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9397.php @@ -0,0 +1,101 @@ + + * If the above type has 63 or more properties, the bug occurs + */ + private static function callable(): array { + return []; + } + + public function callsite(): void { + $result = self::callable(); + foreach ($result as $id => $p) { + assertType(Money::class, $p['foo1']); + assertType(Money::class . '|null', $p['foo2']); + assertType('string', $p['foo3']); + + $baseDeposit = $p['foo2'] ?? Money::zero(); + assertType(Money::class, $p['foo1']); + assertType(Money::class . '|null', $p['foo2']); + assertType('string', $p['foo3']); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9404.php b/tests/PHPStan/Analyser/nsrt/bug-9404.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9404.php rename to tests/PHPStan/Analyser/nsrt/bug-9404.php diff --git a/tests/PHPStan/Analyser/data/bug-9472.php b/tests/PHPStan/Analyser/nsrt/bug-9472.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9472.php rename to tests/PHPStan/Analyser/nsrt/bug-9472.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php b/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php new file mode 100644 index 0000000000..13a26b9582 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php @@ -0,0 +1,105 @@ += 8.1 + +namespace Bug9662Enums; + +use function PHPStan\Testing\assertType; + +enum Suit +{ + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +/** + * @param array $suite + */ +function doEnum(array $suite, array $arr) { + if (in_array('NotAnEnumCase', $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array(Suit::Hearts, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + + if (in_array(Suit::Hearts, $arr) === false) { + assertType('array', $arr); + } else { + assertType("non-empty-array", $arr); + } + assertType('array', $arr); + + + if (in_array('NotAnEnumCase', $arr) === false) { + assertType('array', $arr); + } else { + assertType("non-empty-array", $arr); + } + assertType('array', $arr); +} + +enum StringBackedSuit: string +{ + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +/** + * @param array $suite + */ +function doBackedEnum(array $suite, array $arr, string $s, int $i, $mixed) { + if (in_array('NotAnEnumCase', $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array(StringBackedSuit::Hearts, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + + if (in_array($s, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array($i, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + if (in_array($mixed, $suite) === false) { + assertType('array', $suite); + } else { + assertType("non-empty-array", $suite); + } + assertType('array', $suite); + + + if (in_array(StringBackedSuit::Hearts, $arr) === false) { + assertType('array', $arr); + } else { + assertType("non-empty-array", $arr); + } + assertType('array', $arr); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662.php b/tests/PHPStan/Analyser/nsrt/bug-9662.php new file mode 100644 index 0000000000..4da94355c7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9662.php @@ -0,0 +1,189 @@ + $a + * @param array $strings + * @return void + */ +function doFoo(string $s, $a, $strings, $mixed) { + if (in_array('foo', $a, true)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('foo', $a, false)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('foo', $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('0', $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array('1', $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array(true, $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array(false, $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($s, $a, true)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($s, $a, false)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($s, $a)) { + assertType('non-empty-array', $a); + } else { + assertType("array", $a); + } + assertType('array', $a); + + if (in_array($mixed, $strings, true)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($mixed, $strings, false)) { + assertType('array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($mixed, $strings)) { + assertType('array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, true)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, false)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings)) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, true) === true) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, false) === true) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings) === true) { + assertType('non-empty-array', $strings); + } else { + assertType("array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, true) === false) { + assertType('array', $strings); + } else { + assertType("non-empty-array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings, false) === false) { + assertType('array', $strings); + } else { + assertType("non-empty-array", $strings); + } + assertType('array', $strings); + + if (in_array($s, $strings) === false) { + assertType('array', $strings); + } else { + assertType("non-empty-array", $strings); + } + assertType('array', $strings); +} + +/** + * Add new delivery prices. + * + * @param array $price_list Prices list in multiple arrays (changed to array since 1.5.0) + * @param bool $delete + */ +function addDeliveryPrice($price_list, $delete = false): void +{ + if (!$price_list) { + return; + } + + $keys = array_keys($price_list[0]); + if (!in_array('id_shop', $keys)) { + $keys[] = 'id_shop'; + } + if (!in_array('id_shop_group', $keys)) { + $keys[] = 'id_shop_group'; + } + + var_dump($keys); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9704.php b/tests/PHPStan/Analyser/nsrt/bug-9704.php new file mode 100644 index 0000000000..1d435746a5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9704.php @@ -0,0 +1,54 @@ + + */ + private const TYPES = [ + 'foo' => DateTime::class, + 'bar' => DateTimeImmutable::class, + ]; + + /** + * @template M of self::TYPES + * @template T of key-of + * @param T $type + * + * @return new + */ + public static function get(string $type) : object + { + $class = self::TYPES[$type]; + + return new $class('now'); + } + + /** + * @template T of key-of + * @param T $type + * + * @return new + */ + public static function get2(string $type) : object + { + $class = self::TYPES[$type]; + + return new $class('now'); + } +} + +assertType(DateTime::class, Foo::get('foo')); +assertType(DateTimeImmutable::class, Foo::get('bar')); + +assertType(DateTime::class, Foo::get2('foo')); +assertType(DateTimeImmutable::class, Foo::get2('bar')); + + diff --git a/tests/PHPStan/Analyser/data/bug-9714.php b/tests/PHPStan/Analyser/nsrt/bug-9714.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9714.php rename to tests/PHPStan/Analyser/nsrt/bug-9714.php diff --git a/tests/PHPStan/Analyser/data/bug-9721.php b/tests/PHPStan/Analyser/nsrt/bug-9721.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9721.php rename to tests/PHPStan/Analyser/nsrt/bug-9721.php diff --git a/tests/PHPStan/Analyser/data/bug-9734.php b/tests/PHPStan/Analyser/nsrt/bug-9734.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9734.php rename to tests/PHPStan/Analyser/nsrt/bug-9734.php diff --git a/tests/PHPStan/Analyser/data/bug-9753.php b/tests/PHPStan/Analyser/nsrt/bug-9753.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9753.php rename to tests/PHPStan/Analyser/nsrt/bug-9753.php diff --git a/tests/PHPStan/Analyser/data/bug-9764.php b/tests/PHPStan/Analyser/nsrt/bug-9764.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9764.php rename to tests/PHPStan/Analyser/nsrt/bug-9764.php diff --git a/tests/PHPStan/Analyser/data/bug-9778.php b/tests/PHPStan/Analyser/nsrt/bug-9778.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9778.php rename to tests/PHPStan/Analyser/nsrt/bug-9778.php diff --git a/tests/PHPStan/Analyser/data/bug-9784.php b/tests/PHPStan/Analyser/nsrt/bug-9784.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9784.php rename to tests/PHPStan/Analyser/nsrt/bug-9784.php diff --git a/tests/PHPStan/Analyser/data/bug-9867.php b/tests/PHPStan/Analyser/nsrt/bug-9867.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9867.php rename to tests/PHPStan/Analyser/nsrt/bug-9867.php diff --git a/tests/PHPStan/Analyser/data/bug-987.php b/tests/PHPStan/Analyser/nsrt/bug-987.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-987.php rename to tests/PHPStan/Analyser/nsrt/bug-987.php diff --git a/tests/PHPStan/Analyser/data/bug-9881.php b/tests/PHPStan/Analyser/nsrt/bug-9881.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9881.php rename to tests/PHPStan/Analyser/nsrt/bug-9881.php diff --git a/tests/PHPStan/Analyser/data/bug-9939.php b/tests/PHPStan/Analyser/nsrt/bug-9939.php similarity index 96% rename from tests/PHPStan/Analyser/data/bug-9939.php rename to tests/PHPStan/Analyser/nsrt/bug-9939.php index 16e977e202..5f828bebd2 100644 --- a/tests/PHPStan/Analyser/data/bug-9939.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9939.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 1); namespace Bug9939; diff --git a/tests/PHPStan/Analyser/data/bug-9963.php b/tests/PHPStan/Analyser/nsrt/bug-9963.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9963.php rename to tests/PHPStan/Analyser/nsrt/bug-9963.php diff --git a/tests/PHPStan/Analyser/data/bug-9985.php b/tests/PHPStan/Analyser/nsrt/bug-9985.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9985.php rename to tests/PHPStan/Analyser/nsrt/bug-9985.php diff --git a/tests/PHPStan/Analyser/data/bug-9995.php b/tests/PHPStan/Analyser/nsrt/bug-9995.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-9995.php rename to tests/PHPStan/Analyser/nsrt/bug-9995.php diff --git a/tests/PHPStan/Analyser/data/bug-empty-array.php b/tests/PHPStan/Analyser/nsrt/bug-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-empty-array.php rename to tests/PHPStan/Analyser/nsrt/bug-empty-array.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php new file mode 100644 index 0000000000..14d4ecf708 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-nullsafe-prop-static-access.php @@ -0,0 +1,30 @@ += 8.0 + +declare(strict_types=1); + +namespace BugNullsafePropStaticAccess; + +class A +{ + public function __construct(public readonly B $b) + {} +} + +class B +{ + public static int $value = 0; + + public static function get(): string + { + return 'B'; + } +} + +function foo(?A $a): void +{ + \PHPStan\Testing\assertType('string|null', $a?->b::get()); + \PHPStan\Testing\assertType('string|null', $a?->b->get()); + + \PHPStan\Testing\assertType('int|null', $a?->b::$value); + \PHPStan\Testing\assertType('int|null', $a?->b->value); +} diff --git a/tests/PHPStan/Analyser/data/bug-pr-339.php b/tests/PHPStan/Analyser/nsrt/bug-pr-339.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-pr-339.php rename to tests/PHPStan/Analyser/nsrt/bug-pr-339.php diff --git a/tests/PHPStan/Analyser/nsrt/bug11384.php b/tests/PHPStan/Analyser/nsrt/bug11384.php new file mode 100644 index 0000000000..12020de0b9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug11384.php @@ -0,0 +1,20 @@ + 0) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) > 1) { + assertType("array{'ab', 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) >= 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function arraySmallerThan(): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + if (count($x) < 1) { + assertType("array{}", $x); + } else { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) <= 1) { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{'ab', 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function intUnionCount(): void + { + $count = 1; + if (rand(0, 1)) { + $count++; + } + + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType('1|2', $count); + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + if (count($x) >= $count) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + /** + * @param int<1,2> $count + */ + public function intRangeCount($count): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + if (count($x) >= $count) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} diff --git a/tests/PHPStan/Analyser/data/bug2574.php b/tests/PHPStan/Analyser/nsrt/bug2574.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug2574.php rename to tests/PHPStan/Analyser/nsrt/bug2574.php diff --git a/tests/PHPStan/Analyser/data/bug2577.php b/tests/PHPStan/Analyser/nsrt/bug2577.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug2577.php rename to tests/PHPStan/Analyser/nsrt/bug2577.php diff --git a/tests/PHPStan/Analyser/data/call-user-func-php7.php b/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php similarity index 96% rename from tests/PHPStan/Analyser/data/call-user-func-php7.php rename to tests/PHPStan/Analyser/nsrt/call-user-func-php7.php index 9f88a8d20c..756185972c 100644 --- a/tests/PHPStan/Analyser/data/call-user-func-php7.php +++ b/tests/PHPStan/Analyser/nsrt/call-user-func-php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace CallUserFuncPhp8; diff --git a/tests/PHPStan/Analyser/data/call-user-func.php b/tests/PHPStan/Analyser/nsrt/call-user-func.php similarity index 100% rename from tests/PHPStan/Analyser/data/call-user-func.php rename to tests/PHPStan/Analyser/nsrt/call-user-func.php diff --git a/tests/PHPStan/Analyser/data/callable-in-union.php b/tests/PHPStan/Analyser/nsrt/callable-in-union.php similarity index 56% rename from tests/PHPStan/Analyser/data/callable-in-union.php rename to tests/PHPStan/Analyser/nsrt/callable-in-union.php index b72c0725cc..24db62b428 100644 --- a/tests/PHPStan/Analyser/data/callable-in-union.php +++ b/tests/PHPStan/Analyser/nsrt/callable-in-union.php @@ -1,4 +1,4 @@ -= 7.4 namespace CallableInUnion; @@ -15,3 +15,18 @@ function acceptArrayOrCallable($_) assertType('array', $parameter); return $parameter; }); + +/** + * @param (callable(string): void)|callable(int): void $a + * @return void + */ +function acceptCallableOrCallableLikeArray($a): void +{ + +} + +acceptCallableOrCallableLikeArray(function ($p) { + assertType('int|string', $p); +}); + +acceptCallableOrCallableLikeArray(fn ($p) => assertType('int|string', $p)); diff --git a/tests/PHPStan/Analyser/data/callable-object.php b/tests/PHPStan/Analyser/nsrt/callable-object.php similarity index 100% rename from tests/PHPStan/Analyser/data/callable-object.php rename to tests/PHPStan/Analyser/nsrt/callable-object.php diff --git a/tests/PHPStan/Analyser/data/callable-string.php b/tests/PHPStan/Analyser/nsrt/callable-string.php similarity index 100% rename from tests/PHPStan/Analyser/data/callable-string.php rename to tests/PHPStan/Analyser/nsrt/callable-string.php diff --git a/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php similarity index 100% rename from tests/PHPStan/Analyser/data/callsite-cast-narrowing.php rename to tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php diff --git a/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php b/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php new file mode 100644 index 0000000000..a1eb8830a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/case-insensitive-parent.php @@ -0,0 +1,35 @@ + $positive + * @param int $negative + */ +function integerRangeToString($positive, $negative) +{ + assertType('numeric-string', (string) $positive); + assertType('numeric-string', (string) $negative); + + if ($positive !== 0) { + assertType('non-falsy-string&numeric-string', (string) $positive); + } + if ($negative !== 0) { + assertType('non-falsy-string&numeric-string', (string) $negative); + } +} diff --git a/tests/PHPStan/Analyser/data/catch-without-variable.php b/tests/PHPStan/Analyser/nsrt/catch-without-variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/catch-without-variable.php rename to tests/PHPStan/Analyser/nsrt/catch-without-variable.php diff --git a/tests/PHPStan/Analyser/data/class-constant-native-type.php b/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-constant-native-type.php rename to tests/PHPStan/Analyser/nsrt/class-constant-native-type.php diff --git a/tests/PHPStan/Analyser/data/class-constant-on-expr.php b/tests/PHPStan/Analyser/nsrt/class-constant-on-expr.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-constant-on-expr.php rename to tests/PHPStan/Analyser/nsrt/class-constant-on-expr.php diff --git a/tests/PHPStan/Analyser/data/class-constant-types.php b/tests/PHPStan/Analyser/nsrt/class-constant-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-constant-types.php rename to tests/PHPStan/Analyser/nsrt/class-constant-types.php diff --git a/tests/PHPStan/Analyser/data/class-implements.php b/tests/PHPStan/Analyser/nsrt/class-implements.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-implements.php rename to tests/PHPStan/Analyser/nsrt/class-implements.php diff --git a/tests/PHPStan/Analyser/data/class-reflection-interfaces.php b/tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php similarity index 100% rename from tests/PHPStan/Analyser/data/class-reflection-interfaces.php rename to tests/PHPStan/Analyser/nsrt/class-reflection-interfaces.php diff --git a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php similarity index 100% rename from tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php rename to tests/PHPStan/Analyser/nsrt/classPhpDocs-phpstanPropertyPrefix.php diff --git a/tests/PHPStan/Analyser/data/classPhpDocs.php b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php similarity index 95% rename from tests/PHPStan/Analyser/data/classPhpDocs.php rename to tests/PHPStan/Analyser/nsrt/classPhpDocs.php index 0d447a3d49..f0024022ce 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs.php +++ b/tests/PHPStan/Analyser/nsrt/classPhpDocs.php @@ -9,6 +9,7 @@ * @method array arrayOfStrings() * @psalm-method array arrayOfStrings() * @phpstan-method array arrayOfInts() + * @phan-method array arrayOfStrings() * @method array arrayOfInts() * @method mixed overrodeMethod() * @method static mixed overrodeStaticMethod() diff --git a/tests/PHPStan/Analyser/data/clear-stat-cache.php b/tests/PHPStan/Analyser/nsrt/clear-stat-cache.php similarity index 100% rename from tests/PHPStan/Analyser/data/clear-stat-cache.php rename to tests/PHPStan/Analyser/nsrt/clear-stat-cache.php diff --git a/tests/PHPStan/Analyser/data/cli-globals.php b/tests/PHPStan/Analyser/nsrt/cli-globals.php similarity index 100% rename from tests/PHPStan/Analyser/data/cli-globals.php rename to tests/PHPStan/Analyser/nsrt/cli-globals.php diff --git a/tests/PHPStan/Analyser/data/closure-argument-type.php b/tests/PHPStan/Analyser/nsrt/closure-argument-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/closure-argument-type.php rename to tests/PHPStan/Analyser/nsrt/closure-argument-type.php diff --git a/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php b/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php new file mode 100644 index 0000000000..a34ded1591 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php @@ -0,0 +1,39 @@ + $items + * @param callable(T): U $cb + * @return array + */ + public function doFoo(array $items, callable $cb) + { + + } + + public function doBar() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, function ($item) { + assertType('1|2|3', $item); + return $item; + }); + assertType('array<1|2|3>', $b); + } + + public function doBaz() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, fn ($item) => $item); + assertType('array<1|2|3>', $b); + } + +} diff --git a/tests/PHPStan/Analyser/data/closure-retain-expression-types.php b/tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/closure-retain-expression-types.php rename to tests/PHPStan/Analyser/nsrt/closure-retain-expression-types.php diff --git a/tests/PHPStan/Analyser/data/closure-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php similarity index 100% rename from tests/PHPStan/Analyser/data/closure-return-type-extensions.php rename to tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php diff --git a/tests/PHPStan/Analyser/data/closure-return-type.php b/tests/PHPStan/Analyser/nsrt/closure-return-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/closure-return-type.php rename to tests/PHPStan/Analyser/nsrt/closure-return-type.php diff --git a/tests/PHPStan/Analyser/data/closure-types.php b/tests/PHPStan/Analyser/nsrt/closure-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/closure-types.php rename to tests/PHPStan/Analyser/nsrt/closure-types.php diff --git a/tests/PHPStan/Analyser/data/collected-data.php b/tests/PHPStan/Analyser/nsrt/collected-data.php similarity index 100% rename from tests/PHPStan/Analyser/data/collected-data.php rename to tests/PHPStan/Analyser/nsrt/collected-data.php diff --git a/tests/PHPStan/Analyser/data/compact.php b/tests/PHPStan/Analyser/nsrt/compact.php similarity index 100% rename from tests/PHPStan/Analyser/data/compact.php rename to tests/PHPStan/Analyser/nsrt/compact.php diff --git a/tests/PHPStan/Analyser/data/comparison-operators.php b/tests/PHPStan/Analyser/nsrt/comparison-operators.php similarity index 100% rename from tests/PHPStan/Analyser/data/comparison-operators.php rename to tests/PHPStan/Analyser/nsrt/comparison-operators.php diff --git a/tests/PHPStan/Analyser/data/complex-generics-example.php b/tests/PHPStan/Analyser/nsrt/complex-generics-example.php similarity index 100% rename from tests/PHPStan/Analyser/data/complex-generics-example.php rename to tests/PHPStan/Analyser/nsrt/complex-generics-example.php diff --git a/tests/PHPStan/Analyser/data/composer-array-bug.php b/tests/PHPStan/Analyser/nsrt/composer-array-bug.php similarity index 100% rename from tests/PHPStan/Analyser/data/composer-array-bug.php rename to tests/PHPStan/Analyser/nsrt/composer-array-bug.php diff --git a/tests/PHPStan/Analyser/data/composer-non-empty-array-after-unset.php b/tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php similarity index 100% rename from tests/PHPStan/Analyser/data/composer-non-empty-array-after-unset.php rename to tests/PHPStan/Analyser/nsrt/composer-non-empty-array-after-unset.php diff --git a/tests/PHPStan/Analyser/data/composer-treatPhpDocTypesAsCertainBug.php b/tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php similarity index 100% rename from tests/PHPStan/Analyser/data/composer-treatPhpDocTypesAsCertainBug.php rename to tests/PHPStan/Analyser/nsrt/composer-treatPhpDocTypesAsCertainBug.php diff --git a/tests/PHPStan/Analyser/data/conditional-non-empty-array.php b/tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/conditional-non-empty-array.php rename to tests/PHPStan/Analyser/nsrt/conditional-non-empty-array.php diff --git a/tests/PHPStan/Analyser/data/conditional-types-constant.php b/tests/PHPStan/Analyser/nsrt/conditional-types-constant.php similarity index 100% rename from tests/PHPStan/Analyser/data/conditional-types-constant.php rename to tests/PHPStan/Analyser/nsrt/conditional-types-constant.php diff --git a/tests/PHPStan/Analyser/data/conditional-types-inference.php b/tests/PHPStan/Analyser/nsrt/conditional-types-inference.php similarity index 100% rename from tests/PHPStan/Analyser/data/conditional-types-inference.php rename to tests/PHPStan/Analyser/nsrt/conditional-types-inference.php diff --git a/tests/PHPStan/Analyser/data/conditional-types.php b/tests/PHPStan/Analyser/nsrt/conditional-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/conditional-types.php rename to tests/PHPStan/Analyser/nsrt/conditional-types.php diff --git a/tests/PHPStan/Analyser/data/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php similarity index 100% rename from tests/PHPStan/Analyser/data/conditional-vars.php rename to tests/PHPStan/Analyser/nsrt/conditional-vars.php diff --git a/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php b/tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php rename to tests/PHPStan/Analyser/nsrt/const-expr-phpdoc-type.php diff --git a/tests/PHPStan/Analyser/data/const-in-functions-namespaced.php b/tests/PHPStan/Analyser/nsrt/const-in-functions-namespaced.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-in-functions-namespaced.php rename to tests/PHPStan/Analyser/nsrt/const-in-functions-namespaced.php diff --git a/tests/PHPStan/Analyser/data/const-in-functions.php b/tests/PHPStan/Analyser/nsrt/const-in-functions.php similarity index 100% rename from tests/PHPStan/Analyser/data/const-in-functions.php rename to tests/PHPStan/Analyser/nsrt/const-in-functions.php diff --git a/tests/PHPStan/Analyser/data/constant-array-intersect.php b/tests/PHPStan/Analyser/nsrt/constant-array-intersect.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-array-intersect.php rename to tests/PHPStan/Analyser/nsrt/constant-array-intersect.php diff --git a/tests/PHPStan/Analyser/data/constant-array-optional-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-array-optional-set.php rename to tests/PHPStan/Analyser/nsrt/constant-array-optional-set.php diff --git a/tests/PHPStan/Analyser/data/constant-array-type-identical.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-array-type-identical.php rename to tests/PHPStan/Analyser/nsrt/constant-array-type-identical.php diff --git a/tests/PHPStan/Analyser/data/constant-array-type-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-array-type-set.php rename to tests/PHPStan/Analyser/nsrt/constant-array-type-set.php diff --git a/tests/PHPStan/Analyser/data/constant-array-union-unshift.php b/tests/PHPStan/Analyser/nsrt/constant-array-union-unshift.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-array-union-unshift.php rename to tests/PHPStan/Analyser/nsrt/constant-array-union-unshift.php diff --git a/tests/PHPStan/Analyser/data/constant-phpdoc-type.php b/tests/PHPStan/Analyser/nsrt/constant-phpdoc-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-phpdoc-type.php rename to tests/PHPStan/Analyser/nsrt/constant-phpdoc-type.php diff --git a/tests/PHPStan/Analyser/data/constant-string-unions.php b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php similarity index 100% rename from tests/PHPStan/Analyser/data/constant-string-unions.php rename to tests/PHPStan/Analyser/nsrt/constant-string-unions.php diff --git a/tests/PHPStan/Analyser/data/constant.php b/tests/PHPStan/Analyser/nsrt/constant.php similarity index 95% rename from tests/PHPStan/Analyser/data/constant.php rename to tests/PHPStan/Analyser/nsrt/constant.php index c26edcdd53..491de90a45 100644 --- a/tests/PHPStan/Analyser/data/constant.php +++ b/tests/PHPStan/Analyser/nsrt/constant.php @@ -38,3 +38,4 @@ function doFoo(string $constantName): void assertType('Constant\Suit::Hearts', constant('\Constant\Suit::Hearts')); assertType('*ERROR*', constant('UNDEFINED')); +assertType('*ERROR*', constant('::aa')); diff --git a/tests/PHPStan/Analyser/nsrt/count-maybe.php b/tests/PHPStan/Analyser/nsrt/count-maybe.php new file mode 100644 index 0000000000..255c936d22 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-maybe.php @@ -0,0 +1,192 @@ + 0) { + assertType('float', $notCountable); + } else { + assertType('float', $notCountable); + } + assertType('float', $notCountable); +} + +/** + * @param array|int $maybeMode + */ +function doBar2(float $notCountable, $maybeMode): void +{ + if (count($notCountable, $maybeMode) > 0) { + assertType('float', $notCountable); + } else { + assertType('float', $notCountable); + } + assertType('float', $notCountable); +} + +function doBar3(float $notCountable, float $invalidMode): void +{ + if (count($notCountable, $invalidMode) > 0) { + assertType('float', $notCountable); + } else { + assertType('float', $notCountable); + } + assertType('float', $notCountable); +} + +/** + * @param float|int[] $maybeCountable + */ +function doFoo1($maybeCountable, int $mode): void +{ + if (count($maybeCountable, $mode) > 0) { + assertType('non-empty-array', $maybeCountable); + } else { + assertType('array|float', $maybeCountable); + } + assertType('array|float', $maybeCountable); +} + +/** + * @param float|int[] $maybeCountable + * @param array|int $maybeMode + */ +function doFoo2($maybeCountable, $maybeMode): void +{ + if (count($maybeCountable, $maybeMode) > 0) { + assertType('non-empty-array', $maybeCountable); + } else { + assertType('array|float', $maybeCountable); + } + assertType('array|float', $maybeCountable); +} + +/** + * @param float|int[] $maybeCountable + */ +function doFoo3($maybeCountable, float $invalidMode): void +{ + if (count($maybeCountable, $invalidMode) > 0) { + assertType('non-empty-array', $maybeCountable); + } else { + assertType('array|float', $maybeCountable); + } + assertType('array|float', $maybeCountable); +} + +/** + * @param float|list $maybeCountable + */ +function doFoo4($maybeCountable, int $mode): void +{ + if (count($maybeCountable, $mode) > 0) { + assertType('non-empty-list', $maybeCountable); + } else { + assertType('list|float', $maybeCountable); + } + assertType('list|float', $maybeCountable); +} + +/** + * @param float|list $maybeCountable + * @param array|int $maybeMode + */ +function doFoo5($maybeCountable, $maybeMode): void +{ + if (count($maybeCountable, $maybeMode) > 0) { + assertType('non-empty-list', $maybeCountable); + } else { + assertType('list|float', $maybeCountable); + } + assertType('list|float', $maybeCountable); +} + +/** + * @param float|list $maybeCountable + */ +function doFoo6($maybeCountable, float $invalidMode): void +{ + if (count($maybeCountable, $invalidMode) > 0) { + assertType('non-empty-list', $maybeCountable); + } else { + assertType('list|float', $maybeCountable); + } + assertType('list|float', $maybeCountable); +} + +/** + * @param float|list|Countable $maybeCountable + */ +function doFoo7($maybeCountable, int $mode): void +{ + if (count($maybeCountable, $mode) > 0) { + assertType('non-empty-list|Countable', $maybeCountable); + } else { + assertType('list|Countable|float', $maybeCountable); + } + assertType('list|Countable|float', $maybeCountable); +} + +/** + * @param float|list|Countable $maybeCountable + * @param array|int $maybeMode + */ +function doFoo8($maybeCountable, $maybeMode): void +{ + if (count($maybeCountable, $maybeMode) > 0) { + assertType('non-empty-list|Countable', $maybeCountable); + } else { + assertType('list|Countable|float', $maybeCountable); + } + assertType('list|Countable|float', $maybeCountable); +} + +/** + * @param float|list|Countable $maybeCountable + */ +function doFoo9($maybeCountable, float $invalidMode): void +{ + if (count($maybeCountable, $invalidMode) > 0) { + assertType('non-empty-list|Countable', $maybeCountable); + } else { + assertType('list|Countable|float', $maybeCountable); + } + assertType('list|Countable|float', $maybeCountable); +} + +function doFooBar1(array $countable, int $mode): void +{ + if (count($countable, $mode) > 0) { + assertType('non-empty-array', $countable); + } else { + assertType('array{}', $countable); + } + assertType('array', $countable); +} + +/** + * @param array|int $maybeMode + */ +function doFooBar2(array $countable, $maybeMode): void +{ + if (count($countable, $maybeMode) > 0) { + assertType('non-empty-array', $countable); + } else { + assertType('array{}', $countable); + } + assertType('array', $countable); +} + +function doFooBar3(array $countable, float $invalidMode): void +{ + if (count($countable, $invalidMode) > 0) { + assertType('non-empty-array', $countable); + } else { + assertType('array{}', $countable); + } + assertType('array', $countable); +} diff --git a/tests/PHPStan/Analyser/data/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/count-type.php rename to tests/PHPStan/Analyser/nsrt/count-type.php diff --git a/tests/PHPStan/Analyser/data/countable.php b/tests/PHPStan/Analyser/nsrt/countable.php similarity index 100% rename from tests/PHPStan/Analyser/data/countable.php rename to tests/PHPStan/Analyser/nsrt/countable.php diff --git a/tests/PHPStan/Analyser/data/ctype-digit.php b/tests/PHPStan/Analyser/nsrt/ctype-digit.php similarity index 100% rename from tests/PHPStan/Analyser/data/ctype-digit.php rename to tests/PHPStan/Analyser/nsrt/ctype-digit.php diff --git a/tests/PHPStan/Analyser/data/curl_getinfo.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo.php similarity index 100% rename from tests/PHPStan/Analyser/data/curl_getinfo.php rename to tests/PHPStan/Analyser/nsrt/curl_getinfo.php diff --git a/tests/PHPStan/Analyser/data/curl_getinfo_7.3.php b/tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php similarity index 98% rename from tests/PHPStan/Analyser/data/curl_getinfo_7.3.php rename to tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php index 7d2d6ca8bb..a42f17ab32 100644 --- a/tests/PHPStan/Analyser/data/curl_getinfo_7.3.php +++ b/tests/PHPStan/Analyser/nsrt/curl_getinfo_7.3.php @@ -1,4 +1,4 @@ -= 7.3 namespace CurlGetinfo73; diff --git a/tests/PHPStan/Analyser/data/date-format.php b/tests/PHPStan/Analyser/nsrt/date-format.php similarity index 100% rename from tests/PHPStan/Analyser/data/date-format.php rename to tests/PHPStan/Analyser/nsrt/date-format.php diff --git a/tests/PHPStan/Analyser/data/date-period-return-types.php b/tests/PHPStan/Analyser/nsrt/date-period-return-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/date-period-return-types.php rename to tests/PHPStan/Analyser/nsrt/date-period-return-types.php diff --git a/tests/PHPStan/Analyser/data/date.php b/tests/PHPStan/Analyser/nsrt/date.php similarity index 100% rename from tests/PHPStan/Analyser/data/date.php rename to tests/PHPStan/Analyser/nsrt/date.php diff --git a/tests/PHPStan/Analyser/data/dependent-expression-certainty.php b/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php similarity index 100% rename from tests/PHPStan/Analyser/data/dependent-expression-certainty.php rename to tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php diff --git a/tests/PHPStan/Analyser/data/dependent-variable-certainty.php b/tests/PHPStan/Analyser/nsrt/dependent-variable-certainty.php similarity index 100% rename from tests/PHPStan/Analyser/data/dependent-variable-certainty.php rename to tests/PHPStan/Analyser/nsrt/dependent-variable-certainty.php diff --git a/tests/PHPStan/Analyser/data/dependent-variables-arrow-function.php b/tests/PHPStan/Analyser/nsrt/dependent-variables-arrow-function.php similarity index 100% rename from tests/PHPStan/Analyser/data/dependent-variables-arrow-function.php rename to tests/PHPStan/Analyser/nsrt/dependent-variables-arrow-function.php diff --git a/tests/PHPStan/Analyser/data/dependent-variables-type-guard-same-as-type.php b/tests/PHPStan/Analyser/nsrt/dependent-variables-type-guard-same-as-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/dependent-variables-type-guard-same-as-type.php rename to tests/PHPStan/Analyser/nsrt/dependent-variables-type-guard-same-as-type.php diff --git a/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php b/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php new file mode 100644 index 0000000000..d59a186bf0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-10285-php8.php @@ -0,0 +1,21 @@ += 8.0 + +namespace Discussion10285Php8; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function sayHello(): void + { + $socket = socket_create(AF_INET, SOCK_STREAM, 0); + if($socket === false) return; + $read = [$socket]; + $write = []; + $except = null; + socket_select($read, $write, $except, 0, 1); + assertType('array', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/discussion-10285.php b/tests/PHPStan/Analyser/nsrt/discussion-10285.php new file mode 100644 index 0000000000..4b8da269ad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/discussion-10285.php @@ -0,0 +1,21 @@ +', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/data/discussion-8447.php b/tests/PHPStan/Analyser/nsrt/discussion-8447.php similarity index 100% rename from tests/PHPStan/Analyser/data/discussion-8447.php rename to tests/PHPStan/Analyser/nsrt/discussion-8447.php diff --git a/tests/PHPStan/Analyser/data/discussion-9134.php b/tests/PHPStan/Analyser/nsrt/discussion-9134.php similarity index 100% rename from tests/PHPStan/Analyser/data/discussion-9134.php rename to tests/PHPStan/Analyser/nsrt/discussion-9134.php diff --git a/tests/PHPStan/Analyser/data/discussion-9972.php b/tests/PHPStan/Analyser/nsrt/discussion-9972.php similarity index 100% rename from tests/PHPStan/Analyser/data/discussion-9972.php rename to tests/PHPStan/Analyser/nsrt/discussion-9972.php diff --git a/tests/PHPStan/Analyser/data/div-by-zero.php b/tests/PHPStan/Analyser/nsrt/div-by-zero.php similarity index 100% rename from tests/PHPStan/Analyser/data/div-by-zero.php rename to tests/PHPStan/Analyser/nsrt/div-by-zero.php diff --git a/tests/PHPStan/Analyser/data/dnf.php b/tests/PHPStan/Analyser/nsrt/dnf.php similarity index 100% rename from tests/PHPStan/Analyser/data/dnf.php rename to tests/PHPStan/Analyser/nsrt/dnf.php diff --git a/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php b/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php similarity index 68% rename from tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php rename to tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php index 4b086bc15c..6900709f99 100644 --- a/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php +++ b/tests/PHPStan/Analyser/nsrt/do-not-remember-impure-functions.php @@ -74,3 +74,50 @@ public function doDolor() } } + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/data/ds-copy.php b/tests/PHPStan/Analyser/nsrt/ds-copy.php similarity index 100% rename from tests/PHPStan/Analyser/data/ds-copy.php rename to tests/PHPStan/Analyser/nsrt/ds-copy.php diff --git a/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php new file mode 100644 index 0000000000..ec25be47cd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php @@ -0,0 +1,39 @@ + $a + * @param 'b'|'bb' $b + */ + public function integerRange(int $a, string $b): void + { + assertType("'0 b'|'0 bb'|'1 b'|'1 bb'|'2 b'|'2 bb'|'3 b'|'3 bb'", sprintf('%d %s', $a, $b)); + } + + /** + * @param int<0,64> $a + * @param 'b'|'bb' $b + */ + public function tooBigRange(int $a, string $b): void + { + assertType("non-falsy-string", sprintf('%d %s', $a, $b)); + } + +} diff --git a/tests/PHPStan/Analyser/data/early-termination-phpdoc.php b/tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php similarity index 100% rename from tests/PHPStan/Analyser/data/early-termination-phpdoc.php rename to tests/PHPStan/Analyser/nsrt/early-termination-phpdoc.php diff --git a/tests/PHPStan/Analyser/data/empty-array-shape.php b/tests/PHPStan/Analyser/nsrt/empty-array-shape.php similarity index 100% rename from tests/PHPStan/Analyser/data/empty-array-shape.php rename to tests/PHPStan/Analyser/nsrt/empty-array-shape.php diff --git a/tests/PHPStan/Analyser/data/emptyiterator.php b/tests/PHPStan/Analyser/nsrt/emptyiterator.php similarity index 100% rename from tests/PHPStan/Analyser/data/emptyiterator.php rename to tests/PHPStan/Analyser/nsrt/emptyiterator.php diff --git a/tests/PHPStan/Analyser/data/enum-from.php b/tests/PHPStan/Analyser/nsrt/enum-from.php similarity index 98% rename from tests/PHPStan/Analyser/data/enum-from.php rename to tests/PHPStan/Analyser/nsrt/enum-from.php index 2a51131141..9f65726914 100644 --- a/tests/PHPStan/Analyser/data/enum-from.php +++ b/tests/PHPStan/Analyser/nsrt/enum-from.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types=1); namespace EnumFrom; diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php82.php b/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php similarity index 95% rename from tests/PHPStan/Analyser/data/enum-reflection-php82.php rename to tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php index f67ede0a36..7584e5b4cf 100644 --- a/tests/PHPStan/Analyser/data/enum-reflection-php82.php +++ b/tests/PHPStan/Analyser/nsrt/enum-reflection-php82.php @@ -1,4 +1,4 @@ -= 8.1 += 8.2 namespace EnumReflection82; diff --git a/tests/PHPStan/Analyser/data/enum-reflection.php b/tests/PHPStan/Analyser/nsrt/enum-reflection.php similarity index 100% rename from tests/PHPStan/Analyser/data/enum-reflection.php rename to tests/PHPStan/Analyser/nsrt/enum-reflection.php diff --git a/tests/PHPStan/Analyser/data/enum_exists.php b/tests/PHPStan/Analyser/nsrt/enum_exists.php similarity index 100% rename from tests/PHPStan/Analyser/data/enum_exists.php rename to tests/PHPStan/Analyser/nsrt/enum_exists.php diff --git a/tests/PHPStan/Analyser/data/enums-import-alias.php b/tests/PHPStan/Analyser/nsrt/enums-import-alias.php similarity index 100% rename from tests/PHPStan/Analyser/data/enums-import-alias.php rename to tests/PHPStan/Analyser/nsrt/enums-import-alias.php diff --git a/tests/PHPStan/Analyser/data/enums.php b/tests/PHPStan/Analyser/nsrt/enums.php similarity index 100% rename from tests/PHPStan/Analyser/data/enums.php rename to tests/PHPStan/Analyser/nsrt/enums.php diff --git a/tests/PHPStan/Analyser/data/equal.php b/tests/PHPStan/Analyser/nsrt/equal.php similarity index 100% rename from tests/PHPStan/Analyser/data/equal.php rename to tests/PHPStan/Analyser/nsrt/equal.php diff --git a/tests/PHPStan/Analyser/data/eval-implicit-throw.php b/tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php similarity index 100% rename from tests/PHPStan/Analyser/data/eval-implicit-throw.php rename to tests/PHPStan/Analyser/nsrt/eval-implicit-throw.php diff --git a/tests/PHPStan/Analyser/data/ext-ds.php b/tests/PHPStan/Analyser/nsrt/ext-ds.php similarity index 100% rename from tests/PHPStan/Analyser/data/ext-ds.php rename to tests/PHPStan/Analyser/nsrt/ext-ds.php diff --git a/tests/PHPStan/Analyser/data/extra-extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/extra-extra-int-types.php rename to tests/PHPStan/Analyser/nsrt/extra-extra-int-types.php diff --git a/tests/PHPStan/Analyser/data/extra-int-types.php b/tests/PHPStan/Analyser/nsrt/extra-int-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/extra-int-types.php rename to tests/PHPStan/Analyser/nsrt/extra-int-types.php diff --git a/tests/PHPStan/Analyser/data/extract.php b/tests/PHPStan/Analyser/nsrt/extract.php similarity index 100% rename from tests/PHPStan/Analyser/data/extract.php rename to tests/PHPStan/Analyser/nsrt/extract.php diff --git a/tests/PHPStan/Analyser/data/falsey-coalesce.php b/tests/PHPStan/Analyser/nsrt/falsey-coalesce.php similarity index 100% rename from tests/PHPStan/Analyser/data/falsey-coalesce.php rename to tests/PHPStan/Analyser/nsrt/falsey-coalesce.php diff --git a/tests/PHPStan/Analyser/data/falsey-empty-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php similarity index 100% rename from tests/PHPStan/Analyser/data/falsey-empty-certainty.php rename to tests/PHPStan/Analyser/nsrt/falsey-empty-certainty.php diff --git a/tests/PHPStan/Analyser/data/falsey-isset-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php similarity index 100% rename from tests/PHPStan/Analyser/data/falsey-isset-certainty.php rename to tests/PHPStan/Analyser/nsrt/falsey-isset-certainty.php diff --git a/tests/PHPStan/Analyser/data/falsey-ternary-certainty.php b/tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php similarity index 100% rename from tests/PHPStan/Analyser/data/falsey-ternary-certainty.php rename to tests/PHPStan/Analyser/nsrt/falsey-ternary-certainty.php diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/nsrt/falsy-isset.php similarity index 100% rename from tests/PHPStan/Analyser/data/falsy-isset.php rename to tests/PHPStan/Analyser/nsrt/falsy-isset.php diff --git a/tests/PHPStan/Analyser/data/filesystem-functions.php b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php similarity index 93% rename from tests/PHPStan/Analyser/data/filesystem-functions.php rename to tests/PHPStan/Analyser/nsrt/filesystem-functions.php index 988fbc8dc3..99dbd676f1 100644 --- a/tests/PHPStan/Analyser/data/filesystem-functions.php +++ b/tests/PHPStan/Analyser/nsrt/filesystem-functions.php @@ -1,4 +1,4 @@ -= 7.4 namespace FilesystemFunctions; @@ -59,7 +59,7 @@ public function test3($fh): void public function test4(string $path): void { if (file_get_contents($path) === 'data') { - assertType('\'data\'', file_get_contents($path)); + assertType('string|false', file_get_contents($path)); file_put_contents($path, 'other'); assertType('string|false', file_get_contents($path)); } diff --git a/tests/PHPStan/Analyser/data/filter-input-array.php b/tests/PHPStan/Analyser/nsrt/filter-input-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/filter-input-array.php rename to tests/PHPStan/Analyser/nsrt/filter-input-array.php diff --git a/tests/PHPStan/Analyser/data/filter-input-php7.php b/tests/PHPStan/Analyser/nsrt/filter-input-php7.php similarity index 87% rename from tests/PHPStan/Analyser/data/filter-input-php7.php rename to tests/PHPStan/Analyser/nsrt/filter-input-php7.php index b5ee1d393d..8a87e04edc 100644 --- a/tests/PHPStan/Analyser/data/filter-input-php7.php +++ b/tests/PHPStan/Analyser/nsrt/filter-input-php7.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types=1); namespace FilterInputPhp8; diff --git a/tests/PHPStan/Analyser/data/filter-input.php b/tests/PHPStan/Analyser/nsrt/filter-input.php similarity index 100% rename from tests/PHPStan/Analyser/data/filter-input.php rename to tests/PHPStan/Analyser/nsrt/filter-input.php diff --git a/tests/PHPStan/Analyser/data/filter-var-array.php b/tests/PHPStan/Analyser/nsrt/filter-var-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/filter-var-array.php rename to tests/PHPStan/Analyser/nsrt/filter-var-array.php diff --git a/tests/PHPStan/Analyser/data/filter-var-dynamic-return-type-extension-regression.php b/tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php similarity index 100% rename from tests/PHPStan/Analyser/data/filter-var-dynamic-return-type-extension-regression.php rename to tests/PHPStan/Analyser/nsrt/filter-var-dynamic-return-type-extension-regression.php diff --git a/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php similarity index 100% rename from tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php rename to tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php diff --git a/tests/PHPStan/Analyser/data/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php similarity index 100% rename from tests/PHPStan/Analyser/data/filter-var.php rename to tests/PHPStan/Analyser/nsrt/filter-var.php diff --git a/tests/PHPStan/Analyser/data/finally-scope.php b/tests/PHPStan/Analyser/nsrt/finally-scope.php similarity index 100% rename from tests/PHPStan/Analyser/data/finally-scope.php rename to tests/PHPStan/Analyser/nsrt/finally-scope.php diff --git a/tests/PHPStan/Analyser/data/finite-types.php b/tests/PHPStan/Analyser/nsrt/finite-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/finite-types.php rename to tests/PHPStan/Analyser/nsrt/finite-types.php diff --git a/tests/PHPStan/Analyser/data/first-class-callables.php b/tests/PHPStan/Analyser/nsrt/first-class-callables.php similarity index 100% rename from tests/PHPStan/Analyser/data/first-class-callables.php rename to tests/PHPStan/Analyser/nsrt/first-class-callables.php diff --git a/tests/PHPStan/Analyser/data/fizz-buzz.php b/tests/PHPStan/Analyser/nsrt/fizz-buzz.php similarity index 100% rename from tests/PHPStan/Analyser/data/fizz-buzz.php rename to tests/PHPStan/Analyser/nsrt/fizz-buzz.php diff --git a/tests/PHPStan/Analyser/data/for-loop-i-type.php b/tests/PHPStan/Analyser/nsrt/for-loop-i-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/for-loop-i-type.php rename to tests/PHPStan/Analyser/nsrt/for-loop-i-type.php diff --git a/tests/PHPStan/Analyser/data/foreach-dependent-key-value.php b/tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php similarity index 100% rename from tests/PHPStan/Analyser/data/foreach-dependent-key-value.php rename to tests/PHPStan/Analyser/nsrt/foreach-dependent-key-value.php diff --git a/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php similarity index 100% rename from tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php rename to tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php diff --git a/tests/PHPStan/Analyser/data/fpm-get-status.php b/tests/PHPStan/Analyser/nsrt/fpm-get-status.php similarity index 98% rename from tests/PHPStan/Analyser/data/fpm-get-status.php rename to tests/PHPStan/Analyser/nsrt/fpm-get-status.php index 85efd500c5..8a276ec7ee 100644 --- a/tests/PHPStan/Analyser/data/fpm-get-status.php +++ b/tests/PHPStan/Analyser/nsrt/fpm-get-status.php @@ -1,4 +1,4 @@ -= 7.3 namespace FpmGetStatus; diff --git a/tests/PHPStan/Analyser/nsrt/generic-callables.php b/tests/PHPStan/Analyser/nsrt/generic-callables.php new file mode 100644 index 0000000000..94bb3238a2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-callables.php @@ -0,0 +1,80 @@ +(TClosureRet $val): (TClosureRet|TFuncRet) + */ +function testFuncClosureMixed(mixed $mixed) +{ +} + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return callable(): TFuncRet + */ +function testFuncCallable(mixed $mixed): callable +{ +} + +/** + * @param Closure(TRet $val): TRet $callable + * @param non-empty-list(TRet $val): TRet> $callables + */ +function testClosure(Closure $callable, int $int, string $str, array $callables): void +{ + assertType('Closure(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('string', $callables[0]($str)); + assertType('Closure(): 1', testFuncClosure(1)); +} + +function testClosureMixed(int $int, string $str): void +{ + $closure = testFuncClosureMixed($int); + assertType('Closure(TClosureRet): (int|TClosureRet)', $closure); + assertType('int|string', $closure($str)); +} + +/** + * @param callable(TRet $val): TRet $callable + */ +function testCallable(callable $callable, int $int, string $str): void +{ + assertType('callable(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('callable(): 1', testFuncCallable(1)); +} + +/** + * @param Closure(TRetFirst $valone): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond)) $closure + */ +function testNestedClosures(Closure $closure, string $str, int $int): void +{ + assertType('Closure(TRetFirst): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond))', $closure); + $closure1 = $closure($str); + assertType('Closure(TRetSecond): (string|TRetSecond)', $closure1); + $result = $closure1($int); + assertType('int|string', $result); +} diff --git a/tests/PHPStan/Analyser/data/generic-class-string.php b/tests/PHPStan/Analyser/nsrt/generic-class-string.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-class-string.php rename to tests/PHPStan/Analyser/nsrt/generic-class-string.php diff --git a/tests/PHPStan/Analyser/data/generic-enum-class-string.php b/tests/PHPStan/Analyser/nsrt/generic-enum-class-string.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-enum-class-string.php rename to tests/PHPStan/Analyser/nsrt/generic-enum-class-string.php diff --git a/tests/PHPStan/Analyser/data/generic-generalization.php b/tests/PHPStan/Analyser/nsrt/generic-generalization.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-generalization.php rename to tests/PHPStan/Analyser/nsrt/generic-generalization.php diff --git a/tests/PHPStan/Analyser/nsrt/generic-method-tags.php b/tests/PHPStan/Analyser/nsrt/generic-method-tags.php new file mode 100644 index 0000000000..92fdfaef5c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-method-tags.php @@ -0,0 +1,25 @@ +(TVal $param) + * @method TVal doAnotherThing(int $param) + */ +class Test +{ + public function __call(): mixed + { + } +} + +function test(int $int, string $string): void +{ + $test = new Test(); + + assertType('int', $test->doThing($int)); + assertType('string', $test->doThing($string)); + assertType(TVal::class, $test->doAnotherThing($int)); +} diff --git a/tests/PHPStan/Analyser/data/generic-object-lower-bound.php b/tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-object-lower-bound.php rename to tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.php diff --git a/tests/PHPStan/Analyser/data/generic-offset-get.php b/tests/PHPStan/Analyser/nsrt/generic-offset-get.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-offset-get.php rename to tests/PHPStan/Analyser/nsrt/generic-offset-get.php diff --git a/tests/PHPStan/Analyser/data/generic-parent.php b/tests/PHPStan/Analyser/nsrt/generic-parent.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-parent.php rename to tests/PHPStan/Analyser/nsrt/generic-parent.php diff --git a/tests/PHPStan/Analyser/data/generic-traits.php b/tests/PHPStan/Analyser/nsrt/generic-traits.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-traits.php rename to tests/PHPStan/Analyser/nsrt/generic-traits.php diff --git a/tests/PHPStan/Analyser/data/generic-unions.php b/tests/PHPStan/Analyser/nsrt/generic-unions.php similarity index 100% rename from tests/PHPStan/Analyser/data/generic-unions.php rename to tests/PHPStan/Analyser/nsrt/generic-unions.php diff --git a/tests/PHPStan/Analyser/data/generics-default.php b/tests/PHPStan/Analyser/nsrt/generics-default.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-default.php rename to tests/PHPStan/Analyser/nsrt/generics-default.php diff --git a/tests/PHPStan/Analyser/data/generics-do-not-generalize.php b/tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-do-not-generalize.php rename to tests/PHPStan/Analyser/nsrt/generics-do-not-generalize.php diff --git a/tests/PHPStan/Analyser/data/generics-empty-array.php b/tests/PHPStan/Analyser/nsrt/generics-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-empty-array.php rename to tests/PHPStan/Analyser/nsrt/generics-empty-array.php diff --git a/tests/PHPStan/Analyser/data/generics-reduce-types-first.php b/tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php similarity index 100% rename from tests/PHPStan/Analyser/data/generics-reduce-types-first.php rename to tests/PHPStan/Analyser/nsrt/generics-reduce-types-first.php diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php similarity index 98% rename from tests/PHPStan/Analyser/data/generics.php rename to tests/PHPStan/Analyser/nsrt/generics.php index 61d458c703..bcd7ecf616 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -159,7 +159,7 @@ function testF($arrayOfInt, $callableOrNull) assertType('array', f($arrayOfInt, function ($a): string { return (string)$a; })); - assertType('array', f($arrayOfInt, function ($a) { + assertType('array', f($arrayOfInt, function ($a) { return $a; })); assertType('array', f($arrayOfInt, $callableOrNull)); @@ -1176,6 +1176,7 @@ class PrefixedTemplateWins2 * @template T of Foo * @phpstan-template T of Bar * @psalm-template T of Baz + * @phan-template T of Quux */ class PrefixedTemplateWins3 { @@ -1209,12 +1210,25 @@ class PrefixedTemplateWins5 } +/** + * @phan-template T of Foo + * @phpstan-template T of Bar + */ +class PrefixedTemplateWins6 +{ + + /** @var T */ + public $name; + +} + function testPrefixed( PrefixedTemplateWins $a, PrefixedTemplateWins2 $b, PrefixedTemplateWins3 $c, PrefixedTemplateWins4 $d, - PrefixedTemplateWins5 $e + PrefixedTemplateWins5 $e, + PrefixedTemplateWins6 $f ) { assertType('PHPStan\Generics\FunctionsAssertType\Bar', $a->name); @@ -1222,6 +1236,7 @@ function testPrefixed( assertType('PHPStan\Generics\FunctionsAssertType\Bar', $c->name); assertType('PHPStan\Generics\FunctionsAssertType\Bar', $d->name); assertType('PHPStan\Generics\FunctionsAssertType\Bar', $e->name); + assertType('PHPStan\Generics\FunctionsAssertType\Bar', $f->name); }; /** diff --git a/tests/PHPStan/Analyser/data/get-class-static-class.php b/tests/PHPStan/Analyser/nsrt/get-class-static-class.php similarity index 100% rename from tests/PHPStan/Analyser/data/get-class-static-class.php rename to tests/PHPStan/Analyser/nsrt/get-class-static-class.php diff --git a/tests/PHPStan/Analyser/nsrt/get-debug-type.php b/tests/PHPStan/Analyser/nsrt/get-debug-type.php new file mode 100644 index 0000000000..975ea62613 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/get-debug-type.php @@ -0,0 +1,51 @@ += 8.0 namespace HashFunctions; diff --git a/tests/PHPStan/Analyser/data/hash-functions.php b/tests/PHPStan/Analyser/nsrt/hash-functions.php similarity index 100% rename from tests/PHPStan/Analyser/data/hash-functions.php rename to tests/PHPStan/Analyser/nsrt/hash-functions.php diff --git a/tests/PHPStan/Analyser/nsrt/http-response-header.php b/tests/PHPStan/Analyser/nsrt/http-response-header.php new file mode 100644 index 0000000000..aa39899630 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/http-response-header.php @@ -0,0 +1,8 @@ +', $http_response_header); +assertNativeType('array', $http_response_header); diff --git a/tests/PHPStan/Analyser/data/ibm_db2.php b/tests/PHPStan/Analyser/nsrt/ibm_db2.php similarity index 100% rename from tests/PHPStan/Analyser/data/ibm_db2.php rename to tests/PHPStan/Analyser/nsrt/ibm_db2.php diff --git a/tests/PHPStan/Analyser/data/identical.php b/tests/PHPStan/Analyser/nsrt/identical.php similarity index 100% rename from tests/PHPStan/Analyser/data/identical.php rename to tests/PHPStan/Analyser/nsrt/identical.php diff --git a/tests/PHPStan/Analyser/data/image-size.php b/tests/PHPStan/Analyser/nsrt/image-size.php similarity index 84% rename from tests/PHPStan/Analyser/data/image-size.php rename to tests/PHPStan/Analyser/nsrt/image-size.php index 91ef9e48fb..a68a0706b3 100644 --- a/tests/PHPStan/Analyser/data/image-size.php +++ b/tests/PHPStan/Analyser/nsrt/image-size.php @@ -14,8 +14,8 @@ function imageFoo(): void list($width, $height, $type, $attr) = $imagesize; - assertType('int', $width); - assertType('int', $height); + assertType('int<0, max>', $width); + assertType('int<0, max>', $height); assertType('int', $type); assertType('string', $attr); assertType('string', $imagesize['mime']); @@ -31,8 +31,8 @@ function imagesizeFoo(string $s): void } list($width, $height, $type, $attr) = $imagesize; - assertType('int', $width); - assertType('int', $height); + assertType('int<0, max>', $width); + assertType('int<0, max>', $height); assertType('int', $type); assertType('string', $attr); assertType('string', $imagesize['mime']); diff --git a/tests/PHPStan/Analyser/data/imagick-pixel.php b/tests/PHPStan/Analyser/nsrt/imagick-pixel.php similarity index 100% rename from tests/PHPStan/Analyser/data/imagick-pixel.php rename to tests/PHPStan/Analyser/nsrt/imagick-pixel.php diff --git a/tests/PHPStan/Analyser/data/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php similarity index 100% rename from tests/PHPStan/Analyser/data/implode.php rename to tests/PHPStan/Analyser/nsrt/implode.php diff --git a/tests/PHPStan/Analyser/data/impure-connection-fns.php b/tests/PHPStan/Analyser/nsrt/impure-connection-fns.php similarity index 100% rename from tests/PHPStan/Analyser/data/impure-connection-fns.php rename to tests/PHPStan/Analyser/nsrt/impure-connection-fns.php diff --git a/tests/PHPStan/Analyser/data/impure-constructor.php b/tests/PHPStan/Analyser/nsrt/impure-constructor.php similarity index 100% rename from tests/PHPStan/Analyser/data/impure-constructor.php rename to tests/PHPStan/Analyser/nsrt/impure-constructor.php diff --git a/tests/PHPStan/Analyser/nsrt/impure-error-log.php b/tests/PHPStan/Analyser/nsrt/impure-error-log.php new file mode 100644 index 0000000000..082112b83b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/impure-error-log.php @@ -0,0 +1,14 @@ +fooProp = rand(0, 1); + } + + /** + * @return $this + */ + public function returnsThis($arg) + { + $this->fooProp = rand(0, 1); + } + + /** + * @return $this + * @phpstan-impure + */ + public function returnsThisImpure($arg) + { + $this->fooProp = rand(0, 1); + } + + public function ordinaryMethod(): int + { + return 1; + } + + /** + * @phpstan-impure + * @return int + */ + public function impureMethod(): int + { + $this->fooProp = rand(0, 1); + + return $this->fooProp; + } + + /** + * @impure + * @return int + */ + public function impureMethod2(): int + { + $this->fooProp = rand(0, 1); + + return $this->fooProp; + } + + public function doFoo(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->voidMethod(); + assertType('int', $this->fooProp); + } + + public function doFluent(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->returnsThis(new stdClass()); + assertType('int', $this->fooProp); + } + + public function doFluent2(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->phpDocReturnThis(); + assertType('int', $this->fooProp); + } + + public function doBar(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->ordinaryMethod(); + assertType('1', $this->fooProp); + } + + public function doBaz(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->impureMethod(); + assertType('int', $this->fooProp); + } + + public function doLorem(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->impureMethod2(); + assertType('int', $this->fooProp); + } + +} + +class Person +{ + + public function getName(): ?string + { + } + +} + +class Bar +{ + + public function doFoo(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThis($p); + assertType('string', $p->getName()); + } + + public function doFoo2(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThisImpure($p); + assertType('string|null', $p->getName()); + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/data/in-array-enum.php b/tests/PHPStan/Analyser/nsrt/in-array-enum.php similarity index 100% rename from tests/PHPStan/Analyser/data/in-array-enum.php rename to tests/PHPStan/Analyser/nsrt/in-array-enum.php diff --git a/tests/PHPStan/Analyser/data/in-array-haystack-subtract.php b/tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php similarity index 100% rename from tests/PHPStan/Analyser/data/in-array-haystack-subtract.php rename to tests/PHPStan/Analyser/nsrt/in-array-haystack-subtract.php diff --git a/tests/PHPStan/Analyser/data/in-array-non-empty.php b/tests/PHPStan/Analyser/nsrt/in-array-non-empty.php similarity index 100% rename from tests/PHPStan/Analyser/data/in-array-non-empty.php rename to tests/PHPStan/Analyser/nsrt/in-array-non-empty.php diff --git a/tests/PHPStan/Analyser/data/in-array.php b/tests/PHPStan/Analyser/nsrt/in-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/in-array.php rename to tests/PHPStan/Analyser/nsrt/in-array.php diff --git a/tests/PHPStan/Analyser/nsrt/in_array_loose.php b/tests/PHPStan/Analyser/nsrt/in_array_loose.php new file mode 100644 index 0000000000..4600ae0a13 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/in_array_loose.php @@ -0,0 +1,48 @@ + $a * @param int<0, max> $b + * @param int<16, 32> $c + * @param int<2, 4> $d */ - function divisionLoosesInformation(int $a, int $b): void { - assertType('float|int<0, max>',$a/$b); + function divisionLoosesInformation(int $a, int $b, int $c, int $d): void { + assertType('float|int<0, max>', $a / $b); + assertType('float|int<8, 16>', $c / 2); + assertType('float|int<4, 16>', $c / $d); } /** @@ -342,6 +346,92 @@ public function sayHello($p, $u): void assertType('float|int<-2, 2>', $p / $u); } + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftLeft($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a << 0); + assertType('int<5, max>', $b << 0); + assertType('int', $c << 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d << 0); + assertType('1|3|5', $e << 0); + assertType('*ERROR*', $f << 0); + + assertType('int<-10, 10>', $a << 1); + assertType('int<10, max>', $b << 1); + assertType('int', $c << 1); + assertType('2|50|int<10, 20>|int<60, 80>', $d << 1); + assertType('2|6|10', $e << 1); + assertType('*ERROR*', $f << 1); + + assertType('*ERROR*', $a << -1); + + assertType('int', $a << $b); + + assertType('0', null << 1); + assertType('0', false << 1); + assertType('2', true << 1); + assertType('10', "10" << 0); + assertType('*ERROR*', "ciao" << 0); + assertType('30', 15.9 << 1); + assertType('*ERROR*', array(5) << 1); + + assertType('8', 4.1 << 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float << 1.9); + } + + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftRight($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a >> 0); + assertType('int<5, max>', $b >> 0); + assertType('int', $c >> 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d >> 0); + assertType('1|3|5', $e >> 0); + assertType('*ERROR*', $f >> 0); + + assertType('int<-3, 2>', $a >> 1); + assertType('int<2, max>', $b >> 1); + assertType('int', $c >> 1); + assertType('0|12|int<2, 5>|int<15, 20>', $d >> 1); + assertType('0|1|2', $e >> 1); + assertType('*ERROR*', $f >> 1); + + assertType('*ERROR*', $a >> -1); + + assertType('int', $a >> $b); + + assertType('0', null >> 1); + assertType('0', false >> 1); + assertType('0', true >> 1); + assertType('10', "10" >> 0); + assertType('*ERROR*', "ciao" >> 0); + assertType('7', 15.9 >> 1); + assertType('*ERROR*', array(5) >> 1); + + assertType('2', 4.1 >> 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float >> 1.9); + } + /** * @param int<0, max> $positive * @param int $negative diff --git a/tests/PHPStan/Analyser/data/intersection-static.php b/tests/PHPStan/Analyser/nsrt/intersection-static.php similarity index 100% rename from tests/PHPStan/Analyser/data/intersection-static.php rename to tests/PHPStan/Analyser/nsrt/intersection-static.php diff --git a/tests/PHPStan/Analyser/data/invalid-type-aliases.php b/tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalid-type-aliases.php rename to tests/PHPStan/Analyser/nsrt/invalid-type-aliases.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument-function.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument-function.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument-function.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument-static.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument-static.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument-static.php diff --git a/tests/PHPStan/Analyser/data/invalidate-object-argument.php b/tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-object-argument.php rename to tests/PHPStan/Analyser/nsrt/invalidate-object-argument.php diff --git a/tests/PHPStan/Analyser/data/invalidate-readonly-properties.php b/tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php similarity index 100% rename from tests/PHPStan/Analyser/data/invalidate-readonly-properties.php rename to tests/PHPStan/Analyser/nsrt/invalidate-readonly-properties.php diff --git a/tests/PHPStan/Analyser/data/is-a.php b/tests/PHPStan/Analyser/nsrt/is-a.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-a.php rename to tests/PHPStan/Analyser/nsrt/is-a.php diff --git a/tests/PHPStan/Analyser/data/is-numeric.php b/tests/PHPStan/Analyser/nsrt/is-numeric.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-numeric.php rename to tests/PHPStan/Analyser/nsrt/is-numeric.php diff --git a/tests/PHPStan/Analyser/data/is-subclass-of.php b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php similarity index 100% rename from tests/PHPStan/Analyser/data/is-subclass-of.php rename to tests/PHPStan/Analyser/nsrt/is-subclass-of.php diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-post-81.php similarity index 90% rename from tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php rename to tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-post-81.php index f1149f5f84..8cc3cc8547 100644 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-post-81.php @@ -1,4 +1,4 @@ -= 8.1 namespace IssetCoalesceEmptyTypePost81; diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-pre-81.php similarity index 90% rename from tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php rename to tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-pre-81.php index 25e5035860..a03c000a3b 100644 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type-pre-81.php @@ -1,4 +1,4 @@ -= 8.3 namespace JsonValidate; diff --git a/tests/PHPStan/Analyser/data/key-exists.php b/tests/PHPStan/Analyser/nsrt/key-exists.php similarity index 100% rename from tests/PHPStan/Analyser/data/key-exists.php rename to tests/PHPStan/Analyser/nsrt/key-exists.php diff --git a/tests/PHPStan/Analyser/data/key-of-generic.php b/tests/PHPStan/Analyser/nsrt/key-of-generic.php similarity index 100% rename from tests/PHPStan/Analyser/data/key-of-generic.php rename to tests/PHPStan/Analyser/nsrt/key-of-generic.php diff --git a/tests/PHPStan/Analyser/data/key-of.php b/tests/PHPStan/Analyser/nsrt/key-of.php similarity index 100% rename from tests/PHPStan/Analyser/data/key-of.php rename to tests/PHPStan/Analyser/nsrt/key-of.php diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php new file mode 100644 index 0000000000..caf0a17c87 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -0,0 +1,389 @@ + $items + */ +function foo(array $items) { + assertType('list', $items); + if (count($items) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items) === 0) { + assertType('array{}', $items); + } elseif (count($items) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} + +/** + * @param list $items + */ +function modeCount(array $items, int $mode) { + assertType('list', $items); + if (count($items, $mode) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items, $mode) === 0) { + assertType('array{}', $items); + } elseif (count($items, $mode) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} + +/** + * @param list $items + */ +function modeCountOnMaybeArray(array $items, int $mode) { + assertType('list|int>', $items); + if (count($items, $mode) === 3) { + assertType('non-empty-list|int>', $items); + array_shift($items); + assertType('list|int>', $items); + } elseif (count($items, $mode) === 0) { + assertType('array{}', $items); + } elseif (count($items, $mode) === 5) { + assertType('non-empty-list|int>', $items); + } else { + assertType('non-empty-list|int>', $items); + } + assertType('list|int>', $items); +} + + +/** + * @param list $items + */ +function normalCount(array $items) { + assertType('list', $items); + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items, COUNT_NORMAL) === 0) { + assertType('array{}', $items); + } elseif (count($items, COUNT_NORMAL) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} + +/** + * @param list $items + */ +function recursiveCountOnMaybeArray(array $items):void { + assertType('list|int>', $items); + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('non-empty-list|int>', $items); + array_shift($items); + assertType('list|int>', $items); + } elseif (count($items, COUNT_RECURSIVE) === 0) { + assertType('array{}', $items); + } elseif (count($items, COUNT_RECURSIVE) === 5) { + assertType('non-empty-list|int>', $items); + } else { + assertType('non-empty-list|int>', $items); + } + assertType('list|int>', $items); +} + +/** + * @param list $items + */ +function normalCountOnMaybeArray(array $items):void { + assertType('list|int>', $items); + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{array|int, array|int, array|int}', $items); + array_shift($items); + assertType('array{array|int, array|int}', $items); + } elseif (count($items, COUNT_NORMAL) === 0) { + assertType('array{}', $items); + } elseif (count($items, COUNT_NORMAL) === 5) { + assertType('array{array|int, array|int, array|int, array|int, array|int}', $items); + } else { + assertType('non-empty-list|int>', $items); + } + assertType('list|int>', $items); +} + +class A {} + +/** + * @param list $items + */ +function cannotCountRecursive($items, int $mode) +{ + if (count($items) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } + if (count($items, $mode) === 3) { + assertType('array{ListCount\A, ListCount\A, ListCount\A}', $items); + } +} + +/** + * @param list> $items + */ +function cannotCountRecursiveNestedArray($items, int $mode) +{ + if (count($items) === 3) { + assertType('array{array, array, array}', $items); + } + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{array, array, array}', $items); + } + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('non-empty-list>', $items); + } + if (count($items, $mode) === 3) { + assertType('non-empty-list>', $items); + } +} + +class CountableFoo implements \Countable +{ + public function count(): int + { + return 3; + } +} + +/** + * @param list $items + */ +function cannotCountRecursiveCountable($items, int $mode) +{ + if (count($items) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } + if (count($items, COUNT_NORMAL) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } + if (count($items, COUNT_RECURSIVE) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } + if (count($items, $mode) === 3) { + assertType('array{ListCount\CountableFoo, ListCount\CountableFoo, ListCount\CountableFoo}', $items); + } +} + +function countCountable(CountableFoo $x, int $mode) +{ + if (count($x) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); + + if (count($x, COUNT_NORMAL) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); + + if (count($x, COUNT_RECURSIVE) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); + + if (count($x, $mode) === 3) { + assertType('ListCount\CountableFoo', $x); + } else { + assertType('ListCount\CountableFoo', $x); + } + assertType('ListCount\CountableFoo', $x); +} + +class CountWithOptionalKeys +{ + /** + * @param array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeys($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + } + + /** + * @param array{mixed}|array{0: mixed, 1?: string|null} $row + */ + protected function testOptionalKeysInUnion($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 1) { + assertType('array{mixed}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{mixed, string|null}', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: mixed, 1?: string|null}', $row); + } + } + + /** + * @param array{string}|array{0: int, 1?: string|null} $row + */ + protected function testOptionalKeysInListsOfTaggedUnion($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } + + if (count($row) === 1) { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } else { + assertType('array{0: int, 1?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{int, string|null}', $row); + } else { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 1?: string|null}|array{string}', $row); + } + } + + /** + * @param array{string}|array{0: int, 3?: string|null} $row + */ + protected function testOptionalKeysInUnionArray($row): void + { + if (count($row) === 0) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + + if (count($row) === 1) { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } else { + assertType('array{0: int, 3?: string|null}', $row); + } + + if (count($row) === 2) { + assertType('array{0: int, 3?: string|null}', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + + if (count($row) === 3) { + assertType('*NEVER*', $row); + } else { + assertType('array{0: int, 3?: string|null}|array{string}', $row); + } + } + + /** + * @param array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row + * @param int<2, 3> $twoOrThree + * @param int<2, max> $twoOrMore + * @param int $maxThree + * @param int<10, 11> $tenOrEleven + */ + protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): 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); + } + + 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); + } + + if (count($row) >= $twoOrMore) { + assertType('array{0: int, 1: string|null, 2?: int|null, 3?: float|null}&list', $row); + } else { + assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + } + + if (count($row) >= $maxThree) { + assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + } else { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list', $row); + } + } + + /** + * @param array{string}|array{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row + * @param int<2, 3> $twoOrThree + */ + protected function testOptionalKeysInUnionArrayWithIntRange($row, $twoOrThree): void + { + // doesn't narrow because no list + if (count($row) >= $twoOrThree) { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}|array{string}', $row); + } + } +} diff --git a/tests/PHPStan/Analyser/data/list-shapes.php b/tests/PHPStan/Analyser/nsrt/list-shapes.php similarity index 100% rename from tests/PHPStan/Analyser/data/list-shapes.php rename to tests/PHPStan/Analyser/nsrt/list-shapes.php diff --git a/tests/PHPStan/Analyser/data/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/list-type.php rename to tests/PHPStan/Analyser/nsrt/list-type.php diff --git a/tests/PHPStan/Analyser/data/literal-string.php b/tests/PHPStan/Analyser/nsrt/literal-string.php similarity index 62% rename from tests/PHPStan/Analyser/data/literal-string.php rename to tests/PHPStan/Analyser/nsrt/literal-string.php index 2f7bda2f01..ff63036d9b 100644 --- a/tests/PHPStan/Analyser/data/literal-string.php +++ b/tests/PHPStan/Analyser/nsrt/literal-string.php @@ -7,8 +7,11 @@ class Foo { - /** @param literal-string $literalString */ - public function doFoo($literalString, string $string) + /** + * @param literal-string $literalString + * @param numeric-string $numericString + */ + public function doFoo($literalString, string $string, $numericString) { assertType('literal-string', $literalString); assertType('literal-string', $literalString . ''); @@ -34,6 +37,30 @@ public function doFoo($literalString, string $string) 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)); + // Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ + assertType('non-empty-string', str_repeat($numericString, 100)); + + assertType("''", str_repeat('1.23', 0)); + assertType("''", str_repeat($string, 0)); + assertType("''", str_repeat($numericString, 0)); + + // see https://3v4l.org/U4bM2 + assertType("non-empty-string", str_repeat($numericString, 1)); // could be numeric-string + 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)); + + $x = rand(0,2); + assertType("literal-string", str_repeat('19', $x)); + + $x = rand(-10,-1); + assertType("*NEVER*", str_repeat('19', $x)); assertType("'?,?,?,'", str_repeat('?,', 3)); assertType("*NEVER*", str_repeat('?,', -3)); @@ -57,7 +84,7 @@ public function increment($literalString, string $string) assertType('literal-string', $literalString); $string++; - assertType('string', $string); + assertType('(float|int|string)', $string); } } diff --git a/tests/PHPStan/Analyser/data/loose-comparisons-php7.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php similarity index 97% rename from tests/PHPStan/Analyser/data/loose-comparisons-php7.php rename to tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php index 13b1d288e2..d95fecab11 100644 --- a/tests/PHPStan/Analyser/data/loose-comparisons-php7.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php @@ -1,4 +1,4 @@ -= 8.0 declare(strict_types=1); diff --git a/tests/PHPStan/Analyser/data/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php similarity index 84% rename from tests/PHPStan/Analyser/data/loose-comparisons.php rename to tests/PHPStan/Analyser/nsrt/loose-comparisons.php index 4101a47175..92bec36518 100644 --- a/tests/PHPStan/Analyser/data/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -17,6 +17,7 @@ class HelloWorld * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -31,6 +32,7 @@ public function sayTrue( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -45,6 +47,7 @@ public function sayTrue( assertType('true', $true == $oneStr); assertType('false', $true == $zeroStr); assertType('true', $true == $minusOneStr); + assertType('true', $true == $plusOneStr); assertType('false', $true == $null); assertType('false', $true == $emptyArr); assertType('true', $true == $phpStr); @@ -60,6 +63,7 @@ public function sayTrue( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -74,6 +78,7 @@ public function sayFalse( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -88,6 +93,7 @@ public function sayFalse( assertType('false', $false == $oneStr); assertType('true', $false == $zeroStr); assertType('false', $false == $minusOneStr); + assertType('false', $false == $plusOneStr); assertType('true', $false == $null); assertType('true', $false == $emptyArr); assertType('false', $false == $phpStr); @@ -103,6 +109,7 @@ public function sayFalse( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -117,6 +124,7 @@ public function sayOne( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -131,6 +139,7 @@ public function sayOne( assertType('true', $one == $oneStr); assertType('false', $one == $zeroStr); assertType('false', $one == $minusOneStr); + assertType('true', $one == $plusOneStr); assertType('false', $one == $null); assertType('false', $one == $emptyArr); assertType('false', $one == $phpStr); @@ -146,6 +155,7 @@ public function sayOne( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -160,6 +170,7 @@ public function sayZero( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -174,6 +185,7 @@ public function sayZero( assertType('false', $zero == $oneStr); assertType('true', $zero == $zeroStr); assertType('false', $zero == $minusOneStr); + assertType('false', $zero == $plusOneStr); assertType('true', $zero == $null); assertType('false', $zero == $emptyArr); } @@ -187,6 +199,7 @@ public function sayZero( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -201,6 +214,7 @@ public function sayMinusOne( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -215,6 +229,7 @@ public function sayMinusOne( assertType('false', $minusOne == $oneStr); assertType('false', $minusOne == $zeroStr); assertType('true', $minusOne == $minusOneStr); + assertType('false', $minusOne == $plusOneStr); assertType('false', $minusOne == $null); assertType('false', $minusOne == $emptyArr); assertType('false', $minusOne == $phpStr); @@ -230,6 +245,7 @@ public function sayMinusOne( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -244,6 +260,7 @@ public function sayOneStr( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -258,6 +275,7 @@ public function sayOneStr( assertType('true', $oneStr == $oneStr); assertType('false', $oneStr == $zeroStr); assertType('false', $oneStr == $minusOneStr); + assertType('true', $oneStr == $plusOneStr); assertType('false', $oneStr == $null); assertType('false', $oneStr == $emptyArr); assertType('false', $oneStr == $phpStr); @@ -273,6 +291,7 @@ public function sayOneStr( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -287,6 +306,7 @@ public function sayZeroStr( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -301,6 +321,7 @@ public function sayZeroStr( assertType('false', $zeroStr == $oneStr); assertType('true', $zeroStr == $zeroStr); assertType('false', $zeroStr == $minusOneStr); + assertType('false', $zeroStr == $plusOneStr); assertType('false', $zeroStr == $null); assertType('false', $zeroStr == $emptyArr); assertType('false', $zeroStr == $phpStr); @@ -316,6 +337,7 @@ public function sayZeroStr( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -330,6 +352,7 @@ public function sayMinusOneStr( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -344,6 +367,7 @@ public function sayMinusOneStr( assertType('false', $minusOneStr == $oneStr); assertType('false', $minusOneStr == $zeroStr); assertType('true', $minusOneStr == $minusOneStr); + assertType('false', $minusOneStr == $plusOneStr); assertType('false', $minusOneStr == $null); assertType('false', $minusOneStr == $emptyArr); assertType('false', $minusOneStr == $phpStr); @@ -359,6 +383,53 @@ public function sayMinusOneStr( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr + * @param null $null + * @param array{} $emptyArr + * @param 'php' $phpStr + * @param '' $emptyStr + */ + public function sayPlusOneStr( + $true, + $false, + $one, + $zero, + $minusOne, + $oneStr, + $zeroStr, + $minusOneStr, + $plusOneStr, + $null, + $emptyArr, + $phpStr, + $emptyStr + ): void + { + assertType('true', $plusOneStr == $true); + assertType('false', $plusOneStr == $false); + assertType('true', $plusOneStr == $one); + assertType('false', $plusOneStr == $zero); + assertType('false', $plusOneStr == $minusOne); + assertType('true', $plusOneStr == $oneStr); + assertType('false', $plusOneStr == $zeroStr); + assertType('false', $plusOneStr == $minusOneStr); + assertType('true', $plusOneStr == $plusOneStr); + assertType('false', $plusOneStr == $null); + assertType('false', $plusOneStr == $emptyArr); + assertType('false', $plusOneStr == $phpStr); + assertType('false', $plusOneStr == $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 @@ -373,6 +444,7 @@ public function sayNull( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -387,6 +459,7 @@ public function sayNull( assertType('false', $null == $oneStr); assertType('false', $null == $zeroStr); assertType('false', $null == $minusOneStr); + assertType('false', $null == $plusOneStr); assertType('true', $null == $null); assertType('true', $null == $emptyArr); assertType('false', $null == $phpStr); @@ -402,6 +475,7 @@ public function sayNull( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -416,6 +490,7 @@ public function sayEmptyArray( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -430,6 +505,7 @@ public function sayEmptyArray( assertType('false', $emptyArr == $oneStr); assertType('false', $emptyArr == $zeroStr); assertType('false', $emptyArr == $minusOneStr); + assertType('false', $emptyArr == $plusOneStr); assertType('true', $emptyArr == $null); assertType('true', $emptyArr == $emptyArr); assertType('false', $emptyArr == $phpStr); @@ -445,6 +521,7 @@ public function sayEmptyArray( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -459,6 +536,7 @@ public function sayNonFalsyStr( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -472,6 +550,7 @@ public function sayNonFalsyStr( assertType('false', $phpStr == $oneStr); assertType('false', $phpStr == $zeroStr); assertType('false', $phpStr == $minusOneStr); + assertType('false', $phpStr == $plusOneStr); assertType('false', $phpStr == $null); assertType('false', $phpStr == $emptyArr); assertType('true', $phpStr == $phpStr); @@ -487,6 +566,7 @@ public function sayNonFalsyStr( * @param '1' $oneStr * @param '0' $zeroStr * @param '-1' $minusOneStr + * @param '+1' $plusOneStr * @param null $null * @param array{} $emptyArr * @param 'php' $phpStr @@ -501,6 +581,7 @@ public function sayEmptyStr( $oneStr, $zeroStr, $minusOneStr, + $plusOneStr, $null, $emptyArr, $phpStr, @@ -514,6 +595,7 @@ public function sayEmptyStr( assertType('false', $emptyStr == $oneStr); assertType('false', $emptyStr == $zeroStr); assertType('false', $emptyStr == $minusOneStr); + assertType('false', $emptyStr == $plusOneStr); assertType('true', $emptyStr == $null); assertType('false', $emptyStr == $emptyArr); assertType('false', $emptyStr == $phpStr); diff --git a/tests/PHPStan/Analyser/data/match-expr.php b/tests/PHPStan/Analyser/nsrt/match-expr.php similarity index 92% rename from tests/PHPStan/Analyser/data/match-expr.php rename to tests/PHPStan/Analyser/nsrt/match-expr.php index 68e37bd5bf..40da2c899e 100644 --- a/tests/PHPStan/Analyser/data/match-expr.php +++ b/tests/PHPStan/Analyser/nsrt/match-expr.php @@ -109,6 +109,11 @@ public function doMatch(FinalFoo|FinalBar $class): void FinalFoo::class => assertType(FinalFoo::class, $class), FinalBar::class => assertType(FinalBar::class, $class), }; + + match (get_debug_type($class)) { + FinalFoo::class => assertType(FinalFoo::class, $class), + FinalBar::class => assertType(FinalBar::class, $class), + }; } } diff --git a/tests/PHPStan/Analyser/data/match-expression-inference.php b/tests/PHPStan/Analyser/nsrt/match-expression-inference.php similarity index 100% rename from tests/PHPStan/Analyser/data/match-expression-inference.php rename to tests/PHPStan/Analyser/nsrt/match-expression-inference.php diff --git a/tests/PHPStan/Analyser/data/math.php b/tests/PHPStan/Analyser/nsrt/math.php similarity index 100% rename from tests/PHPStan/Analyser/data/math.php rename to tests/PHPStan/Analyser/nsrt/math.php diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php83.php b/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb-strlen-php83.php rename to tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php index f46d68a3d0..5bd069c873 100644 --- a/tests/PHPStan/Analyser/data/mb-strlen-php83.php +++ b/tests/PHPStan/Analyser/nsrt/mb-strlen-php83.php @@ -1,4 +1,6 @@ -= 8.3 + +declare(strict_types=1); namespace MbStrlenPhp83; diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb_substitute_character-php8.php rename to tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php index b53353bd3a..2933d4ccab 100644 --- a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character-php8.php @@ -1,4 +1,4 @@ -= 8.0 \PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<0, 55295>|int<57344, 1114111>', mb_substitute_character()); \PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('')); diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character.php b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php similarity index 98% rename from tests/PHPStan/Analyser/data/mb_substitute_character.php rename to tests/PHPStan/Analyser/nsrt/mb_substitute_character.php index 9dab962ec5..41a921c9f7 100644 --- a/tests/PHPStan/Analyser/data/mb_substitute_character.php +++ b/tests/PHPStan/Analyser/nsrt/mb_substitute_character.php @@ -1,4 +1,4 @@ -|int<57344, 1114111>', mb_substitute_character()); \PHPStan\Testing\assertType('true', mb_substitute_character('')); diff --git a/tests/PHPStan/Analyser/data/memcache-get.php b/tests/PHPStan/Analyser/nsrt/memcache-get.php similarity index 100% rename from tests/PHPStan/Analyser/data/memcache-get.php rename to tests/PHPStan/Analyser/nsrt/memcache-get.php diff --git a/tests/PHPStan/Analyser/data/minmax-arrays.php b/tests/PHPStan/Analyser/nsrt/minmax-arrays.php similarity index 75% rename from tests/PHPStan/Analyser/data/minmax-arrays.php rename to tests/PHPStan/Analyser/nsrt/minmax-arrays.php index 1a68d50b56..bd94a81fb3 100644 --- a/tests/PHPStan/Analyser/data/minmax-arrays.php +++ b/tests/PHPStan/Analyser/nsrt/minmax-arrays.php @@ -1,4 +1,4 @@ - 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countNormal(array $ints): void +{ + if (count($ints, COUNT_NORMAL) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('false', min($ints)); + assertType('false', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countRecursive(array $ints): void +{ + if (count($ints, COUNT_RECURSIVE) <= 0) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints, COUNT_RECURSIVE) < 1) { + assertType('false', min($ints)); + assertType('false', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} diff --git a/tests/PHPStan/Analyser/data/minmax-php8.php b/tests/PHPStan/Analyser/nsrt/minmax-php8.php similarity index 75% rename from tests/PHPStan/Analyser/data/minmax-php8.php rename to tests/PHPStan/Analyser/nsrt/minmax-php8.php index 3d738d4c38..2c75f363c9 100644 --- a/tests/PHPStan/Analyser/data/minmax-php8.php +++ b/tests/PHPStan/Analyser/nsrt/minmax-php8.php @@ -1,4 +1,4 @@ -= 8.0 namespace MinMaxArraysPhp8; @@ -126,3 +126,52 @@ public function unionType(): void assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); } } + +/** + * @param int[] $ints + */ +function countMode(array $ints, int $mode): void +{ + if (count($ints, $mode) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countNormal(array $ints): void +{ + if (count($ints, COUNT_NORMAL) > 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function countRecursive(array $ints): void +{ + if (count($ints, COUNT_RECURSIVE) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints, COUNT_RECURSIVE) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} diff --git a/tests/PHPStan/Analyser/data/minmax.php b/tests/PHPStan/Analyser/nsrt/minmax.php similarity index 100% rename from tests/PHPStan/Analyser/data/minmax.php rename to tests/PHPStan/Analyser/nsrt/minmax.php diff --git a/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php b/tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php similarity index 100% rename from tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php rename to tests/PHPStan/Analyser/nsrt/missing-closure-native-return-typehint.php diff --git a/tests/PHPStan/Analyser/data/mixed-to-number.php b/tests/PHPStan/Analyser/nsrt/mixed-to-number.php similarity index 100% rename from tests/PHPStan/Analyser/data/mixed-to-number.php rename to tests/PHPStan/Analyser/nsrt/mixed-to-number.php diff --git a/tests/PHPStan/Analyser/data/mixed-typehint.php b/tests/PHPStan/Analyser/nsrt/mixed-typehint.php similarity index 100% rename from tests/PHPStan/Analyser/data/mixed-typehint.php rename to tests/PHPStan/Analyser/nsrt/mixed-typehint.php diff --git a/tests/PHPStan/Analyser/data/model-mixin.php b/tests/PHPStan/Analyser/nsrt/model-mixin.php similarity index 100% rename from tests/PHPStan/Analyser/data/model-mixin.php rename to tests/PHPStan/Analyser/nsrt/model-mixin.php diff --git a/tests/PHPStan/Analyser/data/modulo-operator.php b/tests/PHPStan/Analyser/nsrt/modulo-operator.php similarity index 100% rename from tests/PHPStan/Analyser/data/modulo-operator.php rename to tests/PHPStan/Analyser/nsrt/modulo-operator.php diff --git a/tests/PHPStan/Analyser/data/more-type-strings.php b/tests/PHPStan/Analyser/nsrt/more-type-strings.php similarity index 100% rename from tests/PHPStan/Analyser/data/more-type-strings.php rename to tests/PHPStan/Analyser/nsrt/more-type-strings.php diff --git a/tests/PHPStan/Analyser/data/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php similarity index 95% rename from tests/PHPStan/Analyser/data/more-types.php rename to tests/PHPStan/Analyser/nsrt/more-types.php index 9f646300c9..f1b742f170 100644 --- a/tests/PHPStan/Analyser/data/more-types.php +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -30,7 +30,7 @@ public function doFoo( $nonEmptyMixed ): void { - assertType('callable(): mixed', $pureCallable); + assertType('pure-callable(): mixed', $pureCallable); assertType('array&callable(): mixed', $callableArray); assertType('resource', $closedResource); assertType('resource', $openResource); diff --git a/tests/PHPStan/Analyser/data/multi-assign.php b/tests/PHPStan/Analyser/nsrt/multi-assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/multi-assign.php rename to tests/PHPStan/Analyser/nsrt/multi-assign.php diff --git a/tests/PHPStan/Analyser/data/mysqli-affected-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php similarity index 100% rename from tests/PHPStan/Analyser/data/mysqli-affected-rows.php rename to tests/PHPStan/Analyser/nsrt/mysqli-affected-rows.php diff --git a/tests/PHPStan/Analyser/data/mysqli-result-num-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php similarity index 100% rename from tests/PHPStan/Analyser/data/mysqli-result-num-rows.php rename to tests/PHPStan/Analyser/nsrt/mysqli-result-num-rows.php diff --git a/tests/PHPStan/Analyser/data/mysqli-stmt-affected-rows-and-num-rows.php b/tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php similarity index 100% rename from tests/PHPStan/Analyser/data/mysqli-stmt-affected-rows-and-num-rows.php rename to tests/PHPStan/Analyser/nsrt/mysqli-stmt-affected-rows-and-num-rows.php diff --git a/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php b/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php new file mode 100644 index 0000000000..ceb0c6c78d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/mysqli_fetch_object.php @@ -0,0 +1,16 @@ +fetch_object()); + assertType('MysqliFetchObject\MyClass|false|null', $result->fetch_object(MyClass::class)); + + assertType('stdClass|false|null', mysqli_fetch_object($result)); + assertType('MysqliFetchObject\MyClass|false|null', mysqli_fetch_object($result, MyClass::class)); +} + +class MyClass {} + diff --git a/tests/PHPStan/Analyser/data/named-arguments.php b/tests/PHPStan/Analyser/nsrt/named-arguments.php similarity index 100% rename from tests/PHPStan/Analyser/data/named-arguments.php rename to tests/PHPStan/Analyser/nsrt/named-arguments.php diff --git a/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php new file mode 100644 index 0000000000..8ecf3438e7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/narrow-tagged-union.php @@ -0,0 +1,111 @@ +, numeric-string} $arr */ + public function nestedArrays(array $arr): void + { + // don't narrow when $arr contains recursive arrays + if (count($arr, COUNT_RECURSIVE) === 3) { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + + if (count($arr, COUNT_NORMAL) === 3) { + assertType("array{string, '', non-empty-string}", $arr); + } else { + assertType("array{array, numeric-string}", $arr); + } + assertType("array{array, numeric-string}|array{string, '', non-empty-string}", $arr); + } + + /** @param array{string, '', non-empty-string}|array $arr */ + public function mixedArrays(array $arr): void + { + if (count($arr, COUNT_NORMAL) === 3) { + assertType("non-empty-array", $arr); // could be array{string, '', non-empty-string}|non-empty-array + } else { + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + assertType("array", $arr); // could be array{string, '', non-empty-string}|array + } + + public function arrayIntRangeSize(): void + { + $x = []; + if (rand(0,1)) { + $x[] = 'ab'; + } + if (rand(0,1)) { + $x[] = 'xy'; + } + + if (count($x) === 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} + diff --git a/tests/PHPStan/Analyser/data/native-expressions.php b/tests/PHPStan/Analyser/nsrt/native-expressions.php similarity index 100% rename from tests/PHPStan/Analyser/data/native-expressions.php rename to tests/PHPStan/Analyser/nsrt/native-expressions.php diff --git a/tests/PHPStan/Analyser/data/native-intersection.php b/tests/PHPStan/Analyser/nsrt/native-intersection.php similarity index 100% rename from tests/PHPStan/Analyser/data/native-intersection.php rename to tests/PHPStan/Analyser/nsrt/native-intersection.php diff --git a/tests/PHPStan/Analyser/data/native-reflection-default-values.php b/tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php similarity index 100% rename from tests/PHPStan/Analyser/data/native-reflection-default-values.php rename to tests/PHPStan/Analyser/nsrt/native-reflection-default-values.php diff --git a/tests/PHPStan/Analyser/data/native-types-first-class-callables.php b/tests/PHPStan/Analyser/nsrt/native-types-first-class-callables.php similarity index 92% rename from tests/PHPStan/Analyser/data/native-types-first-class-callables.php rename to tests/PHPStan/Analyser/nsrt/native-types-first-class-callables.php index 4a06c1459d..35b693f916 100644 --- a/tests/PHPStan/Analyser/data/native-types-first-class-callables.php +++ b/tests/PHPStan/Analyser/nsrt/native-types-first-class-callables.php @@ -51,6 +51,10 @@ public function doFoo(): void $i = $h(...); assertType('non-empty-string', $i()); assertNativeType('string', $i()); + + $j = [Foo::class, 'doBar'](...); + assertType('non-empty-string', $j()); + assertNativeType('string', $j()); } } diff --git a/tests/PHPStan/Analyser/data/native-types-ftp-connect-resource.php b/tests/PHPStan/Analyser/nsrt/native-types-ftp-connect-resource.php similarity index 94% rename from tests/PHPStan/Analyser/data/native-types-ftp-connect-resource.php rename to tests/PHPStan/Analyser/nsrt/native-types-ftp-connect-resource.php index 0952898880..a932d93286 100644 --- a/tests/PHPStan/Analyser/data/native-types-ftp-connect-resource.php +++ b/tests/PHPStan/Analyser/nsrt/native-types-ftp-connect-resource.php @@ -1,4 +1,4 @@ -= 8.1 namespace NativeTypesFtpConnect; diff --git a/tests/PHPStan/Analyser/data/native-types.php b/tests/PHPStan/Analyser/nsrt/native-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/native-types.php rename to tests/PHPStan/Analyser/nsrt/native-types.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-incomplete-constructor.php b/tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-incomplete-constructor.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-incomplete-constructor.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types-unwrapping-covariant.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types-unwrapping-covariant.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping-covariant.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types-unwrapping.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types-unwrapping.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types-unwrapping.php diff --git a/tests/PHPStan/Analyser/data/nested-generic-types.php b/tests/PHPStan/Analyser/nsrt/nested-generic-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/nested-generic-types.php rename to tests/PHPStan/Analyser/nsrt/nested-generic-types.php diff --git a/tests/PHPStan/Analyser/data/never.php b/tests/PHPStan/Analyser/nsrt/never.php similarity index 100% rename from tests/PHPStan/Analyser/data/never.php rename to tests/PHPStan/Analyser/nsrt/never.php diff --git a/tests/PHPStan/Analyser/data/new-in-initializers.php b/tests/PHPStan/Analyser/nsrt/new-in-initializers.php similarity index 100% rename from tests/PHPStan/Analyser/data/new-in-initializers.php rename to tests/PHPStan/Analyser/nsrt/new-in-initializers.php diff --git a/tests/PHPStan/Analyser/data/no-named-arguments.php b/tests/PHPStan/Analyser/nsrt/no-named-arguments.php similarity index 100% rename from tests/PHPStan/Analyser/data/no-named-arguments.php rename to tests/PHPStan/Analyser/nsrt/no-named-arguments.php diff --git a/tests/PHPStan/Analyser/data/non-empty-array-key-type.php b/tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php similarity index 100% rename from tests/PHPStan/Analyser/data/non-empty-array-key-type.php rename to tests/PHPStan/Analyser/nsrt/non-empty-array-key-type.php diff --git a/tests/PHPStan/Analyser/data/non-empty-array.php b/tests/PHPStan/Analyser/nsrt/non-empty-array.php similarity index 100% rename from tests/PHPStan/Analyser/data/non-empty-array.php rename to tests/PHPStan/Analyser/nsrt/non-empty-array.php diff --git a/tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php similarity index 100% rename from tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php diff --git a/tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php similarity index 71% rename from tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php index 6b4dce7186..12d3495098 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php @@ -131,6 +131,58 @@ public function variants(string $s) { assertType('non-falsy-string', $s); } assertType('string', $s); + + if (mb_strpos($s, ':') !== false) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + if (mb_strpos($s, ':') === false) { + assertType('string', $s); + } + assertType('string', $s); + + if (mb_strpos($s, ':') === 5) { + assertType('string', $s); // could be non-empty-string + } + assertType('string', $s); + if (mb_strpos($s, ':') !== 5) { + assertType('string', $s); + } + assertType('string', $s); + + if (mb_strrpos($s, ':') !== false) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + + if (mb_stripos($s, ':') !== false) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + + if (mb_strripos($s, ':') !== false) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + + if (mb_strstr($s, ':') === 'hallo') { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + if (mb_strstr($s, ':', true) === 'hallo') { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + if (mb_strstr($s, ':', true) !== false) { + assertType('non-falsy-string', $s); + } + assertType('string', $s); + if (mb_strstr($s, ':', true) === false) { + assertType('string', $s); + } else { + assertType('non-falsy-string', $s); + } + assertType('string', $s); } } diff --git a/tests/PHPStan/Analyser/data/non-empty-string-strcasing-specifying.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-strcasing-specifying.php similarity index 100% rename from tests/PHPStan/Analyser/data/non-empty-string-strcasing-specifying.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-strcasing-specifying.php diff --git a/tests/PHPStan/Analyser/data/non-empty-string-strrchr-specifying.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-strrchr-specifying.php similarity index 100% rename from tests/PHPStan/Analyser/data/non-empty-string-strrchr-specifying.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-strrchr-specifying.php diff --git a/tests/PHPStan/Analyser/data/non-empty-string-strstr-specifying.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-strstr-specifying.php similarity index 89% rename from tests/PHPStan/Analyser/data/non-empty-string-strstr-specifying.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-strstr-specifying.php index d6f51c7903..2b1b2bc0a8 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string-strstr-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-strstr-specifying.php @@ -39,6 +39,10 @@ public function nonEmptyStrstr(string $s, string $needle, bool $before_needle): assertType('non-falsy-string', $s); } assertType('string', $s); + if (mb_strstr($s, $needle, $before_needle) === 'hallo') { + assertType('non-falsy-string', $s); + } + assertType('string', $s); if (strstr($s, $needle, $before_needle) !== 'hallo') { assertType('string', $s); @@ -81,6 +85,10 @@ public function nonEmptyStristr(string $s, string $needle, bool $before_needle): if ('hallo' === stristr($s, 'abc')) { assertType('non-falsy-string', $s); } + assertType('string', $s); + if ('hallo' === mb_stristr($s, 'abc')) { + assertType('non-falsy-string', $s); + } if (stristr($s, $needle, $before_needle) == '') { assertType('string', $s); @@ -107,6 +115,10 @@ public function nonEmptyStrchr(string $s, string $needle, bool $before_needle): if ('hallo' === strchr($s, 'abc')) { assertType('non-falsy-string', $s); } + assertType('string', $s); + if ('hallo' === mb_strchr($s, 'abc')) { + assertType('non-falsy-string', $s); + } if (strchr($s, $needle, $before_needle) == '') { assertType('string', $s); diff --git a/tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-pre-80.php similarity index 97% rename from tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string-substr-pre-80.php index 7300733e12..e1c6f15989 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-pre-80.php @@ -1,4 +1,4 @@ -= 8.0 + +namespace NonEmptyStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param non-empty-string $nonEmpty + * @param positive-int $positiveInt + * @param 1|2|3 $postiveRange + * @param -1|-2|-3 $negativeRange + */ + public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void + { + assertType('string', substr($s, 5)); + + assertType('string', substr($s, -5)); + assertType('non-empty-string', substr($nonEmpty, -5)); + assertType('non-empty-string', substr($nonEmpty, $negativeRange)); + + assertType('string', substr($s, 0, 5)); + assertType('non-empty-string', substr($nonEmpty, 0, 5)); + assertType('non-empty-string', substr($nonEmpty, 0, $postiveRange)); + + assertType('string', substr($nonEmpty, 0, -5)); + + assertType('string', substr($s, 0, $positiveInt)); + assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt)); + } + + /** + * @param non-empty-string $nonEmpty + * @param positive-int $positiveInt + * @param 1|2|3 $postiveRange + * @param -1|-2|-3 $negativeRange + */ + public function doMbSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void + { + assertType('string', mb_substr($s, 5)); + + assertType('string', mb_substr($s, -5)); + assertType('non-empty-string', mb_substr($nonEmpty, -5)); + assertType('non-empty-string', mb_substr($nonEmpty, $negativeRange)); + + assertType('string', mb_substr($s, 0, 5)); + assertType('non-empty-string', mb_substr($nonEmpty, 0, 5)); + assertType('non-empty-string', mb_substr($nonEmpty, 0, $postiveRange)); + + assertType('string', mb_substr($nonEmpty, 0, -5)); + + assertType('string', mb_substr($s, 0, $positiveInt)); + assertType('non-empty-string', mb_substr($nonEmpty, 0, $positiveInt)); + + assertType('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/data/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php similarity index 83% rename from tests/PHPStan/Analyser/data/non-empty-string.php rename to tests/PHPStan/Analyser/nsrt/non-empty-string.php index 60a7b9ec93..976071cb84 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -303,9 +303,10 @@ class MoreNonEmptyStringFunctions /** * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy * @param '1'|'2'|'5'|'10' $constUnion */ - public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUnion) + public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, bool $bool, $constUnion) { assertType('string', addslashes($s)); assertType('non-empty-string', addslashes($nonEmpty)); @@ -349,9 +350,24 @@ public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUni assertType('non-empty-string', preg_quote($nonEmpty)); assertType('string', sprintf($s)); - assertType('non-empty-string', sprintf($nonEmpty)); + assertType('string', sprintf($nonEmpty)); + assertType('string', sprintf($s, $nonEmpty)); + assertType('string', sprintf($nonEmpty, $s)); + assertType('string', sprintf($s, $nonFalsy)); + assertType('string', sprintf($nonFalsy, $s)); + assertType('non-empty-string', sprintf($nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonEmpty, $nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonEmpty, $nonFalsy, $nonFalsy)); + assertType('non-empty-string', sprintf($nonFalsy, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonEmpty, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonEmpty)); + assertType('non-empty-string', sprintf($nonFalsy, $nonFalsy, $nonFalsy)); assertType('string', vsprintf($s, [])); - assertType('non-empty-string', vsprintf($nonEmpty, [])); + assertType('string', vsprintf($nonEmpty, [])); + + assertType('non-empty-string', sprintf("%s0%s", $s, $s)); + assertType('non-empty-string', sprintf("%s0%s%s%s%s", $s, $s, $s, $s, $s)); + assertType('non-empty-string', sprintf("%s0%s%s%s%s%s", $s, $s, $s, $s, $s, $s)); assertType('0', strlen('')); assertType('5', strlen('hallo')); @@ -374,4 +390,22 @@ public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUni assertType('string', str_repeat($s, $i)); } + function multiplesPrintfFormats(string $s) { + $maybeNonEmpty = '%s'; + $maybeNonFalsy = '%s'; + $nonEmpty = '%s0'; + $nonFalsy = '%sAA'; + + if (rand(0,1)) { + $maybeNonEmpty = '%s0'; + $maybeNonFalsy = '%sAA'; + $nonEmpty = '0%s'; + $nonFalsy = 'AA%s'; + } + + assertType('string', sprintf($maybeNonEmpty, $s)); + assertType('string', sprintf($maybeNonFalsy, $s)); + assertType('non-empty-string', sprintf($nonEmpty, $s)); + assertType('non-falsy-string', sprintf($nonFalsy, $s)); + } } diff --git a/tests/PHPStan/Analyser/data/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php similarity index 77% rename from tests/PHPStan/Analyser/data/non-falsy-string.php rename to tests/PHPStan/Analyser/nsrt/non-falsy-string.php index c78bb0daa8..f8c6dc40b7 100644 --- a/tests/PHPStan/Analyser/data/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -10,7 +10,7 @@ class Foo { * @param truthy-string $truthyString */ public function bar($nonFalseyString, $truthyString) { - assertType('int|int<1, max>', (int) $nonFalseyString); + assertType('int', (int) $nonFalseyString); // truthy-string is an alias for non-falsy-string assertType('non-falsy-string', $truthyString); } @@ -74,7 +74,7 @@ function concat(string $s, string $nonFalsey, $numericS, $nonEmpty, $literalStri * @param non-empty-array $arrayOfNonFalsey * @param non-empty-array $nonEmptyArray */ - function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArray) + function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArray, array $arr) { assertType('string', implode($nonFalsey, [])); assertType('non-falsy-string', implode($nonFalsey, $nonEmptyArray)); @@ -104,8 +104,25 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', preg_quote($nonFalsey)); - assertType('non-falsy-string', sprintf($nonFalsey)); - assertType('non-falsy-string', vsprintf($nonFalsey, [])); + assertType('string', sprintf($nonFalsey)); + assertType("'foo'", sprintf('foo')); + assertType("string", sprintf(...$arr)); + assertType("string", sprintf('%s', ...$arr)); + + // empty array only works as long as no placeholder in the pattern + assertType('string', vsprintf($nonFalsey, [])); + assertType('string', vsprintf($nonFalsey, [])); + assertType("string", vsprintf('foo', [])); + + assertType("string", vsprintf('%s', ...$arr)); + assertType("string", vsprintf(...$arr)); + assertType('non-falsy-string', vsprintf('%sAA%s', [$s, $s])); + assertType('non-falsy-string', vsprintf('%d%d', [$s, $s])); // could be non-falsy-string&numeric-string + + assertType('non-falsy-string', sprintf("%sAA%s", $s, $s)); + assertType('non-falsy-string', sprintf("%d%d", $s, $s)); // could be non-falsy-string&numeric-string + assertType('non-falsy-string', sprintf("%sAA%s%s%s%s", $s, $s, $s, $s, $s)); + assertType('non-falsy-string', sprintf("%sAA%s%s%s%s%s", $s, $s, $s, $s, $s, $s)); assertType('int<1, max>', strlen($nonFalsey)); @@ -126,9 +143,9 @@ public function doSubstr($nonFalsey, $positiveInt, $postiveRange, $negativeRange assertType('non-falsy-string', substr($nonFalsey, $negativeRange)); assertType('non-falsy-string', substr($nonFalsey, 0, 5)); - assertType('non-falsy-string', substr($nonFalsey, 0, $postiveRange)); + assertType('non-empty-string', substr($nonFalsey, 0, $postiveRange)); - assertType('non-falsy-string', substr($nonFalsey, 0, $positiveInt)); + assertType('non-empty-string', substr($nonFalsey, 0, $positiveInt)); } function numericIntoFalsy(string $s): void diff --git a/tests/PHPStan/Analyser/data/nullable-closure-parameter.php b/tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php similarity index 100% rename from tests/PHPStan/Analyser/data/nullable-closure-parameter.php rename to tests/PHPStan/Analyser/nsrt/nullable-closure-parameter.php diff --git a/tests/PHPStan/Analyser/data/nullsafe-vs-scalar.php b/tests/PHPStan/Analyser/nsrt/nullsafe-vs-scalar.php similarity index 100% rename from tests/PHPStan/Analyser/data/nullsafe-vs-scalar.php rename to tests/PHPStan/Analyser/nsrt/nullsafe-vs-scalar.php diff --git a/tests/PHPStan/Analyser/data/nullsafe.php b/tests/PHPStan/Analyser/nsrt/nullsafe.php similarity index 100% rename from tests/PHPStan/Analyser/data/nullsafe.php rename to tests/PHPStan/Analyser/nsrt/nullsafe.php diff --git a/tests/PHPStan/Analyser/data/number_format.php b/tests/PHPStan/Analyser/nsrt/number_format.php similarity index 100% rename from tests/PHPStan/Analyser/data/number_format.php rename to tests/PHPStan/Analyser/nsrt/number_format.php diff --git a/tests/PHPStan/Analyser/data/object-shape.php b/tests/PHPStan/Analyser/nsrt/object-shape.php similarity index 100% rename from tests/PHPStan/Analyser/data/object-shape.php rename to tests/PHPStan/Analyser/nsrt/object-shape.php diff --git a/tests/PHPStan/Analyser/data/offset-access.php b/tests/PHPStan/Analyser/nsrt/offset-access.php similarity index 100% rename from tests/PHPStan/Analyser/data/offset-access.php rename to tests/PHPStan/Analyser/nsrt/offset-access.php diff --git a/tests/PHPStan/Analyser/data/offset-value-after-assign.php b/tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/offset-value-after-assign.php rename to tests/PHPStan/Analyser/nsrt/offset-value-after-assign.php diff --git a/tests/PHPStan/Analyser/data/override-root-scope-variable.php b/tests/PHPStan/Analyser/nsrt/override-root-scope-variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/override-root-scope-variable.php rename to tests/PHPStan/Analyser/nsrt/override-root-scope-variable.php diff --git a/tests/PHPStan/Analyser/nsrt/param-closure-this.php b/tests/PHPStan/Analyser/nsrt/param-closure-this.php new file mode 100644 index 0000000000..1801bbef2b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/param-closure-this.php @@ -0,0 +1,318 @@ += 7.4 + +namespace ParamClosureThis; + +use function PHPStan\Testing\assertType; +use function sprintf; + +interface Some +{ + + public function voidMethod(): void; + +} + +class Foo +{ + + public ?string $prop = null; + + /** + * @param-closure-this Some $cb + */ + public function paramClosureClass(callable $cb) + { + + } + + /** + * @param-closure-this self $cb + */ + public function paramClosureSelf(callable $cb) + { + + } + + /** + * @param-closure-this Some $cb + * @param-immediately-invoked-callable $cb + */ + public function paramClosureClassImmediatelyCalled(callable $cb) + { + + } + + /** + * @param-closure-this static $cb + */ + public function paramClosureStatic(callable $cb) + { + + } + + /** + * @param-closure-this ($i is 1 ? Foo : Some) $cb + */ + public function paramClosureConditional(int $i, callable $cb) + { + + } + + /** + * @template T of object + * @param class-string $class + * @param-closure-this T $cb + */ + public function paramClosureGenerics(string $class, callable $cb): void + { + + } + + public function voidMethod(): void + { + + } + + public function doFoo(): void + { + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + + public function doFoo2(): void + { + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(fn () => assertType(Some::class, $this)); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static fn () => assertType('*ERROR*', $this)); + assertType(sprintf('$this(%s)', self::class), $this); + } + + public function doFoo3(): void + { + $a = 1; + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(function () use (&$a) { + assertType(Some::class, $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static function () use (&$a) { + assertType('*ERROR*', $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + } + + public function interplayWithProcessImmediatelyCalledCallable(): void + { + assert($this->prop !== null); + assertType('string', $this->prop); + $this->paramClosureClassImmediatelyCalled(function () { + // $this is Some, not Foo + $this->voidMethod(); + }); + + // keep the narrowed type + assertType('string', $this->prop); + } + + public function interplayWithProcessImmediatelyCalledCallable2(): void + { + $s = new self(); + assert($s->prop !== null); + assertType('string', $s->prop); + $this->paramClosureClassImmediatelyCalled(function () use ($s) { + // $this is Some, not Foo + $this->voidMethod(); + + // but still invalidate $s + $s->voidMethod(); + }); + assertType('string|null', $s->prop); + } + +} + +function (Foo $f): void { + assertType('*ERROR*', $this); + $f->paramClosureClass(function () { + assertType(Some::class, $this); + }); + assertType('*ERROR*', $this); + $f->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + assertType('*ERROR*', $this); +}; + +function (Foo $f): void { + $a = 1; + assertType('*ERROR*', $this); + $f->paramClosureClass(function () use (&$a) { + assertType(Some::class, $this); + }); + assertType('*ERROR*', $this); + $f->paramClosureClass(static function () use (&$a) { + assertType('*ERROR*', $this); + }); + assertType('*ERROR*', $this); +}; + +function (Foo $f): void { + assertType('*ERROR*', $this); + $f->paramClosureClass(fn () => assertType(Some::class, $this)); + assertType('*ERROR*', $this); + $f->paramClosureClass(static fn () => assertType('*ERROR*', $this)); + assertType('*ERROR*', $this); +}; + +class Bar extends Foo +{ + + public function testClosureStatic(): void + { + assertType('$this(ParamClosureThis\Bar)', $this); + $this->paramClosureStatic(function () { + assertType('static(ParamClosureThis\Bar)', $this); + }); + assertType('$this(ParamClosureThis\Bar)', $this); + } + +} + +function (Bar $b): void { + $b->paramClosureStatic(function () { + assertType(Bar::class, $this); + }); +}; + +class ImplicitInheritance extends Foo +{ + + public function paramClosureClass(callable $cb) + { + + } + + public function paramClosureSelf(callable $cb) + { + + } + + public function paramClosureStatic(callable $cb) + { + + } + + public function paramClosureConditional(int $j, callable $ca) + { + // renamed parameter names + } + + public function doFoo(): void + { + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureStatic(function () use (&$a) { + assertType('static(ParamClosureThis\ImplicitInheritance)', $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + +} + +class ImplicitInheritanceMoreComplicated extends Foo +{ + + /** + * @param callable $cb + */ + public function paramClosureClass(callable $cb) + { + + } + + /** + * @param callable $cb + */ + public function paramClosureSelf(callable $cb) + { + + } + + /** + * @param callable $cb + */ + public function paramClosureStatic(callable $cb) + { + + } + + /** + * @param callable $ca + */ + public function paramClosureConditional(int $j, callable $ca) + { + // renamed parameter names + } + + public function doFoo(): void + { + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureStatic(function () use (&$a) { + assertType('static(ParamClosureThis\ImplicitInheritanceMoreComplicated)', $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/param-out-default.php b/tests/PHPStan/Analyser/nsrt/param-out-default.php new file mode 100644 index 0000000000..eed6b94c6f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/param-out-default.php @@ -0,0 +1,36 @@ + : array) $out + */ + public function doFoo(&$out, $flags = 1): void + { + + } + + public function doBar(): void + { + $this->doFoo($a); + assertType('array', $a); + + $this->doFoo($b, 1); + assertType('array', $b); + + $this->doFoo($c, 2); + assertType('array', $c); + } + + public function sayHello(string $row): void + { + preg_match_all('#// error:(.+)#', $row, $matches); + assertType('array{list, list}', $matches); + } + +} diff --git a/tests/PHPStan/Analyser/data/pathinfo-php8.php b/tests/PHPStan/Analyser/nsrt/pathinfo-php8.php similarity index 100% rename from tests/PHPStan/Analyser/data/pathinfo-php8.php rename to tests/PHPStan/Analyser/nsrt/pathinfo-php8.php diff --git a/tests/PHPStan/Analyser/data/pathinfo.php b/tests/PHPStan/Analyser/nsrt/pathinfo.php similarity index 53% rename from tests/PHPStan/Analyser/data/pathinfo.php rename to tests/PHPStan/Analyser/nsrt/pathinfo.php index e7972282fb..b58886e6d1 100644 --- a/tests/PHPStan/Analyser/data/pathinfo.php +++ b/tests/PHPStan/Analyser/nsrt/pathinfo.php @@ -4,7 +4,10 @@ use function PHPStan\Testing\assertType; -function doFoo(string $s, int $i) { +/** + * @param PATHINFO_BASENAME|PATHINFO_EXTENSION $flag + */ +function doFoo(string $s, int $i, $flag) { assertType('array{dirname?: string, basename: string, extension?: string, filename: string}|string', pathinfo($s, $i)); assertType('array{dirname?: string, basename: string, extension?: string, filename: string}', pathinfo($s)); @@ -12,4 +15,12 @@ function doFoo(string $s, int $i) { assertType('string', pathinfo($s, PATHINFO_BASENAME)); assertType('string', pathinfo($s, PATHINFO_EXTENSION)); assertType('string', pathinfo($s, PATHINFO_FILENAME)); + + assertType('string', pathinfo($s, $flag)); + if ($i === PATHINFO_ALL) { + assertType('array{dirname?: string, basename: string, extension?: string, filename: string}', pathinfo($s, $i)); + } + if ($i === PATHINFO_ALL || $i === PATHINFO_DIRNAME) { + assertType('array{dirname?: string, basename: string, extension?: string, filename: string}|string', pathinfo($s, $i)); + } } diff --git a/tests/PHPStan/Analyser/data/pdo-prepare.php b/tests/PHPStan/Analyser/nsrt/pdo-prepare.php similarity index 100% rename from tests/PHPStan/Analyser/data/pdo-prepare.php rename to tests/PHPStan/Analyser/nsrt/pdo-prepare.php diff --git a/tests/PHPStan/Analyser/data/phpdoc-in-closure-bind.php b/tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpdoc-in-closure-bind.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-in-closure-bind.php diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-global.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-global.php diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-namespace.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-namespace.php diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-override.php b/tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-override.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpdoc-pseudotype-override.php rename to tests/PHPStan/Analyser/nsrt/phpdoc-pseudotype-override.php diff --git a/tests/PHPStan/Analyser/data/phpunit-integration.php b/tests/PHPStan/Analyser/nsrt/phpunit-integration.php similarity index 100% rename from tests/PHPStan/Analyser/data/phpunit-integration.php rename to tests/PHPStan/Analyser/nsrt/phpunit-integration.php diff --git a/tests/PHPStan/Analyser/data/pow.php b/tests/PHPStan/Analyser/nsrt/pow.php similarity index 91% rename from tests/PHPStan/Analyser/data/pow.php rename to tests/PHPStan/Analyser/nsrt/pow.php index 82f03b1bae..328de3f172 100644 --- a/tests/PHPStan/Analyser/data/pow.php +++ b/tests/PHPStan/Analyser/nsrt/pow.php @@ -177,3 +177,23 @@ function doFoo(int $intA, int $intB, string $s, bool $bool, $numericS, float $fl assertType('*ERROR*', $bool ** $arr); assertType('*ERROR*', $bool ** []); }; + +function invalidConstantOperands(): void { + assertType('*ERROR*', 'a' ** 1); + assertType('*ERROR*', 1 ** 'a'); + + assertType('*ERROR*', [] ** 1); + assertType('*ERROR*', 1 ** []); + + assertType('*ERROR*', (new \stdClass()) ** 1); + assertType('*ERROR*', 1 ** (new \stdClass())); +} + +function validConstantOperands(): void { + assertType('1', '1' ** 1); + assertType('1', 1 ** '1'); + assertType('1', '1' ** '1'); + + assertType('1', true ** 1); + assertType('1', 1 ** false); +} diff --git a/tests/PHPStan/Analyser/data/pr-1244.php b/tests/PHPStan/Analyser/nsrt/pr-1244.php similarity index 100% rename from tests/PHPStan/Analyser/data/pr-1244.php rename to tests/PHPStan/Analyser/nsrt/pr-1244.php diff --git a/tests/PHPStan/Analyser/data/predefined-constants-php72.php b/tests/PHPStan/Analyser/nsrt/predefined-constants-php72.php similarity index 100% rename from tests/PHPStan/Analyser/data/predefined-constants-php72.php rename to tests/PHPStan/Analyser/nsrt/predefined-constants-php72.php diff --git a/tests/PHPStan/Analyser/data/predefined-constants-php74.php b/tests/PHPStan/Analyser/nsrt/predefined-constants-php74.php similarity index 90% rename from tests/PHPStan/Analyser/data/predefined-constants-php74.php rename to tests/PHPStan/Analyser/nsrt/predefined-constants-php74.php index e0b7f0d156..bc1598d297 100644 --- a/tests/PHPStan/Analyser/data/predefined-constants-php74.php +++ b/tests/PHPStan/Analyser/nsrt/predefined-constants-php74.php @@ -1,4 +1,4 @@ -= 7.4 use function PHPStan\Testing\assertType; diff --git a/tests/PHPStan/Analyser/data/predefined-constants.php b/tests/PHPStan/Analyser/nsrt/predefined-constants.php similarity index 70% rename from tests/PHPStan/Analyser/data/predefined-constants.php rename to tests/PHPStan/Analyser/nsrt/predefined-constants.php index a1f057236c..ed7d7a8577 100644 --- a/tests/PHPStan/Analyser/data/predefined-constants.php +++ b/tests/PHPStan/Analyser/nsrt/predefined-constants.php @@ -3,7 +3,7 @@ use function PHPStan\Testing\assertType; // core, https://www.php.net/manual/en/reserved.constants.php -assertType('non-empty-string', PHP_VERSION); +assertType('non-falsy-string', PHP_VERSION); assertType('int<5, max>', PHP_MAJOR_VERSION); assertType('int<0, max>', PHP_MINOR_VERSION); assertType('int<0, max>', PHP_RELEASE_VERSION); @@ -12,23 +12,23 @@ assertType('0|1', PHP_ZTS); assertType('0|1', PHP_DEBUG); assertType('int<1, max>', PHP_MAXPATHLEN); -assertType('non-empty-string', PHP_OS); -assertType('\'apache\'|\'apache2handler\'|\'cgi\'|\'cli\'|\'cli-server\'|\'embed\'|\'fpm-fcgi\'|\'litespeed\'|\'phpdbg\'|non-empty-string', PHP_SAPI); +assertType('non-falsy-string', PHP_OS); +assertType('\'apache\'|\'apache2handler\'|\'cgi\'|\'cli\'|\'cli-server\'|\'embed\'|\'fpm-fcgi\'|\'litespeed\'|\'phpdbg\'|non-falsy-string', PHP_SAPI); assertType('"\n"|"\r\n"', PHP_EOL); assertType('4|8', PHP_INT_SIZE); assertType('string', DEFAULT_INCLUDE_PATH); assertType('string', PEAR_INSTALL_DIR); assertType('string', PEAR_EXTENSION_DIR); -assertType('non-empty-string', PHP_EXTENSION_DIR); -assertType('non-empty-string', PHP_PREFIX); -assertType('non-empty-string', PHP_BINDIR); -assertType('non-empty-string', PHP_BINARY); -assertType('non-empty-string', PHP_MANDIR); -assertType('non-empty-string', PHP_LIBDIR); -assertType('non-empty-string', PHP_DATADIR); -assertType('non-empty-string', PHP_SYSCONFDIR); -assertType('non-empty-string', PHP_LOCALSTATEDIR); -assertType('non-empty-string', PHP_CONFIG_FILE_PATH); +assertType('non-falsy-string', PHP_EXTENSION_DIR); +assertType('non-falsy-string', PHP_PREFIX); +assertType('non-falsy-string', PHP_BINDIR); +assertType('non-falsy-string', PHP_BINARY); +assertType('non-falsy-string', PHP_MANDIR); +assertType('non-falsy-string', PHP_LIBDIR); +assertType('non-falsy-string', PHP_DATADIR); +assertType('non-falsy-string', PHP_SYSCONFDIR); +assertType('non-falsy-string', PHP_LOCALSTATEDIR); +assertType('non-falsy-string', PHP_CONFIG_FILE_PATH); assertType('string', PHP_CONFIG_FILE_SCAN_DIR); assertType('\'dll\'|\'so\'', PHP_SHLIB_SUFFIX); assertType('1', E_ERROR); @@ -47,7 +47,7 @@ assertType('16384', E_USER_DEPRECATED); assertType('32767', E_ALL); assertType('2048', E_STRICT); -assertType('int<0, max>', __COMPILER_HALT_OFFSET__); +assertType('int<1, max>', __COMPILER_HALT_OFFSET__); assertType('true', true); assertType('false', false); assertType('null', null); @@ -62,15 +62,18 @@ assertType('\':\'|\';\'', PATH_SEPARATOR); // iconv, https://www.php.net/manual/en/iconv.constants.php -assertType('non-empty-string', ICONV_IMPL); +assertType('non-falsy-string', ICONV_IMPL); // libxml, https://www.php.net/manual/en/libxml.constants.php assertType('int<1, max>', LIBXML_VERSION); -assertType('non-empty-string', LIBXML_DOTTED_VERSION); +assertType('non-falsy-string', LIBXML_DOTTED_VERSION); // openssl, https://www.php.net/manual/en/openssl.constants.php assertType('int<1, max>', OPENSSL_VERSION_NUMBER); +// pcre, https://www.php.net/manual/en/pcre.constants.php +assertType('non-falsy-string', PCRE_VERSION); + // other assertType('bool', ZEND_DEBUG_BUILD); assertType('bool', ZEND_THREAD_SAFE); diff --git a/tests/PHPStan/Analyser/data/preg_filter.php b/tests/PHPStan/Analyser/nsrt/preg_filter.php similarity index 100% rename from tests/PHPStan/Analyser/data/preg_filter.php rename to tests/PHPStan/Analyser/nsrt/preg_filter.php diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php new file mode 100644 index 0000000000..c415fca42f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -0,0 +1,177 @@ += 7.2 + +namespace PregMatchAllShapes; + +use function PHPStan\Testing\assertType; + +function (string $size): void { + preg_match_all('/ab(\d+)?/', $size, $matches, PREG_UNMATCHED_AS_NULL); + assertType('array{list, list}', $matches); +}; + +function (string $size): void { + preg_match_all('/ab(?P\d+)?/', $size, $matches); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + preg_match_all('/ab(\d+)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER); + assertType('array{list, list}', $matches); +}; + +function (string $size): void { + preg_match_all('/ab(?P\d+)?/', $size, $matches, PREG_PATTERN_ORDER); + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches)) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) > 0) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) != false) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) == true) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{}", $matches); + } + assertType("array{}|array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)?/', $size, $matches) === 1) { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } else { + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); + } + assertType("array{0: list, num: list<''|numeric-string>, 1: list<''|numeric-string>}", $matches); +}; + +function (string $size): void { + preg_match_all('/a(b)(\d+)?/', $size, $matches, PREG_SET_ORDER); + assertType("list", $matches); +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches)) { + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER)) { + assertType("list", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER)) { + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER)) { + assertType("list", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER)) { + assertType("array{0: list, num: list, 1: list, suffix: list<'ab'|null>, 2: list<'ab'|null>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); + } +}; + +function (string $size): void { + if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { + assertType("array{0: list}>, num: list}>, 1: list}>, suffix: list}>, 2: list}>}", $matches); + } +}; + +class Bug11457 +{ + public function sayHello(string $content): void + { + if (preg_match_all("~text=~mU", $content, $matches, PREG_OFFSET_CAPTURE) === 0) { + return; + } + + assertType('array{list}>}', $matches); + } + + public function sayFoo(string $content): void + { + if (preg_match_all("~text=~mU", $content, $matches, PREG_SET_ORDER) === 0) { + return; + } + + assertType('list', $matches); + } + + public function sayBar(string $content): void + { + if (preg_match_all("~text=~mU", $content, $matches, PREG_PATTERN_ORDER) === 0) { + return; + } + + assertType('array{list}', $matches); + } + + function doFoobar(string $s): void { + if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{list}>, list}>, list}>, list}>}", $matches); + } + } + + function doFoobarNull(string $s): void { + if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { + assertType("array{list}>, list}>, list}>, list}>}", $matches); + } + } +} diff --git a/tests/PHPStan/Analyser/data/preg_match_php7.php b/tests/PHPStan/Analyser/nsrt/preg_match_php7.php similarity index 91% rename from tests/PHPStan/Analyser/data/preg_match_php7.php rename to tests/PHPStan/Analyser/nsrt/preg_match_php7.php index 306a77e80a..0d4887ffea 100644 --- a/tests/PHPStan/Analyser/data/preg_match_php7.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_php7.php @@ -1,4 +1,4 @@ -= 8.0 namespace PregMatchPhp8; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php new file mode 100644 index 0000000000..001ec26d21 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -0,0 +1,718 @@ += 7.2 + +namespace PregMatchShapes; + +use function PHPStan\Testing\assertType; +use InvalidArgumentException; + +function doMatch(string $s): void { + if (preg_match('/Price: /i', $s, $matches)) { + assertType('array{string}', $matches); + } + assertType('array{}|array{string}', $matches); + + if (preg_match('/Price: (£|€)\d+/', $s, $matches)) { + assertType('array{string, non-empty-string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, non-empty-string}', $matches); + + if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { + assertType('array{string, non-empty-string, numeric-string}', $matches); + } + assertType('array{}|array{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{}|array{string, non-empty-string}', $matches); + + if (preg_match('(Price: (£|€))i', $s, $matches)) { + assertType('array{string, non-empty-string}', $matches); + } + assertType('array{}|array{string, non-empty-string}', $matches); + + if (preg_match('_foo(.)\_i_i', $s, $matches)) { + assertType('array{string, non-empty-string}', $matches); + } + assertType('array{}|array{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{}|array{0: 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{}|array{0: 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{}|array{0: 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{}|array{0: string, 1?: non-empty-string}', $matches); + + if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) { + assertType("array{string, 'foo', 'bar', non-falsy-string}", $matches); + } + assertType("array{}|array{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{}|array{0: 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{}|array{0: 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{}|array{0: 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{}|array{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{}|array{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{}|array{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{}|array{0: 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{}|array{0: 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{}|array{0: 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{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); +} + +function doUnknownFlags(string $s, int $flags): void { + if (preg_match('/(foo)(bar)(baz)/xyz', $s, $matches, $flags)) { + assertType('array}|string|null>', $matches); + } + assertType('array}|string|null>', $matches); +} + +function doMultipleAlternativeCaptureGroupsWithSameNameWithModifier(string $s): void { + if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { + assertType("array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: 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); +} + +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{}|array{0: string, Foo: non-empty-string, 1: non-empty-string}|array{0: 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{}|array{string, non-empty-string}', $matches); + + if (preg_match('/\w-(\d+)-(\w)/', $s, $matches)) { + assertType('array{string, numeric-string, non-empty-string}', $matches); + } + assertType('array{}|array{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{}|array{string}', $matches); +} + +function testPregMatchSimpleCondition(string $value): void { + if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { + assertType('array{string, string}', $matches); + } +} + + +function testPregMatchIdenticalToOne(string $value): void { + if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) === 1) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchIdenticalToOneFalseyContext(string $value): void { + if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) !== 1)) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchIdenticalToOneInverted(string $value): void { + if (1 === preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchIdenticalToOneFalseyContextInverted(string $value): void { + if (!(1 !== preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchEqualToOne(string $value): void { + if (preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) == 1) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchEqualToOneFalseyContext(string $value): void { + if (!(preg_match('/%env\((.*)\:.*\)%/U', $value, $matches) != 1)) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchEqualToOneInverted(string $value): void { + if (1 == preg_match('/%env\((.*)\:.*\)%/U', $value, $matches)) { + assertType('array{string, string}', $matches); + } +} + +function testPregMatchEqualToOneFalseyContextInverted(string $value): void { + if (!(1 != preg_match('/%env\((.*)\:.*\)%/U', $value, $matches))) { + assertType('array{string, string}', $matches); + } +} + +function testUnionPattern(string $s): void +{ + if (rand(0,1)) { + $pattern = '/Price: (\d+)/i'; + } else { + $pattern = '/Price: (\d+)(\d+)(\d+)/'; + } + if (preg_match($pattern, $s, $matches)) { + assertType('array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); + } + assertType('array{}|array{string, numeric-string, numeric-string, numeric-string}|array{string, numeric-string}', $matches); +} + +function doFoo(string $row): void +{ + if (preg_match('~^(a(b))$~', $row, $matches) === 1) { + assertType("array{string, 'ab', 'b'}", $matches); + } + if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) { + assertType("array{0: string, 1: non-falsy-string, 2?: 'b'}", $matches); + } + if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) { + assertType("array{0: string, 1?: non-falsy-string, 2?: 'b'}", $matches); + } +} + +function doFoo2(string $row): void +{ + if (preg_match('~^((?\\d{1,6})-)?(?\\d{1,10})/(?\\d{4})$~', $row, $matches) !== 1) { + return; + } + + assertType("array{0: string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); +} + +function doFoo3(string $row): void +{ + if (preg_match('~^(02,([\d.]{10}),(\d+),(\d+),(\d+),)(\d+)$~', $row, $matches) !== 1) { + return; + } + + assertType('array{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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +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); +}; + +function (string $size): void { + if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))?$~', $size, $matches) !== 1) { + throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); + } + assertType("array{string, '', '', '', numeric-string}|array{string, '', '', numeric-string}|array{string, numeric-string, numeric-string}|array{string}", $matches); +}; + +function (string $size): void { + if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?£|€)\d+/', $s, $matches)) { + assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $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); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $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); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches); +} + +function (string $s): void { + if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) { + assertType("array{string, 'z'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{string, 'z'}", $matches); +}; + +function (string $s): void { + if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) { + assertType("array{string, 'z'}", $matches); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{string, 'z'}", $matches); +}; + +function (string $s): void { + if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { + assertType('array{string, numeric-string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{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); + } else { + assertType('array{}', $matches); + } + assertType("array{}|array{0: string, 1: 'z', 2?: 'def'}", $matches); +}; + +function (string $s, $mixed): void { + if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)'. $mixed .'(def)?}', $s, $matches)) { + assertType('array', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array', $matches); +}; + +function (string $s): void { + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) { + assertType("array{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); + } +}; + +function (string $s): void { + if (preg_match('~^((\\d{1,6}).)$~', $s, $matches) === 1) { + assertType("array{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); + } +}; + +function (string $s): void { + if (preg_match('~^([157XY])$~', $s, $matches) === 1) { + assertType("array{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); + } + if (preg_match('/\p{L}[[\]]+([-*|+?{}(?:)]+)([^*|+[:digit:]?{a-z}(\p{L})\a-]+)/', $s, $matches)) { + assertType('array{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); + } + 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); + } + if (preg_match('{([]] [^]])}', $s, $matches)) { + assertType('array{string, non-falsy-string}', $matches); + } + if (preg_match('{([[:digit:]])}', $s, $matches)) { + assertType('array{string, numeric-string}', $matches); + } + if (preg_match('{([\d])(\d)}', $s, $matches)) { + assertType('array{string, numeric-string, numeric-string}', $matches); + } + if (preg_match('{([0-9])}', $s, $matches)) { + assertType('array{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); + } + if (preg_match('{(a)??(b)*+(c++)(d)+?}', $s, $matches)) { + assertType("array{string, ''|'a', string, non-empty-string, non-empty-string}", $matches); + } + if (preg_match('{(.\d)}', $s, $matches)) { + assertType('array{string, non-falsy-string}', $matches); + } + if (preg_match('{(\d.)}', $s, $matches)) { + assertType('array{string, non-falsy-string}', $matches); + } + if (preg_match('{(\d\d)}', $s, $matches)) { + assertType('array{string, non-falsy-string&numeric-string}', $matches); + } + if (preg_match('{(.(\d))}', $s, $matches)) { + assertType('array{string, non-falsy-string, numeric-string}', $matches); + } + if (preg_match('{((\d).)}', $s, $matches)) { + assertType('array{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); + } + if (preg_match('{(x?([1-4])\d)}', $s, $matches)) { + assertType('array{string, non-falsy-string, numeric-string}', $matches); + } + if (preg_match('{([^1-4])}', $s, $matches)) { + assertType('array{string, non-empty-string}', $matches); + } + if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) { + assertType('array{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); + } +} + +function (string $s): void { + preg_match('/%a(\d*)/', $s, $matches); + assertType("array{0?: string, 1?: ''|numeric-string}", $matches); +}; + +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); + } + + public function test2(string $str): void + { + if (preg_match('~^(?:(\w+)::)?(\w+)$~', $str, $matches) === 1) { + assertType('array{string, string, non-empty-string}', $matches); + } + } +} + +function (string $s): void { + if (rand(0,1)) { + $p = '/Price: (£)(abc)/'; + } else { + $p = '/Price: (\d)(b)/'; + } + + if (preg_match($p, $s, $matches)) { + assertType("array{string, '£', 'abc'}|array{string, numeric-string, 'b'}", $matches); + } +}; + +function (string $s): void { + if (rand(0,1)) { + $p = '/Price: (£)/'; + } else { + $p = '/Price: (£|(\d)|(x))/'; + } + + if (preg_match($p, $s, $matches)) { + assertType("array{0: string, 1: non-empty-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); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([0-9])/i', $s, $matches)) { + assertType("array{string, numeric-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([xXa])/i', $s, $matches)) { + assertType("array{string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: ([xXa])/', $s, $matches)) { + assertType("array{string, 'a'|'X'|'x'}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (ba[rz])/', $s, $matches)) { + assertType("array{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); + } +}; + +function (string $s): void { + if (preg_match('/Price: (\s{3}|0)/', $s, $matches)) { + assertType("array{string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (a|0)/', $s, $matches)) { + assertType("array{string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/Price: (aa|0)/', $s, $matches)) { + assertType("array{string, non-empty-string}", $matches); + } +}; + +function (string $s): void { + if (preg_match('/( \d+ )/x', $s, $matches)) { + assertType('array{string, numeric-string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/( .? )/x', $s, $matches)) { + assertType('array{string, string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/( .* )/x', $s, $matches)) { + assertType('array{string, string}', $matches); + } +}; + +function (string $s): void { + if (preg_match('/( .+ )/x', $s, $matches)) { + assertType('array{string, non-empty-string}', $matches); + } +}; + +function (string $value): void +{ + if (preg_match('/^(x)*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}", $matches); + } +}; + +function (string $value): void { + if (preg_match('/^(?:(x)|(y))*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}|array{array{string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + } +}; + +class Bug11479 +{ + static public function sayHello(string $source): void + { + $pattern = "~^(?P\d)?\-?(?P\d)?$~"; + + preg_match($pattern, $source, $matches); + + // for $source = "-1" in $matches is + // array ( + // 0 => '-1', + // 'dateFrom' => '', + // 1 => '', + // 'dateTo' => '1', + // 2 => '1', + //) + + assertType("array{0?: string, dateFrom?: ''|numeric-string, 1?: ''|numeric-string, dateTo?: numeric-string, 2?: numeric-string}", $matches); + } +} + +function (string $s): void { + if (preg_match('~a|(\d)|(\s)~', $s, $matches) === 1) { + assertType("array{0: string, 1?: numeric-string}|array{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); + } +}; + +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); + } +}; + +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); +}; + +function bug11490 (string $expression): void { + $matches = []; + + if (preg_match('/([-+])?([\d]+)%/', $expression, $matches) === 1) { + assertType("array{string, ''|'+'|'-', numeric-string}", $matches); + } +} + +function bug11490b (string $expression): void { + $matches = []; + + if (preg_match('/([\\[+])?([\d]+)%/', $expression, $matches) === 1) { + assertType("array{string, ''|'+'|'[', numeric-string}", $matches); + } +} + diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php new file mode 100644 index 0000000000..34b1b72756 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -0,0 +1,21 @@ += 8.0 + +namespace PregMatchShapesPhp82; + +use function PHPStan\Testing\assertType; + +function doOffsetCaptureWithUnmatchedNull(string $s): void { + // see https://3v4l.org/07rBO#v8.2.9 + if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { + assertType("array{array{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); +} + +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{}|array{string, numeric-string}', $matches); +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php new file mode 100644 index 0000000000..dfbcab477e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -0,0 +1,46 @@ += 8.2 + +namespace PregMatchShapesPhp82; + +use function PHPStan\Testing\assertType; + +// n modifier captures only named groups +// https://php.watch/versions/8.2/preg-n-no-capture-modifier +function doNonAutoCapturingFlag(string $s): void { + if (preg_match('/(\d+)/n', $s, $matches)) { + assertType('array{string}', $matches); + } + assertType('array{}|array{string}', $matches); + + if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { + assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + } + assertType('array{}|array{0: 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{}|array{0: 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); + } +}; +function (string $s): void { + if (preg_match('<(\d+)(?P\d+)>n', $s, $matches)) { + assertType('array{0: 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); + } +}; +function (string $s): void { + if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { + assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches); + } +}; diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php new file mode 100644 index 0000000000..2a3f437a81 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes-php72.php @@ -0,0 +1,29 @@ +', $matches); + return ''; + }, + $s + ); +}; + +function (string $s): void { + preg_replace_callback( + '|

(\s*)\w|', + function ($matches) { + assertType('array{string, string}', $matches); + return ''; + }, + $s + ); +}; + +// The flags parameter was added in PHP 7.4 diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php new file mode 100644 index 0000000000..36b6a51f1f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -0,0 +1,47 @@ += 7.4 + +namespace PregReplaceCallbackMatchShapes; + +use function PHPStan\Testing\assertType; + +function (string $s): void { + preg_replace_callback( + '/(foo)?(bar)?(baz)?/', + function ($matches) { + assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); + return ''; + }, + $s, + -1, + $count, + PREG_UNMATCHED_AS_NULL + ); +}; + +function (string $s): void { + preg_replace_callback( + '/(foo)?(bar)?(baz)?/', + function ($matches) { + assertType("array{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, + -1, + $count, + PREG_OFFSET_CAPTURE + ); +}; + +function (string $s): void { + preg_replace_callback( + '/(foo)?(bar)?(baz)?/', + function ($matches) { + assertType("array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + return ''; + }, + $s, + -1, + $count, + PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL + ); +}; diff --git a/tests/PHPStan/Analyser/data/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php similarity index 100% rename from tests/PHPStan/Analyser/data/preg_split.php rename to tests/PHPStan/Analyser/nsrt/preg_split.php diff --git a/tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php b/tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php new file mode 100644 index 0000000000..a66425e3dd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php @@ -0,0 +1,67 @@ += 8.0 + +namespace PrintFErrorsPhp8; + +use function PHPStan\Testing\assertType; + +function doFoo() +{ + assertType("string", sprintf('%s')); // error + assertType("string", vsprintf('%s')); // error +} diff --git a/tests/PHPStan/Analyser/data/proc_get_status.php b/tests/PHPStan/Analyser/nsrt/proc_get_status.php similarity index 100% rename from tests/PHPStan/Analyser/data/proc_get_status.php rename to tests/PHPStan/Analyser/nsrt/proc_get_status.php diff --git a/tests/PHPStan/Analyser/data/promoted-properties-types.php b/tests/PHPStan/Analyser/nsrt/promoted-properties-types.php similarity index 92% rename from tests/PHPStan/Analyser/data/promoted-properties-types.php rename to tests/PHPStan/Analyser/nsrt/promoted-properties-types.php index 7581c6dbe2..20c9aa11d1 100644 --- a/tests/PHPStan/Analyser/data/promoted-properties-types.php +++ b/tests/PHPStan/Analyser/nsrt/promoted-properties-types.php @@ -103,3 +103,16 @@ function (Baz $baz): void { assertType('array', $baz->anotherPhpDocArray); assertType('stdClass', $baz->templateProperty); }; + +class PromotedPropertyNotNullable +{ + + public function __construct( + public int $intProp = null, + ) {} + +} + +function (PromotedPropertyNotNullable $p) { + assertType('int', $p->intProp); +}; diff --git a/tests/PHPStan/Analyser/data/property-template-tag.php b/tests/PHPStan/Analyser/nsrt/property-template-tag.php similarity index 100% rename from tests/PHPStan/Analyser/data/property-template-tag.php rename to tests/PHPStan/Analyser/nsrt/property-template-tag.php diff --git a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php b/tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php similarity index 100% rename from tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php rename to tests/PHPStan/Analyser/nsrt/psalm-prefix-unresolvable.php diff --git a/tests/PHPStan/Analyser/nsrt/pure-callable.php b/tests/PHPStan/Analyser/nsrt/pure-callable.php new file mode 100644 index 0000000000..39ef172288 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pure-callable.php @@ -0,0 +1,18 @@ += 7.4 namespace ReflectionTypeTest; diff --git a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php similarity index 98% rename from tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php rename to tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php index 41dbe632a6..94b2806850 100644 --- a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php +++ b/tests/PHPStan/Analyser/nsrt/reflectionclass-issue-5511-php8.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types=1); namespace Issue5511; diff --git a/tests/PHPStan/Analyser/data/remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php similarity index 100% rename from tests/PHPStan/Analyser/data/remember-possibly-impure-function-values.php rename to tests/PHPStan/Analyser/nsrt/remember-possibly-impure-function-values.php diff --git a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php similarity index 100% rename from tests/PHPStan/Analyser/data/root-scope-maybe-defined.php rename to tests/PHPStan/Analyser/nsrt/root-scope-maybe-defined.php diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php similarity index 95% rename from tests/PHPStan/Analyser/data/round-php8.php rename to tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php index 85428a2fd6..c618f6c8d9 100644 --- a/tests/PHPStan/Analyser/data/round-php8.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php @@ -1,6 +1,8 @@ -= 8.0 -namespace RoundFamilyTestPHP8; +declare(strict_types=1); + +namespace RoundFamilyTestPHP8StrictTypes; use function PHPStan\Testing\assertType; diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php new file mode 100644 index 0000000000..54836b7623 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -0,0 +1,61 @@ += 8.0 + +namespace RoundFamilyTestPHP8; + +use function PHPStan\Testing\assertType; + +$maybeNull = null; +if (rand(0, 1)) { + $maybeNull = 1.0; +} + +// Round +assertType('float', round(123)); +assertType('float', round(123.456)); +assertType('float', round($_GET['foo'] / 60)); +assertType('float', round('123')); +assertType('float', round('123.456')); +assertType('float', round(null)); +assertType('float', round($maybeNull)); +assertType('float', round(true)); +assertType('float', round(false)); +assertType('*NEVER*', round(new \stdClass)); +assertType('*NEVER*', round('')); +assertType('*NEVER*', round(array())); +assertType('*NEVER*', round(array(123))); +assertType('*NEVER*', round()); +assertType('float', round($_GET['foo'])); + +// Ceil +assertType('float', ceil(123)); +assertType('float', ceil(123.456)); +assertType('float', ceil($_GET['foo'] / 60)); +assertType('float', ceil('123')); +assertType('float', ceil('123.456')); +assertType('float', ceil(null)); +assertType('float', ceil($maybeNull)); +assertType('float', ceil(true)); +assertType('float', ceil(false)); +assertType('*NEVER*', ceil(new \stdClass)); +assertType('*NEVER*', ceil('')); +assertType('*NEVER*', ceil(array())); +assertType('*NEVER*', ceil(array(123))); +assertType('*NEVER*', ceil()); +assertType('float', ceil($_GET['foo'])); + +// Floor +assertType('float', floor(123)); +assertType('float', floor(123.456)); +assertType('float', floor($_GET['foo'] / 60)); +assertType('float', floor('123')); +assertType('float', floor('123.456')); +assertType('float', floor(null)); +assertType('float', floor($maybeNull)); +assertType('float', floor(true)); +assertType('float', floor(false)); +assertType('*NEVER*', floor(new \stdClass)); +assertType('*NEVER*', floor('')); +assertType('*NEVER*', floor(array())); +assertType('*NEVER*', floor(array(123))); +assertType('*NEVER*', floor()); +assertType('float', floor($_GET['foo'])); diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/nsrt/round.php similarity index 98% rename from tests/PHPStan/Analyser/data/round.php rename to tests/PHPStan/Analyser/nsrt/round.php index 3552e2e602..3d181ca50a 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/nsrt/round.php @@ -1,4 +1,4 @@ -addData(321); assertType('SelfOut\\a', $i); + + $i->addData(random_bytes(3)); + assertType('SelfOut\\a', $i); + + $i->setData(true); + assertType('SelfOut\\a', $i); }; diff --git a/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php new file mode 100644 index 0000000000..11869c0623 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php @@ -0,0 +1,673 @@ + 'bar']; + settype($x, 'string'); + assertType('*ERROR*', $x); + + // array to int + $x = []; + settype($x, 'int'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'int'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'int'); + assertType('1', $x); + + // array to integer + $x = []; + settype($x, 'integer'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'integer'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'integer'); + assertType('1', $x); + + // array to float + $x = []; + settype($x, 'float'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'float'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'float'); + assertType('1.0', $x); + + // array to double + $x = []; + settype($x, 'double'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'double'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'double'); + assertType('1.0', $x); + + // array to bool + $x = []; + settype($x, 'bool'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'bool'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'bool'); + assertType('true', $x); + + // array to boolean + $x = []; + settype($x, 'boolean'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'boolean'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'boolean'); + assertType('true', $x); + + // array to array + $x = []; + settype($x, 'array'); + assertType('array{}', $x); + + $x = ['foo']; + settype($x, 'array'); + assertType("array{'foo'}", $x); + + $x = ['foo' => 'bar']; + settype($x, 'array'); + assertType("array{foo: 'bar'}", $x); + + // array to object + $x = []; + settype($x, 'object'); + assertType('stdClass', $x); + + $x = ['foo']; + settype($x, 'object'); + assertType("stdClass", $x); + + $x = ['foo' => 'bar']; + settype($x, 'object'); + assertType("stdClass", $x); + + // array to null + $x = []; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo']; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo' => 'bar']; + settype($x, 'null'); + assertType('null', $x); + + // object to string + $x = new stdClass(); + settype($x, 'string'); + assertType('*ERROR*', $x); + + // object to int + $x = new stdClass(); + settype($x, 'int'); + assertType('*ERROR*', $x); + + // object to integer + $x = new stdClass(); + settype($x, 'integer'); + assertType('*ERROR*', $x); + + // object to float + $x = new stdClass(); + settype($x, 'float'); + assertType('*ERROR*', $x); + + // object to double + $x = new stdClass(); + settype($x, 'double'); + assertType('*ERROR*', $x); + + // object to bool + $x = new stdClass(); + settype($x, 'bool'); + assertType('true', $x); + + // object to boolean + $x = new stdClass(); + settype($x, 'boolean'); + assertType('true', $x); + + // object to array + $x = new stdClass(); + settype($x, 'array'); + assertType('array', $x); + + // object to object + $x = new stdClass(); + settype($x, 'object'); + assertType('stdClass', $x); + + // object to null + $x = new stdClass(); + settype($x, 'null'); + assertType('null', $x); + + // null to string + $x = null; + settype($x, 'string'); + assertType("''", $x); + + // null to int + $x = null; + settype($x, 'int'); + assertType('0', $x); + + // null to integer + $x = null; + settype($x, 'integer'); + assertType('0', $x); + + // null to float + $x = null; + settype($x, 'float'); + assertType('0.0', $x); + + // null to double + $x = null; + settype($x, 'double'); + assertType('0.0', $x); + + // null to bool + $x = null; + settype($x, 'bool'); + assertType('false', $x); + + // null to boolean + $x = null; + settype($x, 'boolean'); + assertType('false', $x); + + // null to array + $x = null; + settype($x, 'array'); + assertType('array{}', $x); + + // null to object + $x = null; + settype($x, 'object'); + assertType('stdClass', $x); + + // null to null + $x = null; + settype($x, 'null'); + assertType('null', $x); + + // Mixed to non-constant. + settype($value, $castTo); + assertType("array|bool|float|int|stdClass|string|null", $value); +} diff --git a/tests/PHPStan/Analyser/data/shadowed-trait-methods.php b/tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/shadowed-trait-methods.php rename to tests/PHPStan/Analyser/nsrt/shadowed-trait-methods.php diff --git a/tests/PHPStan/Analyser/data/shuffle.php b/tests/PHPStan/Analyser/nsrt/shuffle.php similarity index 100% rename from tests/PHPStan/Analyser/data/shuffle.php rename to tests/PHPStan/Analyser/nsrt/shuffle.php diff --git a/tests/PHPStan/Analyser/data/simplexml.php b/tests/PHPStan/Analyser/nsrt/simplexml.php similarity index 100% rename from tests/PHPStan/Analyser/data/simplexml.php rename to tests/PHPStan/Analyser/nsrt/simplexml.php diff --git a/tests/PHPStan/Analyser/data/sizeof-php8.php b/tests/PHPStan/Analyser/nsrt/sizeof-php8.php similarity index 97% rename from tests/PHPStan/Analyser/data/sizeof-php8.php rename to tests/PHPStan/Analyser/nsrt/sizeof-php8.php index 0af3b4062c..a681a9f906 100644 --- a/tests/PHPStan/Analyser/data/sizeof-php8.php +++ b/tests/PHPStan/Analyser/nsrt/sizeof-php8.php @@ -1,4 +1,4 @@ -= 8.0 namespace sizeof_php8; diff --git a/tests/PHPStan/Analyser/data/sizeof.php b/tests/PHPStan/Analyser/nsrt/sizeof.php similarity index 97% rename from tests/PHPStan/Analyser/data/sizeof.php rename to tests/PHPStan/Analyser/nsrt/sizeof.php index 1a080946c4..eec773844c 100644 --- a/tests/PHPStan/Analyser/data/sizeof.php +++ b/tests/PHPStan/Analyser/nsrt/sizeof.php @@ -1,4 +1,4 @@ - 1, + 'five' => 5, + 'three' => 3, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|3|4|5>', $arr1); + assertNativeType('non-empty-list<1|3|4|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|3|4|5>', $arr2); + assertNativeType('non-empty-list<1|3|4|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|3|4|5>', $arr3); + assertNativeType('non-empty-list<1|3|4|5>', $arr3); + } + + public function constantArrayOptionalKey(): void + { + $arr = [ + 'one' => 1, + 'five' => 5, + ]; + if (rand(0, 1)) { + $arr['two'] = 2; + } + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + public function constantArrayUnion(): void + { + $arr = rand(0, 1) ? [ + 'one' => 1, + 'five' => 5, + ] : [ + 'two' => 2, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + /** @param array $arr */ + public function normalArray(array $arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('list', $arr1); + assertNativeType('list', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('list', $arr2); + assertNativeType('list', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('list', $arr3); + assertNativeType('list', $arr3); + } + + public function mixed($arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('mixed', $arr1); + assertNativeType('mixed', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('mixed', $arr2); + assertNativeType('mixed', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('mixed', $arr3); + assertNativeType('mixed', $arr3); + } + + public function notArray(): void + { + $arr = 'foo'; + sort($arr); + assertType("'foo'", $arr); + } +} + +class Bar +{ + + /** + * @template T + * @param T&list $array + * @return list + */ + public function doFoo(array $array) + { + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + usort($array, function (array $a, array $b) { + return $a['a'] <=> $b['a']; + }); + + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + + return $array; + } + +} diff --git a/tests/PHPStan/Analyser/data/specified-types-closure-edge.php b/tests/PHPStan/Analyser/nsrt/specified-types-closure-edge.php similarity index 100% rename from tests/PHPStan/Analyser/data/specified-types-closure-edge.php rename to tests/PHPStan/Analyser/nsrt/specified-types-closure-edge.php diff --git a/tests/PHPStan/Analyser/data/specified-types-closure-use.php b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php similarity index 100% rename from tests/PHPStan/Analyser/data/specified-types-closure-use.php rename to tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php diff --git a/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php b/tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php rename to tests/PHPStan/Analyser/nsrt/splfixedarray-iterator-types.php diff --git a/tests/PHPStan/Analyser/data/sscanf.php b/tests/PHPStan/Analyser/nsrt/sscanf.php similarity index 100% rename from tests/PHPStan/Analyser/data/sscanf.php rename to tests/PHPStan/Analyser/nsrt/sscanf.php diff --git a/tests/PHPStan/Analyser/data/standalone-types.php b/tests/PHPStan/Analyser/nsrt/standalone-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/standalone-types.php rename to tests/PHPStan/Analyser/nsrt/standalone-types.php diff --git a/tests/PHPStan/Analyser/data/static-has-method.php b/tests/PHPStan/Analyser/nsrt/static-has-method.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-has-method.php rename to tests/PHPStan/Analyser/nsrt/static-has-method.php diff --git a/tests/PHPStan/Analyser/data/static-methods.php b/tests/PHPStan/Analyser/nsrt/static-methods.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-methods.php rename to tests/PHPStan/Analyser/nsrt/static-methods.php diff --git a/tests/PHPStan/Analyser/data/static-properties.php b/tests/PHPStan/Analyser/nsrt/static-properties.php similarity index 100% rename from tests/PHPStan/Analyser/data/static-properties.php rename to tests/PHPStan/Analyser/nsrt/static-properties.php diff --git a/tests/PHPStan/Analyser/data/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php similarity index 99% rename from tests/PHPStan/Analyser/data/str-casing.php rename to tests/PHPStan/Analyser/nsrt/str-casing.php index be4ed8e384..df4883845a 100644 --- a/tests/PHPStan/Analyser/data/str-casing.php +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -1,4 +1,4 @@ -= 7.3 namespace StrCasingReturnType; diff --git a/tests/PHPStan/Analyser/data/str-shuffle.php b/tests/PHPStan/Analyser/nsrt/str-shuffle.php similarity index 100% rename from tests/PHPStan/Analyser/data/str-shuffle.php rename to tests/PHPStan/Analyser/nsrt/str-shuffle.php diff --git a/tests/PHPStan/Analyser/data/str_decrement.php b/tests/PHPStan/Analyser/nsrt/str_decrement.php similarity index 96% rename from tests/PHPStan/Analyser/data/str_decrement.php rename to tests/PHPStan/Analyser/nsrt/str_decrement.php index ccae76d568..f0747852c3 100644 --- a/tests/PHPStan/Analyser/data/str_decrement.php +++ b/tests/PHPStan/Analyser/nsrt/str_decrement.php @@ -1,4 +1,6 @@ -= 8.3 + +declare(strict_types = 1); namespace StrDecrementFunctionReturn; diff --git a/tests/PHPStan/Analyser/data/str_increment.php b/tests/PHPStan/Analyser/nsrt/str_increment.php similarity index 96% rename from tests/PHPStan/Analyser/data/str_increment.php rename to tests/PHPStan/Analyser/nsrt/str_increment.php index a83f578ce9..36fde68d6d 100644 --- a/tests/PHPStan/Analyser/data/str_increment.php +++ b/tests/PHPStan/Analyser/nsrt/str_increment.php @@ -1,4 +1,6 @@ -= 8.3 + +declare(strict_types = 1); namespace StrIncrementFunctionReturn; diff --git a/tests/PHPStan/Analyser/nsrt/strlen-int-range.php b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php new file mode 100644 index 0000000000..540b932531 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php @@ -0,0 +1,54 @@ += 7.2 + +namespace StrlenIntRange; + +use function PHPStan\Testing\assertType; + +/** + * @param int<0, 3> $zeroToThree + * @param int<2, 3> $twoOrThree + * @param int<2, max> $twoOrMore + * @param int $maxThree + * @param int<10, 11> $tenOrEleven + */ +function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void +{ + if (strlen($s) >= $zeroToThree) { + assertType('string', $s); + } + if (strlen($s) > $zeroToThree) { + assertType('non-empty-string', $s); + } + + if (strlen($s) >= $twoOrThree) { + assertType('non-falsy-string', $s); + } + if (strlen($s) > $twoOrThree) { + assertType('non-falsy-string', $s); + } + + if (strlen($s) > $twoOrMore) { + assertType('non-falsy-string', $s); + } + + $oneOrMore = $twoOrMore-1; + if (strlen($s) > $oneOrMore) { + assertType('non-falsy-string', $s); + } + if (strlen($s) >= $oneOrMore) { + assertType('non-empty-string', $s); + } + if (strlen($s) <= $oneOrMore) { + assertType('string', $s); + } else { + assertType('non-falsy-string', $s); + } + + if (strlen($s) > $maxThree) { + assertType('string', $s); + } + + if (strlen($s) > $tenOrEleven) { + assertType('non-falsy-string', $s); + } +} diff --git a/tests/PHPStan/Analyser/data/strtotime-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/strtotime-return-type-extensions.php similarity index 100% rename from tests/PHPStan/Analyser/data/strtotime-return-type-extensions.php rename to tests/PHPStan/Analyser/nsrt/strtotime-return-type-extensions.php diff --git a/tests/PHPStan/Analyser/data/strtr.php b/tests/PHPStan/Analyser/nsrt/strtr.php similarity index 100% rename from tests/PHPStan/Analyser/data/strtr.php rename to tests/PHPStan/Analyser/nsrt/strtr.php diff --git a/tests/PHPStan/Analyser/data/strval.php b/tests/PHPStan/Analyser/nsrt/strval.php similarity index 100% rename from tests/PHPStan/Analyser/data/strval.php rename to tests/PHPStan/Analyser/nsrt/strval.php diff --git a/tests/PHPStan/Analyser/data/tagged-unions.php b/tests/PHPStan/Analyser/nsrt/tagged-unions.php similarity index 100% rename from tests/PHPStan/Analyser/data/tagged-unions.php rename to tests/PHPStan/Analyser/nsrt/tagged-unions.php diff --git a/tests/PHPStan/Analyser/data/template-constant-bound.php b/tests/PHPStan/Analyser/nsrt/template-constant-bound.php similarity index 100% rename from tests/PHPStan/Analyser/data/template-constant-bound.php rename to tests/PHPStan/Analyser/nsrt/template-constant-bound.php diff --git a/tests/PHPStan/Analyser/data/template-null-bound.php b/tests/PHPStan/Analyser/nsrt/template-null-bound.php similarity index 100% rename from tests/PHPStan/Analyser/data/template-null-bound.php rename to tests/PHPStan/Analyser/nsrt/template-null-bound.php diff --git a/tests/PHPStan/Analyser/data/ternary-specified-types.php b/tests/PHPStan/Analyser/nsrt/ternary-specified-types.php similarity index 100% rename from tests/PHPStan/Analyser/data/ternary-specified-types.php rename to tests/PHPStan/Analyser/nsrt/ternary-specified-types.php diff --git a/tests/PHPStan/Analyser/data/this-subtractable.php b/tests/PHPStan/Analyser/nsrt/this-subtractable.php similarity index 100% rename from tests/PHPStan/Analyser/data/this-subtractable.php rename to tests/PHPStan/Analyser/nsrt/this-subtractable.php diff --git a/tests/PHPStan/Analyser/data/throw-expr.php b/tests/PHPStan/Analyser/nsrt/throw-expr.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-expr.php rename to tests/PHPStan/Analyser/nsrt/throw-expr.php diff --git a/tests/PHPStan/Analyser/data/throw-points/and.php b/tests/PHPStan/Analyser/nsrt/throw-points/and.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/and.php rename to tests/PHPStan/Analyser/nsrt/throw-points/and.php diff --git a/tests/PHPStan/Analyser/data/throw-points/array-dim-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/array-dim-fetch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/array-dim-fetch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/array.php b/tests/PHPStan/Analyser/nsrt/throw-points/array.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/array.php rename to tests/PHPStan/Analyser/nsrt/throw-points/array.php diff --git a/tests/PHPStan/Analyser/data/throw-points/assign-op.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/assign-op.php rename to tests/PHPStan/Analyser/nsrt/throw-points/assign-op.php diff --git a/tests/PHPStan/Analyser/data/throw-points/assign.php b/tests/PHPStan/Analyser/nsrt/throw-points/assign.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/assign.php rename to tests/PHPStan/Analyser/nsrt/throw-points/assign.php diff --git a/tests/PHPStan/Analyser/data/throw-points/do-while.php b/tests/PHPStan/Analyser/nsrt/throw-points/do-while.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/do-while.php rename to tests/PHPStan/Analyser/nsrt/throw-points/do-while.php diff --git a/tests/PHPStan/Analyser/data/throw-points/for.php b/tests/PHPStan/Analyser/nsrt/throw-points/for.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/for.php rename to tests/PHPStan/Analyser/nsrt/throw-points/for.php diff --git a/tests/PHPStan/Analyser/data/throw-points/foreach.php b/tests/PHPStan/Analyser/nsrt/throw-points/foreach.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/foreach.php rename to tests/PHPStan/Analyser/nsrt/throw-points/foreach.php diff --git a/tests/PHPStan/Analyser/data/throw-points/func-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/func-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/func-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/func-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/if.php b/tests/PHPStan/Analyser/nsrt/throw-points/if.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/if.php rename to tests/PHPStan/Analyser/nsrt/throw-points/if.php diff --git a/tests/PHPStan/Analyser/data/throw-points/method-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/method-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/method-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/method-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/or.php b/tests/PHPStan/Analyser/nsrt/throw-points/or.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/or.php rename to tests/PHPStan/Analyser/nsrt/throw-points/or.php diff --git a/tests/PHPStan/Analyser/data/throw-points/php8/null-safe-method-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/php8/null-safe-method-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/php8/null-safe-method-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/php8/null-safe-method-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/property-fetch.php b/tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/property-fetch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/property-fetch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/static-call.php b/tests/PHPStan/Analyser/nsrt/throw-points/static-call.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/static-call.php rename to tests/PHPStan/Analyser/nsrt/throw-points/static-call.php diff --git a/tests/PHPStan/Analyser/data/throw-points/switch.php b/tests/PHPStan/Analyser/nsrt/throw-points/switch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/switch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/switch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/throw.php b/tests/PHPStan/Analyser/nsrt/throw-points/throw.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/throw.php rename to tests/PHPStan/Analyser/nsrt/throw-points/throw.php diff --git a/tests/PHPStan/Analyser/data/throw-points/try-catch-finally.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch-finally.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/try-catch-finally.php rename to tests/PHPStan/Analyser/nsrt/throw-points/try-catch-finally.php diff --git a/tests/PHPStan/Analyser/data/throw-points/try-catch.php b/tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/try-catch.php rename to tests/PHPStan/Analyser/nsrt/throw-points/try-catch.php diff --git a/tests/PHPStan/Analyser/data/throw-points/variable.php b/tests/PHPStan/Analyser/nsrt/throw-points/variable.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/variable.php rename to tests/PHPStan/Analyser/nsrt/throw-points/variable.php diff --git a/tests/PHPStan/Analyser/data/throw-points/while.php b/tests/PHPStan/Analyser/nsrt/throw-points/while.php similarity index 100% rename from tests/PHPStan/Analyser/data/throw-points/while.php rename to tests/PHPStan/Analyser/nsrt/throw-points/while.php diff --git a/tests/PHPStan/Analyser/data/trait-type-alias.php b/tests/PHPStan/Analyser/nsrt/trait-type-alias.php similarity index 100% rename from tests/PHPStan/Analyser/data/trait-type-alias.php rename to tests/PHPStan/Analyser/nsrt/trait-type-alias.php diff --git a/tests/PHPStan/Analyser/data/trigger-error-php7.php b/tests/PHPStan/Analyser/nsrt/trigger-error-php7.php similarity index 92% rename from tests/PHPStan/Analyser/data/trigger-error-php7.php rename to tests/PHPStan/Analyser/nsrt/trigger-error-php7.php index 68c3916698..374a454453 100644 --- a/tests/PHPStan/Analyser/data/trigger-error-php7.php +++ b/tests/PHPStan/Analyser/nsrt/trigger-error-php7.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace TriggerErrorPhp8; diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/nsrt/type-aliases.php similarity index 100% rename from tests/PHPStan/Analyser/data/type-aliases.php rename to tests/PHPStan/Analyser/nsrt/type-aliases.php diff --git a/tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php b/tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php similarity index 100% rename from tests/PHPStan/Analyser/data/type-change-after-array-access-assignment.php rename to tests/PHPStan/Analyser/nsrt/type-change-after-array-access-assignment.php diff --git a/tests/PHPStan/Analyser/data/uksort-bug.php b/tests/PHPStan/Analyser/nsrt/uksort-bug.php similarity index 100% rename from tests/PHPStan/Analyser/data/uksort-bug.php rename to tests/PHPStan/Analyser/nsrt/uksort-bug.php diff --git a/tests/PHPStan/Analyser/data/unset-conditional-expressions.php b/tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php similarity index 100% rename from tests/PHPStan/Analyser/data/unset-conditional-expressions.php rename to tests/PHPStan/Analyser/nsrt/unset-conditional-expressions.php diff --git a/tests/PHPStan/Analyser/data/value-of-enum.php b/tests/PHPStan/Analyser/nsrt/value-of-enum.php similarity index 100% rename from tests/PHPStan/Analyser/data/value-of-enum.php rename to tests/PHPStan/Analyser/nsrt/value-of-enum.php diff --git a/tests/PHPStan/Analyser/data/value-of-generic.php b/tests/PHPStan/Analyser/nsrt/value-of-generic.php similarity index 100% rename from tests/PHPStan/Analyser/data/value-of-generic.php rename to tests/PHPStan/Analyser/nsrt/value-of-generic.php diff --git a/tests/PHPStan/Analyser/data/value-of.php b/tests/PHPStan/Analyser/nsrt/value-of.php similarity index 100% rename from tests/PHPStan/Analyser/data/value-of.php rename to tests/PHPStan/Analyser/nsrt/value-of.php diff --git a/tests/PHPStan/Analyser/data/var-above-declare.php b/tests/PHPStan/Analyser/nsrt/var-above-declare.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-above-declare.php rename to tests/PHPStan/Analyser/nsrt/var-above-declare.php diff --git a/tests/PHPStan/Analyser/data/var-above-use.php b/tests/PHPStan/Analyser/nsrt/var-above-use.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-above-use.php rename to tests/PHPStan/Analyser/nsrt/var-above-use.php diff --git a/tests/PHPStan/Analyser/data/var-in-and-out-of-function.php b/tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php similarity index 100% rename from tests/PHPStan/Analyser/data/var-in-and-out-of-function.php rename to tests/PHPStan/Analyser/nsrt/var-in-and-out-of-function.php diff --git a/tests/PHPStan/Analyser/data/variadic-parameter-php8.php b/tests/PHPStan/Analyser/nsrt/variadic-parameter-php8.php similarity index 93% rename from tests/PHPStan/Analyser/data/variadic-parameter-php8.php rename to tests/PHPStan/Analyser/nsrt/variadic-parameter-php8.php index 52de2402f1..7a1dc1d2f7 100644 --- a/tests/PHPStan/Analyser/data/variadic-parameter-php8.php +++ b/tests/PHPStan/Analyser/nsrt/variadic-parameter-php8.php @@ -1,4 +1,4 @@ -= 8.0 namespace VariadicParameterPHP8; diff --git a/tests/PHPStan/Analyser/nsrt/weakMap.php b/tests/PHPStan/Analyser/nsrt/weakMap.php new file mode 100644 index 0000000000..049e3dfb13 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/weakMap.php @@ -0,0 +1,32 @@ += 8.0 + +declare(strict_types = 1); + +namespace weakMap; + +use WeakMap; +use function PHPStan\Testing\assertType; + +interface Foo {} +interface Bar {} + +/** + * @param WeakMap $weakMap + */ +function weakMapOffsetGetNotNullable(WeakMap $weakMap, Foo $foo): void +{ + $bar = $weakMap[$foo]; + + assertType(Bar::class, $bar); +} + + +/** + * @param WeakMap $weakMap + */ +function weakMapOffsetGetNullable(WeakMap $weakMap, Foo $foo): void +{ + $bar = $weakMap[$foo]; + + assertType( 'weakMap\\Bar|null', $bar); +} diff --git a/tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php b/tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php similarity index 100% rename from tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php rename to tests/PHPStan/Analyser/nsrt/weird-array_key_exists-issue.php diff --git a/tests/PHPStan/Analyser/data/weird-strlen-cases.php b/tests/PHPStan/Analyser/nsrt/weird-strlen-cases.php similarity index 76% rename from tests/PHPStan/Analyser/data/weird-strlen-cases.php rename to tests/PHPStan/Analyser/nsrt/weird-strlen-cases.php index 071f351e3f..d53f71f973 100644 --- a/tests/PHPStan/Analyser/data/weird-strlen-cases.php +++ b/tests/PHPStan/Analyser/nsrt/weird-strlen-cases.php @@ -8,14 +8,16 @@ class Foo { /** + * @param 'foo'|'foooooo' $constUnionString * @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 */ - public function strlenTests($constUnionMixed, float $float, $intFloat, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull): void + public function strlenTests(string $constUnionString, $constUnionMixed, float $float, $intFloat, $nonEmptyStringIntFloat, $emptyStringFalseNull, $emptyStringBoolNull): void { + assertType('3|7', strlen($constUnionString)); assertType('int<0, 4>', strlen($constUnionMixed)); assertType('3', strlen(123)); assertType('1', strlen(true)); diff --git a/tests/PHPStan/Analyser/param-closure-this-stubs.neon b/tests/PHPStan/Analyser/param-closure-this-stubs.neon new file mode 100644 index 0000000000..bbfb15154d --- /dev/null +++ b/tests/PHPStan/Analyser/param-closure-this-stubs.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/param-closure-this-stubs.stub diff --git a/tests/PHPStan/Analyser/parameter-closure-type-extension-arrow-function.neon b/tests/PHPStan/Analyser/parameter-closure-type-extension-arrow-function.neon new file mode 100644 index 0000000000..2ab556183c --- /dev/null +++ b/tests/PHPStan/Analyser/parameter-closure-type-extension-arrow-function.neon @@ -0,0 +1,16 @@ +services: + - + class: ParameterClosureTypeExtensionArrowFunction\FunctionParameterClosureTypeExtension + tags: + - phpstan.functionParameterClosureTypeExtension + + - + class: ParameterClosureTypeExtensionArrowFunction\MethodParameterClosureTypeExtension + tags: + - phpstan.methodParameterClosureTypeExtension + + - + class: ParameterClosureTypeExtensionArrowFunction\StaticMethodParameterClosureTypeExtension + tags: + - phpstan.staticMethodParameterClosureTypeExtension + diff --git a/tests/PHPStan/Analyser/parameter-closure-type-extension.neon b/tests/PHPStan/Analyser/parameter-closure-type-extension.neon new file mode 100644 index 0000000000..8fda3ae221 --- /dev/null +++ b/tests/PHPStan/Analyser/parameter-closure-type-extension.neon @@ -0,0 +1,15 @@ +services: + - + class: ParameterClosureTypeExtension\FunctionParameterClosureTypeExtension + tags: + - phpstan.functionParameterClosureTypeExtension + + - + class: ParameterClosureTypeExtension\MethodParameterClosureTypeExtension + tags: + - phpstan.methodParameterClosureTypeExtension + + - + class: ParameterClosureTypeExtension\StaticMethodParameterClosureTypeExtension + tags: + - phpstan.staticMethodParameterClosureTypeExtension diff --git a/tests/PHPStan/Analyser/parameter-out.neon b/tests/PHPStan/Analyser/parameter-out.neon new file mode 100644 index 0000000000..bb8bcdcaef --- /dev/null +++ b/tests/PHPStan/Analyser/parameter-out.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\ParamOutFunctionExtension + tags: + - phpstan.functionParameterOutTypeExtension + - + class: PHPStan\Tests\ParamOutMethodExtension + tags: + - phpstan.methodParameterOutTypeExtension + - + class: PHPStan\Tests\ParamOutStaticMethodExtension + tags: + - phpstan.staticMethodParameterOutTypeExtension diff --git a/tests/PHPStan/Analyser/traits/AnonymousClassUsingTrait.php b/tests/PHPStan/Analyser/traits/AnonymousClassUsingTrait.php index 924b814eb8..7387e1634e 100644 --- a/tests/PHPStan/Analyser/traits/AnonymousClassUsingTrait.php +++ b/tests/PHPStan/Analyser/traits/AnonymousClassUsingTrait.php @@ -2,7 +2,7 @@ namespace AnonymousTraitClass; -new class implements FooInterface { +$a = new class implements FooInterface { use TraitWithTypeSpecification; diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php b/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php deleted file mode 100644 index ee7dd1f215..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php +++ /dev/null @@ -1,184 +0,0 @@ -deleteCache(); - - if ($this->originalTraitOneContents !== null) { - $this->revertTrait(__DIR__ . '/data/TraitOne.php', $this->originalTraitOneContents); - } - - if ($this->originalTraitTwoContents !== null) { - $this->revertTrait(__DIR__ . '/data/TraitTwo.php', $this->originalTraitTwoContents); - } - } - - public function dataCachingIssue(): array - { - return [ - [ - false, - false, - [], - ], - [ - false, - true, - [ - 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.', - ], - ], - [ - true, - false, - [ - 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.', - ], - ], - [ - true, - true, - [ - 'Method TraitsCachingIssue\TestClassUsingTrait::doBar() should return stdClass but returns Exception.', - 'Method class@anonymous/TestClassUsingTrait.php:20::doBar() should return stdClass but returns Exception.', - ], - ], - ]; - } - - /** - * @dataProvider dataCachingIssue - * @param bool $changeOne - * @param bool $changeTwo - * @param string[] $expectedErrors - */ - public function testCachingIssue( - bool $changeOne, - bool $changeTwo, - array $expectedErrors - ): void - { - $this->deleteCache(); - [$statusCode, $errors] = $this->runPhpStan(); - $this->assertSame([], $errors); - $this->assertSame(0, $statusCode); - - if ($changeOne) { - $this->originalTraitOneContents = $this->changeTrait(__DIR__ . '/data/TraitOne.php'); - } - if ($changeTwo) { - $this->originalTraitTwoContents = $this->changeTrait(__DIR__ . '/data/TraitTwo.php'); - } - - $fileHelper = new FileHelper(__DIR__); - - $errorPath = $fileHelper->normalizePath(__DIR__ . '/data/TestClassUsingTrait.php'); - [$statusCode, $errors] = $this->runPhpStan(); - - if (count($expectedErrors) === 0) { - $this->assertSame(0, $statusCode); - $this->assertArrayNotHasKey($errorPath, $errors); - return; - } - - $this->assertSame(1, $statusCode); - $this->assertArrayHasKey($errorPath, $errors); - $this->assertSame(count($expectedErrors), $errors[$errorPath]['errors']); - - foreach ($errors[$errorPath]['messages'] as $i => $error) { - $this->assertSame($expectedErrors[$i], $error['message']); - } - } - - /** - * @return array{int, mixed[]} - */ - private function runPhpStan(): array - { - $phpstanBinPath = __DIR__ . '/../../../../bin/phpstan'; - exec(sprintf('%s %s clear-result-cache --configuration %s', escapeshellarg(PHP_BINARY), $phpstanBinPath, escapeshellarg(__DIR__ . '/phpstan.neon')), $clearResultCacheOutputLines, $clearResultCacheExitCode); - if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache.'); - } - - exec( - sprintf( - '%s %s analyse --no-progress --level 8 --configuration %s --error-format json %s', - escapeshellarg(PHP_BINARY), - $phpstanBinPath, - escapeshellarg(__DIR__ . '/phpstan.neon'), - escapeshellarg(__DIR__ . '/data') - ), - $output, - $statusCode - ); - $stringOutput = implode("\n", $output); - $json = \Nette\Utils\Json::decode($stringOutput, \Nette\Utils\Json::FORCE_ARRAY); - - return [$statusCode, $json['files']]; - } - - private function deleteCache(): void - { - $dir = __DIR__ . '/tmp/cache'; - if (!file_exists($dir)) { - return; - } - - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator(__DIR__ . '/tmp/cache', RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($files as $fileinfo) { - if ($fileinfo->isDir()) { - rmdir($fileinfo->getRealPath()); - continue; - } - - unlink($fileinfo->getRealPath()); - } - } - - private function changeTrait(string $traitPath): string - { - $originalTraitContents = FileReader::read($traitPath); - $traitContents = str_replace('use stdClass as Foo;', 'use Exception as Foo;', $originalTraitContents); - $result = file_put_contents($traitPath, $traitContents); - if ($result === false) { - $this->fail(sprintf('Could not save file %s', $traitPath)); - } - - return $originalTraitContents; - } - - private function revertTrait(string $traitPath, string $originalTraitContents): void - { - $result = file_put_contents($traitPath, $originalTraitContents); - if ($result === false) { - $this->fail(sprintf('Could not save file %s', $traitPath)); - } - } - -} diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore b/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore deleted file mode 100644 index d6b7ef32c8..0000000000 --- a/tests/PHPStan/Analyser/traitsCachingIssue/tmp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/tests/PHPStan/Command/AnalyseCommandTest.php b/tests/PHPStan/Command/AnalyseCommandTest.php index 7a548a5d54..2bec6dfa21 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -8,6 +8,7 @@ use Throwable; use function chdir; use function getcwd; +use function microtime; use function realpath; use function sprintf; use const DIRECTORY_SEPARATOR; @@ -104,7 +105,7 @@ public static function autoDiscoveryPathsProvider(): array */ private function runCommand(int $expectedStatusCode, array $parameters = []): string { - $commandTester = new CommandTester(new AnalyseCommand([])); + $commandTester = new CommandTester(new AnalyseCommand([], microtime(true))); $commandTester->execute([ 'paths' => [__DIR__ . DIRECTORY_SEPARATOR . 'test'], diff --git a/tests/PHPStan/Command/AnalysisResultTest.php b/tests/PHPStan/Command/AnalysisResultTest.php index ea7cb93fc4..2ea3344a9b 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -45,6 +45,7 @@ public function testErrorsAreSortedByFileNameAndLine(): void true, 0, false, + [], ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index 8c29068a98..bbb00f122c 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -234,12 +234,12 @@ public function dataParameters(): array __DIR__ . '/exclude-paths/full.neon', [ 'excludePaths' => [ - 'analyse' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', - ], 'analyseAndScan' => [ __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', ], + 'analyse' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', + ], ], ], ], @@ -247,13 +247,13 @@ public function dataParameters(): array __DIR__ . '/exclude-paths/including.neon', [ 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test3', + ], 'analyse' => [ __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test', __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test2', ], - 'analyseAndScan' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'exclude-paths' . DIRECTORY_SEPARATOR . 'test3', - ], ], ], ], diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index 4066079d7f..ac972f04a9 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -74,7 +74,7 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'foo.php', ], [ - 'message' => '#^Foo$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -103,7 +103,7 @@ public function dataFormatterOutputProvider(): iterable 'path' => 'foo.php', ], [ - 'message' => '#^Foo$#', + 'message' => '#^Foo\$#', 'count' => 1, 'path' => 'foo.php', ], @@ -150,6 +150,7 @@ public function testFormatErrorMessagesRegexEscape(): void true, 0, false, + [], ); $formatter->formatErrors( $result, @@ -189,6 +190,7 @@ public function testEscapeDiNeon(): void true, 0, false, + [], ); $formatter->formatErrors( @@ -255,6 +257,7 @@ public function testOutputOrdering(array $errors): void true, 0, false, + [], ); $formatter->formatErrors( @@ -414,6 +417,7 @@ public function testEndOfFileNewlines( true, 0, false, + [], ); $resource = fopen('php://memory', 'w', false); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php index 661768da45..b49c717f22 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -163,6 +163,7 @@ public function testFormatErrors(array $errors, string $expectedOutput): void true, 0, true, + [], ), $this->getOutput(), ); diff --git a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php index 172a74ede5..6618b6effe 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -64,7 +64,7 @@ public function dataFormatterOutputProvider(): iterable - + @@ -80,7 +80,7 @@ public function dataFormatterOutputProvider(): iterable - + ', @@ -98,12 +98,12 @@ public function dataFormatterOutputProvider(): iterable - + - + ', @@ -156,6 +156,7 @@ public function testTraitPath(): void true, 0, false, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' @@ -186,6 +187,7 @@ public function testIdentifier(): void true, 0, true, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' diff --git a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php index b6a921e06c..3a93977ce5 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php @@ -45,7 +45,7 @@ public function dataFormatterOutputProvider(): iterable 0, '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 ::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo -::error file=foo.php,line=1,col=0::Foo +::error file=foo.php,line=1,col=0::Foo ::error file=foo.php,line=5,col=0::Bar%0ABar2 ', ]; @@ -56,7 +56,7 @@ public function dataFormatterOutputProvider(): iterable 0, 2, '::error ::first generic error -::error ::second generic error +::error ::second generic ', ]; @@ -67,10 +67,10 @@ public function dataFormatterOutputProvider(): iterable 2, '::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=2,col=0::Bar%0ABar2 ::error file=folder with unicode 😃/file name with "spaces" and unicode 😃.php,line=4,col=0::Foo -::error file=foo.php,line=1,col=0::Foo +::error file=foo.php,line=1,col=0::Foo ::error file=foo.php,line=5,col=0::Bar%0ABar2 ::error ::first generic error -::error ::second generic error +::error ::second generic ', ]; } diff --git a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php index e4d63c28d7..78df3d79dd 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php @@ -88,8 +88,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -152,8 +152,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -194,8 +194,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", "severity": "major", "location": { "path": "", @@ -236,8 +236,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "Foo", - "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "description": "Foo", + "fingerprint": "d7002959fc192c81d51fc41b0a3f240617a1aa35361867b5e924ae8d7fec39cb", "severity": "major", "location": { "path": "with space/and unicode \ud83d\ude03/project/foo.php", @@ -269,8 +269,8 @@ public function dataFormatterOutputProvider(): iterable } }, { - "description": "second generic error", - "fingerprint": "f49870714e8ce889212aefb50f718f88ae63d00dd01c775b7bac86c4466e96f0", + "description": "second generic", + "fingerprint": "adc18b2c27b0ecad40aed7975b165cbe357f0cbba58582af91c0a2e7fa5d77ab", "severity": "major", "location": { "path": "", diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index 3e6008cdf4..9a1eca0188 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -105,7 +105,7 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, @@ -136,7 +136,7 @@ public function dataFormatterOutputProvider(): iterable "files":[], "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -172,7 +172,7 @@ public function dataFormatterOutputProvider(): iterable "errors":2, "messages":[ { - "message": "Foo", + "message": "Foo", "line": 1, "ignorable": true }, @@ -187,7 +187,7 @@ public function dataFormatterOutputProvider(): iterable }, "errors": [ "first generic error", - "second generic error" + "second generic" ] }', ]; @@ -253,7 +253,7 @@ public function testFormatTip(string $tip, string $expectedTip): void $formatter = new JsonErrorFormatter(false); $formatter->formatErrors(new AnalysisResult([ new Error('Foo', '/foo/bar.php', 1, true, null, null, $tip), - ], [], [], [], [], false, null, true, 0, false), $this->getOutput()); + ], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); $content = $this->getOutputContent(); $json = Json::decode($content, Json::FORCE_ARRAY); diff --git a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php index 7a913521ef..f83f162bb2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php @@ -74,7 +74,7 @@ public function dataFormatterOutputProvider(): Generator - + @@ -93,7 +93,7 @@ public function dataFormatterOutputProvider(): Generator - + ', @@ -112,7 +112,7 @@ public function dataFormatterOutputProvider(): Generator - + @@ -121,7 +121,7 @@ public function dataFormatterOutputProvider(): Generator - + ', diff --git a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php index 5b61402bbd..ffe1c436d3 100644 --- a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php @@ -11,72 +11,106 @@ class RawErrorFormatterTest extends ErrorFormatterTestCase public function dataFormatterOutputProvider(): iterable { yield [ - 'No errors', - 0, - 0, - 0, - '', + 'message' => 'No errors', + 'exitCode' => 0, + 'numFileErrors' => 0, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '', ]; yield [ - 'One file error', - 1, - 1, - 0, - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", + 'message' => 'One file error', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", ]; yield [ - 'One generic error', - 1, - 0, - 1, - '?:?:first generic error' . "\n", + 'message' => 'One generic error', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 1, + 'verbose' => false, + 'expected' => '?:?:first generic error' . "\n", ]; yield [ - 'Multiple file errors', - 1, - 4, - 0, - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar' . "\nBar2\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:1:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:5:Bar' . "\nBar2\n", + 'message' => 'Multiple file errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar +Bar2 +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo +/data/folder/with space/and unicode 😃/project/foo.php:1:Foo +/data/folder/with space/and unicode 😃/project/foo.php:5:Bar +Bar2 +', ]; yield [ - 'Multiple generic errors', - 1, - 0, - 2, - '?:?:first generic error' . "\n" . - '?:?:second generic error' . "\n", + 'message' => 'Multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 2, + 'verbose' => false, + 'expected' => '?:?:first generic error +?:?:second generic +', ]; yield [ - 'Multiple file, multiple generic errors', - 1, - 4, - 2, - '?:?:first generic error' . "\n" . - '?:?:second generic error' . "\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar' . "\nBar2\n" . - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:1:Foo' . "\n" . - '/data/folder/with space/and unicode 😃/project/foo.php:5:Bar' . "\nBar2\n", + 'message' => 'Multiple file, multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 2, + 'verbose' => false, + 'expected' => '?:?:first generic error +?:?:second generic +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:2:Bar +Bar2 +/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo +/data/folder/with space/and unicode 😃/project/foo.php:1:Foo +/data/folder/with space/and unicode 😃/project/foo.php:5:Bar +Bar2 +', + ]; + + yield [ + 'message' => 'One file error with tip', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => false, + 'expected' => '/data/folder/with space/and unicode 😃/project/foo.php:5:Foobar\Buz +', + ]; + + yield [ + 'message' => 'One file error with tip and verbose', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => true, + 'expected' => '/data/folder/with space/and unicode 😃/project/foo.php:5:Foobar\Buz [identifier=foobar.buz] +', ]; } /** * @dataProvider dataFormatterOutputProvider - * + * @param array{int, int}|int $numFileErrors */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, + bool $verbose, string $expected, ): void { @@ -84,10 +118,10 @@ public function testFormatErrors( $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput(), + $this->getOutput(false, $verbose), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + $this->assertEquals($expected, $this->getOutputContent(false, $verbose), sprintf('%s: output do not match', $message)); } } diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index 5232f164ff..85121ad337 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -11,7 +11,6 @@ use function getenv; use function putenv; use function sprintf; -use const PHP_VERSION_ID; class TableErrorFormatterTest extends ErrorFormatterTestCase { @@ -24,6 +23,7 @@ protected function setUp(): void protected function tearDown(): void { putenv('COLUMNS'); + putenv('TERM_PROGRAM'); } public function dataFormatterOutputProvider(): iterable @@ -33,6 +33,7 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 0, 'numFileErrors' => 0, 'numGenericErrors' => 0, + 'verbose' => false, 'extraEnvVars' => [], 'expected' => ' [OK] No errors @@ -45,6 +46,7 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 1, 'numFileErrors' => 1, 'numGenericErrors' => 0, + 'verbose' => false, 'extraEnvVars' => [], 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php @@ -63,6 +65,7 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 1, 'numFileErrors' => 0, 'numGenericErrors' => 1, + 'verbose' => false, 'extraEnvVars' => [], 'expected' => ' -- --------------------- Error @@ -81,6 +84,7 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 1, 'numFileErrors' => 4, 'numGenericErrors' => 0, + 'verbose' => false, 'extraEnvVars' => [], 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php @@ -93,7 +97,7 @@ public function dataFormatterOutputProvider(): iterable ------ ---------- Line foo.php ------ ---------- - 1 Foo + 1 Foo 5 Bar Bar2 💡 a tip @@ -109,13 +113,14 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 1, 'numFileErrors' => 0, 'numGenericErrors' => 2, + 'verbose' => false, 'extraEnvVars' => [], - 'expected' => ' -- ---------------------- + 'expected' => ' -- ----------------------- Error - -- ---------------------- + -- ----------------------- first generic error - second generic error - -- ---------------------- + second generic + -- ----------------------- [ERROR] Found 2 errors @@ -128,6 +133,7 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 1, 'numFileErrors' => 4, 'numGenericErrors' => 2, + 'verbose' => false, 'extraEnvVars' => [], 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php @@ -140,18 +146,18 @@ public function dataFormatterOutputProvider(): iterable ------ ---------- Line foo.php ------ ---------- - 1 Foo + 1 Foo 5 Bar Bar2 💡 a tip ------ ---------- - -- ---------------------- + -- ----------------------- Error - -- ---------------------- + -- ----------------------- first generic error - second generic error - -- ---------------------- + second generic + -- ----------------------- [ERROR] Found 6 errors @@ -163,6 +169,7 @@ public function dataFormatterOutputProvider(): iterable 'exitCode' => 1, 'numFileErrors' => 1, 'numGenericErrors' => 0, + 'verbose' => false, 'extraEnvVars' => ['TERM_PROGRAM=vscode'], 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php @@ -171,6 +178,47 @@ public function dataFormatterOutputProvider(): iterable ------ ------------------------------------------------------------------- + [ERROR] Found 1 error + +', + ]; + + yield [ + 'message' => 'One file error with tip', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => false, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------ + Line foo.php + ------ ------------ + 5 Foobar\Buz + 💡 a tip + ------ ------------ + + + [ERROR] Found 1 error + +', + ]; + + yield [ + 'message' => 'One file error with tip and verbose', + 'exitCode' => 1, + 'numFileErrors' => [5, 6], + 'numGenericErrors' => 0, + 'verbose' => true, + 'extraEnvVars' => [], + 'expected' => ' ------ ---------------- + Line foo.php + ------ ---------------- + 5 Foobar\Buz + 🪪 foobar.buz + 💡 a tip + ------ ---------------- + + [ERROR] Found 1 error ', @@ -179,39 +227,39 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider + * @param array{int, int}|int $numFileErrors * @param array $extraEnvVars */ public function testFormatErrors( string $message, int $exitCode, - int $numFileErrors, + array|int $numFileErrors, int $numGenericErrors, + bool $verbose, array $extraEnvVars, string $expected, ): void { - if (PHP_VERSION_ID >= 80100) { - self::markTestSkipped('Skipped on PHP 8.1 because of different result'); - } $formatter = $this->createErrorFormatter(null); + // NOTE: extra env vars need to be cleared in tearDown() foreach ($extraEnvVars as $envVar) { putenv($envVar); } $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput(), + $this->getOutput(false, $verbose), ), sprintf('%s: response code do not match', $message)); - $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); + $this->assertEquals($expected, $this->getOutputContent(false, $verbose), sprintf('%s: output do not match', $message)); } public function testEditorUrlWithTrait(): void { $formatter = $this->createErrorFormatter('editor://%file%/%line%'); $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false), $this->getOutput()); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); $this->assertStringContainsString('Bar.php', $this->getOutputContent()); } @@ -224,7 +272,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'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false), $this->getOutput(true)); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); $this->assertStringContainsString('editor://custom/path/rel/Foo.php', $this->getOutputContent(true)); } @@ -233,7 +281,7 @@ public function testEditorUrlWithCustomTitle(): void { $formatter = $this->createErrorFormatter('editor://any', '%relFile%:%line%'); $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false), $this->getOutput(true)); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); $this->assertStringContainsString('rel/Foo.php:12', $this->getOutputContent(true)); } @@ -260,6 +308,7 @@ public function testBug6727(): void true, 0, false, + [], ), $this->getOutput(), ); diff --git a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php index c0932ac1d1..9e91634634 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php @@ -48,7 +48,7 @@ public function dataFormatterOutputProvider(): iterable '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'2\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'4\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ', ]; @@ -60,7 +60,7 @@ public function dataFormatterOutputProvider(): iterable 2, '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] -##teamcity[inspection typeId=\'phpstan\' message=\'second generic error\' file=\'.\' SEVERITY=\'ERROR\'] +##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] ', ]; @@ -72,10 +72,10 @@ public function dataFormatterOutputProvider(): iterable '##teamcity[inspectionType id=\'phpstan\' name=\'phpstan\' category=\'phpstan\' description=\'phpstan Inspection\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'2\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'folder with unicode 😃/file name with "spaces" and unicode 😃.php\' line=\'4\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] -##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] +##teamcity[inspection typeId=\'phpstan\' message=\'Foo\' file=\'foo.php\' line=\'1\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'\'] ##teamcity[inspection typeId=\'phpstan\' message=\'Bar||nBar2\' file=\'foo.php\' line=\'5\' SEVERITY=\'ERROR\' ignorable=\'1\' tip=\'a tip\'] ##teamcity[inspection typeId=\'phpstan\' message=\'first generic error\' file=\'.\' SEVERITY=\'ERROR\'] -##teamcity[inspection typeId=\'phpstan\' message=\'second generic error\' file=\'.\' SEVERITY=\'ERROR\'] +##teamcity[inspection typeId=\'phpstan\' message=\'second generic\' file=\'.\' SEVERITY=\'ERROR\'] ', ]; } diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index 9dff05c44f..98a6dc58cb 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -126,7 +126,7 @@ public function testValidate( bool $expectAllErrorsIgnored, ): void { - $grammar = new Read('hoa://Library/Regex/Grammar.pp'); + $grammar = new Read(__DIR__ . '/../../../resources/RegexGrammar.pp'); $parser = Llk::load($grammar); $validator = new IgnoredRegexValidator($parser, self::getContainer()->getByType(TypeStringResolver::class)); diff --git a/tests/PHPStan/Composer/AutoloadFilesTest.php b/tests/PHPStan/Composer/AutoloadFilesTest.php index b16b469e75..889d37e1c7 100644 --- a/tests/PHPStan/Composer/AutoloadFilesTest.php +++ b/tests/PHPStan/Composer/AutoloadFilesTest.php @@ -61,9 +61,7 @@ public function testExpectedFiles(): void 'jetbrains/phpstorm-stubs/PhpStormStubsMap.php', // added to phpstan-dist/bootstrap.php '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-timer/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'react/promise/src/functions_include.php', // added to phpstan-dist/bootstrap.php - 'ringcentral/psr7/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'symfony/deprecation-contracts/function.php', // afaik polyfills aren't necessary 'symfony/polyfill-ctype/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-intl-grapheme/bootstrap.php', // afaik polyfills aren't necessary diff --git a/tests/PHPStan/File/FileExcluderTest.php b/tests/PHPStan/File/FileExcluderTest.php index eebe8236e6..7477416b6c 100644 --- a/tests/PHPStan/File/FileExcluderTest.php +++ b/tests/PHPStan/File/FileExcluderTest.php @@ -19,7 +19,7 @@ public function testFilesAreExcludedFromAnalysingOnWindows( { $this->skipIfNotOnWindows(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, false); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } @@ -127,7 +127,7 @@ public function testFilesAreExcludedFromAnalysingOnUnix( { $this->skipIfNotOnUnix(); - $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, false); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } @@ -208,4 +208,70 @@ public function dataExcludeOnUnix(): array ]; } + public function dataNoImplicitWildcard(): iterable + { + yield [ + __DIR__ . '/tests/foo.php', + [ + __DIR__ . '/test', + ], + false, + true, + ]; + + yield [ + __DIR__ . '/tests/foo.php', + [ + __DIR__ . '/test', + ], + true, + false, + ]; + + yield [ + __DIR__ . '/test/foo.php', + [ + __DIR__ . '/test', + ], + true, + true, + ]; + + yield [ + __DIR__ . '/FileExcluderTest.php', + [ + __DIR__ . '/FileExcluderTest.php', + ], + true, + true, + ]; + + yield [ + __DIR__ . '/tests/foo.php', + [ + __DIR__ . '/test*', + ], + true, + true, + ]; + } + + /** + * @dataProvider dataNoImplicitWildcard + * @param string[] $analyseExcludes + */ + public function testNoImplicitWildcard( + string $filePath, + array $analyseExcludes, + bool $noImplicitWildcard, + bool $isExcluded, + ): void + { + $this->skipIfNotOnUnix(); + + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, $noImplicitWildcard); + + $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); + } + } diff --git a/tests/PHPStan/File/test/.gitkeep b/tests/PHPStan/File/test/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/PHPStan/Generics/data/classes-4.json b/tests/PHPStan/Generics/data/classes-4.json new file mode 100644 index 0000000000..3b7dd01977 --- /dev/null +++ b/tests/PHPStan/Generics/data/classes-4.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to new PHPStan\\Generics\\Classes\\SomeRule() on a separate line has no effect.", + "line": 283, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-2.json b/tests/PHPStan/Generics/data/variance-2.json index 40e142c2fa..e0db4f89f6 100644 --- a/tests/PHPStan/Generics/data/variance-2.json +++ b/tests/PHPStan/Generics/data/variance-2.json @@ -74,4 +74,4 @@ "line": 153, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php index 5a7b686525..df0dd443c2 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class InferPrivatePropertyTypeFromConstructorIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php index a247012c5e..854d9ded9e 100644 --- a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class NamedArgumentsIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php index 1c4dcf3142..f4138d043f 100644 --- a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php +++ b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class StubValidatorIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/StubsIntegrationTest.php b/tests/PHPStan/Levels/StubsIntegrationTest.php index 11ad056599..dfec2e90f5 100644 --- a/tests/PHPStan/Levels/StubsIntegrationTest.php +++ b/tests/PHPStan/Levels/StubsIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class StubsIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/data/acceptTypes-4.json b/tests/PHPStan/Levels/data/acceptTypes-4.json new file mode 100644 index 0000000000..fbcb96fc5a --- /dev/null +++ b/tests/PHPStan/Levels/data/acceptTypes-4.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 531, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 532, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 542, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 543, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index cc889b6d52..88d4749a45 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -94,16 +94,6 @@ "line": 251, "ignorable": true }, - { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 319, - "ignorable": true - }, - { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 320, - "ignorable": true - }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float given.", "line": 412, @@ -164,6 +154,11 @@ "line": 585, "ignorable": true }, + { + "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects static(Levels\\AcceptTypes\\RequireObjectWithoutClassType), object given.", + "line": 648, + "ignorable": true + }, { "message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).", "line": 671, diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index b43e3e9303..9e0afc2f64 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -35,25 +35,35 @@ "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 283, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 284, "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 301, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 302, "ignorable": true }, + { + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 319, + "ignorable": true + }, + { + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 320, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|int given.", "line": 415, @@ -129,11 +139,6 @@ "line": 647, "ignorable": true }, - { - "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.", - "line": 648, - "ignorable": true - }, { "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).", "line": 690, diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7.json b/tests/PHPStan/Levels/data/arrayDimFetches-7.json index 8ff137110b..23df32943a 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -28,5 +28,10 @@ "message": "Cannot access offset 'foo' on iterable.", "line": 58, "ignorable": true + }, + { + "message": "Cannot access offset 'foo' on iterable.", + "line": 66, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/binaryOps.php b/tests/PHPStan/Levels/data/binaryOps.php index 7b04fedbae..fd53d3f946 100644 --- a/tests/PHPStan/Levels/data/binaryOps.php +++ b/tests/PHPStan/Levels/data/binaryOps.php @@ -18,14 +18,14 @@ public function doFoo( $stringOrObject ) { - $int + $int; - $int + $intOrString; - $int + $stringOrObject; - $int + $string; - $string + $string; - $intOrString + $stringOrObject; - $intOrString + $string; - $stringOrObject + $stringOrObject; + $result = $int + $int; + $result = $int + $intOrString; + $result = $int + $stringOrObject; + $result = $int + $string; + $result = $string + $string; + $result = $intOrString + $stringOrObject; + $result = $intOrString + $string; + $result = $stringOrObject + $stringOrObject; } } diff --git a/tests/PHPStan/Levels/data/callableCalls.php b/tests/PHPStan/Levels/data/callableCalls.php index 4b22fa723c..52f311d8d8 100644 --- a/tests/PHPStan/Levels/data/callableCalls.php +++ b/tests/PHPStan/Levels/data/callableCalls.php @@ -23,7 +23,7 @@ public function doFoo( $c(); $d(); $f = function (int $i) { - + echo '1'; }; $f(1); $f(1.1); diff --git a/tests/PHPStan/Levels/data/callableVariance-4.json b/tests/PHPStan/Levels/data/callableVariance-4.json new file mode 100644 index 0000000000..1af09ec95a --- /dev/null +++ b/tests/PHPStan/Levels/data/callableVariance-4.json @@ -0,0 +1,27 @@ +[ + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 81, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 82, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 83, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 84, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 85, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/callableVariance-5.json b/tests/PHPStan/Levels/data/callableVariance-5.json index 81682ac6e7..c37c7adeb0 100644 --- a/tests/PHPStan/Levels/data/callableVariance-5.json +++ b/tests/PHPStan/Levels/data/callableVariance-5.json @@ -1,6 +1,6 @@ [ { - "message": "Parameter #1 $ of callable callable(Levels\\CallableVariance\\B): void expects Levels\\CallableVariance\\B, Levels\\CallableVariance\\A given.", + "message": "Parameter #1 of callable callable(Levels\\CallableVariance\\B): void expects Levels\\CallableVariance\\B, Levels\\CallableVariance\\A given.", "line": 14, "ignorable": true }, @@ -39,4 +39,4 @@ "line": 85, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/clone.php b/tests/PHPStan/Levels/data/clone.php index 6bd72cfe38..4697debb13 100644 --- a/tests/PHPStan/Levels/data/clone.php +++ b/tests/PHPStan/Levels/data/clone.php @@ -26,14 +26,14 @@ public function doFoo( $mixed ) { - clone $int; - clone $intOrString; - clone $foo; - clone $nullableFoo; - clone $fooOrInt; - clone $nullableInt; - clone $nullableUnion; - clone $mixed; + $result = clone $int; + $result = clone $intOrString; + $result = clone $foo; + $result = clone $nullableFoo; + $result = clone $fooOrInt; + $result = clone $nullableInt; + $result = clone $nullableUnion; + $result = clone $mixed; } } diff --git a/tests/PHPStan/Levels/data/comparison.php b/tests/PHPStan/Levels/data/comparison.php index d208e6051c..ee10f825be 100644 --- a/tests/PHPStan/Levels/data/comparison.php +++ b/tests/PHPStan/Levels/data/comparison.php @@ -8,29 +8,29 @@ class Foo private const FOO_CONST = 'foo'; /** - * @param \stdClass $object + * @param \stdClass $object * @param int $int - * @param float $float + * @param float $float * @param string $string * @param int|string $intOrString * @param int|\stdClass $intOrObject */ public function doFoo( - \stdClass $object, + \stdClass $object, int $int, float $float, string $string, $intOrString, - $intOrObject + $intOrObject ) { - $object == $int; - $object == $float; - $object == $string; - $object == $intOrString; - $object == $intOrObject; + $result = $object == $int; + $result = $object == $float; + $result = $object == $string; + $result = $object == $intOrString; + $result = $object == $intOrObject; - self::FOO_CONST === 'bar'; + $result = self::FOO_CONST === 'bar'; } public function doBar(\ffmpeg_movie $movie): void diff --git a/tests/PHPStan/Levels/data/unreachable-0.json b/tests/PHPStan/Levels/data/unreachable-0.json new file mode 100644 index 0000000000..5091fec153 --- /dev/null +++ b/tests/PHPStan/Levels/data/unreachable-0.json @@ -0,0 +1,12 @@ +[ + { + "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 d43f7df7d9..6f937312ae 100644 --- a/tests/PHPStan/Levels/data/unreachable-4.json +++ b/tests/PHPStan/Levels/data/unreachable-4.json @@ -69,11 +69,6 @@ "line": 89, "ignorable": true }, - { - "message": "Unused result of ternary operator.", - "line": 89, - "ignorable": true - }, { "message": "Ternary operator condition is always true.", "line": 94, diff --git a/tests/PHPStan/Parser/CachedParserTest.php b/tests/PHPStan/Parser/CachedParserTest.php index 76bb9e215d..13505dbce7 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Parser; +use Generator; +use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; @@ -13,22 +15,20 @@ class CachedParserTest extends PHPStanTestCase /** * @dataProvider dataParseFileClearCache - * @param int $cachedNodesByStringCountMax - * @param int $cachedNodesByStringCountExpected */ public function testParseFileClearCache( int $cachedNodesByStringCountMax, - int $cachedNodesByStringCountExpected + int $cachedNodesByStringCountExpected, ): void { $parser = new CachedParser( $this->getParserMock(), - $cachedNodesByStringCountMax + $cachedNodesByStringCountMax, ); $this->assertEquals( $cachedNodesByStringCountMax, - $parser->getCachedNodesByStringCountMax() + $parser->getCachedNodesByStringCountMax(), ); // Add strings to cache @@ -38,16 +38,16 @@ public function testParseFileClearCache( $this->assertEquals( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByStringCount() + $parser->getCachedNodesByStringCount(), ); $this->assertCount( $cachedNodesByStringCountExpected, - $parser->getCachedNodesByString() + $parser->getCachedNodesByString(), ); } - public function dataParseFileClearCache(): \Generator + public function dataParseFileClearCache(): Generator { yield 'even' => [ 'cachedNodesByStringCountMax' => 50, @@ -70,9 +70,9 @@ private function getParserMock(): Parser&MockObject return $mock; } - private function getPhpParserNodeMock(): \PhpParser\Node&MockObject + private function getPhpParserNodeMock(): Node&MockObject { - return $this->createMock(\PhpParser\Node::class); + return $this->createMock(Node::class); } public function testParseTheSameFileWithDifferentMethod(): void diff --git a/tests/PHPStan/Parser/RichParserTest.php b/tests/PHPStan/Parser/RichParserTest.php index 228e562277..103eb129b4 100644 --- a/tests/PHPStan/Parser/RichParserTest.php +++ b/tests/PHPStan/Parser/RichParserTest.php @@ -61,14 +61,6 @@ public function dataLinesToIgnore(): iterable ], ]; - yield [ - ' ['return.ref', 'return.non'], - ], - ]; - yield [ ' ['return.ref', 'return.non'], ], @@ -216,13 +208,57 @@ public function dataLinesToIgnore(): iterable yield [ ' ['identifier', 'identifier2'], + ], + ]; + + yield [ + ' ['identifier', 'identifier2', 'identifier3'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['identifier'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + 2 => ['identifier'], ], ]; } @@ -237,8 +273,8 @@ public function testLinesToIgnore(string $code, array $expectedLines): void $parser = self::getContainer()->getService('currentPhpVersionRichParser'); $ast = $parser->parseString($code); $lines = $ast[0]->getAttribute('linesToIgnore'); - $this->assertSame($expectedLines, $lines); $this->assertNull($ast[0]->getAttribute('linesToIgnoreParseErrors')); + $this->assertSame($expectedLines, $lines); } public function dataLinesToIgnoreParseErrors(): iterable @@ -251,7 +287,51 @@ public function dataLinesToIgnoreParseErrors(): iterable ' * return.non,' . PHP_EOL . ' */', [ - 4 => ['Unexpected comma (,)'], + 4 => ['Unexpected comma (,) after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected comma (,) after @phpstan-ignore, expected identifier'], ], ]; @@ -259,7 +339,7 @@ public function dataLinesToIgnoreParseErrors(): iterable ' ['Closing parenthesis ")" before opening parenthesis "("'], + 2 => ['Unexpected T_CLOSE_PARENTHESIS after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS'], ], ]; @@ -267,7 +347,131 @@ public function dataLinesToIgnoreParseErrors(): iterable ' ['Unclosed opening parenthesis "(" without closing parenthesis ")"'], + 2 => ['Unexpected end, unclosed opening parenthesis'], + ], + ]; + + yield [ + ' ['Unexpected T_OPEN_PARENTHESIS after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected end after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ['Unexpected comma (,) after @phpstan-ignore, expected identifier'], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čumim' after @phpstan-ignore, expected identifier"], + ], + ]; + + yield [ + ' ['Unexpected end, unclosed opening parenthesis'], + ], + ]; + + yield [ + ' ['Unexpected identifier after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS'], + ], + ]; + + yield [ + ' ['Unexpected T_CLOSE_PARENTHESIS after T_CLOSE_PARENTHESIS, expected comma (,) or end'], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čoun' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čoun' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER 'čičí' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER '--' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER '[comment]' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ['Unexpected T_OPEN_PARENTHESIS after T_CLOSE_PARENTHESIS, expected comma (,) or end'], + ], + ]; + + yield [ + ' ["Unexpected T_OTHER '://example.com' after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS"], + ], + ]; + + yield [ + ' ['Unexpected end after comma (,), expected identifier'], ], ]; } diff --git a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php index e7ea48c599..03218eff04 100644 --- a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -47,7 +47,7 @@ public function testGetProjectStubFilesWhenPathContainsWindowsSeparator(): void */ private function createDefaultStubFilesProvider(array $stubFiles): DefaultStubFilesProvider { - return new DefaultStubFilesProvider($this->getContainer(), $stubFiles, $this->currentWorkingDirectory); + return new DefaultStubFilesProvider($this->getContainer(), $stubFiles, [$this->currentWorkingDirectory]); } } diff --git a/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php new file mode 100644 index 0000000000..9310f617f1 --- /dev/null +++ b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php @@ -0,0 +1,107 @@ +> + */ +class AnonymousClassReflectionTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class (self::createReflectionProvider()) implements Rule { + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isAnonymous()) { + return []; + } + + Assert::assertTrue($node->getAttribute('anonymousClass')); + + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($node, $scope); + + return [ + RuleErrorBuilder::message(sprintf( + "name: %s\ndisplay name: %s", + $classReflection->getName(), + $classReflection->getDisplayName(), + ))->identifier('test.anonymousClassReflection')->build(), + ]; + } + + }; + } + + public function testReflection(): void + { + $this->analyse([__DIR__ . '/data/anonymous-classes.php'], [ + [ + implode("\n", [ + 'name: AnonymousClass0c307d7b8501323d1d30b0afea7e0578', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:5', + ]), + 5, + ], + [ + implode("\n", [ + 'name: AnonymousClassa16017c480192f8fbf3c03e17840e99c', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:1', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClassd68d75f1cdac379350e3027c09a7c5a0', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:2', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClass75aa798fed4f30306c14dcf03a50878c', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:3', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClass4fcabdc52bfed5f8c101f3f89b2180bd', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:1', + ]), + 9, + ], + [ + implode("\n", [ + 'name: AnonymousClass0e77d7995f4c47dcd5402817970fd7e0', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:2', + ]), + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php index f56a41be51..6e4c91a783 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -14,7 +14,7 @@ function testFunctionForLocator(): void // phpcs:disable { - + echo 'test'; } class AutoloadSourceLocatorTest extends PHPStanTestCase diff --git a/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php new file mode 100644 index 0000000000..77d575716e --- /dev/null +++ b/tests/PHPStan/Reflection/Constant/RuntimeConstantReflectionTest.php @@ -0,0 +1,58 @@ += 80100 ? TrinaryLogic::createYes() : TrinaryLogic::createNo(), + null, + ]; + + yield [ + new Name('\CURLOPT_FTP_SSL'), + TrinaryLogic::createYes(), + 'use CURLOPT_USE_SSL instead.', + ]; + + yield [ + new Name('\DeprecatedConst\FINE'), + TrinaryLogic::createNo(), + null, + ]; + yield [ + new Name('\DeprecatedConst\MY_CONST'), + TrinaryLogic::createYes(), + null, + ]; + yield [ + new Name('\DeprecatedConst\MY_CONST2'), + TrinaryLogic::createYes(), + "don't use it!", + ]; + } + + /** + * @dataProvider dataDeprecatedConstants + */ + public function testDeprecatedConstants(Name $constName, TrinaryLogic $isDeprecated, ?string $deprecationMessage): void + { + require_once __DIR__ . '/data/deprecated-constant.php'; + + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertTrue($reflectionProvider->hasConstant($constName, null)); + $this->assertSame($isDeprecated->describe(), $reflectionProvider->getConstant($constName, null)->isDeprecated()->describe()); + $this->assertSame($deprecationMessage, $reflectionProvider->getConstant($constName, null)->getDeprecatedDescription()); + } + +} diff --git a/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php b/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php new file mode 100644 index 0000000000..9dfc01dea9 --- /dev/null +++ b/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php @@ -0,0 +1,15 @@ + TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $type = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $type, diff --git a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php index e6fe0eed75..d7b8ee4599 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php @@ -31,6 +31,7 @@ use function mkdir; use function sort; use function strpos; +use function substr; use function trim; use const PHP_INT_MAX; use const PHP_VERSION_ID; @@ -73,6 +74,10 @@ private static function generateSymbolDescription(string $symbol): string { [$type, $name] = explode(' ', $symbol); + if ($name === '') { + throw new ShouldNotHappenException(); + } + try { switch ($type) { case 'FUNCTION': @@ -152,6 +157,9 @@ private static function getPhpSymbolsFile(): string return __DIR__ . '/data/golden/phpSymbols.txt'; } + /** + * @param non-empty-string $functionName + */ private static function generateFunctionDescription(string $functionName): string { $nameNode = new Name($functionName); @@ -410,6 +418,8 @@ private static function generateVariantsDescription(string $name, array $variant private static function generateClassPropertyDescription(string $propertyName): string { [$className, $propertyName] = explode('::', $propertyName); + // remove $ + $propertyName = substr($propertyName, 1); $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); diff --git a/tests/PHPStan/Reflection/ReflectionProviderTest.php b/tests/PHPStan/Reflection/ReflectionProviderTest.php index 06f7545d49..02dfd6869f 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -51,6 +51,8 @@ public function dataFunctionThrowType(): iterable /** * @dataProvider dataFunctionThrowType + * + * @param non-empty-string $functionName */ public function testFunctionThrowType(string $functionName, ?Type $expectedThrowType): void { @@ -96,6 +98,8 @@ public function dataFunctionDeprecated(): iterable /** * @dataProvider dataFunctionDeprecated + * + * @param non-empty-string $functionName */ public function testFunctionDeprecated(string $functionName, bool $isDeprecated): void { @@ -110,7 +114,7 @@ public function dataMethodThrowType(): array [ DateTime::class, '__construct', - new ObjectType('Exception'), + new ObjectType('DateMalformedStringException'), ], [ DateTime::class, diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index ad5aa4077f..88a1b9d9c7 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -14,6 +14,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; @@ -189,7 +190,8 @@ public function dataMethods(): array 'optional' => true, 'type' => new UnionType([ new ObjectWithoutClassType(), - new StringType(), + new ClassStringType(), + new ConstantStringType('static'), new NullType(), ]), 'nativeType' => new UnionType([ diff --git a/tests/PHPStan/Reflection/data/anonymous-classes.php b/tests/PHPStan/Reflection/data/anonymous-classes.php new file mode 100644 index 0000000000..9336316ff9 --- /dev/null +++ b/tests/PHPStan/Reflection/data/anonymous-classes.php @@ -0,0 +1,13 @@ +bleedingEdge), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge, false, false), ); } diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index e0a4ce3798..87b5a12e5f 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -45,6 +45,22 @@ public function testDuplicateKeys(): void 'Array has 2 duplicate keys with value 2 ($idx, $idx).', 55, ], + [ + 'Array has 2 duplicate keys with value 0 (0, 0).', + 63, + ], + [ + 'Array has 2 duplicate keys with value 101 (101, 101).', + 67, + ], + [ + 'Array has 2 duplicate keys with value 102 (102, 102).', + 69, + ], + [ + 'Array has 2 duplicate keys with value -41 (-41, -41).', + 76, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php index b6abc55a97..dbb64b29ca 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -15,9 +17,11 @@ class IterableInForeachRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false)); + return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testCheckWithMaybes(): void @@ -80,4 +84,60 @@ public function testBug4335(): void $this->analyse([__DIR__ . '/data/bug-4335.php'], []); } + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Argument of an invalid type T of mixed supplied for foreach, only iterables are supported.', + 11, + ], + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 14, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 17, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/foreach-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 12402d1ee7..deacff9371 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -15,15 +15,21 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + 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, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), true, ); } @@ -105,17 +111,13 @@ public function testRule(): void 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 228, ], - [ - 'Offset string does not exist on array.', - 240, - ], [ 'Cannot access offset \'a\' on Closure(): void.', 253, ], [ - 'Offset string does not exist on array.', - 308, + 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', + 258, ], [ 'Offset null does not exist on array.', @@ -250,17 +252,13 @@ public function testRuleBleedingEdge(): void 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 228, ], - [ - 'Offset string does not exist on array.', - 240, - ], [ 'Cannot access offset \'a\' on Closure(): void.', 253, ], [ - 'Offset string does not exist on array.', - 308, + 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', + 258, ], [ 'Offset null does not exist on array.', @@ -589,7 +587,7 @@ public function testBug5758(): void public function testBug5223(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-5223.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5223.php'], [ [ 'Offset \'something\' does not exist on array{categoryKeys: array, tagNames: array}.', 26, @@ -680,12 +678,16 @@ public function testBug8068(): void "Cannot access offset 'path' on Closure.", 18, ], + [ + "Cannot access offset 'path' on iterable.", + 26, + ], ]); } public function testBug6243(): void { - if (PHP_VERSION_ID < 704000) { + if (PHP_VERSION_ID < 70400) { $this->markTestSkipped('Test requires PHP 7.4.'); } @@ -747,4 +749,119 @@ public function testBug8166(): void ]); } + public function testBug10926(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-10926.php'], [ + [ + 'Cannot access offset \'a\' on stdClass.', + 10, + ], + ]); + } + + public function testMixed(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-mixed.php'], [ + [ + 'Cannot access offset 5 on T of mixed.', + 11, + ], + [ + 'Cannot access offset 5 on mixed.', + 16, + ], + [ + 'Cannot access offset 5 on mixed.', + 21, + ], + ]); + } + + public function testOffsetAccessLegal(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-legal.php'], [ + [ + 'Cannot access offset 0 on Closure(): void.', + 7, + ], + [ + 'Cannot access offset 0 on stdClass.', + 12, + ], + [ + 'Cannot access offset 0 on array{\'test\'}|stdClass.', + 96, + ], + [ + 'Cannot access offset 0 on array{\'test\'}|(Closure(): void).', + 98, + ], + ]); + } + + public function testNonExistentParentOffsetAccessLegal(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-legal-non-existent-parent.php'], [ + [ + 'Cannot access offset 0 on parent.', + 9, + ], + ]); + } + + public function dataReportPossiblyNonexistentArrayOffset(): iterable + { + yield [false, false, []]; + yield [false, true, [ + [ + 'Offset string might not exist on array{foo: 1}.', + 20, + ], + ]]; + yield [true, false, [ + [ + "Offset 'foo' might not exist on array.", + 9, + ], + ]]; + yield [true, true, [ + [ + "Offset 'foo' might not exist on array.", + 9, + ], + [ + 'Offset string might not exist on array{foo: 1}.', + 20, + ], + ]]; + } + + /** + * @dataProvider dataReportPossiblyNonexistentArrayOffset + * @param list $errors + */ + public function testReportPossiblyNonexistentArrayOffset(bool $reportPossiblyNonexistentGeneralArrayOffset, bool $reportPossiblyNonexistentConstantArrayOffset, array $errors): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = $reportPossiblyNonexistentGeneralArrayOffset; + $this->reportPossiblyNonexistentConstantArrayOffset = $reportPossiblyNonexistentConstantArrayOffset; + + $this->analyse([__DIR__ . '/data/report-possibly-nonexistent-array-offset.php'], $errors); + } + + public function testBug10997(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->analyse([__DIR__ . '/data/bug-10997.php'], [ + [ + 'Offset int<0, 4> might not exist on array{1, 2, 3, 4}.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index ea8b897b57..bf704ac725 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -153,7 +153,7 @@ public function testBug1714(): void public function testBug8015(): void { $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8015.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8015.php'], []); } } diff --git a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php index 8306d8040d..32d4900686 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -13,9 +15,13 @@ class UnpackIterableInArrayRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testRule(): void @@ -50,4 +56,60 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Only iterables can be unpacked, T of mixed given.', + 11, + ], + [ + 'Only iterables can be unpacked, mixed given.', + 12, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Only iterables can be unpacked, mixed given.', + 13, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/unpack-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10926.php b/tests/PHPStan/Rules/Arrays/data/bug-10926.php new file mode 100644 index 0000000000..e8316112d5 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10926.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug10926; + +class HelloWorld +{ + public function sayHello(?\stdClass $date): void + { + $date ??= new \stdClass(); + echo isset($date['a']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10997.php b/tests/PHPStan/Rules/Arrays/data/bug-10997.php new file mode 100644 index 0000000000..183004bdc2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10997.php @@ -0,0 +1,17 @@ + 2, + 100 => 3, + 'This key is ignored' => 42, + 4, // Key is 101 + 10 => 5, + 6, // Key is 102 + 101 => 7, + 102 => 8, + ]; + + $foo2 = [ + '-42' => 1, + 2, // The key is -41 + 0 => 3, + -41 => 4, + ]; + + $foo3 = [ + $int => 33, + 0 => 1, + 2, // Because of `$int` key, the key value cannot be known. + 1 => 3, + ]; + + $foo4 = [ + 1, + 2, + 3, + ]; + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/foreach-mixed.php b/tests/PHPStan/Rules/Arrays/data/foreach-mixed.php new file mode 100644 index 0000000000..6a2a305fae --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/foreach-mixed.php @@ -0,0 +1,19 @@ += 8.0 + +namespace ForeachMixed; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + foreach ($t as $v) { + } + + foreach ($explicit as $v) { + } + + foreach ($implicit as $v) { + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php b/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php new file mode 100644 index 0000000000..f5189b550f --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php @@ -0,0 +1,11 @@ += 8.0 + +namespace OffsetAccessLegal; + +function closure(): void +{ + (function(){})[0] ?? "error"; +} + +function nonArrayAccessibleObject() +{ + (new \stdClass())[0] ?? "error"; +} + +function arrayAccessibleObject() +{ + (new class implements \ArrayAccess { + public function offsetExists($offset) { + return true; + } + + public function offsetGet($offset) { + return $offset; + } + + public function offsetSet($offset, $value) { + } + + public function offsetUnset($offset) { + } + })[0] ?? "ok"; +} + +function array_(): void +{ + [0][0] ?? "ok"; +} + +function integer(): void +{ + (0)[0] ?? 'ok'; +} + +function float(): void +{ + (0.0)[0] ?? 'ok'; +} + +function null(): void +{ + (null)[0] ?? 'ok'; +} + +function bool(): void +{ + (true)[0] ?? 'ok'; +} + +function void(): void +{ + ((function (){})())[0] ?? 'ok'; +} + +function resource(): void +{ + (tmpfile())[0] ?? 'ok'; +} + +function offsetAccessibleMaybeAndLegal(): void +{ + $arrayAccessible = rand() ? (new class implements \ArrayAccess { + public function offsetExists($offset) { + return true; + } + + public function offsetGet($offset) { + return $offset; + } + + public function offsetSet($offset, $value) { + } + + public function offsetUnset($offset) { + } + }) : false; + + ($arrayAccessible)[0] ?? "ok"; + + (rand() ? "string" : true)[0] ?? "ok"; +} + +function offsetAccessibleMaybeAndIllegal(): void +{ + $arrayAccessible = rand() ? new \stdClass() : ['test']; + + ($arrayAccessible)[0] ?? "error"; + + (rand() ? function(){} : ['test'])[0] ?? "error"; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php b/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php new file mode 100644 index 0000000000..9f3300ce65 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php @@ -0,0 +1,22 @@ += 8.0 + +namespace OffsetAccessMixed; + +/** + * @template T + * @param T $a + */ +function foo(mixed $a): void +{ + var_dump($a[5]); +} + +function foo2(mixed $a): void +{ + var_dump($a[5]); +} + +function foo3($a): void +{ + var_dump($a[5]); +} diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php new file mode 100644 index 0000000000..fc54d96f00 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -0,0 +1,60 @@ + 1]; + echo $a[$s]; + } + + /** + * @param array{bool|float|int|string|null} $a + * @return void + */ + public function testConstantArray(array $a): void + { + echo $a[0]; + } + + /** + * @param array $a + * @return void + */ + public function testConstantArray2(array $a): void + { + if (isset($a[0])) { + echo $a[0]; + } + } + + /** + * @param array{0: '9', A: 'Z', a: 'z'} $a + * @param '0'|'A'|'a' $dim + */ + public function testDimUnion(array $a, string $dim): void + { + echo $a[$dim]; + } + + /** + * @param non-empty-list $a + */ + public function nonEmpty(array $a): void + { + echo $a[0]; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/unpack-mixed.php b/tests/PHPStan/Rules/Arrays/data/unpack-mixed.php new file mode 100644 index 0000000000..0270fbc3eb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/unpack-mixed.php @@ -0,0 +1,14 @@ += 8.0 + +namespace UnpackMixed; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump([...$t]); + var_dump([...$explicit]); + var_dump([...$implicit]); +} diff --git a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index 89100fa22c..5734b47928 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -13,10 +15,14 @@ class InvalidCastRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, false, false, true, false)); + return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testRule(): void @@ -82,4 +88,84 @@ public function testCastObjectToString(): void ]); } + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Cannot cast T to int.', + 11, + ], + [ + 'Cannot cast T to float.', + 13, + ], + [ + 'Cannot cast T to string.', + 14, + ], + [ + 'Cannot cast mixed to int.', + 18, + ], + [ + 'Cannot cast mixed to float.', + 20, + ], + [ + 'Cannot cast mixed to string.', + 21, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Cannot cast mixed to int.', + 25, + ], + [ + 'Cannot cast mixed to float.', + 27, + ], + [ + 'Cannot cast mixed to string.', + 28, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkImplicitMixed = $checkImplicitMixed; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/mixed-cast.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Cast/data/mixed-cast.php b/tests/PHPStan/Rules/Cast/data/mixed-cast.php new file mode 100644 index 0000000000..73085a755e --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/mixed-cast.php @@ -0,0 +1,31 @@ += 8.0 + +namespace MixedCast; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump((int) $t); + var_dump((bool) $t); + var_dump((float) $t); + var_dump((string) $t); + var_dump((array) $t); + var_dump((object) $t); + + var_dump((int) $explicit); + var_dump((bool) $explicit); + var_dump((float) $explicit); + var_dump((string) $explicit); + var_dump((array) $explicit); + var_dump((object) $explicit); + + var_dump((int) $implicit); + var_dump((bool) $implicit); + var_dump((float) $implicit); + var_dump((string) $implicit); + var_dump((array) $implicit); + var_dump((object) $implicit); +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 42201f8276..6fa6252277 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -38,7 +40,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index cd0391d560..b132e3fe08 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -37,7 +39,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 37bc5cf229..42378de1f2 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -19,8 +21,16 @@ class ClassConstantRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false, false, true, false), new ClassCaseSensitivityCheck($broker, true), new PhpVersion($this->phpVersion)); + $reflectionProvider = $this->createReflectionProvider(); + return new ClassConstantRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new PhpVersion($this->phpVersion), + ); } public function testClassConstant(): void @@ -396,4 +406,18 @@ public function testClassConstFetchDefined(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 28, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php index e3ba1adb14..fa3ca260c9 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassInClassExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInClassExtendsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } @@ -91,4 +96,63 @@ public function testEnums(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 34, + $tip, + ], + [ + 'Referencing prefixed Rector class: RectorPrefix202302\AClass.', + 56, + $tip, + ], + [ + 'Referencing prefixed PHP-Scoper class: _PhpScoper19ae93be897e\AClass.', + 59, + $tip, + ], + [ + 'Referencing prefixed PHPUnit class: PHPUnitPHAR\SebastianBergmann\Diff\Exception.', + 62, + 'This is most likely unintentional. Did you mean to type \SebastianBergmann\Diff\Exception?', + ], + [ + 'Referencing prefixed Box class: _HumbugBox02f3b3909847\AClass.', + 73, + $tip, + ], + ]); + } + + public function testReadonly(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('This test needs PHP 8.2'); + } + + $this->analyse([__DIR__ . '/data/extends-readonly-class.php'], [ + [ + 'Readonly class ExtendsReadOnlyClass\Foo extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 25, + ], + [ + 'Non-readonly class ExtendsReadOnlyClass\Bar extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 30, + ], + [ + 'Anonymous non-readonly class extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 35, + ], + [ + 'Anonymous readonly class extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php index 0a0ced5620..4018d2ca62 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,10 +16,13 @@ class ExistingClassInInstanceOfRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInInstanceOfRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php index 0e78d7b9fc..49b083cd46 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassInTraitUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInTraitUseRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index 0c09626ef8..12183ebc85 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassesInClassImplementsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInClassImplementsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php index c6cdcc20ad..f83d296867 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -18,7 +20,10 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInEnumImplementsRule( - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index 4a3a30cf20..cadda3b992 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php new file mode 100644 index 0000000000..9d3ea64034 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -0,0 +1,60 @@ + + */ +class ForbiddenNameCheckExtensionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new InstantiationRule( + $reflectionProvider, + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge(parent::getAdditionalConfigFiles(), [ + __DIR__ . '/data/forbidden-name-class-extension.neon', + ]); + } + + public function testInternalClassFromExtensions(): void + { + $this->analyse([__DIR__ . '/data/forbidden-name-class-extension.php'], [ + [ + 'Referencing prefixed Doctrine class: App\GeneratedProxy\__CG__\App\TestDoctrineEntity.', + 31, + 'This is most likely unintentional. Did you mean to type \App\TestDoctrineEntity?', + ], + [ + 'Referencing prefixed PHPStan class: _PHPStan_15755dag8c\TestPhpStanEntity.', + 32, + 'This is most likely unintentional. Did you mean to type \TestPhpStanEntity?', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index 271ab2dd83..562401245b 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -600,7 +600,7 @@ public function testBug10201(): void $this->checkAlwaysTrueInstanceOf = true; $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-10201.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10201.php'], [ [ 'Instanceof between string and Bug10201\Hello will always evaluate to false.', 13, diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 5f9b6c7cb3..36dc32c352 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -21,11 +23,14 @@ class InstantiationRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( - $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), - new ClassCaseSensitivityCheck($broker, true), + $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 ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), ); } @@ -271,6 +276,10 @@ public function testPromotedProperties(): void 'Parameter #2 $bar of class InstantiationPromotedProperties\Bar constructor expects array, array given.', 33, ], + [ + 'Parameter #1 $intProp of class InstantiationPromotedProperties\PromotedPropertyNotNullable constructor expects int, null given.', + 46, + ], ]); } @@ -448,7 +457,7 @@ public function testBug3311a(): void public function testBug9341(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-9341.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9341.php'], []); } public function testBug7574(): void @@ -471,4 +480,27 @@ public function testBug10324(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 30, + $tip, + ], + ]); + } + + public function testBug9659(): void + { + $this->analyse([__DIR__ . '/data/bug-9659.php'], []); + } + + public function testBug10248(): void + { + $this->analyse([__DIR__ . '/data/bug-10248.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/MixinRuleTest.php b/tests/PHPStan/Rules/Classes/MixinRuleTest.php index 0a2c9a4922..a396245146 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -22,7 +24,10 @@ protected function getRule(): Rule return new MixinRule( $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new GenericObjectTypeCheck(), new MissingTypehintCheck(true, true, true, true, []), new UnresolvableTypeHelper(), @@ -56,7 +61,6 @@ public function testRule(): void [ 'PHPDoc tag @mixin contains generic class ReflectionClass but does not specify its types: T', 50, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'PHPDoc tag @mixin contains unknown class MixinRule\UnknownestClass.', diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index 7aa4f4e62f..b6530920df 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -43,4 +43,9 @@ public function testBug1917(): void $this->analyse([__DIR__ . '/data/bug-1917.php'], []); } + public function testBug10865(): void + { + $this->analyse([__DIR__ . '/data/bug-10865.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-10248.php b/tests/PHPStan/Rules/Classes/data/bug-10248.php new file mode 100644 index 0000000000..ab21ff74b2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10248.php @@ -0,0 +1,31 @@ +=8.0 + +namespace Bug10248; + +class A { + public function __construct(DateTimeInterface|float $value) { + var_dump($value); + } +} + +class B { + public function __construct(float $value) { + var_dump($value); + } +} + +/** + * @return int + */ +function getInt(): int{return 1;} + +/** + * @return int<0, max> + */ +function getRangeInt(): int{return 1;} + +new A(123); +new A(getInt()); +new A(getRangeInt()); + +new B(getRangeInt()); diff --git a/tests/PHPStan/Rules/Classes/data/bug-10865.php b/tests/PHPStan/Rules/Classes/data/bug-10865.php new file mode 100644 index 0000000000..7fd8f0d04a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10865.php @@ -0,0 +1,21 @@ + $args */ + public function __construct(array $args) { + + var_dump($args); + } +} + +class Test extends TestParent { + + public function __construct(int $a) { + + parent::__construct(get_defined_vars()); + //parent::__construct(func_get_args()); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-9659.php b/tests/PHPStan/Rules/Classes/data/bug-9659.php new file mode 100644 index 0000000000..b78128fdca --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9659.php @@ -0,0 +1,17 @@ +=8.0 + +namespace Bug9659; + +class HelloWorld +{ + /** + * @param float|null $timeout + */ + public function __construct($timeout = null) + { + var_dump($timeout); + } +} + +new HelloWorld(20); // working +new HelloWorld(random_int(20, 80)); // broken diff --git a/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php new file mode 100644 index 0000000000..cf134c3a2b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php @@ -0,0 +1,41 @@ + 'App\GeneratedProxy\__CG__', + ]; + } + +} + +$doctrineEntity = new \App\GeneratedProxy\__CG__\App\TestDoctrineEntity(); +$phpStanEntity = new \_PHPStan_15755dag8c\TestPhpStanEntity(); diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php b/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php index c8e8ed29f0..d113ac2f70 100644 --- a/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php +++ b/tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php @@ -1,4 +1,4 @@ -= 8.0 += 7.4 + +namespace _PHPStan_156ee64ba; // mimicks a prefixed class, as contained in phpstan.phar releases + +class PrefixedRuntimeException extends \RuntimeException {} + +class AClass { + const Test = 1; +} + + +namespace TestPhpstanInternalClass; + +use _PHPStan_156ee64ba\PrefixedRuntimeException; + +function doFoo(\_PHPStan_156ee64ba\AClass $e) { + try { + + } catch (\_PHPStan_156ee64ba\PrefixedRuntimeException $exception) { + + } +} + +class Foo { + private \_PHPStan_156ee64ba\AClass $e; + + public function doFoo() { + echo \_PHPStan_156ee64ba\AClass::Test; + + new \_PHPStan_156ee64ba\AClass(); + } +} + +class Bar extends \_PHPStan_156ee64ba\AClass +{} + +namespace RectorPrefix202302; // mimicks a prefixed class, as contained in rector.phar releases + +class AClass { + const Test = 1; +} + + +namespace _PhpScoper19ae93be897e; // mimicks a prefixed class, as generated by PHP-Scoper with default settings + +class AClass { + const Test = 1; +} + +namespace PHPUnitPHAR\SebastianBergmann\Diff; // mimicks a prefixed class, as contained in PHPUnit phar + +class Exception{} + +namespace TestPhpstanInternalClass2; + +class FooBar extends \RectorPrefix202302\AClass +{} + +class Baz extends \_PhpScoper19ae93be897e\AClass +{} + +class BazBar extends \PHPUnitPHAR\SebastianBergmann\Diff\Exception +{} + +namespace _HumbugBox02f3b3909847; // mimicks a prefixed class, as generated by Box + +class AClass { + const Test = 1; +} + +namespace TestHumbugInternalClass; + +class FooBar extends \_HumbugBox02f3b3909847\AClass +{} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 6ebe5bc544..d8a3d75a45 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -378,7 +378,7 @@ public function testBugComposerDependentVariables(): void public function testBug2231(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-2231.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-2231.php'], [ [ 'Result of && is always false.', 21, diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index b8b0777ca2..ee616efbc5 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -435,7 +435,16 @@ public function testBug6551(): void { $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = true; - $this->analyse([__DIR__ . '/data/bug-6551.php'], []); + $this->analyse([__DIR__ . '/data/bug-6551.php'], [ + [ + 'Result of || is always true.', + 49, + ], + [ + 'Result of || is always true.', + 61, + ], + ]); } public function testBug4004(): void diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 979af0dc12..a7c2629d16 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -162,4 +162,10 @@ public function testBug2499(): void $this->analyse([__DIR__ . '/data/bug-2499.php'], []); } + public function testBug10561(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10561.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index a4ecef3fed..88d9315d84 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -612,7 +612,7 @@ public function testConditionalTypesInference(): void { $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/conditional-types-inference.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/conditional-types-inference.php'], [ [ 'Call to function testIsInt() with string will always evaluate to false.', 49, @@ -661,7 +661,7 @@ public function testBug7224(): void { $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-7224.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7224.php'], []); } public function testBug4708(): void @@ -750,14 +750,14 @@ public function testBug8752(): void { $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); } public function testDiscussion9134(): void { $this->checkAlwaysTrueCheckTypeFunctionCall = true; $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/discussion-9134.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/discussion-9134.php'], []); } public function testImpossibleMethodExistOnGenericClassString(): void @@ -1023,6 +1023,45 @@ static function (array $i): array { $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); } + public function testNonStrictInArray(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662.php'], []); + } + + public function testNonStrictInArrayEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662-enums.php'], [ + [ + "Call to function in_array() with 'NotAnEnumCase' and array will always evaluate to false.", + 19, + $tipText, + ], + [ + "Call to function in_array() with 'NotAnEnumCase' and array will always evaluate to false.", + 62, + $tipText, + ], + [ + 'Call to function in_array() with string and array will always evaluate to false.', + 77, + ], + [ + 'Call to function in_array() with int and array will always evaluate to false.', + 84, + ], + ]); + } + public function testLooseComparisonAgainstEnumsNoPhpdoc(): void { if (PHP_VERSION_ID < 80100) { @@ -1036,4 +1075,30 @@ public function testLooseComparisonAgainstEnumsNoPhpdoc(): void $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); } + public function testBug10502(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10502.php'], [ + [ + "Call to function is_callable() with array{ArrayObject, 'count'} will always evaluate to true.", + 23, + ], + [ + "Call to function is_callable() with array{1: 'count', 0: ArrayObject} will always evaluate to true.", + 24, + $tipText, + ], + ]); + } + + public function testAlwaysTruePregMatch(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/always-true-preg-match.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index b267bfc3e1..b727ba76e2 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -174,6 +174,26 @@ public function testEnums(): void '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, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\DifferentEnum::ONE is always false.', + 95, + ], + [ + 'Match arm comparison between MatchEnums\Foo and MatchEnums\Foo::ONE is always false.', + 104, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\Foo::ONE is always false.', + 113, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\DifferentEnum::ONE is always false.', + 113, + ], ]); } @@ -268,7 +288,7 @@ public function testBug8240(): void $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8240.php'], [ [ - 'Match arm comparison between Bug8240\Foo and Bug8240\Foo::BAR is always true.', + 'Match arm comparison between Bug8240\Foo::BAR and Bug8240\Foo::BAR is always true.', 13, 'Remove remaining cases below this one and this error will disappear too.', ], @@ -328,7 +348,7 @@ public function testLastArmAlwaysTrue(): void 42, ], [ - 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Bar) and LastMatchArmAlwaysTrue\Bar::ONE is always true.', + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Bar)&LastMatchArmAlwaysTrue\Bar::ONE and LastMatchArmAlwaysTrue\Bar::ONE is always true.', 62, $tipText, ], @@ -543,4 +563,40 @@ public function testBugUnhandledTrueWithComplexCondition(): void $this->analyse([__DIR__ . '/data/bug-unhandled-true-with-complex-condition.php'], []); } + public function testBug11246(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11246.php'], []); + } + + public function testBug9879(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9879.php'], []); + } + + public function testBug11313(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11313.php'], []); + } + + public function testBug9436(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-9436.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index c2837a9c6e..ae47818512 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -210,7 +210,7 @@ public function testBug7075(): void public function testBug8803(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8803.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8803.php'], []); } public function testBug8938(): void @@ -225,4 +225,10 @@ public function testBug5005(): void $this->analyse([__DIR__ . '/data/bug-5005.php'], []); } + public function testBug6467(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6467.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index f23be18f9d..e1c99901de 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -689,7 +689,7 @@ public function testBug8516(): void public function testPhpUnitIntegration(): void { $this->checkAlwaysTrueStrictComparison = true; - $this->analyse([__DIR__ . '/../../Analyser/data/phpunit-integration.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/phpunit-integration.php'], []); } public function testBug8586(): void @@ -794,7 +794,7 @@ public function testLastConditionAlwaysTrue(): void public function testBug3019(): void { $this->checkAlwaysTrueStrictComparison = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3019.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3019.php'], []); } public function testBug7578(): void @@ -917,11 +917,11 @@ public function testBug5978(): void if (PHP_VERSION_ID >= 80000) { $expectedErrors = [ [ - 'Strict comparison using === between string and false will always evaluate to false.', + 'Strict comparison using === between non-empty-string and false will always evaluate to false.', 7, ], [ - 'Strict comparison using === between string and null will always evaluate to false.', + 'Strict comparison using === between non-empty-string and null will always evaluate to false.', 7, ], ]; @@ -1013,7 +1013,49 @@ public function testBug9723b(): void public function testBug8366(): void { $this->checkAlwaysTrueStrictComparison = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8366.php'], []); + $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.", + 39, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testBug9804(): void + { + $this->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 testBug10493(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-10493.php'], []); } } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php index 317f5f98aa..c145cc7cfa 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php @@ -99,7 +99,7 @@ public function testReportPhpDoc(): void public function testBug3019(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3019.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3019.php'], []); } public function testBug7686(): void diff --git a/tests/PHPStan/Rules/Comparison/data/always-true-preg-match.php b/tests/PHPStan/Rules/Comparison/data/always-true-preg-match.php new file mode 100644 index 0000000000..160f21791a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/always-true-preg-match.php @@ -0,0 +1,23 @@ +\S+::\S+)/', $test, $matches)) { + $test = $matches['name']; + } + + return $test; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10493.php b/tests/PHPStan/Rules/Comparison/data/bug-10493.php new file mode 100644 index 0000000000..9cf0c1334e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10493.php @@ -0,0 +1,24 @@ += 8.1 + +namespace Bug10493; + +class Foo +{ + public function __construct( + private readonly ?string $old, + private readonly ?string $new, + ) + { + } + + public function foo(): ?string + { + $return = sprintf('%s%s', $this->old, $this->new); + + if ($return === '') { + return null; + } + + return $return; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10502.php b/tests/PHPStan/Rules/Comparison/data/bug-10502.php new file mode 100644 index 0000000000..da5e519a34 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10502.php @@ -0,0 +1,26 @@ + $x */ +function doFoo(?ArrayObject $x):void { + $callable1 = [$x, 'count']; + $callable2 = array_reverse($callable1, true); + + var_dump( + is_callable($callable1), + is_callable($callable2) + ); +} + +function doBar():void { + $callable1 = [new ArrayObject([0]), 'count']; + $callable2 = array_reverse($callable1, true); + + var_dump( + is_callable($callable1), + is_callable($callable2) + ); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10561.php b/tests/PHPStan/Rules/Comparison/data/bug-10561.php new file mode 100644 index 0000000000..71f7bf9d4c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10561.php @@ -0,0 +1,33 @@ + $inner_arr1 */ + $inner_arr1 = $arr1['inner_arr']; + /** @var array $inner_arr2 */ + $inner_arr2 = $arr2['inner_arr']; + + if (!$inner_arr1) { + return; + } + if (!$inner_arr2) { + return; + } + + $arr_intersect = array_intersect_key($inner_arr1, $inner_arr2); + if ($arr_intersect) { + echo "not empty\n"; + } else { + echo "empty\n"; + } +} + +$arr1 = ['inner_arr' => ['a' => 'b']]; +$arr2 = ['inner_arr' => ['c' => 'd']]; +func($arr1, $arr2); // Outputs "empty" diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10697.php b/tests/PHPStan/Rules/Comparison/data/bug-10697.php new file mode 100644 index 0000000000..2bc2e574e9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10697.php @@ -0,0 +1,12 @@ + $foo1 + * @param Collection $foo2 + */ + public static function compare(Collection $foo1, Collection $foo2): bool + { + return $foo1 === $foo2; + } +} + +/** + * @param Collection $collection + */ +function test(Collection $collection): bool +{ + return Comparator::compare($collection, $collection); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11246.php b/tests/PHPStan/Rules/Comparison/data/bug-11246.php new file mode 100644 index 0000000000..3c718c00ec --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11246.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug11246; + +$var = 0; +foreach ([1, 2, 3, 4, 5] as $index) { + $var++; + + match ($var % 5) { + 1 => 'c27ba0', + 2 => '5b9bd5', + 3 => 'ed7d31', + 4 => 'ffc000', + default => '674ea7', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11313.php b/tests/PHPStan/Rules/Comparison/data/bug-11313.php new file mode 100644 index 0000000000..84375ca499 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11313.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug11313; + +enum Foo: string +{ + case CaseOne = 'one'; + case CaseTwo = 'two'; +} + +enum Bar: string +{ + case CaseThree = 'Three'; +} + +function test(Foo|Bar $union): bool +{ + return match ($union) { + Bar::CaseThree, + Foo::CaseOne => true, + Foo::CaseTwo => false, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6467.php b/tests/PHPStan/Rules/Comparison/data/bug-6467.php new file mode 100644 index 0000000000..722d952afd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6467.php @@ -0,0 +1,16 @@ + $null) { + $success = $values[$index] < $expected; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6551.php b/tests/PHPStan/Rules/Comparison/data/bug-6551.php index 3b3e9574a6..561fbc9cfd 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-6551.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-6551.php @@ -46,7 +46,7 @@ function (): void { foreach ($data as $key => $value) { $match = []; - assertType('bool', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); + assertType('true', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); } }; @@ -58,6 +58,6 @@ function (): void { ]; foreach ($data as $key => $value) { - assertType('bool', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); + assertType('true', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); } }; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9436.php b/tests/PHPStan/Rules/Comparison/data/bug-9436.php new file mode 100644 index 0000000000..55846cc903 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9436.php @@ -0,0 +1,15 @@ += 8.0 + +namespace Bug9436; + +$foo = rand(0, 100); + +if (!in_array($foo, [0, 1, 2])) { + exit(); +} + +$bar = match ($foo) { + 0 => 'a', + 1 => 'b', + 2 => 'c', +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9804.php b/tests/PHPStan/Rules/Comparison/data/bug-9804.php new file mode 100644 index 0000000000..723f8ba159 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9804.php @@ -0,0 +1,34 @@ +|int<1, max> and 0 will always evaluate to false. + $firstLetterAsInt = (int)substr($someString, 0, 1); + if ($firstLetterAsInt === 0) { + return; + } + } + + public function pass(?string $someString): void + { + // Line below is the only difference to "error" method + if ($someString === null) { + return; + } + + // All ok + $firstLetterAsInt = (int)substr($someString, 0, 1); + if ($firstLetterAsInt === 0) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9879.php b/tests/PHPStan/Rules/Comparison/data/bug-9879.php new file mode 100644 index 0000000000..3223872658 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9879.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug9879; + +final class A { + public function test(): void + { + for($idx = 0; $idx < 6; $idx += 1) { + match($idx % 3) { + 0 => 1, + 1 => 2, + 2 => 0, + }; + } + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php index d9fb0b0845..814d93ae60 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php @@ -967,3 +967,12 @@ function checkSuperGlobals(): void if (is_int($k)) {} } } + +/** + * @param resource $resource + */ +function checkClosedResource($resource): void { + if (!is_resource($resource)) { + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/match-enums.php b/tests/PHPStan/Rules/Comparison/data/match-enums.php index d96b958d59..43e765c552 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-enums.php +++ b/tests/PHPStan/Rules/Comparison/data/match-enums.php @@ -78,4 +78,51 @@ public function doBaz(Foo $foo, Foo $bar): int }; } + public function doFoo2(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::ONE => 'one2', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doFoo3(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + DifferentEnum::ONE => 'one2', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doFoo4(Foo $foo): int + { + return match ($foo) { + Foo::ONE, Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doFoo5(Foo $foo): int + { + return match ($foo) { + Foo::ONE, DifferentEnum::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + +} + +enum DifferentEnum: int +{ + + case ONE = 1; + case TWO = 2; + case THREE = 3; + } diff --git a/tests/PHPStan/Rules/Comparison/data/match-expr.php b/tests/PHPStan/Rules/Comparison/data/match-expr.php index 37d5e01c1e..5970804bb4 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-expr.php +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -203,3 +203,15 @@ public function doMatch(FinalFoo|FinalBar $class): void } } +class TestGetDebugType +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_debug_type($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +} diff --git a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php index 6bcddaf2d6..89b6302e8a 100644 --- a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -29,7 +29,6 @@ public function testRule(): void [ 'Constant MissingClassConstantTypehint\Foo::BAZ with generic class MissingClassConstantTypehint\Bar does not specify its types: T', 17, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Constant MissingClassConstantTypehint\Foo::LOREM type has no signature specified for callable.', @@ -46,4 +45,23 @@ public function testBug8957(): void $this->analyse([__DIR__ . '/data/bug-8957.php'], []); } + public function testRuleShouldNotApplyToNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('This test needs PHP 8.3'); + } + + $this->analyse([__DIR__ . '/data/class-constant-native-type.php'], [ + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::B type has no value type specified in iterable type array.', + 19, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::D with generic class ClassConstantNativeTypeForMissingTypehintRule\Bar does not specify its types: T', + 24, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/data/class-constant-native-type.php b/tests/PHPStan/Rules/Constants/data/class-constant-native-type.php new file mode 100644 index 0000000000..fb3b694d57 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-constant-native-type.php @@ -0,0 +1,25 @@ += 8.3 + +namespace ClassConstantNativeTypeForMissingTypehintRule; + +/** @template T */ +class Bar +{ + +} + +const ConstantWithObjectInstanceForNativeType = new Bar(); + +class Foo +{ + + public const array A = []; + + /** @var array */ + public const array B = []; + + public const Bar C = ConstantWithObjectInstanceForNativeType; + + /** @var Bar */ + public const Bar D = ConstantWithObjectInstanceForNativeType; +} diff --git a/tests/PHPStan/Rules/DeadCode/BetterNoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/BetterNoopRuleTest.php new file mode 100644 index 0000000000..7519e3fc48 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/BetterNoopRuleTest.php @@ -0,0 +1,161 @@ + + */ +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 new file mode 100644 index 0000000000..ac630c4880 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToConstructorStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-constructor-without-impure-points.php'], [ + [ + 'Call to new CallToConstructorWithoutImpurePoints\Foo() on a separate line has no effect.', + 15, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureNewCollector($this->createReflectionProvider()), + new ConstructorWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..3c5fe7a2b9 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-function-without-impure-points.php'], [ + [ + 'Call to function CallToFunctionWithoutImpurePoints\myFunc() on a separate line has no effect.', + 29, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureFuncCallCollector($this->createReflectionProvider()), + new FunctionWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..77feb89d7d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,83 @@ + + */ +class CallToMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-method-without-impure-points.php'], [ + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 8, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\foo::finalFunc() on a separate line has no effect.', + 30, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 36, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 39, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalSubSubY::mySubSubFunc() on a separate line has no effect.', + 40, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 41, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\AbstractFoo::myFunc() on a separate line has no effect.', + 119, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\CallsPrivateMethodWithoutImpurePoints::doBar() on a separate line has no effect.', + 127, + ], + ]); + } + + public function testBug11011(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-11011.php'], [ + [ + 'Call to method Bug11011\AnotherPureImpl::doFoo() on a separate line has no effect.', + 32, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureMethodCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..74258d25a1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,69 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToStaticMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-static-method-without-impure-points.php'], [ + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 6, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 16, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 18, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 20, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubFunc() on a separate line has no effect.', + 21, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 48, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 53, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 58, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureStaticCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php index 48864332f9..edffaa5b9a 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()), true); + return new NoopRule(new ExprPrinter(new Printer()), false); } public function testRule(): void @@ -77,37 +77,6 @@ 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, - ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 0efdb81138..191ba39134 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -218,4 +218,10 @@ public function testBug8966(): void ]); } + public function testBug11179(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11179.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index 83a1e8ef83..246237f97f 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Rules\DeadCode; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtension; +use PHPStan\Rules\Methods\DirectAlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -14,7 +17,19 @@ class UnusedPrivateMethodRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedPrivateMethodRule(); + return new UnusedPrivateMethodRule( + new DirectAlwaysUsedMethodExtensionProvider([ + new class() implements AlwaysUsedMethodExtension { + + public function isAlwaysUsed(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->is('UnusedPrivateMethod\IgnoredByExtension') + && $methodReflection->getName() === 'foo'; + } + + }, + ]), + ); } public function testRule(): void @@ -40,6 +55,10 @@ public function testRule(): void 'Method UnusedPrivateMethod\Lorem::doBaz() is unused.', 99, ], + [ + 'Method UnusedPrivateMethod\IgnoredByExtension::bar() is unused.', + 181, + ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 649afd822b..e17c06a69d 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -304,4 +304,15 @@ public function testBug10059(): void $this->analyse([__DIR__ . '/data/bug-10059.php'], []); } + public function testBug10628(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-10628.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10628.php b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php new file mode 100644 index 0000000000..3fee75ba1e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug10628; + +use stdClass; + +interface Bar +{ + + public function bazName(): string; + +} + +final class Foo +{ + public function __construct( + private Bar $bar, + ) { + } + + public function __invoke(): stdClass + { + return $this->getMixed()->get( + name: $this->bar->bazName(), + ); + } + + public function getMixed(): mixed + { + + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11001.php b/tests/PHPStan/Rules/DeadCode/data/bug-11001.php new file mode 100644 index 0000000000..5351fc8302 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11001.php @@ -0,0 +1,38 @@ += 7.4 + +namespace Bug11001; + +class Foo +{ + + /** @var int */ + public $x; + + /** @var int */ + public $foo; + + public function doFoo(): void + { + $x = new self(); + + (function () use ($x) { + unset($x->foo); + })(); + } + +} + +class Foo2 +{ + public function test(): void + { + \Closure::bind(fn () => $this->status = 5, $this)(); + } + + public function test2(): void + { + \Closure::bind(function () { + $this->status = 5; + }, $this)(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11011.php b/tests/PHPStan/Rules/DeadCode/data/bug-11011.php new file mode 100644 index 0000000000..af820bde68 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11011.php @@ -0,0 +1,35 @@ += 8.0 + +namespace Bug11011; + +final class ImpureImpl { + /** @phpstan-impure */ + public function doFoo() { + echo "yes"; + $_SESSION['ab'] = 1; + } +} + +final class PureImpl { + public function doFoo(): bool { + return true; + } +} + +final class AnotherPureImpl { + public function doFoo(): bool { + return true; + } +} + +class User { + function doBar(PureImpl|ImpureImpl $f): bool { + $f->doFoo(); + return true; + } + + function doBar2(PureImpl|AnotherPureImpl $f): bool { + $f->doFoo(); + return true; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-11179.php b/tests/PHPStan/Rules/DeadCode/data/bug-11179.php new file mode 100644 index 0000000000..aba6adb265 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-11179.php @@ -0,0 +1,14 @@ +myFunc(); + $x->myFUNC(); + $x->throwingFUNC(); + $x->throwingFunc(); + $x->funcWithRef(); + $x->impureFunc(); + $x->callingImpureFunc(); + + $a = $x->myFunc(); + + $xy = new y(); + if (rand(0,1)) { + $xy = new finalX(); + } + $xy->myFunc(); + + $xy = new Y(); // case-insensitive class name + if (rand(0,1)) { + $xy = new finalX(); + } + $xy->myFunc(); + + $foo = new Foo(); + $foo->finalFunc(); + $foo->finalThrowingFunc(); + $foo->throwingFunc(); + + $subY = new subY(); + $subY->myFunc(); + $subY->myFinalBaseFunc(); + + $subSubY = new finalSubSubY(); + $subSubY->myFunc(); + $subSubY->mySubSubFunc(); + $subSubY->myFinalBaseFunc(); +}; + +class y +{ + function myFunc() + { + } + final function myFinalBaseFunc() + { + } +} + +class subY extends y { +} + +final class finalSubSubY extends subY { + function mySubSubFunc() + { + } +} + +final class finalX { + function myFunc() + { + } + + function throwingFunc() + { + throw new \Exception(); + } + + function funcWithRef(&$a) + { + } + + /** @phpstan-impure */ + function impureFunc() + { + } + + function callingImpureFunc() + { + $this->impureFunc(); + } +} + +class foo +{ + final function finalFunc() + { + } + + final function finalThrowingFunc() + { + throw new \Exception(); + } + + function throwingFunc() + { + throw new \Exception(); + } +} + +abstract class AbstractFoo +{ + + function myFunc() + { + } + +} +final class FinalFoo extends AbstractFoo +{ + +} + +function (FinalFoo $foo): void { + $foo->myFunc(); +}; + +class CallsPrivateMethodWithoutImpurePoints +{ + + public function doFoo(): void + { + $this->doBar(); + } + + private function doBar(): int + { + return 1; + } + +} + +class TestIgnoring +{ + + public function doFoo(): void + { + $this->doBar(); // @phpstan-ignore method.resultUnused + } + + private function doBar(): int + { + return 1; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php new file mode 100644 index 0000000000..3dfcff73d4 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php @@ -0,0 +1,122 @@ +i = 1; + } +} + +class ChildOfParentWithConstructor extends ParentWithConstructor +{ + public function __construct() + { + parent::__construct(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/noop-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/noop-impure-points.php new file mode 100644 index 0000000000..cf52aa513f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/noop-impure-points.php @@ -0,0 +1,117 @@ +doBar(); + $b && $this->doBaz(); + $b && $this->doLorem(); + } + + /** + * @phpstan-pure + */ + public function doBar(): bool + { + return true; + } + + /** + * @phpstan-impure + */ + public function doBaz(): bool + { + return true; + } + + public function doLorem(): bool + { + return true; + } + + public function doExit(): void + { + exit(1); + } + + public function doAssign(bool $b): void + { + $b ? $a = 1 : ''; + $b ? $this->foo = 1 : ''; + } + + public function doClosures(int $i): void + { + $a = static function () { + echo '1'; + }; + $a(); + + $b = static function () { + return 1 + 1; + }; + $b(); + + $ref = 1; + $c = static function () use (&$ref) { + $ref++; + }; + $c(); + + $d = function () { + self::$foo = 1; + }; + $d(); + + $e = function () { + self::$staticProp = 1; + }; + $e(); + + $i(); + } + + public function doFunctionWithByRef(bool $b, array $a): void + { + $func = $b ? 'array_unshift' : 'array_push'; + $func($a, 1); + } + + public function anonymousClassWithSideEffect(): void + { + new class () { + public function __construct() + { + echo '1'; + } + }; + } + + public function anonymousClassWithoutConstructor(): void + { + new class () { + }; + } + + public function anonymousClassWithPureConstructor(): void + { + new class () { + + /** @var int */ + private $i; + + public function __construct() + { + $this->i = 1; + } + + }; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php index 3bdf538e6b..ce43e40a90 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php @@ -171,3 +171,14 @@ public function doTest(): void } } + +class IgnoredByExtension +{ + private function foo(): void + { + } + + private function bar(): void + { + } +} diff --git a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php index acc2a2c8c1..7a2ff3aa6a 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -82,4 +82,24 @@ public function testBug10377(): void ]); } + public function testBug11179(): void + { + $this->analyse([__DIR__ . '/../DeadCode/data/bug-11179.php'], [ + [ + 'Dumped type: string', + 9, + ], + ]); + } + + public function testBug11179NoNamespace(): void + { + $this->analyse([__DIR__ . '/data/bug-11179-no-namespace.php'], [ + [ + 'Dumped type: string', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php b/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php new file mode 100644 index 0000000000..c7f0ad68f8 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php @@ -0,0 +1,13 @@ + + */ +class CatchWithUnthrownExceptionRuleStubsTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/catch-with-unthrown-exception-stubs.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 55, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/catch-with-unthrown-exception-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 4ef9cc3fd1..0958befd2f 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -125,6 +125,22 @@ public function testRule(): void 'Dead catch - Exception is never thrown in the try block.', 555, ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 629, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 647, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 741, + ], + [ + 'Dead catch - ArithmeticError is never thrown in the try block.', + 762, + ], ]); } @@ -605,6 +621,10 @@ public function testBug5650(): void public function testBug9568(): void { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-9568.php'], []); } diff --git a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php index 6af5ae9a2c..36346ad255 100644 --- a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,10 +16,13 @@ class CaughtExceptionExistenceRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new CaughtExceptionExistenceRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ); } @@ -52,4 +57,17 @@ public function testBug3690(): void $this->analyse([__DIR__ . '/data/bug-3690.php'], []); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 19, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index d085543b84..ba61e2900a 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; +use function sprintf; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -27,7 +29,7 @@ protected function getRule(): Rule public function testRule(): void { - $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], [ + $errors = [ [ 'Method MissingExceptionMethodThrows\Foo::doBaz() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', 23, @@ -41,10 +43,31 @@ public function testRule(): void 34, ], [ - 'Method MissingExceptionMethodThrows\Foo::dateTimeZoneDoesThrows() throws checked exception Exception but it\'s missing from the PHPDoc @throws tag.', + sprintf( + 'Method MissingExceptionMethodThrows\Foo::dateTimeZoneDoesThrows() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + PHP_VERSION_ID >= 80300 ? 'DateInvalidTimeZoneException' : 'Exception', + ), 95, ], - ]); + [ + sprintf( + 'Method MissingExceptionMethodThrows\Foo::dateIntervalDoesThrows() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', + PHP_VERSION_ID >= 80300 ? 'DateMalformedIntervalStringException' : 'Exception', + ), + 105, + ], + ]; + if (PHP_VERSION_ID >= 80300) { + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeModifyDoesThrows() throws checked exception DateMalformedStringException but it\'s missing from the PHPDoc @throws tag.', + 121, + ]; + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeModifyDoesThrows() throws checked exception DateMalformedStringException but it\'s missing from the PHPDoc @throws tag.', + 122, + ]; + } + $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors); } } diff --git a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php index d40a19b5f1..98c3737887 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -40,6 +41,10 @@ public function testRule(): void 'Method TooWideThrowsMethod\ParentClass::doFoo() has LogicException in PHPDoc @throws tag but it\'s not thrown.', 77, ], + [ + 'Method TooWideThrowsMethod\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 167, + ], ]); } @@ -48,4 +53,27 @@ public function testBug6233(): void $this->analyse([__DIR__ . '/data/bug-6233.php'], []); } + 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.', + 19, + ], + ]); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/immediately-called-fcc.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon b/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon new file mode 100644 index 0000000000..f21387ab6a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon @@ -0,0 +1,5 @@ +parameters: + exceptions: + implicitThrows: false + stubFiles: + - data/catch-with-unthrown-exception-stubs.stub diff --git a/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php new file mode 100644 index 0000000000..e9e6908560 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php @@ -0,0 +1,72 @@ +transactional(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo2(): void + { + try { + \MyFunction\doFoo(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo3(array $a): void + { + try { + uksort($a, function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo4(\Ds\Deque $deque): void + { + try { + $deque->filter(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub new file mode 100644 index 0000000000..44993928da --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub @@ -0,0 +1,49 @@ + + * @param-immediately-invoked-callable $callback + */ + public function filter(callable $callback = null): Deque + { + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php b/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php new file mode 100644 index 0000000000..307639c826 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php @@ -0,0 +1,41 @@ += 8.0 + +namespace ImmediatelyCalledArrowFunction; + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(fn () => throw new \InvalidArgumentException(), $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = fn () => throw new \InvalidArgumentException(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = fn () => throw new \InvalidArgumentException(); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (fn () => throw new \InvalidArgumentException())(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php b/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php new file mode 100644 index 0000000000..35d36afcac --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php @@ -0,0 +1,82 @@ += 8.1 + +namespace ImmediatelyCalledFcc; + +class Foo +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $g = $f(...); + $g(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(): void + { + $f = fn () => throw new \InvalidArgumentException(); + $g = $f(...); + $g(); + } + + /** + * @throws \InvalidArgumentException + */ + public function throwsInvalidArgumentException() + { + throw new \InvalidArgumentException(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(): void + { + $f = $this->throwsInvalidArgumentException(...); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(): void + { + $f = alsoThrowsInvalidArgumentException(...); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo5(): void + { + $f = [$this, 'throwsInvalidArgumentException']; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo6(): void + { + $f = 'ImmediatelyCalledFcc\\alsoThrowsInvalidArgumentException'; + $f(); + } + +} + +/** + * @throws \InvalidArgumentException + */ +function alsoThrowsInvalidArgumentException() +{ + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php index 7df5ea9915..21cfdf1072 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php @@ -100,4 +100,26 @@ public function dateTimeZoneDoesNotThrowCaseInsensitive(): void new \DaTetImezOnE('UTC'); } + public function dateIntervalDoesThrows(string $i): void + { + new \DateInterval($i); + } + + public function dateIntervalDoeNotThrow(): void + { + new \DateInterval('P7D'); + } + + public function dateTimeModifyDoeNotThrow(\DateTime $dt, \DateTimeImmutable $dti): void + { + $dt->modify('+1 day'); + $dti->modify('+1 day'); + } + + public function dateTimeModifyDoesThrows(\DateTime $dt, \DateTimeImmutable $dti, string $m): void + { + $dt->modify($m); + $dti->modify($m); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php index 819d89c87b..a5c79a5ec8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php @@ -147,3 +147,49 @@ public function doBaz(): void } } + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(function () { + throw new \InvalidArgumentException(); + }, $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = function () { + throw new \InvalidArgumentException(); + }; + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (function () { + throw new \InvalidArgumentException(); + })(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php index 9254768fc8..0327fcb086 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -576,3 +576,217 @@ public function doBaz(string $string): void } } + +/** @throws void */ +function acceptCallable(callable $cb): void +{ + +} + +/** + * @throws void + * @param-later-invoked-callable $cb + */ +function acceptCallableAndCallLater(callable $cb): void +{ + +} + +class CallCallable +{ + + /** + * @throws void + */ + public function doFoo(callable $cb): void + { + try { + $cb(); + } catch (\Exception $e) { + + } + } + + public function passCallableToFunction(): void + { + try { + // immediately called by default + acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function passCallableToFunction2(): void + { + try { + // later called thanks to @param-later-invoked-callable + acceptCallableAndCallLater(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** @throws void */ + public function acceptCallable(callable $cb): void + { + + } + + public function passCallableToMethod(): void + { + try { + // later called by default + $this->acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws void + * @param-immediately-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable extends CallCallable +{ + + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable2 extends CallCallable +{ + + /** + * @param callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable3 extends CallCallable +{ + + /** + * @param callable $cb + * @param-later-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // later called thanks to @param-later-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class TestIntdivWithRange +{ + /** + * @param int $int + * @param int $negativeInt + * @param int<1, max> $positiveInt + */ + public function doFoo(int $int, int $negativeInt, int $positiveInt): void + { + try { + intdiv($int, $positiveInt); + intdiv($positiveInt, $negativeInt); + intdiv($negativeInt, $positiveInt); + intdiv($positiveInt, $positiveInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, $negativeInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($negativeInt, $negativeInt); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($positiveInt, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($negativeInt, $int); + } catch (\ArithmeticError $e) { + + } + try { + intdiv($int, '-1,5'); + } catch (\ArithmeticError $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php index 1c62d47af2..95ac9ed295 100644 --- a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php @@ -20,6 +20,7 @@ protected function getRule(): Rule public function testFile(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $expectedErrors = [ [ 'Parameter #1 $array (array{1, 3}) to function array_filter does not contain falsy values, the array will always stay the same.', @@ -40,6 +41,7 @@ public function testFile(): void [ 'Parameter #1 $array (array) to function array_filter does not contain falsy values, the array will always stay the same.', 20, + $tipText, ], [ 'Parameter #1 $array (array{0}) to function array_filter contains falsy values only, the result will always be an empty array.', @@ -60,6 +62,7 @@ public function testFile(): void [ 'Parameter #1 $array (array) to function array_filter contains falsy values only, the result will always be an empty array.', 27, + $tipText, ], [ 'Parameter #1 $array (array{}) to function array_filter is empty, call has no effect.', @@ -72,10 +75,13 @@ public function testFile(): void public function testBug2065WithPhpDocTypesAsCertain(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $expectedErrors = [ [ 'Parameter #1 $array (array) to function array_filter does not contain falsy values, the array will always stay the same.', 12, + $tipText, ], ]; diff --git a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php new file mode 100644 index 0000000000..c945b1efea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -0,0 +1,90 @@ + + */ +class ArrayValuesRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new ArrayValuesRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain); + } + + public function testFile(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $expectedErrors = [ + [ + 'Parameter #1 $array (array{0, 1, 3}) of array_values is already a list, call has no effect.', + 8, + ], + [ + 'Parameter #1 $array (array{1, 3}) of array_values is already a list, call has no effect.', + 9, + ], + [ + 'Parameter #1 $array (array{\'test\'}) of array_values is already a list, call has no effect.', + 10, + ], + [ + 'Parameter #1 $array (array{\'\', \'test\'}) of array_values is already a list, call has no effect.', + 12, + ], + [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 14, + $tipText, + ], + [ + 'Parameter #1 $array (array{0}) of array_values is already a list, call has no effect.', + 17, + ], + [ + 'Parameter #1 $array (array{null, null}) of array_values is already a list, call has no effect.', + 19, + ], + [ + 'Parameter #1 $array (array{null, 0}) of array_values is already a list, call has no effect.', + 20, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 21, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 25, + $tipText, + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $expectedErrors[] = [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 28, + $tipText, + ]; + } else { + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 27, + ]; + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 28, + ]; + } + + $this->analyse([__DIR__ . '/data/array_values_list.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index dfad3ae5b2..7f3aab6ac2 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -37,7 +39,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index e34d62059b..bf46cd56a6 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -60,4 +60,18 @@ public function testBug8179(): void $this->analyse([__DIR__ . '/data/bug-8179.php'], []); } + public function testBugSpaceship(): void + { + $this->analyse([__DIR__ . '/data/bug-spaceship.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + 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 f069801781..bcd1c9ee87 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -190,7 +190,7 @@ public function dataBug3566(): array true, [ [ - 'Parameter #1 $ of closure expects int, TMemberType given.', + 'Parameter #1 of closure expects int, TMemberType given.', 29, ], ], @@ -280,10 +280,40 @@ public function testBug6485(): void { $this->analyse([__DIR__ . '/data/bug-6485.php'], [ [ - 'Parameter #1 $ of closure expects never, TBlockType of Bug6485\Block given.', + 'Parameter #1 of closure expects never, TBlockType of Bug6485\Block given.', 33, ], ]); } + public function testBug6633(): void + { + $this->analyse([__DIR__ . '/data/bug-6633.php'], []); + } + + public function testBug3818b(): void + { + $this->analyse([__DIR__ . '/data/bug-3818b.php'], []); + } + + public function testBug9594(): void + { + $this->analyse([__DIR__ . '/data/bug-9594.php'], []); + } + + public function testBug9614(): void + { + $this->analyse([__DIR__ . '/data/bug-9614.php'], []); + } + + public function testBug10814(): void + { + $this->analyse([__DIR__ . '/data/bug-10814.php'], [ + [ + 'Parameter #1 of closure expects DateTime, DateTimeImmutable given.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 94f5f794a9..3ddad85c09 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -297,6 +297,10 @@ public function testPassingNonVariableToParameterPassedByReference(): void 'Parameter #1 $array of function reset expects array|object, null given.', 39, ], + [ + 'Parameter #1 $s of function PassedByReference\bar expects string, int given.', + 48, + ], ]); } @@ -554,12 +558,12 @@ public function testArrayReduceCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 13, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 22, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], @@ -574,12 +578,12 @@ public function testArrayReduceArrowFunctionCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 11, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-falsy-string given.', 18, 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], @@ -632,7 +636,7 @@ 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): non-falsy-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&non-falsy-string&numeric-string) given.', 14, ], [ @@ -868,13 +872,13 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->checkExplicitMixed = $checkExplicitMixed; $errors = [ [ - 'Parameter #2 $callback of function array_filter expects (callable(int): mixed)|null, Closure(string): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(int): bool)|null, Closure(string): true given.', 17, ], ]; if ($checkExplicitMixed) { $errors[] = [ - 'Parameter #2 $callback of function array_filter expects (callable(mixed): mixed)|null, Closure(int): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(mixed): bool)|null, Closure(int): true given.', 20, 'Type #1 from the union: Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ]; @@ -1122,12 +1126,7 @@ public function testBug7211(): void public function testBug5474(): void { - $this->analyse([__DIR__ . '/../Comparison/data/bug-5474.php'], [ - [ - 'Parameter #1 $data of function Bug5474\testData expects array{test: int}, *NEVER* given.', - 26, - ], - ]); + $this->analyse([__DIR__ . '/../Comparison/data/bug-5474.php'], []); } public function testBug6261(): void @@ -1276,35 +1275,35 @@ public function testBug5986(): void public function testBug7239(): void { $tipText = 'array{} is empty.'; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-7239.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7239.php'], [ [ 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', - 14, + 16, $tipText, ], [ 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', - 15, + 17, $tipText, ], [ 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', - 21, + 23, $tipText, ], [ 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', - 22, + 24, $tipText, ], [ 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', - 32, + 34, $tipText, ], [ 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', - 33, + 35, $tipText, ], ]); @@ -1609,4 +1608,112 @@ public function testBug9697(): void $this->analyse([__DIR__ . '/data/bug-9697.php'], []); } + public function testDiscussion10454(): void + { + $this->analyse([__DIR__ . '/data/discussion-10454.php'], [ + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 13, + ], + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 23, + ], + ]); + } + + public function testBug10527(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/bug-10527.php'], []); + } + + public function testBug10626(): void + { + $this->analyse([__DIR__ . '/data/bug-10626.php'], [ + [ + 'Parameter #1 $value of function Bug10626\intByValue expects int, string given.', + 16, + ], + [ + 'Parameter #1 $value of function Bug10626\intByReference expects int, string given.', + 17, + ], + ]); + } + + public function testArgon2PasswordHash(): void + { + $this->analyse([__DIR__ . '/data/argon2id-password-hash.php'], []); + } + + public function testParamClosureThis(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/function-call-param-closure-this.php'], [ + [ + 'Parameter #1 $cb of function FunctionCallParamClosureThis\acceptClosure expects bindable closure, static closure given.', + 18, + ], + [ + 'Parameter #1 $cb of function FunctionCallParamClosureThis\acceptClosure expects bindable closure, static closure given.', + 23, + ], + ]); + } + + public function testBug10297(): void + { + $this->analyse([__DIR__ . '/data/bug-10297.php'], []); + } + + public function testBug10974(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10974.php'], []); + } + + public function testCountArrayShift(): void + { + if (PHP_VERSION_ID < 80000) { + $errors = [ + [ + 'Parameter #1 $var of function count expects array|Countable, array|false given.', + 8, + ], + [ + 'Parameter #1 $var of function count expects array|Countable, array|false given.', + 16, + ], + ]; + } else { + $errors = [ + [ + 'Parameter #1 $value of function count expects array|Countable, array|false given.', + 8, + ], + [ + 'Parameter #1 $value of function count expects array|Countable, array|false given.', + 16, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/count-array-shift.php'], $errors); + } + + public function testBug11506(): void + { + $this->analyse([__DIR__ . '/data/bug-11506.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index 163038e8d9..85c1f400ee 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -24,14 +24,6 @@ public function testRule(): void 'Call to function sprintf() on a separate line has no effect.', 13, ], - [ - 'Call to function file_get_contents() on a separate line has no effect.', - 14, - ], - [ - 'Call to function file_get_contents() on a separate line has no effect.', - 22, - ], [ 'Call to function var_export() on a separate line has no effect.', 24, @@ -47,22 +39,6 @@ public function testRule(): void } $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects-8.0.php'], [ - [ - 'Call to function file_get_contents() on a separate line has no effect.', - 15, - ], - [ - 'Call to function file_get_contents() on a separate line has no effect.', - 16, - ], - [ - 'Call to function file_get_contents() on a separate line has no effect.', - 17, - ], - [ - 'Call to function file_get_contents() on a separate line has no effect.', - 18, - ], [ 'Call to function var_export() on a separate line has no effect.', 19, @@ -71,6 +47,10 @@ public function testRule(): void 'Call to function print_r() on a separate line has no effect.', 20, ], + [ + 'Call to function highlight_string() on a separate line has no effect.', + 21, + ], ]); } @@ -91,9 +71,17 @@ public function testPhpDoc(): void 10, ], [ - 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure4() on a separate line has no effect.', 11, ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure5() on a separate line has no effect.', + 12, + ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 13, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index 8f5fcb4e6b..f4a3d5a1bb 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -37,7 +39,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index 5fe0446016..020000b38a 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -30,15 +31,15 @@ public function testClosureReturnTypeRule(): void 28, ], [ - 'Anonymous function should return ClosureReturnTypes\Foo but returns ClosureReturnTypes\Bar.', + 'Anonymous function should return ClosureReturnTypes\Bar&ClosureReturnTypes\Foo but returns ClosureReturnTypes\Bar.', 35, ], [ - 'Anonymous function should return SomeOtherNamespace\Foo but returns ClosureReturnTypes\Foo.', + 'Anonymous function should return ClosureReturnTypes\Foo&SomeOtherNamespace\Foo but returns ClosureReturnTypes\Foo.', 39, ], [ - 'Anonymous function should return SomeOtherNamespace\Baz but returns ClosureReturnTypes\Foo.', + 'Anonymous function should return ClosureReturnTypes\Foo&SomeOtherNamespace\Baz but returns ClosureReturnTypes\Foo.', 46, ], [ @@ -128,4 +129,13 @@ public function testBug7220(): void $this->analyse([__DIR__ . '/data/bug-7220.php'], []); } + 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 e2cdf27f1b..2b2cec639b 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,21 @@ class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false), new PhpVersion(PHP_VERSION_ID)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInArrowFunctionTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + new PhpVersion(PHP_VERSION_ID), + ); } public function testRule(): void @@ -76,7 +91,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 17, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 19, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 25, + ], + ], ], [ 80000, @@ -93,6 +121,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 11, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 19, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 23, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], ], ], ]; diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index f3e02354dd..439f5bdd71 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,20 @@ class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInClosureTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -121,7 +135,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 29, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 33, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 45, + ], + ], ], [ 80000, @@ -138,6 +165,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 17, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], ], ], ]; diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 31de1353ad..227044f2d7 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,20 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -202,7 +216,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Function RequiredAfterOptional\doAmet() uses native union types but they're supported only on PHP 8.0 and later.", + 34, + ], + [ + "Function RequiredAfterOptional\doConsectetur() uses native union types but they're supported only on PHP 8.0 and later.", + 38, + ], + [ + "Function RequiredAfterOptional\doSed() uses native union types but they're supported only on PHP 8.0 and later.", + 50, + ], + ], ], [ 80000, @@ -219,6 +246,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 18, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 38, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 46, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 50, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], ], ], ]; diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 0491852e08..553ab6c462 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -37,7 +39,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php index 547f66ebc6..44755df63d 100644 --- a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php @@ -15,7 +15,7 @@ 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)); + return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false, false, true, false), false); } public function testFile(): void diff --git a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..25f0facf27 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -0,0 +1,103 @@ + + */ +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))); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $array of function implode expects array, array> given.', + 8, + ], + [ + 'Parameter $separator of function implode expects array, array> given.', + 9, + ], + [ + 'Parameter $array of function implode expects array, array> given.', + 10, + ], + [ + 'Parameter $array of function implode expects array, array> given.', + 11, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array given.', + 12, + ], + ]); + } + + public function testImplode(): void + { + $this->analyse([__DIR__ . '/data/implode.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array|string> given.', + 9, + ], + [ + 'Parameter #1 $array of function implode expects array, array> given.', + 11, + ], + [ + 'Parameter #1 $array of function implode expects array, array> given.', + 12, + ], + [ + 'Parameter #1 $array of function implode expects array, array> given.', + 13, + ], + [ + 'Parameter #2 $array of function implode expects array, array> given.', + 15, + ], + [ + 'Parameter #2 $array of function join expects array, array> given.', + 16, + ], + ]); + } + + public function testBug6000(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); + } + + public function testBug8467a(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index db5cc3d857..a12ee381e9 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, [])); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); } public function testRule(): void @@ -51,12 +51,10 @@ public function testRule(): void [ 'Function MissingFunctionParameterTypehint\acceptsGenericInterface() has parameter $i with generic interface MissingFunctionParameterTypehint\GenericInterface but does not specify its types: T, U', 111, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionParameterTypehint\acceptsGenericClass() has parameter $c with generic class MissingFunctionParameterTypehint\GenericClass but does not specify its types: A, B', 130, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionParameterTypehint\missingIterableTypehint() has parameter $iterable with no value type specified in iterable type iterable.', @@ -82,6 +80,19 @@ public function testRule(): void 'Function MissingFunctionParameterTypehint\missingCallableSignature() has parameter $cb with no signature specified for callable.', 161, ], + [ + 'Function MissingParamOutType\oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 173, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Function MissingParamOutType\generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 181, + ], + [ + 'Function MissingParamClosureThisType\generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 191, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php index 0861bfb2ce..37cd3ffb81 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php @@ -37,17 +37,14 @@ public function testRule(): void [ 'Function MissingFunctionReturnTypehint\returnsGenericInterface() return type with generic interface MissingFunctionReturnTypehint\GenericInterface does not specify its types: T, U', 70, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\returnsGenericClass() return type with generic class MissingFunctionReturnTypehint\GenericClass does not specify its types: A, B', 89, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\genericGenericMissingTemplateArgs() return type with generic class MissingFunctionReturnTypehint\GenericClass does not specify its types: A, B', 105, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Function MissingFunctionReturnTypehint\closureWithNoPrototype() return type has no signature specified for Closure.', diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 73fa588489..60d620474c 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -37,7 +39,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..8619497c4f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -0,0 +1,242 @@ + + */ +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))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/param-castable-to-string-functions.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 16, + ], + [ + 'Parameter #2 $arrays of function array_intersect expects an array of values castable to string, array given.', + 17, + ], + [ + 'Parameter #3 of function array_intersect expects an array of values castable to string, array given.', + 18, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 19, + ], + [ + 'Parameter #2 $arrays of function array_diff_assoc expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array> given.', + 22, + ], + [ + 'Parameter #1 $array of function natsort expects an array of values castable to string, array> given.', + 24, + ], + [ + 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array> given.', + 25, + ], + [ + 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array> given.', + 26, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array> given.', + 27, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $keys of function array_combine expects an array of values castable to string, array> given.', + 7, + ], + [ + 'Parameter $keys of function array_fill_keys expects an array of values castable to string, array> given.', + 9, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 12, + ], + [ + 'Parameter #2 $arrays of function array_intersect expects an array of values castable to string, array given.', + 13, + ], + [ + 'Parameter #3 of function array_intersect expects an array of values castable to string, array given.', + 14, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 15, + ], + [ + 'Parameter #2 $arrays of function array_diff_assoc expects an array of values castable to string, array given.', + 16, + ], + [ + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array given.', + 18, + ], + [ + 'Parameter #1 $array of function natsort expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array given.', + 21, + ], + [ + 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array given.', + 22, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 23, + ], + ]); + } + + public function testBug5848(): void + { + $this->analyse([__DIR__ . '/data/bug-5848.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_diff expects an array of values castable to string, array given.', + 8, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 8, + ], + ])); + } + + public function testBug3946(): void + { + $this->analyse([__DIR__ . '/data/bug-3946.php'], [ + [ + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array|Bug3946\stdClass|float|int|string> given.', + 8, + ], + ]); + } + + public function testBug11111(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11111.php'], [ + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 23, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 26, + ], + ]); + } + + public function testBug11141(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11141.php'], [ + [ + 'Parameter #1 $array of function array_diff expects an array of values castable to string, array given.', + 22, + ], + [ + 'Parameter #2 $arrays of function array_diff expects an array of values castable to string, array given.', + 22, + ], + ]); + } + + /** + * @param list $errors + * @return list + */ + private function hackParameterNames(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function array_diff', + '$array of function array_diff_assoc', + '$array of function array_intersect', + '$arrays of function array_intersect', + '$arrays of function array_diff', + '$arrays of function array_diff_assoc', + '$array of function natsort', + '$array of function natcasesort', + '$array of function array_count_values', + '#3 of function array_intersect', + ], + [ + '$arr1 of function array_diff', + '$arr1 of function array_diff_assoc', + '$arr1 of function array_intersect', + '$arr2 of function array_intersect', + '$arr2 of function array_diff', + '$arr2 of function array_diff_assoc', + '$array_arg of function natsort', + '$array_arg of function natcasesort', + '$input of function array_count_values', + '#3 $args of function array_intersect', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php new file mode 100644 index 0000000000..d9f2375bfc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php @@ -0,0 +1,74 @@ + + */ +class PrintfArrayParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PrintfArrayParametersRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + $this->createReflectionProvider(), + ); + } + + public function testFile(): void + { + $this->analyse([__DIR__ . '/data/vprintf.php'], [ + [ + 'Call to vsprintf contains 2 placeholders, 1 value given.', + 10, + ], + [ + 'Call to vsprintf contains 0 placeholders, 1 value given.', + 11, + ], + [ + 'Call to vsprintf contains 1 placeholder, 2 values given.', + 12, + ], + [ + 'Call to vsprintf contains 2 placeholders, 1 value given.', + 13, + ], + [ + 'Call to vsprintf contains 2 placeholders, 0 values given.', + 14, + ], + [ + 'Call to vsprintf contains 2 placeholders, 0 values given.', + 15, + ], + [ + 'Call to vsprintf contains 4 placeholders, 0 values given.', + 16, + ], + [ + 'Call to vsprintf contains 5 placeholders, 2 values given.', + 18, + ], + [ + 'Call to vsprintf contains 1 placeholder, 2 values given.', + 21, + ], + [ + 'Call to vsprintf contains 1 placeholder, 1-2 values given.', + 29, + ], + [ + 'Call to vprintf contains 2 placeholders, 1 value given.', + 34, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index 4e2880fda7..252f2919ec 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -15,7 +15,10 @@ class PrintfParametersRuleTest extends RuleTestCase protected function getRule(): Rule { - return new PrintfParametersRule(new PhpVersion(PHP_VERSION_ID)); + return new PrintfParametersRule( + new PrintfHelper(new PhpVersion(PHP_VERSION_ID)), + $this->createReflectionProvider(), + ); } public function testFile(): void diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index bb99cc426a..45ddca1767 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -276,4 +276,18 @@ public function testBug5592(): void $this->analyse([__DIR__ . '/data/bug-5592.php'], []); } + public function testBug10732(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-10732.php'], []); + } + + public function testBug11518(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-11518.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..98076839d1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -0,0 +1,179 @@ + + */ +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))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array> given.', + 16, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 19, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, array> given.', + 21, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, array> given.', + 22, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string, array> given.', + 23, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 25, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, array> given.', + 26, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, array> given.', + 27, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to float, array given.', + 31, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string and float, array given.', + 32, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 33, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string and float, array> given.', + 34, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $array of function array_unique expects an array of values castable to string, array> given.', + 7, + ], + [ + 'Parameter $array of function sort expects an array of values castable to string, array> given.', + 9, + ], + [ + 'Parameter $array of function rsort expects an array of values castable to string, array> given.', + 10, + ], + [ + 'Parameter $array of function asort expects an array of values castable to string, array> given.', + 11, + ], + [ + 'Parameter $array of function arsort expects an array of values castable to string, array> given.', + 12, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 12, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 14, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, array given.', + 15, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, array given.', + 16, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string, array given.', + 17, + ], + ]); + } + + public function testBug11167(): void + { + $this->analyse([__DIR__ . '/data/bug-11167.php'], []); + } + + /** + * @param list $errors + * @return list + */ + private function hackParameterNames(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function sort', + '$array of function rsort', + '$array of function asort', + '$array of function arsort', + ], + [ + '$array_arg of function sort', + '$array_arg of function rsort', + '$array_arg of function asort', + '$array_arg of function arsort', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php new file mode 100644 index 0000000000..422103bd37 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/UselessFunctionReturnValueRuleTest.php @@ -0,0 +1,54 @@ + + */ +class UselessFunctionReturnValueRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UselessFunctionReturnValueRule( + $this->createReflectionProvider(), + ); + } + + public function testUselessReturnValue(): void + { + $this->analyse([__DIR__ . '/data/useless-fn-return.php'], [ + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 47, + ], + [ + 'Return value of function var_export() is always null and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 56, + ], + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 64, + ], + ]); + } + + public function testUselessReturnValuePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/useless-fn-return-php8.php'], [ + [ + 'Return value of function print_r() is always true and the result is printed instead of being returned. Pass in true as parameter #2 $return to return the output instead.', + 18, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php new file mode 100644 index 0000000000..e50e81894c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php @@ -0,0 +1,7 @@ + $list */ +$list = [1, 2, 3]; +/** @var list $list */ +$array = ['a' => 1, 'b' => 2, 'c' => 3]; + +array_values([0,1,3]); +array_values([1,3]); +array_values(['test']); +array_values(['a' => 'test']); +array_values(['', 'test']); +array_values(['a' => '', 'b' => 'test']); +array_values($list); +array_values($array); + +array_values([0]); +array_values(['a' => null, 'b' => null]); +array_values([null, null]); +array_values([null, 0]); +array_values([]); + +/** @var array{} $empty */ +$empty = doFoo(); +array_values($empty); + +array_values(unused: true, array: $array); +array_values(unused: true, array: $list); diff --git a/tests/PHPStan/Rules/Functions/data/bug-10297.php b/tests/PHPStan/Rules/Functions/data/bug-10297.php new file mode 100644 index 0000000000..ed3a8cf3dd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10297.php @@ -0,0 +1,86 @@ + $stream + * @param callable(T, K): iterable $fn + * + * @return Generator + */ +function scollect(iterable $stream, callable $fn): Generator +{ + foreach ($stream as $key => $value) { + yield from $fn($value, $key); + } +} + +/** + * @template K of array-key + * @template T + * @template L of array-key + * @template U + * + * @param array $array + * @param callable(T, K): iterable $fn + * + * @return array + */ +function collectWithKeys(array $array, callable $fn): array +{ + $map = []; + $counter = 0; + + try { + foreach (scollect($array, $fn) as $key => $value) { + $map[$key] = $value; + ++$counter; + } + } catch (TypeError) { + throw new UnexpectedValueException('The key yielded in the callable is not compatible with the type "array-key".'); + } + + if ($counter !== count($map)) { + throw new UnexpectedValueException( + 'Data loss occurred because of duplicated keys. Use `collect()` if you do not care about ' . + 'the yielded keys, or use `scollect()` if you need to support duplicated keys (as arrays cannot).', + ); + } + + return $map; +} + +class SomeUnitTest +{ + /** + * @return iterable + */ + public static function someProvider(): iterable + { + $unsupportedTypes = [ + // this one does not work: + 'Not a Number' => NAN, + // these work: + 'Infinity' => INF, + stdClass::class => new stdClass(), + self::class => self::class, + 'hello there' => 'hello there', + 'array' => [[42]], + ]; + + yield from collectWithKeys($unsupportedTypes, static function (mixed $value, string $type): iterable { + $error = sprintf('Some %s error message', $type); + + yield sprintf('"%s" something something', $type) => [$value, [$error, $error, $error]]; + }); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10527.php b/tests/PHPStan/Rules/Functions/data/bug-10527.php new file mode 100644 index 0000000000..568ed074e2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10527.php @@ -0,0 +1,14 @@ +, 1: list} $tuple + */ + public function sayHello(array $tuple): void + { + array_map(fn (string $first, string $second) => $first . $second, ...$tuple); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10626.php b/tests/PHPStan/Rules/Functions/data/bug-10626.php new file mode 100644 index 0000000000..e0e855c825 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10626.php @@ -0,0 +1,17 @@ + $items + * @return void + */ + public function __construct(protected array $items = []) {} + + /** + * Run a map over each of the items. + * + * @template TMapValue + * + * @param callable(TValue): TMapValue $callback + * @return static + */ + public function map(callable $callback) + { + return new self(array_map($callback, $this->items)); + } +} + +/** + * I'd expect this to work? + * + * @param Collection> $collection + * @return Collection> + */ +function current(Collection $collection): Collection +{ + return $collection->map(fn(array $item) => $item); +} + +/** + * Removing the Typehint works + * + * @param Collection> $collection + * @return Collection> + */ +function removeTypeHint(Collection $collection): Collection +{ + return $collection->map(fn($item) => $item); +} + +/** + * Typehint works for simple type + * + * @param Collection $collection + * @return Collection + */ +function simplerType(Collection $collection): Collection +{ + return $collection->map(fn(string $item) => $item); +} + +/** + * Typehint works for arrays + * + * @param array> $collection + * @return array> + */ +function useArraysInstead(array $collection): array +{ + return array_map( + fn(array $item) => $item, + $collection, + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10814.php b/tests/PHPStan/Rules/Functions/data/bug-10814.php new file mode 100644 index 0000000000..a1c7ed8cbe --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10814.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug10974; + +function non(): void {} +function single(string $str): void {} +/** @param non-empty-array $strs */ +function multiple(array $strs): void {} + +/** @param array $arr */ +function test(array $arr): void +{ + match (count($arr)) + { + 0 => non(), + 1 => single(reset($arr)), + default => multiple($arr) + }; + + if (empty($arr)) { + non(); + } elseif (count($arr) === 1) { + single(reset($arr)); + } else { + multiple($arr); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11111.php b/tests/PHPStan/Rules/Functions/data/bug-11111.php new file mode 100644 index 0000000000..c36a4ae778 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11111.php @@ -0,0 +1,26 @@ += 8.1 + +namespace Bug11111; + +enum Language: string +{ + case ENG = 'eng'; + case FRE = 'fre'; + case GER = 'ger'; + case ITA = 'ita'; + case SPA = 'spa'; + case DUT = 'dut'; + case DAN = 'dan'; +} + +/** @var Language[] $langs */ +$langs = [ + Language::ENG, + Language::GER, + Language::DAN, +]; + +$array = array_fill_keys($langs, null); +unset($array[Language::GER]); + +var_dump(array_fill_keys([Language::ITA, Language::DUT], null)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11141.php b/tests/PHPStan/Rules/Functions/data/bug-11141.php new file mode 100644 index 0000000000..f9eaddf4fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11141.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug11141; + +enum Language: string +{ + case ENG = 'eng'; + case FRE = 'fre'; + case GER = 'ger'; + case ITA = 'ita'; + case SPA = 'spa'; + case DUT = 'dut'; + case DAN = 'dan'; +} + +$langs = [ + Language::ENG, + Language::GER, + Language::DAN, +]; + +$result = array_diff($langs, [Language::DAN]); diff --git a/tests/PHPStan/Rules/Functions/data/bug-11167.php b/tests/PHPStan/Rules/Functions/data/bug-11167.php new file mode 100644 index 0000000000..8afdd21029 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11167.php @@ -0,0 +1,5 @@ +handleA(...) : $this->handleB(...); + + $method($obj); + } + + private function handleA(A $a): void + { + } + + private function handleB(B $b): void + { + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3946.php b/tests/PHPStan/Rules/Functions/data/bug-3946.php new file mode 100644 index 0000000000..bdb12cccb0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3946.php @@ -0,0 +1,8 @@ +test(); diff --git a/tests/PHPStan/Rules/Functions/data/bug-6633.php b/tests/PHPStan/Rules/Functions/data/bug-6633.php new file mode 100644 index 0000000000..31bc3cf7d8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6633.php @@ -0,0 +1,71 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9594.php b/tests/PHPStan/Rules/Functions/data/bug-9594.php new file mode 100644 index 0000000000..9e5a0b96f7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9594.php @@ -0,0 +1,26 @@ + [1, 2, 3], + 'greet' => fn (int $value) => 'I am '.$value, + ], + [ + 'elements' => ['hello', 'world'], + 'greet' => fn (string $value) => 'I am '.$value, + ], + ]; + + foreach ($data as $entry) { + foreach ($entry['elements'] as $element) { + $entry['greet']($element); + } + } + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9614.php b/tests/PHPStan/Rules/Functions/data/bug-9614.php new file mode 100644 index 0000000000..1209501f2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9614.php @@ -0,0 +1,27 @@ + function() { + return 'test'; + }, + 'foo' => function($a) { + return 'foo'; + }, + 'bar' => function($a, $b) { + return 'bar'; + } + ]; + + if (!isset($funcs[$key])) { + return ''; + } + + return $funcs[$key]($a, $b); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php b/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php new file mode 100644 index 0000000000..5347b1116f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php @@ -0,0 +1,14 @@ += 7.4 + +namespace BugAnonymousFunctionMethodConstant; + +$a = fn() => __FUNCTION__; +$b = fn() => __METHOD__; + +$c = function() { return __FUNCTION__; }; +$d = function() { return __METHOD__; }; + +\PHPStan\Testing\assertType("'{closure}'", $a()); +\PHPStan\Testing\assertType("'{closure}'", $b()); +\PHPStan\Testing\assertType("'{closure}'", $c()); +\PHPStan\Testing\assertType("'{closure}'", $d()); diff --git a/tests/PHPStan/Rules/Functions/data/bug-spaceship.php b/tests/PHPStan/Rules/Functions/data/bug-spaceship.php new file mode 100644 index 0000000000..3200b58dc7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-spaceship.php @@ -0,0 +1,6 @@ + $a <=> $b); diff --git a/tests/PHPStan/Rules/Functions/data/count-array-shift.php b/tests/PHPStan/Rules/Functions/data/count-array-shift.php new file mode 100644 index 0000000000..fcbb82b2ae --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/count-array-shift.php @@ -0,0 +1,19 @@ +|false $a */ +function foo($a): void +{ + while (count($a) > 0) { + array_shift($a); + } +} + +/** @param non-empty-array|false $a */ +function bar($a): void +{ + while (count($a) > 0) { + array_shift($a); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/discussion-10454.php b/tests/PHPStan/Rules/Functions/data/discussion-10454.php new file mode 100644 index 0000000000..67adaf0dd0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/discussion-10454.php @@ -0,0 +1,29 @@ += 7.4 + +namespace FunctionCallParamClosureThis; + +/** + * @param-closure-this \stdClass $cb + */ +function acceptClosure(callable $cb): void +{ + +} + +function (): void { + acceptClosure(function () { + + }); + + acceptClosure(static function () { + + }); + + acceptClosure(fn () => 1); + acceptClosure(static fn () => 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php index d9a9d2c7fa..497de17310 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php @@ -18,6 +18,7 @@ public function noEffect(string $url, $resourceOrNull) file_get_contents($url, context: $resourceOrNull); var_export([], return: true); print_r([], return: true); + highlight_string($url, return: true); } /** @@ -29,6 +30,7 @@ public function hasSideEffect(string $url, $resource) file_get_contents($url, context: $resource); var_export(value: []); print_r(value: []); + highlight_string($url); } } diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc-definition.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc-definition.php index 50fa23a63f..37035159b8 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc-definition.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc-definition.php @@ -19,6 +19,16 @@ function pure2(string $a): string {return $a;} */ function pure3(string $a): string {return $a;} +/** + * @phan-pure + */ +function pure4(string $a): string {return $a;} + +/** + * @phan-side-effect-free + */ +function pure5(string $a): string {return $a;} + /** * @phpstan-pure * @throws void diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc.php index 61e9d9bae5..0a1d11060b 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-phpdoc.php @@ -8,6 +8,8 @@ function(): void pure1('test'); pure2('test'); pure3('test'); + pure4('test'); + pure5('test'); pureAndThrowsVoid(); pureAndThrowsException(); }; diff --git a/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..9916cd2e2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-enum.php @@ -0,0 +1,13 @@ += 8.1 + +namespace ImplodeParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + implode(',', [FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..362f02ea8b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ImplodeParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + // implode weirdness + implode(array: [['a']], separator: ','); + implode(separator: [['a']]); + implode(',', array: [['a']]); + implode(separator: ',', array: [['']]); +} + +function wrongNumberOfArguments(): void +{ + implode(array: ','); + join(array: ','); +} diff --git a/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php b/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php index 87bfdff02a..7b01bfbf96 100644 --- a/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php +++ b/tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php @@ -164,3 +164,32 @@ function missingCallableSignature(callable $cb) } } + +namespace MissingParamOutType { + /** + * @param array $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +namespace MissingParamClosureThisType { + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..bbf189cf96 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php @@ -0,0 +1,24 @@ += 8.1 + +namespace ParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_intersect([FooEnum::A], ['a']); + array_intersect(['a'], [FooEnum::A]); + array_intersect(['a'], [], [FooEnum::A]); + array_diff(['a'], [FooEnum::A]); + array_diff_assoc(['a'], [FooEnum::A]); + + array_combine([FooEnum::A], [['b']]); + $arr1 = [FooEnum::A]; + natsort($arr1); + natcasesort($arr1); + array_count_values($arr1); + array_fill_keys($arr1, 5); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..b8790a475d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php @@ -0,0 +1,23 @@ += 8.0 + +namespace ParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_combine(values: [['b']], keys: [['a']]); + $arr1 = [['a']]; + array_fill_keys(value: 5, keys: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_combine(values: [[5]]); + array_fill_keys(value: [5]); +} + +function validUsages() +{ + array_combine(values: [['b']], keys: ['a']); + $arr1 = ['a']; + array_fill_keys(value: 5, keys: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php new file mode 100644 index 0000000000..008c2d0142 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php @@ -0,0 +1,63 @@ += 7.4 += 8.0 namespace RequiredAfterOptional; @@ -9,3 +9,17 @@ fn (int $foo = 1, $bar): int => 1; // not OK fn (bool $foo = true, $bar): int => 1; // not OK + +fn (?int $foo = 1, $bar): int => 1; // not OK + +fn (?int $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = 1, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar): int => 1; // not OK + +fn (mixed $foo = 1, $bar): int => 1; // not OK + +fn (mixed $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): int => 1; // not OK diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php index fdd7db4709..da96ec6909 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -17,3 +17,31 @@ function (int $foo = 1, $bar): void // not OK function(bool $foo = true, $bar): void // not OK { }; + +function (?int $foo = 1, $bar): void // not OK +{ +}; + +function (?int $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = 1, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar): void // not OK +{ +}; + +function (mixed $foo = 1, $bar): void // not OK +{ +}; + +function (mixed $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +}; diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php index 373a441903..8937d262a7 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -22,3 +22,31 @@ function doLorem(bool $foo = true, $bar): void // not OK function doIpsum(bool $foo = true, ...$bar): void // OK { } + +function doDolor(?int $foo = 1, $bar): void // not OK +{ +} + +function doSit(?int $foo = null, $bar): void // not OK +{ +} + +function doAmet(int|null $foo = 1, $bar): void // not OK +{ +} + +function doConsectetur(int|null $foo = null, $bar): void // not OK +{ +} + +function doAdipiscing(mixed $foo = 1, $bar): void // not OK +{ +} + +function doElit(mixed $foo = null, $bar): void // not OK +{ +} + +function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..e3911d590f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-enum.php @@ -0,0 +1,26 @@ += 8.1 + +namespace SortParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages():void +{ + array_unique(['a', FooEnum::A]); + $arr1 = [FooEnum::A]; + sort($arr1, SORT_STRING); + rsort($arr1, SORT_LOCALE_STRING); + asort($arr1, SORT_STRING | SORT_FLAG_CASE); + arsort($arr1, SORT_LOCALE_STRING | SORT_FLAG_CASE); +} + +function validUsages(): void +{ + $arr = [FooEnum::A, 1]; + array_unique($arr, SORT_REGULAR); + sort($arr, SORT_REGULAR); + rsort($arr, 128); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..2a69d861ce --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php @@ -0,0 +1,32 @@ += 8.0 + +namespace SortParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_unique(flags: SORT_STRING, array: [['a'], ['b']]); + $arr1 = [['a']]; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_unique(flags: SORT_STRING); + sort(flags: SORT_STRING); + rsort(flags: SORT_STRING); + asort(flags: SORT_STRING); + arsort(flags: SORT_STRING); +} + +function validUsages() +{ + array_unique(flags: SORT_STRING, array: ['a', 'b']); + $arr1 = ['a']; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php new file mode 100644 index 0000000000..e1e0ff0dca --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php @@ -0,0 +1,71 @@ += 8.0 + +namespace UselessFunctionReturnPhp8; + +class FooClass +{ + public function explicitReturnNamed(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r(return: true, value: [ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } + + public function explicitNoReturnNamed(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r(return: false, value: [ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/useless-fn-return.php b/tests/PHPStan/Rules/Functions/data/useless-fn-return.php new file mode 100644 index 0000000000..204371923b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/useless-fn-return.php @@ -0,0 +1,71 @@ + 1, + 'spracheid' => 2, + ], true) + ); + + $x = print_r([ + 'template' => 1, + 'spracheid' => 2, + ], true); + + print_r([ + 'template' => 1, + 'spracheid' => 2, + ]); + + error_log( + "Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool) + ); + + print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool); + + $x = print_r([ + 'template' => 1, + 'spracheid' => 2, + ], $bool); + } + + public function missesReturn(): void + { + error_log( + "Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ]) + ); + } + + public function missesReturnVarDump(): string + { + return "Email-Template couldn't be found by parameters:" . var_export([ + 'template' => 1, + 'spracheid' => 2, + ]); + } + + public function explicitNoReturn(): void + { + error_log("Email-Template couldn't be found by parameters:" . print_r([ + 'template' => 1, + 'spracheid' => 2, + ], false) + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/vprintf.php b/tests/PHPStan/Rules/Functions/data/vprintf.php new file mode 100644 index 0000000000..de3f640310 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/vprintf.php @@ -0,0 +1,64 @@ +analyse([__DIR__ . '/data/bug-11517.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generators/data/bug-11517.php b/tests/PHPStan/Rules/Generators/data/bug-11517.php new file mode 100644 index 0000000000..56b64a5bd0 --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-11517.php @@ -0,0 +1,30 @@ + + */ + public function bug(): iterable + { + yield from []; + } + + /** + * @return iterable + */ + public function fine(): iterable + { + yield from []; + } + + /** + * @return iterable + */ + public function finetoo(): iterable + { + yield from []; + } +} diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index ae78243b8c..eb34897e10 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -43,7 +43,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooWrongClassExtended extends generic class ClassAncestorsExtends\FooGeneric but does not specify its types: T, U', 43, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsExtends\FooWrongTypeInExtendsTag @extends tag contains incompatible type class-string.', @@ -52,7 +51,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooWrongTypeInExtendsTag extends generic class ClassAncestorsExtends\FooGeneric but does not specify its types: T, U', 51, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type ClassAncestorsExtends\FooGeneric in PHPDoc tag @extends does not specify all template types of class ClassAncestorsExtends\FooGeneric: T, U', @@ -89,7 +87,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooExtendsGenericClass extends generic class ClassAncestorsExtends\FooGeneric but does not specify its types: T, U', 174, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric9.', @@ -106,7 +103,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FilterIteratorChild extends generic class FilterIterator but does not specify its types: TKey, TValue, TIterator', 215, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsExtends\FooObjectStorage @extends tag contains incompatible type ClassAncestorsExtends\FooObjectStorage.', @@ -115,7 +111,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooObjectStorage extends generic class SplObjectStorage but does not specify its types: TObject, TData', 226, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsExtends\FooCollection @extends tag contains incompatible type ClassAncestorsExtends\FooCollection&iterable.', @@ -124,7 +119,6 @@ public function testRuleExtends(): void [ 'Class ClassAncestorsExtends\FooCollection extends generic class ClassAncestorsExtends\AbstractFooCollection but does not specify its types: T', 239, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsExtends\FooGeneric in PHPDoc tag @extends is not allowed.', @@ -151,12 +145,10 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooWrongClassImplemented implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsImplements\FooWrongClassImplemented implements generic interface ClassAncestorsImplements\FooGeneric3 but does not specify its types: T, W', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsImplements\FooWrongTypeInImplementsTag @implements tag contains incompatible type class-string.', @@ -165,7 +157,6 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooWrongTypeInImplementsTag implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 60, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type ClassAncestorsImplements\FooGeneric in PHPDoc tag @implements does not specify all template types of interface ClassAncestorsImplements\FooGeneric: T, U', @@ -210,7 +201,6 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooImplementsGenericInterface implements generic interface ClassAncestorsImplements\FooGeneric but does not specify its types: T, U', 198, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in implemented type ClassAncestorsImplements\FooGeneric9 of class ClassAncestorsImplements\FooGeneric10.', @@ -223,7 +213,6 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooIterator implements generic interface Iterator but does not specify its types: TKey, TValue', 222, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Class ClassAncestorsImplements\FooCollection @implements tag contains incompatible type ClassAncestorsImplements\FooCollection&iterable.', @@ -232,7 +221,6 @@ public function testRuleImplements(): void [ 'Class ClassAncestorsImplements\FooCollection implements generic interface ClassAncestorsImplements\AbstractFooCollection but does not specify its types: T', 235, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsImplements\FooGeneric in PHPDoc tag @implements is not allowed.', diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 683f974e57..788be4cbd5 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,13 +17,16 @@ class ClassTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new ClassTemplateTypeRule( new TemplateTypeCheck( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new GenericObjectTypeCheck(), $typeAliasResolver, true, diff --git a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php index 6b19578ec2..a7851f6c52 100644 --- a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -44,7 +44,6 @@ public function testRule(): void [ 'Enum EnumGenericAncestors\Foo4 implements generic interface EnumGenericAncestors\Generic but does not specify its types: T, U', 40, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type EnumGenericAncestors\Generic in PHPDoc tag @implements does not specify all template types of interface EnumGenericAncestors\Generic: T, U', diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 1fe4441393..35df206865 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,21 @@ class FunctionTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php index f0e148bf56..c57774efed 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php @@ -133,12 +133,10 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\FooWrongClassImplemented extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Interface InterfaceAncestorsExtends\FooWrongClassImplemented extends generic interface InterfaceAncestorsExtends\FooGeneric3 but does not specify its types: T, W', 52, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Interface InterfaceAncestorsExtends\FooWrongTypeInImplementsTag @extends tag contains incompatible type class-string.', @@ -147,7 +145,6 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\FooWrongTypeInImplementsTag extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 60, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type InterfaceAncestorsExtends\FooGeneric in PHPDoc tag @extends does not specify all template types of interface InterfaceAncestorsExtends\FooGeneric: T, U', @@ -192,7 +189,6 @@ public function testRuleExtends(): void [ 'Interface InterfaceAncestorsExtends\ExtendsGenericInterface extends generic interface InterfaceAncestorsExtends\FooGeneric but does not specify its types: T, U', 197, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Template type T is declared as covariant, but occurs in invariant position in extended type InterfaceAncestorsExtends\FooGeneric9 of interface InterfaceAncestorsExtends\FooGeneric10.', diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index ac5755f763..0623a7d1c2 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,11 +16,20 @@ class InterfaceTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new InterfaceTemplateTypeRule( - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } diff --git a/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php new file mode 100644 index 0000000000..e754794566 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php @@ -0,0 +1,60 @@ + + */ +class MethodTagTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->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 GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-tag-template.php'], [ + [ + 'PHPDoc tag @method template U for method MethodTagTemplate\HelloWorld::sayHello() has invalid bound type MethodTagTemplate\Nonexisting.', + 13, + ], + [ + 'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::sayHello() cannot have existing class stdClass as its name.', + 13, + ], + [ + 'PHPDoc tag @method template T for method MethodTagTemplate\HelloWorld::sayHello() shadows @template T for class MethodTagTemplate\HelloWorld.', + 13, + ], + [ + 'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::typeAlias() cannot have existing type alias TypeAlias as its name.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index 9e5a896865..a845993344 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,21 @@ class MethodTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 1e51ca4be5..99ad839123 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,21 @@ class TraitTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } diff --git a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php index 3ec30d55c6..2101cedb5c 100644 --- a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -40,7 +40,6 @@ public function testRule(): void [ 'Class UsedTraits\Baz uses generic trait UsedTraits\GenericTrait but does not specify its types: T', 38, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Generic type UsedTraits\GenericTrait in PHPDoc tag @use specifies 2 template types, but trait UsedTraits\GenericTrait supports only 1: T', @@ -53,7 +52,6 @@ public function testRule(): void [ 'Trait UsedTraits\NestedTrait uses generic trait UsedTraits\GenericTrait but does not specify its types: T', 54, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Call-site variance annotation of covariant Throwable in generic type UsedTraits\GenericTrait in PHPDoc tag @use is not allowed.', diff --git a/tests/PHPStan/Rules/Generics/data/bug-3769.php b/tests/PHPStan/Rules/Generics/data/bug-3769.php index 07cbd0b860..aeb273d479 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-3769.php +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -29,6 +29,7 @@ function foo( $a = assertType('array', stringValues($foo)); $a = assertType('array', stringValues($bar)); $a = assertType('array', stringValues($baz)); + echo 'test'; }; /** @@ -37,6 +38,7 @@ function foo( */ function fooUnion($foo): void { $a = assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo); + echo 'test'; } /** diff --git a/tests/PHPStan/Rules/Generics/data/method-tag-template.php b/tests/PHPStan/Rules/Generics/data/method-tag-template.php new file mode 100644 index 0000000000..77a6f202c2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-tag-template.php @@ -0,0 +1,15 @@ +(T $a, U $b, stdClass $c) + * @method void typeAlias(TypeAlias $a) + */ +class HelloWorld +{ +} diff --git a/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php index ea1b9d67b9..c34760e39e 100644 --- a/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php +++ b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php @@ -20,23 +20,23 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/ignore-parse-error.php'], [ [ - 'Parse error in @phpstan-ignore: Unexpected comma (,)', + 'Parse error in @phpstan-ignore: Unexpected comma (,) after comma (,), expected identifier', 10, ], [ - 'Parse error in @phpstan-ignore: Closing parenthesis ")" before opening parenthesis "("', + 'Parse error in @phpstan-ignore: Unexpected T_CLOSE_PARENTHESIS after identifier, expected comma (,) or end or T_OPEN_PARENTHESIS', 13, ], [ - 'Parse error in @phpstan-ignore: Unclosed opening parenthesis "(" without closing parenthesis ")"', - 18, + 'Parse error in @phpstan-ignore: Unexpected end, unclosed opening parenthesis', + 19, ], [ - 'Parse error in @phpstan-ignore: First token is not an identifier', + 'Parse error in @phpstan-ignore: Unexpected T_OTHER \'čičí\' after @phpstan-ignore, expected identifier', 23, ], [ - 'Parse error in @phpstan-ignore: Missing identifier', + 'Parse error in @phpstan-ignore: Unexpected end after @phpstan-ignore, expected identifier', 27, ], ]); @@ -46,7 +46,7 @@ public function testRuleWithUnusedTrait(): void { $this->analyse([__DIR__ . '/data/ignore-parse-error-trait.php'], [ [ - 'Parse error in @phpstan-ignore: Unexpected comma (,)', + 'Parse error in @phpstan-ignore: Unexpected comma (,) after comma (,), expected identifier', 10, ], ]); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index e67ccac827..8877e53e31 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -499,6 +499,10 @@ public function testCallMethods(): void 1490, 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], + [ + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], [ 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, @@ -819,6 +823,10 @@ public function testCallMethodsOnThisOnly(): void 1490, 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], + [ + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], [ 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, @@ -960,6 +968,10 @@ public function testClosureBind(): void 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', 44, ], + [ + 'Parameter #2 $newScope of method Closure::bindTo() expects \'static\'|class-string|object|null, \'CallClosureBind\\\Bar3\' given.', + 74, + ], ]); } @@ -1547,6 +1559,10 @@ public function dataExplicitMixed(): array 'Cannot call method foo() on mixed.', 17, ], + [ + 'Cannot call method foo() on T of mixed.', + 26, + ], [ 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', 43, @@ -1600,10 +1616,6 @@ public function dataImplicitMixed(): array 'Parameter #1 $i of method CheckImplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', 42, ], - [ - 'Parameter #1 $i of method CheckImplicitMixedMethodCall\Bar::doBar() expects int, T given.', - 65, - ], [ 'Parameter #1 $cb of method CheckImplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', 139, @@ -1997,7 +2009,7 @@ public function testBug4557(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4557.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4557.php'], []); } public function testBug4209(): void @@ -2005,7 +2017,7 @@ public function testBug4209(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4209.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209.php'], []); } public function testBug4209Two(): void @@ -2013,7 +2025,7 @@ public function testBug4209Two(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4209-2.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4209-2.php'], []); } public function testBug3321(): void @@ -2021,7 +2033,7 @@ public function testBug3321(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3321.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3321.php'], []); } public function testBug4498(): void @@ -2029,7 +2041,7 @@ public function testBug4498(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4498.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4498.php'], []); } public function testBug3922(): void @@ -2037,7 +2049,7 @@ public function testBug3922(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3922.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3922.php'], [ [ 'Parameter #1 $query of method Bug3922\FooQueryHandler::handle() expects Bug3922\FooQuery, Bug3922\BarQuery given.', 63, @@ -2050,7 +2062,7 @@ public function testBug4642(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4642.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4642.php'], []); } public function testBug4008(): void @@ -2153,7 +2165,7 @@ public function testGenericObjectLowerBound(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/generic-object-lower-bound.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], [ [ 'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection, GenericObjectLowerBound\Collection given.', 48, @@ -2393,6 +2405,18 @@ public function testEnums(): void 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{true} given.', 70, ], + [ + 'Parameter #1 $one of method CallMethodInEnum\TestPassingEnums::requireOne() expects CallMethodInEnum\TestPassingEnums::ONE, $this(CallMethodInEnum\TestPassingEnums)&CallMethodInEnum\TestPassingEnums given.', + 91, + ], + [ + 'Parameter #1 $one of method CallMethodInEnum\TestPassingEnums::requireOne() expects CallMethodInEnum\TestPassingEnums::ONE, $this(CallMethodInEnum\TestPassingEnums)&CallMethodInEnum\TestPassingEnums given.', + 99, + ], + [ + 'Parameter #1 $one of method CallMethodInEnum\TestPassingEnums::requireOne() expects CallMethodInEnum\TestPassingEnums::ONE, $this(CallMethodInEnum\TestPassingEnums)&CallMethodInEnum\TestPassingEnums given.', + 106, + ], ]); } @@ -2405,7 +2429,7 @@ public function testBug6239(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-6293.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6293.php'], []); } public function testBug6306(): void @@ -2809,7 +2833,7 @@ public function testBug8752(): void $this->checkNullables = true; $this->checkUnionTypes = true; $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], [ [ 'Cannot call method abc() on class-string.', 18, @@ -3172,4 +3196,130 @@ public function testRequireImplements(): void ]); } + public function testBug6371(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-6371.php'], [ + [ + 'Parameter #1 $t of method Bug6371\HelloWorld::compare() expects int, true given.', + 24, + ], + [ + 'Parameter #2 $k of method Bug6371\HelloWorld::compare() expects string, false given.', + 24, + ], + ]); + } + + public function testBugTemplateMixedUnionIntersect(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-template-mixed-union-intersect.php'], [ + [ + 'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface&T of mixed::bar().', + 17, + ], + [ + 'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface::bar().', + 20, + ], + [ + 'Cannot call method foo() on BugTemplateMixedUnionIntersect\FooInterface|T of mixed.', + 23, + ], + [ + 'Cannot call method foo() on mixed.', + 25, + ], + ]); + } + + public function testBug9009(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-9009.php'], []); + } + + public function testBuSplObjectStorageRemove(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-SplObjectStorage-remove.php'], [ + // removeNoIntersect should be reported, but unfortunately it cannot be expressed by the type system. + ]); + } + + public function testClosureBindToParamClosureThis(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-to-param-closure-this.php'], [ + [ + 'Parameter #1 $newThis of method Closure::bindTo() expects stdClass, ClosureBindToParamClosureThis\Foo given.', + 23, + ], + ]); + } + + public function testPureCallable(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/pure-callable-accepts.php'], [ + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, callable(): mixed given.', + 33, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 35, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 36, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, Closure(): 1 given.', + 41, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureClosure() expects pure-Closure, Closure(): 1 given.', + 61, + ], + ]); + } + + public function testClosureParameterGenerics(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/closure-parameter-generics.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 670e24f5be..27718dc9c7 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -11,6 +13,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -23,13 +27,35 @@ class CallStaticMethodsRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); return new CallStaticMethodsRule( - new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + true, + ), + new FunctionCallParametersCheck( + $ruleLevelHelper, + new NullsafeCheck(), + new PhpVersion(80000), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + true, + ), ); } @@ -224,7 +250,7 @@ public function testCallStaticMethods(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Call to an undefined static method static(CallStaticMethods\CallWithStatic)::nonexistent().', + 'Call to an undefined static method CallStaticMethods\CallWithStatic::nonexistent().', 344, ], ]); @@ -603,6 +629,21 @@ public function testBug5781(): void ]); } + public function testBug8296(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-8296.php'], [ + [ + 'Parameter #1 $objects of static method Bug8296\VerifyLoginTask::continueDump() expects array, array given.', + 12, + ], + [ + 'Parameter #1 $string of static method Bug8296\VerifyLoginTask::stringByRef() expects string, int given.', + 15, + ], + ]); + } + public function testRequireExtends(): void { $this->checkThisOnly = false; @@ -629,4 +670,175 @@ public function testRequireImplements(): void ]); } + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Cannot call static method foo() on mixed.', + 17, + ], + [ + 'Cannot call static method foo() on T of mixed.', + 26, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, mixed given.', + 43, + ], + [ + 'Only iterables can be unpacked, mixed given in argument #1.', + 52, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, T given.', + 81, + ], + [ + 'Only iterables can be unpacked, T of mixed given in argument #1.', + 84, + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callAcceptsExplicitMixed() expects callable(mixed): void, Closure(int): void given.', + 134, + 'Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 161, + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 168, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Cannot call static method foo() on mixed.', + 16, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, mixed given.', + 42, + ], + [ + 'Only iterables can be unpacked, mixed given in argument #1.', + 51, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/call-static-method-mixed.php'], $errors); + } + + public function testBugWrongMethodNameWithTemplateMixed(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-wrong-method-name-with-template-mixed.php'], [ + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 14, + ], + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 25, + ], + [ + 'Call to an undefined static method T of object&UnitEnum::from().', + 36, + ], + [ + 'Call to an undefined static method UnitEnum::from().', + 43, + ], + ]); + } + + public function testConditionalParam(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/conditional-param.php'], [ + [ + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects string, true given.', + 16, + ], + [ + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects bool, string given.', + 20, + ], + [ + // wrong + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects string, true given.', + 22, + ], + ]); + } + + public function testClosureBindParamClosureThis(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-param-closure-this.php'], [ + [ + 'Parameter #2 $newThis of static method Closure::bind() expects stdClass, ClosureBindParamClosureThis\Foo given.', + 25, + ], + ]); + } + + public function testClosureBind(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/closure-bind.php'], [ + [ + 'Parameter #3 $newScope of static method Closure::bind() expects \'static\'|class-string|object|null, \'CallClosureBind\\\Bar3\' given.', + 68, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php index 917cc77f8b..29e99526e2 100644 --- a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php @@ -13,7 +13,7 @@ class CallToConstructorStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToConstructorStatementWithoutSideEffectsRule($this->createReflectionProvider()); + return new CallToConstructorStatementWithoutSideEffectsRule($this->createReflectionProvider(), true); } public function testRule(): void @@ -23,6 +23,14 @@ public function testRule(): void 'Call to Exception::__construct() on a separate line has no effect.', 6, ], + [ + 'Call to new PDOStatement() on a separate line has no effect.', + 11, + ], + [ + 'Call to new stdClass() on a separate line has no effect.', + 12, + ], [ 'Call to ConstructorStatementNoSideEffects\ConstructorWithPure::__construct() on a separate line has no effect.', 57, @@ -31,6 +39,10 @@ public function testRule(): void 'Call to ConstructorStatementNoSideEffects\ConstructorWithPureAndThrowsVoid::__construct() on a separate line has no effect.', 58, ], + [ + 'Call to new ConstructorStatementNoSideEffects\NoConstructor() on a separate line has no effect.', + 68, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php index 29fae1feb4..f1d3d5902d 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -63,15 +63,23 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure5() on a separate line has no effect.', + 59, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index 324e3c33c7..03cbdaa49d 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -44,19 +44,27 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/static-method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure5() on a separate line has no effect.', + 59, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\PureThrows::pureAndThrowsVoid() on a separate line has no effect.', - 67, + 85, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index fd8e406297..a0cf90c942 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,20 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -196,7 +210,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Method RequiredAfterOptional\Foo::doAmet() uses native union types but they're supported only on PHP 8.0 and later.", + 33, + ], + [ + "Method RequiredAfterOptional\Foo::doConsectetur() uses native union types but they're supported only on PHP 8.0 and later.", + 37, + ], + [ + "Method RequiredAfterOptional\Foo::doSed() uses native union types but they're supported only on PHP 8.0 and later.", + 49, + ], + ], ], [ 80000, @@ -213,6 +240,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 21, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 8, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 49, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 8, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 49, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 49, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], ], ], ]; diff --git a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php index 62c35c0573..e55eb6eede 100644 --- a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php @@ -66,7 +66,16 @@ public function testDefaultValueForPromotedProperty(): void 'Default value of the parameter #2 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', 10, ], + [ + 'Default value of the parameter #4 $intProp (null) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 12, + ], ]); } + public function testBug10956(): void + { + $this->analyse([__DIR__ . '/data/bug-10956.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 325115301c..9ce75af6e6 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -39,7 +41,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index f311149397..fecb8a1045 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, [])); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); } public function testRule(): void @@ -48,27 +48,40 @@ public function testRule(): void [ 'Method MissingMethodParameterTypehint\Bar::acceptsGenericInterface() has parameter $i with generic interface MissingMethodParameterTypehint\GenericInterface but does not specify its types: T, U', 91, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\Bar::acceptsGenericClass() has parameter $c with generic class MissingMethodParameterTypehint\GenericClass but does not specify its types: A, B', 101, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CollectionIterableAndGeneric::acceptsCollection() has parameter $collection with generic interface DoctrineIntersectionTypeIsSupertypeOf\Collection but does not specify its types: TKey, T', 111, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CollectionIterableAndGeneric::acceptsCollection2() has parameter $collection with generic interface DoctrineIntersectionTypeIsSupertypeOf\Collection but does not specify its types: TKey, T', 119, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodParameterTypehint\CallableSignature::doFoo() has parameter $cb with no signature specified for callable.', 180, ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 207, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 215, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamClosureThisType::generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 226, + ], + [ + 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', + 238, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); @@ -101,7 +114,6 @@ public function testDeepInspectTypes(): void [ 'Method DeepInspectTypes\Foo::doBar() has parameter $bars with generic class DeepInspectTypes\Bar but does not specify its types: T', 17, - MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index a4e64d8b6f..0762900901 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -44,12 +44,10 @@ public function testRule(): void [ 'Method MissingMethodReturnTypehint\Bar::returnsGenericInterface() return type with generic interface MissingMethodReturnTypehint\GenericInterface does not specify its types: T, U', 79, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodReturnTypehint\Bar::returnsGenericClass() return type with generic class MissingMethodReturnTypehint\GenericClass does not specify its types: A, B', 89, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Method MissingMethodReturnTypehint\CallableSignature::doFoo() return type has no signature specified for callable.', @@ -65,7 +63,7 @@ public function testIndirectInheritanceBug2740(): void public function testArrayTypehintWithoutNullInPhpDoc(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/array-typehint-without-null-in-phpdoc.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/array-typehint-without-null-in-phpdoc.php'], []); } public function testBug4415(): void diff --git a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php index 73cd3c3f68..b954794c6e 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php @@ -29,7 +29,7 @@ public function testRule(): void public function testNullsafeVsScalar(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/nullsafe-vs-scalar.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/nullsafe-vs-scalar.php'], []); } public function testBug8664(): void @@ -43,7 +43,7 @@ public function testBug9293(): void $this->markTestSkipped('Test requires PHP 8.0.'); } - $this->analyse([__DIR__ . '/../../Analyser/data/bug-9293.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9293.php'], []); } public function testBug6922b(): void diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 7f83d1cdea..826586689a 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -770,6 +770,10 @@ public function testOverrideAttribute(): void 'Method OverrideAttribute\Bar::test2() has #[\Override] attribute but does not override any method.', 24, ], + [ + 'Method OverrideAttribute\ChildOfParentWithConstructor::__construct() has #[\Override] attribute but does not override any method.', + 42, + ], ]); } @@ -783,6 +787,10 @@ public function dataCheckMissingOverrideAttribute(): iterable 'Method CheckMissingOverrideAttr\Bar::doFoo() overrides method CheckMissingOverrideAttr\Foo::doFoo() but is missing the #[\Override] attribute.', 18, ], + [ + 'Method CheckMissingOverrideAttr\ChildOfParentWithAbstractConstructor::__construct() overrides method CheckMissingOverrideAttr\ParentWithAbstractConstructor::__construct() but is missing the #[\Override] attribute.', + 49, + ], ]]; } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 0939b3fca9..6303bdcdc4 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -507,7 +507,7 @@ public function testBug4795(): void public function testBug4803(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4803.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4803.php'], []); } public function testBug7020(): void @@ -757,7 +757,7 @@ public function testBug7904(): void public function testBug7996(): void { $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-7996.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7996.php'], []); } public function testBug6358(): void @@ -812,16 +812,21 @@ public function testBug7519(): void public function testBug8223(): void { $this->checkBenevolentUnionTypes = true; - $this->analyse([__DIR__ . '/data/bug-8223.php'], [ - [ - 'Method Bug8223\HelloWorld::sayHello() should return DateTimeImmutable but returns (DateTimeImmutable|false).', - 11, - ], - [ - 'Method Bug8223\HelloWorld::sayHello2() should return array but returns array.', - 21, - ], - ]); + + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Method Bug8223\HelloWorld::sayHello() should return DateTimeImmutable but returns (DateTimeImmutable|false).', + 11, + ], + [ + 'Method Bug8223\HelloWorld::sayHello2() should return array but returns array.', + 21, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-8223.php'], $errors); } public function testBug8146bErrors(): void @@ -1019,4 +1024,24 @@ public function testBug5008(): void $this->analyse([__DIR__ . '/data/bug-5008.php'], []); } + public function testArrayPushPreservesList(): void + { + $this->analyse([__DIR__ . '/data/array-push-preserves-list.php'], []); + } + + public function testBug10721(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10721.php'], []); + } + + public function testBug11491(): void + { + $this->analyse([__DIR__ . '/data/bug-11491.php'], []); + } + + public function testBug3759(): void + { + $this->analyse([__DIR__ . '/data/bug-3759.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php index 0f5b3087ed..c726ab7fdb 100644 --- a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -23,7 +25,16 @@ protected function getRule(): Rule $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false); return new StaticMethodCallableRule( - new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + true, + ), new PhpVersion($this->phpVersion), ); } @@ -96,7 +107,7 @@ public function testRule(): void public function testBug8752(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); } public function testCallsOnGenericClassString(): void diff --git a/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php new file mode 100644 index 0000000000..d892ab794d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php @@ -0,0 +1,83 @@ + $a + * @return list + */ + public function doFoo(array $a): array + { + array_push($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_push($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_push($b, ...$a); + + return $b; + } + +} + +class Bar +{ + + /** + * @param list $a + * @return list + */ + public function doFoo(array $a): array + { + array_unshift($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_unshift($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_unshift($b, ...$a); + + return $b; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10956.php b/tests/PHPStan/Rules/Methods/data/bug-10956.php new file mode 100644 index 0000000000..459d5bd6c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10956.php @@ -0,0 +1,19 @@ +id; + } + + /** @return non-empty-string */ + public function name(): string + { + return $this->name; + } + + /** @return non-empty-string */ + public function toFacetValue(): string + { + return sprintf( + '%s%s%s', + $this->name, + self::SEPARATOR, + $this->id, + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3759.php b/tests/PHPStan/Rules/Methods/data/bug-3759.php new file mode 100644 index 0000000000..ddbafbefdc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3759.php @@ -0,0 +1,31 @@ + ['x' => 'x'], + 'minor' => ['y' => 'y'], + 'patch' => ['z' => 'z'], + ]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6371.php b/tests/PHPStan/Rules/Methods/data/bug-6371.php new file mode 100644 index 0000000000..aa1d09f62f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6371.php @@ -0,0 +1,25 @@ + $hw + * @return void + */ +function foo (HelloWorld $hw): void { + $hw->compare(1, 'foo'); + $hw->compare(true, false); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-8296.php b/tests/PHPStan/Rules/Methods/data/bug-8296.php new file mode 100644 index 0000000000..b257780358 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8296.php @@ -0,0 +1,29 @@ + new stdClass(), + "b" => true + ]; + self::continueDump($dummy); + + $string = 12345; + self::stringByRef($string); + } + + /** + * @phpstan-param array $objects + * @phpstan-param-out array $objects + */ + private static function continueDump(array &$objects) : void{ + + } + + private static function stringByRef(string &$string) : void{ + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9009.php b/tests/PHPStan/Rules/Methods/data/bug-9009.php new file mode 100644 index 0000000000..a60ca63003 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9009.php @@ -0,0 +1,26 @@ +addHook($fx); + } +} + +(new Hook())->addHookDynamic(function (Hook $hook) { + return new \stdClass(); +}); diff --git a/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php b/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php new file mode 100644 index 0000000000..3470979dd6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php @@ -0,0 +1,80 @@ + */ +class HelloWorld +{ + /** @var ObjectStorage */ + private \SplObjectStorage $foo; + + /** + * @param ObjectStorage $other + * @return ObjectStorage + */ + public function removeSame(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeNarrower(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeWider(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removePossibleIntersect(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeNoIntersect(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php b/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php new file mode 100644 index 0000000000..ce0c88fbe2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php @@ -0,0 +1,26 @@ += 8.0 + +namespace BugTemplateMixedUnionIntersect; + +interface FooInterface +{ + public function foo(): int; +} + +/** + * @template T of mixed + * @param T $a + */ +function foo(mixed $a, FooInterface $b, mixed $c): void +{ + if ($a instanceof FooInterface) { + var_dump($a->bar()); + } + if ($c instanceof FooInterface) { + var_dump($c->bar()); + } + $d = rand() > 1 ? $a : $b; + var_dump($d->foo()); + $d = rand() > 1 ? $c : $b; + var_dump($d->foo()); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php b/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php new file mode 100644 index 0000000000..e4148acbb2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php @@ -0,0 +1,46 @@ += 8.1 + +namespace BugWrongMethodNameWithTemplateMixed; + +class HelloWorld +{ + /** + * @template T + * @param T $val + */ + public function foo(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of mixed + * @param T $val + */ + public function foo2(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of object + * @param T $val + */ + public function foo3(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + public function foo4(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php index 986fc854cb..5fc2439152 100644 --- a/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php +++ b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php @@ -70,3 +70,41 @@ function doFooArray() { $this->helloArray([true]); } } + +enum TestPassingEnums { + case ONE; + case TWO; + + /** + * @param self::ONE $one + * @return void + */ + public function requireOne(self $one): void + { + + } + + public function doFoo(): void + { + match ($this) { + self::ONE => $this->requireOne($this), + self::TWO => $this->requireOne($this), + }; + } + + public function doFoo2(): void + { + match ($this) { + self::ONE => $this->requireOne($this), + default => $this->requireOne($this), + }; + } + + public function doFoo3(): void + { + match ($this) { + self::TWO => $this->requireOne($this), + default => $this->requireOne($this), + }; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php new file mode 100644 index 0000000000..4dcb333ed1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php @@ -0,0 +1,171 @@ += 8.0 + +namespace CallStaticMethodMixed; + +class Foo +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + $implicit::foo(); + $explicit::foo(); + } + + /** + * @template T + * @param T $t + */ + public function doBar($t): void + { + $t::foo(); + } + +} + +class Bar +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + self::doBar($implicit); + self::doBar($explicit); + + self::acceptImplicitMixed($implicit); + self::acceptImplicitMixed($explicit); + + self::acceptExplicitMixed($implicit); + self::acceptExplicitMixed($explicit); + + self::acceptVariadicArguments(...$implicit); + self::acceptVariadicArguments(...$explicit); + } + + public static function doBar(int $i): void + { + + } + + public static function acceptImplicitMixed($mixed): void + { + + } + + public static function acceptExplicitMixed(mixed $mixed): void + { + + } + + public static function acceptVariadicArguments(mixed... $args): void + { + + } + + /** + * @template T + * @param T $t + */ + public function doLorem($t): void + { + self::doBar($t); + self::acceptImplicitMixed($t); + self::acceptExplicitMixed($t); + self::acceptVariadicArguments(...$t); + } + +} + +class CallableMixed +{ + + /** + * @param callable(mixed): void $cb + */ + public static function callAcceptsExplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(int): void $cb + */ + public static function callAcceptsInt(callable $cb): void + { + + } + + /** + * @param callable(): mixed $cb + */ + public static function callReturnsExplicitMixed(callable $cb): void + { + + } + + public static function callReturnsImplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public static function callReturnsInt(callable $cb): void + { + + } + + public static function doLorem(int $i, mixed $explicitMixed, $implicitMixed): void + { + $acceptsInt = function (int $i): void { + + }; + self::callAcceptsExplicitMixed($acceptsInt); + self::callAcceptsInt($acceptsInt); + + $acceptsExplicitMixed = function (mixed $m): void { + + }; + self::callAcceptsExplicitMixed($acceptsExplicitMixed); + self::callAcceptsInt($acceptsExplicitMixed); + + $acceptsImplicitMixed = function ($m): void { + + }; + self::callAcceptsExplicitMixed($acceptsImplicitMixed); + self::callAcceptsInt($acceptsImplicitMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + self::callReturnsExplicitMixed($returnsInt); + self::callReturnsImplicitMixed($returnsInt); + self::callReturnsInt($returnsInt); + + $returnsExplicitMixed = function () use ($explicitMixed): mixed { + return $explicitMixed; + }; + self::callReturnsExplicitMixed($returnsExplicitMixed); + self::callReturnsImplicitMixed($returnsExplicitMixed); + self::callReturnsInt($returnsExplicitMixed); + + $returnsImplicitMixed = function () use ($implicitMixed): mixed { + return $implicitMixed; + }; + self::callReturnsExplicitMixed($returnsImplicitMixed); + self::callReturnsImplicitMixed($returnsImplicitMixed); + self::callReturnsInt($returnsImplicitMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php b/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php index 3e1cf69a07..0206b87068 100644 --- a/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php +++ b/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php @@ -21,3 +21,31 @@ public function doFoo(): void } } + +class ParentWithConstructor +{ + + public function __construct() {} + +} + + +class ChildOfParentWithConstructor extends ParentWithConstructor { + + public function __construct() {} + +} + +abstract class ParentWithAbstractConstructor +{ + + abstract public function __construct(); + +} + + +class ChildOfParentWithAbstractConstructor extends ParentWithAbstractConstructor { + + public function __construct() {} + +} diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this.php b/tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this.php new file mode 100644 index 0000000000..e251f00969 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this.php @@ -0,0 +1,28 @@ +bindTo(new self()); // not checked + + // overwritten + $b = function (): void { + + }; + $b->bindTo(new self()); // not checked + + $c->bindTo(new \stdClass()); // ok + $c->bindTo(new self()); // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind.php b/tests/PHPStan/Rules/Methods/data/closure-bind.php index f6aa9e05b7..57b9ccf5ff 100644 --- a/tests/PHPStan/Rules/Methods/data/closure-bind.php +++ b/tests/PHPStan/Rules/Methods/data/closure-bind.php @@ -49,4 +49,33 @@ public function fooMethod(): Foo })->call(new Foo()); } + public function x(): bool + { + return 1.0; + } + + public function testClassString(): bool + { + $fx = function () { + return $this->x(); + }; + + $res = 0.0; + $res += \Closure::bind($fx, $this)(); + $res += \Closure::bind($fx, $this, 'static')(); + $res += \Closure::bind($fx, $this, Foo2::class)(); + $res += \Closure::bind($fx, $this, 'CallClosureBind\Bar2')(); + $res += \Closure::bind($fx, $this, 'CallClosureBind\Bar3')(); + + $res += $fx->bindTo($this)(); + $res += $fx->bindTo($this, 'static')(); + $res += $fx->bindTo($this, Foo2::class)(); + $res += $fx->bindTo($this, 'CallClosureBind\Bar2')(); + $res += $fx->bindTo($this, 'CallClosureBind\Bar3')(); + + return $res; + } + } + +class Bar2 extends Bar {} diff --git a/tests/PHPStan/Rules/Methods/data/closure-parameter-generics.php b/tests/PHPStan/Rules/Methods/data/closure-parameter-generics.php new file mode 100644 index 0000000000..29ee3b1663 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/closure-parameter-generics.php @@ -0,0 +1,38 @@ +retryableTransaction(function (Transaction $tr) { + return $tr; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/conditional-param.php b/tests/PHPStan/Rules/Methods/data/conditional-param.php new file mode 100644 index 0000000000..06adf7b93e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/conditional-param.php @@ -0,0 +1,23 @@ + $flags + */ + public static function replaceCallback($demoArg, int $flags = 0): void + {} +} + +function (): void { + HelloWorld::replaceCallback(true); // correct, error expected + HelloWorld::replaceCallback("string"); // correct + + HelloWorld::replaceCallback(true, PREG_OFFSET_CAPTURE); // correct + HelloWorld::replaceCallback("string", PREG_OFFSET_CAPTURE); // correct, error expected + + HelloWorld::replaceCallback(true, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL); // should not report error +}; diff --git a/tests/PHPStan/Rules/Methods/data/constructor-statement-no-side-effects.php b/tests/PHPStan/Rules/Methods/data/constructor-statement-no-side-effects.php index 5b82190623..02f109f3c2 100644 --- a/tests/PHPStan/Rules/Methods/data/constructor-statement-no-side-effects.php +++ b/tests/PHPStan/Rules/Methods/data/constructor-statement-no-side-effects.php @@ -58,3 +58,12 @@ function(): void { new ConstructorWithPureAndThrowsVoid(); new ConstructorWithPureAndThrowsException(); }; + +class NoConstructor +{ + +} + +function (): void { + new NoConstructor(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php b/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php index 649b7b2e56..a2c881155b 100644 --- a/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php +++ b/tests/PHPStan/Rules/Methods/data/default-value-for-promoted-property.php @@ -8,7 +8,8 @@ class Foo public function __construct( private int $foo = 'foo', /** @var int */ private $foo = '', - private int $baz = 1 + private int $baz = 1, + private int $intProp = null, ) {} } diff --git a/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects-phpdoc.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects-phpdoc.php index 1f6d4969b2..23bc2b43a2 100644 --- a/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects-phpdoc.php +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-no-side-effects-phpdoc.php @@ -32,6 +32,22 @@ function pure3(string $a): string { return $a; } + + /** + * @phan-pure + */ + function pure4(string $a): string + { + return $a; + } + + /** + * @phan-side-effect-free + */ + function pure5(string $a): string + { + return $a; + } } function(): void { @@ -39,4 +55,6 @@ function(): void { (new Bzz())->pure1('test'); (new Bzz())->pure2('test'); (new Bzz())->pure3('test'); + (new Bzz())->pure4('test'); + (new Bzz())->pure5('test'); }; diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index 8346689258..d5a333491b 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -197,3 +197,47 @@ public function unserialize($data): void } } + +class MissingParamOutType { + + /** + * @param array $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +class MissingParamClosureThisType { + + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } + +} + +class MissingPureClosureSignatureType { + + /** + * @param pure-Closure $cb + */ + function doFoo(\Closure $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/override-attribute.php b/tests/PHPStan/Rules/Methods/data/override-attribute.php index 0b0600be2c..ca16bdba5b 100644 --- a/tests/PHPStan/Rules/Methods/data/override-attribute.php +++ b/tests/PHPStan/Rules/Methods/data/override-attribute.php @@ -28,3 +28,33 @@ public function test2(): void } } + +class ParentWithConstructor +{ + + public function __construct() {} + +} + + +class ChildOfParentWithConstructor extends ParentWithConstructor { + + #[\Override] + public function __construct() {} + +} + +abstract class ParentWithAbstractConstructor +{ + + abstract public function __construct(); + +} + + +class ChildOfParentWithAbstractConstructor extends ParentWithAbstractConstructor { + + #[\Override] + public function __construct() {} + +} diff --git a/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php new file mode 100644 index 0000000000..3c801d2090 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/pure-callable-accepts.php @@ -0,0 +1,68 @@ +acceptsCallable($cb); + $this->acceptsCallable($pureCb); + $this->acceptsPureCallable($cb); + $this->acceptsPureCallable($pureCb); + $this->acceptsInt($cb); + $this->acceptsInt($pureCb); + + $this->acceptsPureCallable(function (): int { + return 1; + }); + $this->acceptsPureCallable(function (): int { + sleep(1); + + return 1; + }); + } + + /** + * @param pure-Closure $cb + */ + public function acceptsPureClosure(\Closure $cb): void + { + + } + + public function doFoo2(): void + { + $this->acceptsPureClosure(function (): int { + return 1; + }); + $this->acceptsPureClosure(function (): int { + sleep(1); + + return 1; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php index 647a05f3af..d93cdd1bd8 100644 --- a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ - 8.0 namespace RequiredAfterOptional; @@ -22,4 +22,31 @@ public function doLorem(bool $foo = true, $bar): void // not OK { } + public function doDolor(?int $foo = 1, $bar): void // not OK + { + } + + public function doSit(?int $foo = null, $bar): void // not OK + { + } + + public function doAmet(int|null $foo = 1, $bar): void // not OK + { + } + + public function doConsectetur(int|null $foo = null, $bar): void // not OK + { + } + + public function doAdipiscing(mixed $foo = 1, $bar): void // not OK + { + } + + public function doElit(mixed $foo = null, $bar): void // not OK + { + } + + public function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK + { + } } diff --git a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects-phpdoc.php b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects-phpdoc.php index 9eaa60284e..52a21511cb 100644 --- a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects-phpdoc.php +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects-phpdoc.php @@ -32,6 +32,22 @@ static function pure3(string $a): string { return $a; } + + /** + * @phan-pure + */ + static function pure4(string $a): string + { + return $a; + } + + /** + * @phan-side-effect-free + */ + static function pure5(string $a): string + { + return $a; + } } function(): void { @@ -39,6 +55,8 @@ function(): void { BzzStatic::pure1('test'); BzzStatic::pure2('test'); BzzStatic::pure3('test'); + BzzStatic::pure4('test'); + BzzStatic::pure5('test'); }; class PureThrows diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index 377a296017..ad9fc94be5 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -40,7 +40,11 @@ public function testRule(): void ], [ 'Method MissingReturn\Foo::doLorem() should return int but return statement is missing.', - 36, + 39, + ], + [ + 'Method MissingReturn\Foo::doLorem() should return int but return statement is missing.', + 47, ], [ 'Anonymous function should return int but return statement is missing.', @@ -106,6 +110,18 @@ public function testRule(): void 'Method MissingReturn\NeverReturn::doBaz2() should always throw an exception or terminate script execution but doesn\'t do that.', 481, ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo() should return int but return statement is missing.', + 514, + ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo() should return int but return statement is missing.', + 515, + ], + [ + 'Method MissingReturn\MorePreciseMissingReturnLines::doFoo2() should return int but return statement is missing.', + 524, + ], ]); } @@ -273,7 +289,7 @@ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void { $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; $this->checkPhpDocMissingReturn = true; - $this->analyse([__DIR__ . '/../../Analyser/data/model-mixin.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/model-mixin.php'], [ [ 'Method ModelMixin\Model::__callStatic() should return mixed but return statement is missing.', 13, diff --git a/tests/PHPStan/Rules/Missing/data/missing-return.php b/tests/PHPStan/Rules/Missing/data/missing-return.php index 5f46dcfdde..31ef820be6 100644 --- a/tests/PHPStan/Rules/Missing/data/missing-return.php +++ b/tests/PHPStan/Rules/Missing/data/missing-return.php @@ -504,3 +504,42 @@ function () { } } + +class MorePreciseMissingReturnLines +{ + + public function doFoo(): int + { + if (doFoo()) { + echo 1; + } elseif (doBar()) { + + } else { + return 1; + } + } + + public function doFoo2(): int + { + if (doFoo()) { + return 1; + } elseif (doBar()) { + return 2; + } + } + +} + +class AnonymousFunctionOnlySometimesThrowsException +{ + + public function doFoo(): void + { + $cb = function (): void { + if (rand(0, 1)) { + throw new \Exception('bad luck'); + } + }; + } + +} diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php index 481511cf87..06a4be03c5 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,8 +16,15 @@ class ExistingNamesInGroupUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingNamesInGroupUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingNamesInGroupUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index 7389c4b58e..372a5b311a 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,8 +16,15 @@ class ExistingNamesInUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingNamesInUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingNamesInUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ); } public function testRule(): void @@ -47,4 +56,17 @@ public function testRule(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 14, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 7729265a85..41c947e937 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -15,11 +15,16 @@ class InvalidBinaryOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { return new InvalidBinaryOperationRule( new ExprPrinter(new Printer()), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), + true, ); } @@ -246,6 +251,22 @@ public function testRule(): void 'Binary operation "+" between int and array{} results in an error.', 259, ], + [ + 'Binary operation "%" between array and 3 results in an error.', + 267, + ], + [ + 'Binary operation "%" between 3 and array results in an error.', + 268, + ], + [ + 'Binary operation "%" between object and 3 results in an error.', + 270, + ], + [ + 'Binary operation "%" between 3 and object results in an error.', + 271, + ], ]); } @@ -261,7 +282,7 @@ public function testBug3515(): void public function testBug8827(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8827.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8827.php'], []); } public function testRuleWithNullsafeVariant(): void @@ -278,4 +299,503 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug5309(): void + { + $this->analyse([__DIR__ . '/data/bug-5309.php'], []); + } + + public function testBinaryMixed(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-binary-mixed.php'], [ + [ + 'Binary operation "." between T and \'a\' results in an error.', + 11, + ], + [ + 'Binary operation ".=" between \'a\' and T results in an error.', + 13, + ], + [ + 'Binary operation "**" between T and 2 results in an error.', + 15, + ], + [ + 'Binary operation "*" between T and 2 results in an error.', + 16, + ], + [ + 'Binary operation "/" between T and 2 results in an error.', + 17, + ], + [ + 'Binary operation "%" between T and 2 results in an error.', + 18, + ], + [ + 'Binary operation "+" between T and 2 results in an error.', + 19, + ], + [ + 'Binary operation "-" between T and 2 results in an error.', + 20, + ], + [ + 'Binary operation "<<" between T and 2 results in an error.', + 21, + ], + [ + 'Binary operation ">>" between T and 2 results in an error.', + 22, + ], + [ + 'Binary operation "&" between T and 2 results in an error.', + 23, + ], + [ + 'Binary operation "|" between T and 2 results in an error.', + 24, + ], + [ + 'Binary operation "+=" between 5 and T results in an error.', + 26, + ], + [ + 'Binary operation "-=" between 5 and T results in an error.', + 29, + ], + [ + 'Binary operation "*=" between 5 and T results in an error.', + 32, + ], + [ + 'Binary operation "**=" between 5 and T results in an error.', + 35, + ], + [ + 'Binary operation "/=" between 5 and T results in an error.', + 38, + ], + [ + 'Binary operation "%=" between 5 and T results in an error.', + 41, + ], + [ + 'Binary operation "&=" between 5 and T results in an error.', + 44, + ], + [ + 'Binary operation "|=" between 5 and T results in an error.', + 47, + ], + [ + 'Binary operation "^=" between 5 and T results in an error.', + 50, + ], + [ + 'Binary operation "<<=" between 5 and T results in an error.', + 53, + ], + [ + 'Binary operation ">>=" between 5 and T results in an error.', + 56, + ], + [ + 'Binary operation "." between mixed and \'a\' results in an error.', + 61, + ], + [ + 'Binary operation ".=" between \'a\' and mixed results in an error.', + 63, + ], + [ + 'Binary operation "**" between mixed and 2 results in an error.', + 65, + ], + [ + 'Binary operation "*" between mixed and 2 results in an error.', + 66, + ], + [ + 'Binary operation "/" between mixed and 2 results in an error.', + 67, + ], + [ + 'Binary operation "%" between mixed and 2 results in an error.', + 68, + ], + [ + 'Binary operation "+" between mixed and 2 results in an error.', + 69, + ], + [ + 'Binary operation "-" between mixed and 2 results in an error.', + 70, + ], + [ + 'Binary operation "<<" between mixed and 2 results in an error.', + 71, + ], + [ + 'Binary operation ">>" between mixed and 2 results in an error.', + 72, + ], + [ + 'Binary operation "&" between mixed and 2 results in an error.', + 73, + ], + [ + 'Binary operation "|" between mixed and 2 results in an error.', + 74, + ], + [ + 'Binary operation "+=" between 5 and mixed results in an error.', + 76, + ], + [ + 'Binary operation "-=" between 5 and mixed results in an error.', + 79, + ], + [ + 'Binary operation "*=" between 5 and mixed results in an error.', + 82, + ], + [ + 'Binary operation "**=" between 5 and mixed results in an error.', + 85, + ], + [ + 'Binary operation "/=" between 5 and mixed results in an error.', + 88, + ], + [ + 'Binary operation "%=" between 5 and mixed results in an error.', + 91, + ], + [ + 'Binary operation "&=" between 5 and mixed results in an error.', + 94, + ], + [ + 'Binary operation "|=" between 5 and mixed results in an error.', + 97, + ], + [ + 'Binary operation "^=" between 5 and mixed results in an error.', + 100, + ], + [ + 'Binary operation "<<=" between 5 and mixed results in an error.', + 103, + ], + [ + 'Binary operation ">>=" between 5 and mixed results in an error.', + 106, + ], + [ + 'Binary operation "." between mixed and \'a\' results in an error.', + 111, + ], + [ + 'Binary operation ".=" between \'a\' and mixed results in an error.', + 113, + ], + [ + 'Binary operation "**" between mixed and 2 results in an error.', + 115, + ], + [ + 'Binary operation "*" between mixed and 2 results in an error.', + 116, + ], + [ + 'Binary operation "/" between mixed and 2 results in an error.', + 117, + ], + [ + 'Binary operation "%" between mixed and 2 results in an error.', + 118, + ], + [ + 'Binary operation "+" between mixed and 2 results in an error.', + 119, + ], + [ + 'Binary operation "-" between mixed and 2 results in an error.', + 120, + ], + [ + 'Binary operation "<<" between mixed and 2 results in an error.', + 121, + ], + [ + 'Binary operation ">>" between mixed and 2 results in an error.', + 122, + ], + [ + 'Binary operation "&" between mixed and 2 results in an error.', + 123, + ], + [ + 'Binary operation "|" between mixed and 2 results in an error.', + 124, + ], + [ + 'Binary operation "+=" between 5 and mixed results in an error.', + 126, + ], + [ + 'Binary operation "-=" between 5 and mixed results in an error.', + 129, + ], + [ + 'Binary operation "*=" between 5 and mixed results in an error.', + 132, + ], + [ + 'Binary operation "**=" between 5 and mixed results in an error.', + 135, + ], + [ + 'Binary operation "/=" between 5 and mixed results in an error.', + 138, + ], + [ + 'Binary operation "%=" between 5 and mixed results in an error.', + 141, + ], + [ + 'Binary operation "&=" between 5 and mixed results in an error.', + 144, + ], + [ + 'Binary operation "|=" between 5 and mixed results in an error.', + 147, + ], + [ + 'Binary operation "^=" between 5 and mixed results in an error.', + 150, + ], + [ + 'Binary operation "<<=" between 5 and mixed results in an error.', + 153, + ], + [ + 'Binary operation ">>=" between 5 and mixed results in an error.', + 156, + ], + ]); + } + + public function testBug7538(): void + { + $this->analyse([__DIR__ . '/data/bug-7538.php'], [ + [ + 'Binary operation "%" between stdClass and stdClass results in an error.', + 7, + ], + ]); + } + + public function testBug10440(): void + { + $this->analyse([__DIR__ . '/data/bug-10440.php'], [ + [ + 'Binary operation "%" between array{} and array{\'\'} results in an error.', + 8, + ], + ]); + } + + public function testBenevolentUnion(): void + { + $this->analyse([__DIR__ . '/data/binary-op-benevolent-union.php'], [ + [ + 'Binary operation "+" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\Foo results in an error.', + 12, + ], + [ + 'Binary operation "+=" between BinaryOpBenevolentUnion\Foo and (array|bool|int|object|resource) results in an error.', + 24, + ], + [ + 'Binary operation "**" between (array|bool|int|object|resource) and array{} results in an error.', + 42, + ], + [ + 'Binary operation "**" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 43, + ], + [ + 'Binary operation "**=" between array{} and (array|bool|int|object|resource) results in an error.', + 52, + ], + [ + 'Binary operation "**=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 55, + ], + [ + 'Binary operation "*" between (array|bool|int|object|resource) and array{} results in an error.', + 73, + ], + [ + 'Binary operation "*" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 74, + ], + [ + 'Binary operation "*=" between array{} and (array|bool|int|object|resource) results in an error.', + 83, + ], + [ + 'Binary operation "*=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 86, + ], + [ + 'Binary operation "/" between (array|bool|int|object|resource) and array{} results in an error.', + 104, + ], + [ + 'Binary operation "/" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 105, + ], + [ + 'Binary operation "/=" between array{} and (array|bool|int|object|resource) results in an error.', + 114, + ], + [ + 'Binary operation "/=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 117, + ], + [ + 'Binary operation "%" between (array|bool|int|object|resource) and array{} results in an error.', + 135, + ], + [ + 'Binary operation "%" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 136, + ], + [ + 'Binary operation "%=" between array{} and (array|bool|int|object|resource) results in an error.', + 145, + ], + [ + 'Binary operation "%=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 148, + ], + [ + 'Binary operation "-" between (array|bool|int|object|resource) and array{} results in an error.', + 166, + ], + [ + 'Binary operation "-" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 167, + ], + [ + 'Binary operation "-=" between array{} and (array|bool|int|object|resource) results in an error.', + 176, + ], + [ + 'Binary operation "-=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 179, + ], + [ + 'Binary operation "." between (array|bool|int|object|resource) and array{} results in an error.', + 197, + ], + [ + 'Binary operation "." between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 198, + ], + [ + 'Binary operation ".=" between array{} and (array|bool|int|object|resource) results in an error.', + 207, + ], + [ + 'Binary operation ".=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 210, + ], + [ + 'Binary operation "<<" between (array|bool|int|object|resource) and array{} results in an error.', + 228, + ], + [ + 'Binary operation "<<" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 229, + ], + [ + 'Binary operation "<<=" between array{} and (array|bool|int|object|resource) results in an error.', + 238, + ], + [ + 'Binary operation "<<=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 241, + ], + [ + 'Binary operation ">>" between (array|bool|int|object|resource) and array{} results in an error.', + 259, + ], + [ + 'Binary operation ">>" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 260, + ], + [ + 'Binary operation ">>=" between array{} and (array|bool|int|object|resource) results in an error.', + 269, + ], + [ + 'Binary operation ">>=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 272, + ], + [ + 'Binary operation "&" between (array|bool|int|object|resource) and array{} results in an error.', + 290, + ], + [ + 'Binary operation "&" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 291, + ], + [ + 'Binary operation "&=" between array{} and (array|bool|int|object|resource) results in an error.', + 300, + ], + [ + 'Binary operation "&=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 303, + ], + [ + 'Binary operation "^" between (array|bool|int|object|resource) and array{} results in an error.', + 321, + ], + [ + 'Binary operation "^" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 322, + ], + [ + 'Binary operation "^=" between array{} and (array|bool|int|object|resource) results in an error.', + 331, + ], + [ + 'Binary operation "^=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 334, + ], + [ + 'Binary operation "|" between (array|bool|int|object|resource) and array{} results in an error.', + 352, + ], + [ + 'Binary operation "|" between (array|bool|int|object|resource) and BinaryOpBenevolentUnion\\Foo results in an error.', + 353, + ], + [ + 'Binary operation "|=" between array{} and (array|bool|int|object|resource) results in an error.', + 362, + ], + [ + 'Binary operation "|=" between BinaryOpBenevolentUnion\\Foo and (array|bool|int|object|resource) results in an error.', + 365, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php index 7eda97b50c..5042bb336c 100644 --- a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Operators; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; /** @@ -11,9 +12,17 @@ class InvalidIncDecOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new InvalidIncDecOperationRule(false); + return new InvalidIncDecOperationRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), + true, + false, + ); } public function testRule(): void @@ -31,6 +40,108 @@ public function testRule(): void 'Cannot use ++ on stdClass.', 17, ], + [ + 'Cannot use ++ on InvalidIncDec\\ClassWithToString.', + 19, + ], + [ + 'Cannot use -- on InvalidIncDec\\ClassWithToString.', + 21, + ], + [ + 'Cannot use ++ on array{}.', + 23, + ], + [ + 'Cannot use -- on array{}.', + 25, + ], + [ + 'Cannot use ++ on resource.', + 28, + ], + [ + 'Cannot use -- on resource.', + 32, + ], + ]); + } + + public function testMixed(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-inc-dec-mixed.php'], [ + [ + 'Cannot use ++ on T of mixed.', + 12, + ], + [ + 'Cannot use ++ on T of mixed.', + 14, + ], + [ + 'Cannot use -- on T of mixed.', + 16, + ], + [ + 'Cannot use -- on T of mixed.', + 18, + ], + [ + 'Cannot use ++ on mixed.', + 24, + ], + [ + 'Cannot use ++ on mixed.', + 26, + ], + [ + 'Cannot use -- on mixed.', + 28, + ], + [ + 'Cannot use -- on mixed.', + 30, + ], + [ + 'Cannot use ++ on mixed.', + 36, + ], + [ + 'Cannot use ++ on mixed.', + 38, + ], + [ + 'Cannot use -- on mixed.', + 40, + ], + [ + 'Cannot use -- on mixed.', + 42, + ], + ]); + } + + public function testUnion(): void + { + $this->analyse([__DIR__ . '/data/invalid-inc-dec-union.php'], [ + [ + 'Cannot use ++ on array|bool|float|int|object|string|null.', + 24, + ], + [ + 'Cannot use -- on array|bool|float|int|object|string|null.', + 26, + ], + [ + 'Cannot use ++ on (array|object).', + 29, + ], + [ + 'Cannot use -- on (array|object).', + 31, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php index fbc6a265f9..2475fa3a80 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Operators; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; /** @@ -11,9 +12,16 @@ class InvalidUnaryOperationRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new InvalidUnaryOperationRule(); + return new InvalidUnaryOperationRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false), + true, + ); } public function testRule(): void @@ -39,6 +47,124 @@ public function testRule(): void 'Unary operation "~" on array{} results in an error.', 24, ], + [ + 'Unary operation "~" on bool results in an error.', + 36, + ], + [ + 'Unary operation "+" on array results in an error.', + 38, + ], + [ + 'Unary operation "-" on array results in an error.', + 39, + ], + [ + 'Unary operation "~" on array results in an error.', + 40, + ], + [ + 'Unary operation "+" on object results in an error.', + 42, + ], + [ + 'Unary operation "-" on object results in an error.', + 43, + ], + [ + 'Unary operation "~" on object results in an error.', + 44, + ], + [ + 'Unary operation "+" on resource results in an error.', + 50, + ], + [ + 'Unary operation "-" on resource results in an error.', + 51, + ], + [ + 'Unary operation "~" on resource results in an error.', + 52, + ], + [ + 'Unary operation "~" on null results in an error.', + 61, + ], + ]); + } + + public function testMixed(): void + { + $this->checkImplicitMixed = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/invalid-unary-mixed.php'], [ + [ + 'Unary operation "+" on T results in an error.', + 11, + ], + [ + 'Unary operation "-" on T results in an error.', + 12, + ], + [ + 'Unary operation "~" on T results in an error.', + 13, + ], + [ + 'Unary operation "+" on mixed results in an error.', + 18, + ], + [ + 'Unary operation "-" on mixed results in an error.', + 19, + ], + [ + 'Unary operation "~" on mixed results in an error.', + 20, + ], + [ + 'Unary operation "+" on mixed results in an error.', + 25, + ], + [ + 'Unary operation "-" on mixed results in an error.', + 26, + ], + [ + 'Unary operation "~" on mixed results in an error.', + 27, + ], + ]); + } + + public function testUnion(): void + { + $this->analyse([__DIR__ . '/data/unary-union.php'], [ + [ + 'Unary operation "+" on array|bool|float|int|object|string|null results in an error.', + 21, + ], + [ + 'Unary operation "-" on array|bool|float|int|object|string|null results in an error.', + 22, + ], + [ + 'Unary operation "~" on array|bool|float|int|object|string|null results in an error.', + 23, + ], + [ + 'Unary operation "+" on (array|object) results in an error.', + 25, + ], + [ + 'Unary operation "-" on (array|object) results in an error.', + 26, + ], + [ + 'Unary operation "~" on (array|object) results in an error.', + 27, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/binary-op-benevolent-union.php b/tests/PHPStan/Rules/Operators/data/binary-op-benevolent-union.php new file mode 100644 index 0000000000..1163df914a --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/binary-op-benevolent-union.php @@ -0,0 +1,377 @@ +|int|object|bool|resource> $benevolent + */ +function plus($benevolent, Foo $object): void +{ + echo $benevolent + 1; + echo $benevolent + []; + echo $benevolent + $object; + echo $benevolent + '123'; + echo $benevolent + 1.23; + echo $benevolent + $benevolent; + + $a = 1; + $a += $benevolent; + + $a = []; + $a += $benevolent; + + $a = $object; + $a += $benevolent; + + $a = '123'; + $a += $benevolent; + + $a = 1.23; + $a += $benevolent; + + $a = $benevolent; + $a += $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function exponent($benevolent, Foo $object): void +{ + echo $benevolent ** 1; + echo $benevolent ** []; + echo $benevolent ** $object; + echo $benevolent ** '123'; + echo $benevolent ** 1.23; + echo $benevolent ** $benevolent; + + $a = 1; + $a **= $benevolent; + + $a = []; + $a **= $benevolent; + + $a = $object; + $a **= $benevolent; + + $a = '123'; + $a **= $benevolent; + + $a = 1.23; + $a **= $benevolent; + + $a = $benevolent; + $a **= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function mul($benevolent, Foo $object): void +{ + echo $benevolent * 1; + echo $benevolent * []; + echo $benevolent * $object; + echo $benevolent * '123'; + echo $benevolent * 1.23; + echo $benevolent * $benevolent; + + $a = 1; + $a *= $benevolent; + + $a = []; + $a *= $benevolent; + + $a = $object; + $a *= $benevolent; + + $a = '123'; + $a *= $benevolent; + + $a = 1.23; + $a *= $benevolent; + + $a = $benevolent; + $a *= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function div($benevolent, Foo $object): void +{ + echo $benevolent / 1; + echo $benevolent / []; + echo $benevolent / $object; + echo $benevolent / '123'; + echo $benevolent / 1.23; + echo $benevolent / $benevolent; + + $a = 1; + $a /= $benevolent; + + $a = []; + $a /= $benevolent; + + $a = $object; + $a /= $benevolent; + + $a = '123'; + $a /= $benevolent; + + $a = 1.23; + $a /= $benevolent; + + $a = $benevolent; + $a /= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function mod($benevolent, Foo $object): void +{ + echo $benevolent % 1; + echo $benevolent % []; + echo $benevolent % $object; + echo $benevolent % '123'; + echo $benevolent % 1.23; + echo $benevolent % $benevolent; + + $a = 1; + $a %= $benevolent; + + $a = []; + $a %= $benevolent; + + $a = $object; + $a %= $benevolent; + + $a = '123'; + $a %= $benevolent; + + $a = 1.23; + $a %= $benevolent; + + $a = $benevolent; + $a %= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function minus($benevolent, Foo $object): void +{ + echo $benevolent - 1; + echo $benevolent - []; + echo $benevolent - $object; + echo $benevolent - '123'; + echo $benevolent - 1.23; + echo $benevolent - $benevolent; + + $a = 1; + $a -= $benevolent; + + $a = []; + $a -= $benevolent; + + $a = $object; + $a -= $benevolent; + + $a = '123'; + $a -= $benevolent; + + $a = 1.23; + $a -= $benevolent; + + $a = $benevolent; + $a -= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function concat($benevolent, Foo $object): void +{ + echo $benevolent . 1; + echo $benevolent . []; + echo $benevolent . $object; + echo $benevolent . '123'; + echo $benevolent . 1.23; + echo $benevolent . $benevolent; + + $a = 1; + $a .= $benevolent; + + $a = []; + $a .= $benevolent; + + $a = $object; + $a .= $benevolent; + + $a = '123'; + $a .= $benevolent; + + $a = 1.23; + $a .= $benevolent; + + $a = $benevolent; + $a .= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function lshift($benevolent, Foo $object): void +{ + echo $benevolent << 1; + echo $benevolent << []; + echo $benevolent << $object; + echo $benevolent << '123'; + echo $benevolent << 1.23; + echo $benevolent << $benevolent; + + $a = 1; + $a <<= $benevolent; + + $a = []; + $a <<= $benevolent; + + $a = $object; + $a <<= $benevolent; + + $a = '123'; + $a <<= $benevolent; + + $a = 1<<23; + $a <<= $benevolent; + + $a = $benevolent; + $a <<= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function rshift($benevolent, Foo $object): void +{ + echo $benevolent >> 1; + echo $benevolent >> []; + echo $benevolent >> $object; + echo $benevolent >> '123'; + echo $benevolent >> 1.23; + echo $benevolent >> $benevolent; + + $a = 1; + $a >>= $benevolent; + + $a = []; + $a >>= $benevolent; + + $a = $object; + $a >>= $benevolent; + + $a = '123'; + $a >>= $benevolent; + + $a = 1>>23; + $a >>= $benevolent; + + $a = $benevolent; + $a >>= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function bitAnd($benevolent, Foo $object): void +{ + echo $benevolent & 1; + echo $benevolent & []; + echo $benevolent & $object; + echo $benevolent & '123'; + echo $benevolent & 1.23; + echo $benevolent & $benevolent; + + $a = 1; + $a &= $benevolent; + + $a = []; + $a &= $benevolent; + + $a = $object; + $a &= $benevolent; + + $a = '123'; + $a &= $benevolent; + + $a = 1.23; + $a &= $benevolent; + + $a = $benevolent; + $a &= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function bitXor($benevolent, Foo $object): void +{ + echo $benevolent ^ 1; + echo $benevolent ^ []; + echo $benevolent ^ $object; + echo $benevolent ^ '123'; + echo $benevolent ^ 1.23; + echo $benevolent ^ $benevolent; + + $a = 1; + $a ^= $benevolent; + + $a = []; + $a ^= $benevolent; + + $a = $object; + $a ^= $benevolent; + + $a = '123'; + $a ^= $benevolent; + + $a = 1.23; + $a ^= $benevolent; + + $a = $benevolent; + $a ^= $benevolent; +} + +/** + * @param __benevolent|int|object|bool|resource> $benevolent + */ +function bitOr($benevolent, Foo $object): void +{ + echo $benevolent | 1; + echo $benevolent | []; + echo $benevolent | $object; + echo $benevolent | '123'; + echo $benevolent | 1.23; + echo $benevolent | $benevolent; + + $a = 1; + $a |= $benevolent; + + $a = []; + $a |= $benevolent; + + $a = $object; + $a |= $benevolent; + + $a = '123'; + $a |= $benevolent; + + $a = 1.23; + $a |= $benevolent; + + $a = $benevolent; + $a |= $benevolent; +} + +class Foo {} diff --git a/tests/PHPStan/Rules/Operators/data/bug-10440.php b/tests/PHPStan/Rules/Operators/data/bug-10440.php new file mode 100644 index 0000000000..704cb8a695 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-10440.php @@ -0,0 +1,8 @@ + 0) { + $x += 1; + } + if ($x > 0) { + return 5 / $x; + } + + return 1.0; +} + diff --git a/tests/PHPStan/Rules/Operators/data/bug-7538.php b/tests/PHPStan/Rules/Operators/data/bug-7538.php new file mode 100644 index 0000000000..4ead552b2b --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-7538.php @@ -0,0 +1,7 @@ + 0; + var_dump($a ** 2); + var_dump($a * 2); + var_dump($a / 2); + var_dump($a % 2); + var_dump($a + 2); + var_dump($a - 2); + var_dump($a << 2); + var_dump($a >> 2); + var_dump($a & 2); + var_dump($a | 2); + $c = 5; + $c += $a; + + $c = 5; + $c -= $a; + + $c = 5; + $c *= $a; + + $c = 5; + $c **= $a; + + $c = 5; + $c /= $a; + + $c = 5; + $c %= $a; + + $c = 5; + $c &= $a; + + $c = 5; + $c |= $a; + + $c = 5; + $c ^= $a; + + $c = 5; + $c <<= $a; + + $c = 5; + $c >>= $a; +} + +function explicitMixed(mixed $a): void +{ + var_dump($a . 'a'); + $b = 'a'; + $b .= $a; + $bool = rand() > 0; + var_dump($a ** 2); + var_dump($a * 2); + var_dump($a / 2); + var_dump($a % 2); + var_dump($a + 2); + var_dump($a - 2); + var_dump($a << 2); + var_dump($a >> 2); + var_dump($a & 2); + var_dump($a | 2); + $c = 5; + $c += $a; + + $c = 5; + $c -= $a; + + $c = 5; + $c *= $a; + + $c = 5; + $c **= $a; + + $c = 5; + $c /= $a; + + $c = 5; + $c %= $a; + + $c = 5; + $c &= $a; + + $c = 5; + $c |= $a; + + $c = 5; + $c ^= $a; + + $c = 5; + $c <<= $a; + + $c = 5; + $c >>= $a; +} + +function implicitMixed($a): void +{ + var_dump($a . 'a'); + $b = 'a'; + $b .= $a; + $bool = rand() > 0; + var_dump($a ** 2); + var_dump($a * 2); + var_dump($a / 2); + var_dump($a % 2); + var_dump($a + 2); + var_dump($a - 2); + var_dump($a << 2); + var_dump($a >> 2); + var_dump($a & 2); + var_dump($a | 2); + $c = 5; + $c += $a; + + $c = 5; + $c -= $a; + + $c = 5; + $c *= $a; + + $c = 5; + $c **= $a; + + $c = 5; + $c /= $a; + + $c = 5; + $c %= $a; + + $c = 5; + $c &= $a; + + $c = 5; + $c |= $a; + + $c = 5; + $c ^= $a; + + $c = 5; + $c <<= $a; + + $c = 5; + $c >>= $a; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index ef5f6e4c58..60d71d4ba1 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -258,3 +258,15 @@ function benevolentPlus(array $a, int $i): void { function (int $int) { $int + []; }; + +function testMod(array $a, object $o): void { + echo 4 % 3; + echo '4' % 3; + echo 4 % '3'; + + echo $a % 3; + echo 3 % $a; + + echo $o % 3; + echo 3 % $o; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php new file mode 100644 index 0000000000..4e190cc73c --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec-mixed.php @@ -0,0 +1,43 @@ + $benevolentUnion + * @param string|int|float|bool|null $okUnion + * @param scalar|null|array|object $union + * @param __benevolent $badBenevolentUnion + */ +function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void +{ + $a = $benevolentUnion; + $a++; + $a = $benevolentUnion; + --$a; + + $a = $okUnion; + $a++; + $a = $okUnion; + --$a; + + $a = $union; + $a++; + $a = $union; + --$a; + + $a = $badBenevolentUnion; + $a++; + $a = $badBenevolentUnion; + --$a; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php index 9e551e875f..aee9fba6fa 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php @@ -2,7 +2,7 @@ namespace InvalidIncDec; -function ($a, int $i, ?float $j, string $str, \stdClass $std) { +function ($a, int $i, ?float $j, string $str, \stdClass $std, \SimpleXMLElement $simpleXMLElement) { $a++; $b = [1]; @@ -15,4 +15,41 @@ function ($a, int $i, ?float $j, string $str, \stdClass $std) { $j++; $str++; $std++; + $classWithToString = new ClassWithToString(); + $classWithToString++; + $classWithToString = new ClassWithToString(); + --$classWithToString; + $arr = []; + $arr++; + $arr = []; + --$arr; + + if (($f = fopen('php://stdin', 'r')) !== false) { + $f++; + } + + if (($f = fopen('php://stdin', 'r')) !== false) { + --$f; + } + + $bool = true; + $bool++; + $bool = false; + --$bool; + $null = null; + $null++; + $null = null; + --$null; + $a = $simpleXMLElement; + $a++; + $a = $simpleXMLElement; + --$a; }; + +class ClassWithToString +{ + public function __toString(): string + { + return 'foo'; + } +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php b/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php new file mode 100644 index 0000000000..a82faa213a --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-unary-mixed.php @@ -0,0 +1,28 @@ + $benevolentUnion + * @param numeric-string|int|float $okUnion + * @param scalar|null|array|object $union + * @param __benevolent $badBenevolentUnion + */ +function foo($benevolentUnion, $okUnion, $union, $badBenevolentUnion): void +{ + +$benevolentUnion; + -$benevolentUnion; + ~$benevolentUnion; + + +$okUnion; + -$okUnion; + ~$okUnion; + + +$union; + -$union; + ~$union; + + +$badBenevolentUnion; + -$badBenevolentUnion; + ~$badBenevolentUnion; +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 439c68f90f..223fd616b0 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -16,10 +20,25 @@ class IncompatiblePhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), new GenericObjectTypeCheck(), new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), ); } @@ -172,6 +191,14 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @return is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', 319, ], + [ + 'PHPDoc tag @param for parameter $cb contains unresolvable type.', + 328, + ], + [ + 'PHPDoc tag @param for parameter $cl contains unresolvable type.', + 328, + ], ]); } @@ -291,4 +318,128 @@ public function testBug10097(): void $this->analyse([__DIR__ . '/data/bug-10097.php'], []); } + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callables-incompatible.php'], [ + [ + 'PHPDoc tag @param for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 11, + ], + [ + 'PHPDoc tag @param for parameter $existingTypeAlias template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 18, + ], + [ + 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 25, + ], + [ + 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', + 32, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunction.', + 40, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 47, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for method GenericCallablesIncompatible\Test::testShadowMethod.', + 60, + ], + [ + 'PHPDoc tag @param for parameter $shadows template U of Closure(T): T shadows @template U for class GenericCallablesIncompatible\Test.', + 60, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for method GenericCallablesIncompatible\Test::testShadowMethodReturn.', + 68, + ], + [ + 'PHPDoc tag @return template U of Closure(T): T shadows @template U for class GenericCallablesIncompatible\Test.', + 68, + ], + [ + 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 76, + ], + [ + 'PHPDoc tag @return template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 83, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 90, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', + 97, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunctionReturn.', + 105, + ], + [ + 'PHPDoc tag @param for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 117, + ], + [ + 'PHPDoc tag @param for parameter $existingTypeAlias template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 124, + ], + [ + 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 131, + ], + [ + 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', + 138, + ], + [ + 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 145, + ], + [ + 'PHPDoc tag @return template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 152, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 159, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', + 166, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClass template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOut.', + 175, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClasses template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOutArray.', + 183, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsReturnArray.', + 191, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for class GenericCallablesIncompatible\Test3.', + 203, + ], + ]); + } + + public function testBug10622(): void + { + $this->analyse([__DIR__ . '/data/bug-10622.php'], []); + } + + public function testBug10622B(): void + { + $this->analyse([__DIR__ . '/data/bug-10622b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index 5f68803d9d..d8ec3604b3 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,9 +18,24 @@ class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePropertyPhpDocTypeRule( new GenericObjectTypeCheck(), new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), ); } @@ -150,4 +169,30 @@ public function testBug7240(): void $this->analyse([__DIR__ . '/data/bug-7240.php'], []); } + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callable-properties.php'], [ + [ + 'PHPDoc tag @var template T of Closure(T): T shadows @template T for class GenericCallableProperties\Test.', + 16, + ], + [ + 'PHPDoc tag @var template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 21, + ], + [ + 'PHPDoc tag @var template of callable(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 26, + ], + [ + 'PHPDoc tag @var template TNull of callable(TNull): TNull with bound type null is not supported.', + 31, + ], + [ + 'PHPDoc tag @var template TInvalid of callable(TInvalid): TInvalid has invalid bound type GenericCallableProperties\Invalid.', + 36, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php index f8158edb1e..b0bb271349 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php @@ -132,7 +132,7 @@ public function testInvalidTypeInTypeAlias(): void $this->analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ [ 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65', - 12, + 15, ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index 7adb36a7d5..9d4dc0bb3e 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -18,11 +20,14 @@ class InvalidPhpDocVarTagTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InvalidPhpDocVarTagTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new GenericObjectTypeCheck(), new MissingTypehintCheck(true, true, true, true, []), new UnresolvableTypeHelper(), @@ -91,7 +96,6 @@ public function testRule(): void [ 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGeneric but does not specify its types: T, U', 61, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php index 90120196da..2328aeb0d7 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -77,6 +77,28 @@ public function testInheritedPhpDocs(): void ]); } + public function testThrowsWithRequireExtends(): void + { + $this->analyse([__DIR__ . '/data/throws-with-require.php'], [ + [ + 'PHPDoc tag @throws with type ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 25, + ], + [ + 'PHPDoc tag @throws with type DateTimeInterface|ThrowsWithRequire\\RequiresExtendsExceptionInterface is not subtype of Throwable', + 39, + ], + [ + 'PHPDoc tag @throws with type Exception|ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 46, + ], + [ + 'PHPDoc tag @throws with type Iterator&ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 74, + ], + ]); + } + public function dataMergeInheritedPhpDocs(): array { return [ diff --git a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php index 3e2d87ea5a..932a09c299 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -57,4 +57,19 @@ public function testRule(): void ]); } + public function testBug10573(): void + { + $this->analyse([__DIR__ . '/data/bug-10573.php'], []); + } + + public function testBug10214(): void + { + $this->analyse([__DIR__ . '/data/bug-10214.php'], []); + } + + public function testBug10594(): void + { + $this->analyse([__DIR__ . '/data/bug-10594.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php index 56feeb7ae5..48258202dc 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -63,6 +63,10 @@ public function testRule(): void 'Condition "array{foo: string} is array{foo: int}" in conditional return type is always false.', 156, ], + [ + 'Condition "int is int" in conditional return type is always true.', + 185, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php index c07d216650..5a24e75502 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -19,7 +21,10 @@ protected function getRule(): Rule return new RequireExtendsDefinitionClassRule( new RequireExtendsCheck( - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php index 39676e8db8..746c3b9007 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,7 +21,10 @@ protected function getRule(): Rule return new RequireExtendsDefinitionTraitRule( $reflectionProvider, new RequireExtendsCheck( - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php index 495de2a598..ad2f2f7cba 100644 --- a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -19,7 +21,10 @@ protected function getRule(): Rule return new RequireImplementsDefinitionTraitRule( $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ); } diff --git a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php index 56f693f67f..f3ea56d12f 100644 --- a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -13,7 +13,7 @@ class VarTagChangedExpressionTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true)); + return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true, true)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 959a87c847..6083de943d 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -17,11 +17,13 @@ class WrongVariableNameInVarTagRuleTest extends RuleTestCase private bool $checkTypeAgainstPhpDocType = false; + private bool $strictWideningCheck = false; + protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), - new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType), + new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType, $this->strictWideningCheck), $this->checkTypeAgainstNativeType, ); } @@ -71,63 +73,63 @@ public function testRule(): void ], [ 'Multiple PHPDoc @var tags above single variable assignment are not supported.', - 125, + 126, ], [ 'Variable $b in PHPDoc tag @var does not exist.', - 134, + 135, ], [ 'PHPDoc tag @var does not specify variable name.', - 155, + 156, ], [ 'PHPDoc tag @var does not specify variable name.', - 176, + 177, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 210, + 211, ], [ 'PHPDoc tag @var above foreach loop does not specify variable name.', - 234, + 235, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $bar in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 262, + 263, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 268, + 269, ], [ 'PHPDoc tag @var above assignment does not specify variable name.', - 274, + 275, ], [ 'Variable $slots in PHPDoc tag @var does not match assigned variable $itemSlots.', - 280, + 281, ], [ 'PHPDoc tag @var above a class has no effect.', - 300, + 301, ], [ 'PHPDoc tag @var above a method has no effect.', - 304, + 305, ], [ 'PHPDoc tag @var above a function has no effect.', - 312, + 313, ], ]); } @@ -196,8 +198,7 @@ public function testEnums(): void public function dataReportWrongType(): iterable { - yield [false, false, []]; - yield [true, false, [ + $nativeCheckOnly = [ [ 'PHPDoc tag @var with type string|null is not subtype of native type string.', 14, @@ -250,9 +251,105 @@ public function dataReportWrongType(): iterable 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', 204, ], + ]; + + 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, [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 29, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 35, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of type Iterator.', + 44, + ], + /*[ + // reported by VarTagChangedExpressionTypeRule + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ],*/ + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 137, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 160, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 163, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], ]]; - yield [false, true, []]; - yield [true, true, [ + yield [true, true, true, [ [ 'PHPDoc tag @var with type string|null is not subtype of native type string.', 14, @@ -269,6 +366,10 @@ public function dataReportWrongType(): iterable 'PHPDoc tag @var with type int is not subtype of type string.', 29, ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 32, + ], [ 'PHPDoc tag @var with type array is not subtype of type list.', 35, @@ -281,6 +382,10 @@ public function dataReportWrongType(): iterable 'PHPDoc tag @var with type Iterator is not subtype of type Iterator.', 44, ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 47, + ], /*[ // reported by VarTagChangedExpressionTypeRule 'PHPDoc tag @var with type string is not subtype of type int.', @@ -294,6 +399,10 @@ public function dataReportWrongType(): iterable 'PHPDoc tag @var with type int is not subtype of native type string.', 109, ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 122, + ], [ 'PHPDoc tag @var with type array is not subtype of type array.', 137, @@ -310,6 +419,14 @@ public function dataReportWrongType(): iterable 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', 148, ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 154, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 157, + ], [ 'PHPDoc tag @var with type array> is not subtype of type array>.', 160, @@ -342,6 +459,10 @@ public function dataReportWrongType(): iterable 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', 204, ], + [ + 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', + 235, + ], ]]; } @@ -377,10 +498,16 @@ public function dataPermutateCheckTypeAgainst(): iterable * @dataProvider dataReportWrongType * @param list $expectedErrors */ - public function testReportWrongType(bool $checkTypeAgainstNativeType, bool $checkTypeAgainstPhpDocType, array $expectedErrors): void + 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); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php new file mode 100644 index 0000000000..b1292d2ba5 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php @@ -0,0 +1,42 @@ + + */ +class PostVoter extends Voter { + function supports($attribute, $subject): bool + { + return $attribute === 'POST_READ' && $subject instanceof Post; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php new file mode 100644 index 0000000000..dfc27264bb --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php @@ -0,0 +1,34 @@ +getParameters(); + if (count($parameters) >= 2) { + return $parameters[1]->getType() !== null && ($parameters[1]->getType() instanceof ReflectionNamedType && $parameters[1]->getType()->getName() !== 'string'); + } + return true; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php new file mode 100644 index 0000000000..22caf7f0a2 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php @@ -0,0 +1,30 @@ + : SupportCollection) + */ + public function map(callable $callback) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php new file mode 100644 index 0000000000..23110ad14c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php @@ -0,0 +1,71 @@ + + */ +class FooBoxedArray +{ + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct(array $value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function get(): array + { + return $this->value; + } +} + +/** + * @template TKey of object|array + * @template TValue of object|array + */ +class FooMap +{ + /** + * @var array)>, + * \WeakReference<(TValue is object ? TValue : FooBoxedArray)> + * }> + */ + protected $weakKvByIndex = []; + + /** + * @template T of TKey|TValue + * + * @param T $value + * + * @return (T is object ? T : FooBoxedArray) + */ + protected function boxValue($value): object + { + return is_array($value) + ? new FooBoxedArray($value) + : $value; + } + + /** + * @template T of TKey|TValue + * + * @param (T is object ? T : FooBoxedArray) $value + * + * @return T + */ + protected function unboxValue(object $value) + { + return $value instanceof FooBoxedArray + ? $value->get() + : $value; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php new file mode 100644 index 0000000000..0e81027bb4 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callable-properties.php @@ -0,0 +1,42 @@ += 8.0 + +namespace GenericCallableProperties; + +use Closure; +use stdClass; + +/** + * @template T + */ +class Test +{ + /** + * @var Closure(T): T + */ + private Closure $shadows; + + /** + * @var Closure(stdClass): stdClass + */ + private Closure $existingClass; + + /** + * @var callable(TypeAlias): TypeAlias + */ + private $typeAlias; + + /** + * @var callable(TNull): TNull + */ + private $unsupported; + + /** + * @var callable(TInvalid): TInvalid + */ + private $invalid; + + /** + * @param Closure(T): T $notReported + */ + public function __construct(private Closure $notReported) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php new file mode 100644 index 0000000000..238d822894 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php @@ -0,0 +1,204 @@ + 8.0 + +namespace GenericCallablesIncompatible; + +use Closure; +use stdClass; + +/** + * @param Closure(stdClass $val): stdClass $existingClass + */ +function existingClass(Closure $existingClass): void +{ +} + +/** + * @param Closure(TypeAlias $val): TypeAlias $existingTypeAlias + */ +function existingTypeAlias(Closure $existingTypeAlias): void +{ +} + +/** + * @param Closure(T $val): T $invalidBoundType + */ +function invalidBoundType(Closure $invalidBoundType): void +{ +} + +/** + * @param Closure(T $val): T $notSupported + */ +function notSupported(Closure $notSupported): void +{ +} + +/** + * @template T + * @param Closure(T $val): T $shadows + */ +function testShadowFunction(Closure $shadows): void +{ +} + +/** + * @param-out Closure(stdClass $val): stdClass $existingClass + */ +function existingClassParamOut(Closure &$existingClass): void +{ +} + +/** + * @template U + */ +class Test +{ + /** + * @template T + * @param Closure(T $val): T $shadows + */ + function testShadowMethod(Closure $shadows): void + { + } + + /** + * @template T + * @return Closure(T $val): T + */ + function testShadowMethodReturn(): Closure + { + } +} + +/** + * @return Closure(stdClass $val): stdClass + */ +function existingClassReturn(): Closure +{ +} + +/** + * @return Closure(TypeAlias $val): TypeAlias + */ +function existingTypeAliasReturn(): Closure +{ +} + +/** + * @return Closure(T $val): T + */ +function invalidBoundTypeReturn(): Closure +{ +} + +/** + * @return Closure(T $val): T + */ +function notSupportedReturn(): Closure +{ +} + +/** + * @template T + * @return Closure(T $val): T + */ +function testShadowFunctionReturn(): Closure +{ +} + +/** + * @template U + */ +class Test2 +{ + /** + * @param Closure(stdClass $val): stdClass $existingClass + */ + public function existingClass(Closure $existingClass): void + { + } + + /** + * @param Closure(TypeAlias $val): TypeAlias $existingTypeAlias + */ + public function existingTypeAlias(Closure $existingTypeAlias): void + { + } + + /** + * @param Closure(T $val): T $invalidBoundType + */ + public function invalidBoundType(Closure $invalidBoundType): void + { + } + + /** + * @param Closure(T $val): T $notSupported + */ + public function notSupported(Closure $notSupported): void + { + } + + /** + * @return Closure(stdClass $val): stdClass + */ + public function existingClassReturn(): Closure + { + } + + /** + * @return Closure(TypeAlias $val): TypeAlias + */ + public function existingTypeAliasReturn(): Closure + { + } + + /** + * @return Closure(T $val): T + */ + public function invalidBoundTypeReturn(): Closure + { + } + + /** + * @return Closure(T $val): T + */ + public function notSupportedReturn(): Closure + { + } +} + +/** + * @template T + * @param-out Closure(T $val): T $existingClass + */ +function shadowsParamOut(Closure &$existingClass): void +{ +} + +/** + * @template T + * @param-out list(T $val): T> $existingClasses + */ +function shadowsParamOutArray(array &$existingClasses): void +{ +} + +/** + * @template T + * @return list(T $val): T> + */ +function shadowsReturnArray(): array +{ +} + +/** + * @template T + */ +class Test3 +{ + /** + * @param Closure(T): T $shadows + */ + public function __construct(private Closure $shadows) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 9d5deafd01..078e004d26 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -320,3 +320,12 @@ function genericIncompatibleTypeProjection($foo) { } + +/** + * @param pure-callable(): void $cb + * @param pure-Closure(): void $cl + */ +function pureCallableCannotReturnVoid(callable $cb, \Closure $cl): void +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php index 04dd9ca0ba..b99b3e5577 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php @@ -8,6 +8,9 @@ * * @psalm-type Foo array{} * @psalm-type InvalidFoo what{} + * + * @phan-type Foo = array{} + * @phan-type InvalidFoo = what{} */ class Foo { diff --git a/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php index c8f0cb05fd..7829e8c71a 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php @@ -175,3 +175,15 @@ public function foo(): bool } } + +class ParamOut +{ + + /** + * @param-out ($i is int ? 1 : 2) $out + */ + public function doFoo(int $i, &$out) { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php b/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php new file mode 100644 index 0000000000..9e816d8a66 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php @@ -0,0 +1,76 @@ +analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); } + public function testBug10477(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10477.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index cf9a670917..7697f826e3 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -929,4 +929,24 @@ public function testRequireExtends(): void ]); } + public function testBug8629(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-8629.php'], []); + } + + public function testBug9694(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-9694.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index 174d06ad24..e8cd8306de 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -18,7 +20,14 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesInAssignRule( - new AccessStaticPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new ClassCaseSensitivityCheck($reflectionProvider, true)), + new AccessStaticPropertiesRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + ), ); } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 3b71c235fb..14e4779f22 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,7 +22,10 @@ protected function getRule(): Rule return new AccessStaticPropertiesRule( $reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), ); } diff --git a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php index 50b4abe32b..5c92162410 100644 --- a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php @@ -64,4 +64,9 @@ public function testBug7933(): void $this->analyse([__DIR__ . '/data/bug-7933.php'], []); } + public function testBug10987(): void + { + $this->analyse([__DIR__ . '/data/bug-10987.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index ee02ea4ac6..97a9f1b1f6 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,10 +21,13 @@ class ExistingClassesInPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInPropertiesRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersion), true, diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index 747be201bb..bd92fe752e 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -39,21 +39,19 @@ public function testRule(): void ], [ 'Property MissingPropertyTypehint\Bar::$foo with generic interface MissingPropertyTypehint\GenericInterface does not specify its types: T, U', - 74, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 77, ], [ 'Property MissingPropertyTypehint\Bar::$baz with generic class MissingPropertyTypehint\GenericClass does not specify its types: A, B', - 80, - 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + 83, ], [ 'Property MissingPropertyTypehint\CallableSignature::$cb type has no signature specified for callable.', - 93, + 96, ], [ 'Property MissingPropertyTypehint\NestedArrayInProperty::$args type has no value type specified in iterable type array.', - 103, + 106, MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], ]); diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index c07a25f502..02e81f3f55 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -23,6 +23,9 @@ protected function getRule(): Rule self::getContainer(), [ 'MissingReadOnlyPropertyAssign\\TestCase::setUp', + 'Bug10523\\Controller::init', + 'Bug10523\\MultipleWrites::init', + 'Bug10523\\SingleWriteInConstructorCalledMethod::init', ], ), ); @@ -261,4 +264,124 @@ public function testAnonymousReadonlyClass(): void ]); } + public function testBug10523(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-10523.php'], [ + [ + 'Readonly property Bug10523\MultipleWrites::$userAccount is already assigned.', + 55, + ], + ]); + } + + public function testBug10822(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-10822.php'], []); + } + + public function testRedeclaredReadonlyProperties(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [ + [ + 'Readonly property RedeclareReadonlyProperty\B1::$myProp is already assigned.', + 16, + ], + [ + 'Readonly property RedeclareReadonlyProperty\B5::$myProp is already assigned.', + 50, + ], + [ + 'Readonly property RedeclareReadonlyProperty\B7::$myProp is already assigned.', + 70, + ], + [ + 'Readonly property class@anonymous/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php:117::$myProp is already assigned.', + 121, + ], + [ + 'Class RedeclareReadonlyProperty\B16 has an uninitialized readonly property $myProp. Assign it in the constructor.', + 195, + ], + [ + 'Class RedeclareReadonlyProperty\C17 has an uninitialized readonly property $aProp. Assign it in the constructor.', + 218, + ], + [ + 'Class RedeclareReadonlyProperty\B18 has an uninitialized readonly property $aProp. Assign it in the constructor.', + 233, + ], + ]); + } + + public function testRedeclaredPropertiesOfReadonlyClass(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->analyse([__DIR__ . '/data/redeclare-property-of-readonly-class.php'], [ + [ + 'Readonly property RedeclarePropertyOfReadonlyClass\B1::$promotedProp is already assigned.', + 15, + ], + ]); + } + + public function testBug8101(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8101.php'], [ + [ + 'Readonly property Bug8101\B::$myProp is already assigned.', + 12, + ], + ]); + } + + public function testBug9863(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9863.php'], [ + [ + 'Readonly property Bug9863\ReadonlyChildWithoutIsset::$foo is already assigned.', + 17, + ], + [ + 'Class Bug9863\ReadonlyParentWithIsset has an uninitialized readonly property $foo. Assign it in the constructor.', + 23, + ], + [ + 'Access to an uninitialized readonly property Bug9863\ReadonlyParentWithIsset::$foo.', + 28, + ], + ]); + } + + public function testBug9864(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9864.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php index b442a9dd2f..0a0c64ded7 100644 --- a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -47,7 +47,7 @@ public function testBug5172(): void $this->markTestSkipped('Test requires PHP 8.0.'); } - $this->analyse([__DIR__ . '/../../Analyser/data/bug-5172.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5172.php'], []); } public function testBug7980(): void @@ -61,12 +61,12 @@ public function testBug7980(): void public function testBug8517(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-8517.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8517.php'], []); } public function testBug9105(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-9105.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9105.php'], []); } public function testBug6922(): void diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index f8e50f870e..b6d394d30b 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -36,7 +38,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index 197473fbf5..e5496fc646 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -31,87 +31,99 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc.php'], [ [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$foo is assigned outside of the constructor.', - 40, + 47, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of the constructor.', + 49, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$bar is assigned outside of its declaring class.', - 53, + 61, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 54, + 62, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$psalm is assigned outside of its declaring class.', - 55, + 63, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of its declaring class.', + 64, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$bar is assigned outside of its declaring class.', - 60, + 69, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$psalm is assigned outside of its declaring class.', - 61, + 70, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of its declaring class.', + 71, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 68, + 78, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\FooArrays::$details is assigned outside of the constructor.', - 87, + 97, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\FooArrays::$details is assigned outside of the constructor.', - 88, + 98, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\NotThis::$foo is not assigned on $this.', - 118, + 128, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', - 134, + 144, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', - 135, + 145, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', - 137, + 147, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\ListAssign::$foo is assigned outside of the constructor.', - 158, + 168, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\ListAssign::$foo is assigned outside of the constructor.', - 163, + 173, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 173, + 183, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 174, + 184, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Immutable::$foo is assigned outside of the constructor.', - 237, + 247, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\B::$b is assigned outside of the constructor.', - 269, + 279, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\A::$a is assigned outside of its declaring class.', - 270, + 280, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\C::$c is assigned outside of the constructor.', - 283, + 293, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index fec1fb1192..90da9c44ec 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -140,4 +140,18 @@ public function testReadOnlyClasses(): void ]); } + public function testBug6773(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6773.php'], [ + [ + 'Readonly property Bug6773\Repository::$data is assigned outside of the constructor.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 7374c31b0f..09f34d1d40 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -586,4 +586,48 @@ public function testBug7087(): void $this->analyse([__DIR__ . '/data/bug-7087.php'], []); } + public function testUnset(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/property-type-after-unset.php'], [ + [ + 'Property PropertyTypeAfterUnset\Foo::$nonEmpty (non-empty-array) does not accept array.', + 19, + 'array might be empty.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$listProp (list) does not accept array, int>.', + 20, + 'array, int> might not be a list.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$nestedListProp (array>) does not accept array, int>>.', + 21, + 'array, int> might not be a list.', + ], + ]); + } + + public function testGenericsInCallableInConstructor(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-in-callable-in-constructor.php'], []); + } + + public function testBug11275(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11275.php'], [ + [ + 'Property Bug11275\D::$b (list) does not accept array.', + 50, + 'array might not be a list.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 39fdf5acc1..340c1331d9 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -202,4 +202,18 @@ public function testBug9831(): void ]); } + public function testRedeclareReadonlyProperties(): void + { + $this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [ + [ + 'Class RedeclareReadonlyProperty\B19 has an uninitialized property $prop2. Give it default value or assign it in the constructor.', + 249, + ], + [ + 'Access to an uninitialized property RedeclareReadonlyProperty\B19::$prop2.', + 260, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-10523.php b/tests/PHPStan/Rules/Properties/data/bug-10523.php new file mode 100644 index 0000000000..a5e88ebcd2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10523.php @@ -0,0 +1,84 @@ += 8.1 + +namespace Bug10523; + +final class Controller +{ + private readonly B $userAccount; + + public function __construct() + { + $this->userAccount = new B(); + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $x = $this->userAccount; + } + +} + +class B {} + +final class MultipleWrites +{ + private readonly B $userAccount; + + public function __construct() + { + $this->userAccount = new B(); + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $this->userAccount = new B(); + } + +} + + +final class SingleWriteInConstructorCalledMethod +{ + private readonly B $userAccount; + + public function __construct() + { + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $this->userAccount = new B(); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10822.php b/tests/PHPStan/Rules/Properties/data/bug-10822.php new file mode 100644 index 0000000000..35ed77467b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10822.php @@ -0,0 +1,294 @@ += 8.1 + +namespace Bug10822; + +enum Deprecation: string +{ + case callString = 'call-string'; + case userAuthored = 'user-authored'; +} + +interface FileLocation +{ + public function getOffset(): int; + + public function getLine(): int; + + public function getColumn(): int; +} + +interface FileSpan +{ + public function getSourceUrl(): ?string; + + public function getStart(): FileLocation; + + public function getEnd(): FileLocation; +} + +final class Frame +{ + public function __construct(private readonly string $url, private readonly ?int $line, private readonly ?int $column, private readonly ?string $member) + { + } + + public function getMember(): ?string + { + return $this->member; + } + + public function getLocation(): string + { + $library = $this->url; + + if ($this->line === null) { + return $library; + } + + if ($this->column === null) { + return $library . ' ' . $this->line; + } + + return $library . ' ' . $this->line . ':' . $this->column; + } +} + +final class Trace +{ + /** + * @param list $frames + */ + public function __construct(public readonly array $frames) + { + } +} + +interface DeprecationAwareLoggerInterface +{ + public function warn(string $message, bool $deprecation = false, ?FileSpan $span = null, ?Trace $trace = null): void; + + public function warnForDeprecation(Deprecation $deprecation, string $message, ?FileSpan $span = null, ?Trace $trace = null): void; +} + +interface AstNode +{ + public function getSpan(): FileSpan; +} + +final class SassScriptException extends \Exception +{ +} + +final class SassRuntimeException extends \Exception +{ + public readonly FileSpan $span; + public readonly Trace $sassTrace; + + public function __construct(string $message, FileSpan $span, ?Trace $sassTrace = null, ?\Throwable $previous = null) + { + $this->span = $span; + $this->sassTrace = $sassTrace ?? new Trace([]); + + parent::__construct($message, 0, $previous); + } +} + +interface SassCallable +{ + public function getName(): string; +} + +class BuiltInCallable implements SassCallable +{ + /** + * @param callable(list): Value $callback + */ + public static function function (string $name, string $arguments, callable $callback): BuiltInCallable + { + return new BuiltInCallable($name, [[$arguments, $callback]]); + } + + /** + * @param list): Value}> $overloads + */ + private function __construct(private readonly string $name, public readonly array $overloads) + { + } + + public function getName(): string + { + return $this->name; + } +} + +abstract class Value +{ + public function assertString(string $name): SassString + { + throw new SassScriptException("\$$name: this is not a string."); + } +} + +final class SassString extends Value +{ + public function __construct( + private readonly string $text, + public readonly bool $hasQuotes, + ) + { + } + + public function getText(): string + { + return $this->text; + } + + public function assertString(string $name): SassString + { + return $this; + } +} + +final class SassMixin extends Value +{ + public function __construct(public readonly SassCallable $callable) + { + } +} + +interface ImportCache +{ + public function humanize(string $uri): string; +} + +final class Environment +{ + public function getMixin(string $name): SassCallable + { + throw new \BadMethodCallException('not implemented yet'); + } +} + +class EvaluateVisitor +{ + private readonly ImportCache $importCache; + + /** + * @var array + */ + public array $builtInFunctions = []; + + private readonly DeprecationAwareLoggerInterface $logger; + + /** + * @var array> + */ + private array $warningsEmitted = []; + + private Environment $environment; + + private string $member = "root stylesheet"; + + private ?AstNode $callableNode = null; + + /** + * @var list + */ + private array $stack = []; + + public function __construct(ImportCache $importCache, DeprecationAwareLoggerInterface $logger) + { + $this->importCache = $importCache; + $this->logger = $logger; + $this->environment = new Environment(); + + // These functions are defined in the context of the evaluator because + // they need access to the environment or other local state. + $metaFunctions = [ + BuiltInCallable::function('get-mixin', '$name', function ($arguments) { + $name = $arguments[0]->assertString('name'); + + \assert($this->callableNode !== null); + $callable = $this->addExceptionSpan($this->callableNode, function () use ($name) { + return $this->environment->getMixin(str_replace('_', '-', $name->getText())); + }); + + return new SassMixin($callable); + }), + ]; + + foreach ($metaFunctions as $function) { + $this->builtInFunctions[$function->getName()] = $function; + } + } + + private function stackFrame(string $member, FileSpan $span): Frame + { + $url = $span->getSourceUrl(); + + if ($url !== null) { + $url = $this->importCache->humanize($url); + } + + return new Frame( + $url ?? $span->getSourceUrl() ?? '-', + $span->getStart()->getLine() + 1, + $span->getStart()->getColumn() + 1, + $member + ); + } + + private function stackTrace(?FileSpan $span = null): Trace + { + $frames = []; + + foreach ($this->stack as [$member, $nodeWithSpan]) { + $frames[] = $this->stackFrame($member, $nodeWithSpan->getSpan()); + } + + if ($span !== null) { + $frames[] = $this->stackFrame($this->member, $span); + } + + return new Trace(array_reverse($frames)); + } + + public function warn(string $message, FileSpan $span, ?Deprecation $deprecation = null): void + { + $spanString = ($span->getSourceUrl() ?? '') . "\0" . $span->getStart()->getOffset() . "\0" . $span->getEnd()->getOffset(); + + if (isset($this->warningsEmitted[$message][$spanString])) { + return; + } + $this->warningsEmitted[$message][$spanString] = true; + + $trace = $this->stackTrace($span); + + if ($deprecation === null) { + $this->logger->warn($message, false, $span, $trace); + } else { + $this->logger->warnForDeprecation($deprecation, $message, $span, $trace); + } + } + + /** + * Runs $callback, and converts any {@see SassScriptException}s it throws to + * {@see SassRuntimeException}s with $nodeWithSpan's source span. + * + * @template T + * + * @param callable(): T $callback + * + * @return T + * + * @throws SassRuntimeException + */ + private function addExceptionSpan(AstNode $nodeWithSpan, callable $callback, bool $addStackFrame = true) + { + try { + return $callback(); + } catch (SassScriptException $e) { + throw new SassRuntimeException($e->getMessage(), $nodeWithSpan->getSpan(), $this->stackTrace($addStackFrame ? $nodeWithSpan->getSpan() : null), $e); + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10987.php b/tests/PHPStan/Rules/Properties/data/bug-10987.php new file mode 100644 index 0000000000..f3856d7303 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10987.php @@ -0,0 +1,21 @@ += 7.4 + +namespace Bug11275; + +/** + * @no-named-arguments + */ +final class A +{ + /** + * @var list + */ + private array $b; + + public function __construct(B ...$b) + { + $this->b = $b; + } +} + +final class B +{ +} + +final class C +{ + /** + * @var list + */ + private array $b; + + /** + * @no-named-arguments + */ + public function __construct(B ...$b) + { + $this->b = $b; + } +} + +final class D +{ + /** + * @var list + */ + private array $b; + + public function __construct(B ...$b) + { + $this->b = $b; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6773.php b/tests/PHPStan/Rules/Properties/data/bug-6773.php new file mode 100644 index 0000000000..e83ed3166a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6773.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug6773; + +final class Repository +{ + /** + * @param array $data + */ + public function __construct(private readonly array $data) + { + } + + public function remove(string $key): void + { + unset($this->data[$key]); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8101.php b/tests/PHPStan/Rules/Properties/data/bug-8101.php new file mode 100644 index 0000000000..09010120d9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8101.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug8101; + +class A { + public function __construct(public readonly int $myProp) {} +} + +class B extends A { + // This should be reported as an error, as a readonly prop cannot be redeclared. + public function __construct(public readonly int $myProp) { + parent::__construct($myProp); + } +} + +$foo = new B(7); diff --git a/tests/PHPStan/Rules/Properties/data/bug-8629.php b/tests/PHPStan/Rules/Properties/data/bug-8629.php new file mode 100644 index 0000000000..b8fc89ee21 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8629.php @@ -0,0 +1,10 @@ +nodeType); +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-9694.php b/tests/PHPStan/Rules/Properties/data/bug-9694.php new file mode 100644 index 0000000000..96cd448073 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9694.php @@ -0,0 +1,20 @@ += 8.0 + +class TotpEnrollment +{ + public bool $confirmed; +} + +class User +{ + public ?TotpEnrollment $totpEnrollment; +} + +function () { + $user = new User(); + + return match ($user->totpEnrollment === null) { + true => false, + false => $user->totpEnrollment->confirmed, + }; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-9863.php b/tests/PHPStan/Rules/Properties/data/bug-9863.php new file mode 100644 index 0000000000..49d8f404ad --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9863.php @@ -0,0 +1,49 @@ += 8.1 + +namespace Bug9863; + +class ReadonlyParentWithoutIsset +{ + public function __construct( + public readonly int $foo + ) {} +} + +class ReadonlyChildWithoutIsset extends ReadonlyParentWithoutIsset +{ + public function __construct( + public readonly int $foo = 42 + ) { + parent::__construct($foo); + } +} + +class ReadonlyParentWithIsset +{ + public readonly int $foo; + + public function __construct( + int $foo + ) { + if (! isset($this->foo)) { + $this->foo = $foo; + } + } +} + +class ReadonlyChildWithIsset extends ReadonlyParentWithIsset +{ + public function __construct( + public readonly int $foo = 42 + ) { + parent::__construct($foo); + } +} + +$a = new ReadonlyParentWithoutIsset(0); +$b = new ReadonlyChildWithoutIsset(); +$c = new ReadonlyChildWithoutIsset(1); + +$x = new ReadonlyParentWithIsset(2); +$y = new ReadonlyChildWithIsset(); +$z = new ReadonlyChildWithIsset(3); diff --git a/tests/PHPStan/Rules/Properties/data/bug-9864.php b/tests/PHPStan/Rules/Properties/data/bug-9864.php new file mode 100644 index 0000000000..4790a1a5ae --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9864.php @@ -0,0 +1,31 @@ += 8.2 + +namespace Bug9864; + +readonly abstract class UuidValueObject +{ + public function __construct(public string $value) + { + $this->ensureIsValidUuid($value); + } + + private function ensureIsValidUuid(string $value): void + { + } +} + + +final readonly class ProductId extends UuidValueObject +{ + public string $value; + + public function __construct( + string $value + ) { + parent::__construct($value); + } +} + +var_dump(new ProductId('test')); + +// property is assigned on parent class, no need to reassing, specially for readonly properties diff --git a/tests/PHPStan/Rules/Properties/data/generics-in-callable-in-constructor.php b/tests/PHPStan/Rules/Properties/data/generics-in-callable-in-constructor.php new file mode 100644 index 0000000000..42dc135584 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/generics-in-callable-in-constructor.php @@ -0,0 +1,40 @@ + */ + private $differ; + + public function doFoo(): void + { + $this->differ = new Differ(static function ($a, $b) { + return false; + }); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php index aa42587eab..952016e860 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -42,6 +42,9 @@ class PrefixedTags /** @psalm-var int */ private $fooPsalm; + /** @phan-var int */ + private $fooPhan; + } /** diff --git a/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php new file mode 100644 index 0000000000..5c2cee2758 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php @@ -0,0 +1,40 @@ + */ + private $nonEmpty; + + /** @var list */ + private $listProp; + + /** @var array> */ + private $nestedListProp; + + public function doFoo(int $i, int $j) + { + unset($this->nonEmpty[$i]); + unset($this->listProp[$i]); + unset($this->nestedListProp[$i][$j]); + } + +} + +class Bar +{ + + /** @var array> */ + private $prop; + + /** + * @param int|string $key + */ + public function doFoo($key): void + { + unset($this->prop[$key]['foo']); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php index 4d8c32463f..55af82fe30 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php @@ -29,16 +29,24 @@ class Foo */ public $psalm; + /** + * @var int + * @phan-read-only + */ + public $phan; + public function __construct(int $foo) { $this->foo = $foo; // constructor - fine $this->psalm = $foo; // constructor - fine + $this->phan = $foo; // constructor - fine } public function setFoo(int $foo): void { $this->foo = $foo; // setter - report $this->psalm = $foo; // do not report -allowed private mutation + $this->phan = $foo; // setter - report } } @@ -53,12 +61,14 @@ public function __construct(int $bar) $this->bar = $bar; // report - not in declaring class $this->baz = $baz; // report - not in declaring class $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class } public function setBar(int $bar): void { $this->bar = $bar; // report - not in declaring class $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class } } diff --git a/tests/PHPStan/Rules/Properties/data/redeclare-property-of-readonly-class.php b/tests/PHPStan/Rules/Properties/data/redeclare-property-of-readonly-class.php new file mode 100644 index 0000000000..672de2c5e8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/redeclare-property-of-readonly-class.php @@ -0,0 +1,50 @@ += 8.2 + +namespace RedeclarePropertyOfReadonlyClass; + +readonly class A { + public function __construct(public int $promotedProp) + { + } +} + +readonly class B1 extends A { + // $promotedProp is written twice + public function __construct(public int $promotedProp) + { + parent::__construct(5); + } +} + +readonly class B2 extends A { + // Don't get confused by standard parameter with same name + public function __construct(int $promotedProp) + { + parent::__construct($promotedProp); + } +} + +readonly class B3 extends A { + // This is allowed, because we don't write to the property. + public int $promotedProp; + + public function __construct() + { + parent::__construct(7); + } +} + +readonly class B4 extends A { + // The second write is not from the constructor. It is an error, but it is handled by different rule. + public int $promotedProp; + + public function __construct() + { + parent::__construct(7); + } + + public function set(): void + { + $this->promotedProp = 7; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php b/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php new file mode 100644 index 0000000000..a9e611d3b9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php @@ -0,0 +1,262 @@ += 8.1 + +namespace RedeclareReadonlyProperty; + +class A { + protected readonly string $nonPromotedProp; + + public function __construct(public readonly int $myProp) { + $this->nonPromotedProp = 'aaa'; + } +} + +class B1 extends A { + // This should be reported as an error, as a readonly prop cannot be redeclared. + public function __construct(public readonly int $myProp) { + parent::__construct($myProp); + } +} + +class B2 extends A { + // different property + public function __construct(public readonly int $foo) { + parent::__construct($foo); + } +} + +class B3 extends A { + // We don't call the parent constructor, so it's fine. + public function __construct(public readonly int $myProp) { + } +} + +class B4 extends A { + protected readonly string + $foo, + // We can't detect this at the moment. + $nonPromotedProp; + public function __construct() { + $this->foo = 'xyz'; + $this->nonPromotedProp = 'bbb'; + parent::__construct(5); + } +} + +class B5 extends A { + // Error: we can't both write the property ourselves and call the parent constructor. + public readonly int $myProp; + public function __construct() { + $this->myProp = 7; + parent::__construct(5); + } +} + +class B6 extends A { + // This is fine - we don't call parent constructor; + public readonly int $myProp; + public function __construct() { + $this->myProp = 5; + } +} + +class B7 extends A { + // Call parent constructor indirectly. + public function __construct(public readonly int $myProp) { + $this->foo(); + } + + private function foo(): void + { + A::__construct(5); + } +} + +class B8 extends A { + // Don't get confused by prop declaration in anonymous class. + public function __construct() { + parent::__construct(5); + $c = new class { + public function __construct(public readonly int $myProp) + { + } + }; + } +} + +class B9 extends A { + // Don't get confused by constructor call in anonymous class + public readonly int $myProp; + public function __construct() { + $this->myProp = 5; + $c = new class extends A { + public function __construct() + { + parent::__construct(5); + } + }; + } +} + +class B10 extends A { + // Don't get confused by promoted properties in anonymous class + public function __construct() { + parent::__construct(5); + $c = new class (5) { + public function __construct(public readonly int $myProp) + { + } + }; + } +} + +class B11 extends A { + // This is fine - we don't call the parent constructor. + public readonly int $myProp; + public function __construct() { + $this->myProp = 5; + $c = new class ('aaa') extends A { + // Detect redeclaration even inside anonymous classes. + public function __construct(public readonly int $myProp) + { + parent::__construct(5); + } + }; + } +} + +class A12 { + public function __construct(public readonly int $aProp) + { + } +} + +class B12 extends A12 { + public function __construct(public readonly int $bProp) + { + parent::__construct(15); + } +} + +class C12 extends B12 { + // This is OK, because we call A12's constructor, not B12's. + public function __construct(public readonly int $bProp) { + A12::__construct(15); + } +} + +class B12_1 extends A12 { + public function __construct(public readonly int $bProp) + { + parent::__construct(15); + } +} + +class C12_1 extends B12_1 { + // This is an error, but we can't detect it at the moment. + public function __construct(public readonly int $aProp) { + parent::__construct(15); + } +} + +class A13 { + public function __construct(private readonly int $privateProp) + { + } +} + +class B13 extends A13 { + // This is OK, A's prop is private + public function __construct(public readonly int $privateProp) + { + parent::__construct(15); + } +} + +class B14 { + public function __construct(public readonly int $myProp) { + // Don't get confused by same property in non-parent's constructor. + A::__construct(7); + } +} + +class B15 extends A { + public function __construct(public readonly int $myProp) { + self:foo(); + } + + public static function foo(): void + { + // Don't get confused by calling the parent constructor from static scope. + parent::__construct(7); + } +} + +class B16 extends A { + public readonly int $myProp; + + public function __construct(A $other) { + // Don't get confused by calling the constructor on other object. + $other::__construct(7); + $other->__construct(7); + } +} + +class A17 { + public function __construct(public readonly int $aProp) + { + } +} + +class B17 extends A17 { + public function __construct() + { + } +} + +class C17 extends B17 { + // Error: $aProp may be unassigned, because B's constructor may not call A's + public readonly int $aProp; + + public function __construct() { + parent::__construct(); + } +} + +class A18 { + public function __construct(private readonly int $aProp) + { + } +} + +class B18 extends A18 { + // Make surer that we don't get confused by parent's private property. + public readonly int $aProp; + + public function __construct() + { + parent::__construct(7); + } +} + +class A19 { + public function __construct(public int $prop1, public int $prop2) + { + } +} + +class B19 extends A19 { + public int $prop1; + public int $prop2; + + public function __construct() + { + if (rand()) { + parent::__construct(5, 6); + } else { + $this->prop1 = 7; + } + + // Error: this may not be assigned + var_dump($this->prop2); + } +} diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php new file mode 100644 index 0000000000..c310f6177c --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -0,0 +1,170 @@ + + */ +class PureFunctionRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new PureFunctionRule(new FunctionPurityCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/pure-function.php'], [ + [ + 'Function PureFunction\doFoo() is marked as pure but parameter $p is passed by reference.', + 8, + ], + [ + 'Impure echo in pure function PureFunction\doFoo().', + 10, + ], + [ + 'Function PureFunction\doFoo2() is marked as pure but returns void.', + 16, + ], + [ + 'Impure exit in pure function PureFunction\doFoo2().', + 18, + ], + [ + 'Impure property assignment in pure function PureFunction\doFoo3().', + 26, + ], + [ + 'Possibly impure call to a callable in pure function PureFunction\testThese().', + 60, + ], + [ + 'Possibly impure call to a callable in pure function PureFunction\testThese().', + 61, + ], + [ + 'Impure call to function PureFunction\impureFunction() in pure function PureFunction\testThese().', + 63, + ], + [ + 'Impure call to function PureFunction\voidFunction() in pure function PureFunction\testThese().', + 64, + ], + [ + 'Possibly impure call to function PureFunction\possiblyImpureFunction() in pure function PureFunction\testThese().', + 65, + ], + [ + 'Possibly impure call to unknown function in pure function PureFunction\testThese().', + 66, + ], + [ + 'Function PureFunction\actuallyPure() is marked as impure but does not have any side effects.', + 72, + ], + [ + 'Function PureFunction\emptyVoidFunction() returns void but does not have any side effects.', + 84, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 102, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 103, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 105, + ], + [ + 'Impure global variable in pure function PureFunction\functionWithGlobal().', + 118, + ], + [ + 'Impure static variable in pure function PureFunction\functionWithStaticVariable().', + 128, + ], + [ + 'Possibly impure call to a Closure in pure function PureFunction\callsClosures().', + 139, + ], + [ + 'Possibly impure call to a Closure in pure function PureFunction\callsClosures().', + 140, + ], + [ + 'Impure output between PHP opening and closing tags in pure function PureFunction\justContainsInlineHtml().', + 160, + ], + ]); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/first-class-callable-pure-function.php'], [ + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 61, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 64, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 70, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 73, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 75, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 81, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 84, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 90, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 93, + ], + [ + 'Possibly impure call to a callable in pure function FirstClassCallablePureFunction\callCallbackImmediately().', + 102, + ], + ]); + } + + public function testBug11361(): void + { + $this->analyse([__DIR__ . '/data/bug-11361-pure.php'], [ + [ + 'Impure call to a Closure with by-ref parameter in pure function Bug11361Pure\foo().', + 14, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php new file mode 100644 index 0000000000..4ec5028172 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -0,0 +1,197 @@ + + */ +class PureMethodRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + public function getRule(): Rule + { + return new PureMethodRule(new FunctionPurityCheck()); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pure-method.php'], [ + [ + 'Method PureMethod\Foo::doFoo() is marked as pure but parameter $p is passed by reference.', + 11, + ], + [ + 'Impure echo in pure method PureMethod\Foo::doFoo().', + 13, + ], + [ + 'Method PureMethod\Foo::doFoo2() is marked as pure but returns void.', + 19, + ], + [ + 'Impure die in pure method PureMethod\Foo::doFoo2().', + 21, + ], + [ + 'Impure property assignment in pure method PureMethod\Foo::doFoo3().', + 29, + ], + [ + 'Impure call to method PureMethod\Foo::voidMethod() in pure method PureMethod\Foo::doFoo4().', + 71, + ], + [ + 'Impure call to method PureMethod\Foo::impureVoidMethod() in pure method PureMethod\Foo::doFoo4().', + 72, + ], + [ + 'Possibly impure call to method PureMethod\Foo::returningMethod() in pure method PureMethod\Foo::doFoo4().', + 73, + ], + [ + 'Impure call to method PureMethod\Foo::impureReturningMethod() in pure method PureMethod\Foo::doFoo4().', + 75, + ], + [ + 'Possibly impure call to unknown method in pure method PureMethod\Foo::doFoo4().', + 76, + ], + [ + 'Impure call to method PureMethod\Foo::voidMethod() in pure method PureMethod\Foo::doFoo5().', + 84, + ], + [ + 'Impure call to method PureMethod\Foo::impureVoidMethod() in pure method PureMethod\Foo::doFoo5().', + 85, + ], + [ + 'Possibly impure call to method PureMethod\Foo::returningMethod() in pure method PureMethod\Foo::doFoo5().', + 86, + ], + [ + 'Impure call to method PureMethod\Foo::impureReturningMethod() in pure method PureMethod\Foo::doFoo5().', + 88, + ], + [ + 'Possibly impure call to unknown method in pure method PureMethod\Foo::doFoo5().', + 89, + ], + [ + 'Impure instantiation of class PureMethod\ImpureConstructor in pure method PureMethod\TestConstructors::doFoo().', + 140, + ], + [ + 'Possibly impure instantiation of class PureMethod\PossiblyImpureConstructor in pure method PureMethod\TestConstructors::doFoo().', + 141, + ], + [ + 'Possibly impure instantiation of unknown class in pure method PureMethod\TestConstructors::doFoo().', + 142, + ], + [ + 'Method PureMethod\ActuallyPure::doFoo() is marked as impure but does not have any side effects.', + 153, + ], + [ + 'Impure echo in pure method PureMethod\ExtendingClass::pure().', + 183, + ], + [ + 'Method PureMethod\ExtendingClass::impure() is marked as impure but does not have any side effects.', + 187, + ], + [ + 'Method PureMethod\ClassWithVoidMethods::privateEmptyVoidFunction() returns void but does not have any side effects.', + 214, + ], + [ + 'Impure assign to superglobal variable in pure method PureMethod\ClassWithVoidMethods::purePostGetAssign().', + 230, + ], + [ + 'Impure assign to superglobal variable in pure method PureMethod\ClassWithVoidMethods::purePostGetAssign().', + 231, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', + 295, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', + 296, + ], + [ + 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', + 330, + ], + [ + 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', + 330, + ], + ]); + } + + 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 property assignment in pure method PureConstructor\Foo::__construct().', + 19, + ], + [ + 'Method PureConstructor\Bar::__construct() is marked as impure but does not have any side effects.', + 30, + ], + [ + 'Impure property assignment in pure method PureConstructor\AssignOtherThanThis::__construct().', + 49, + ], + ]); + } + + public function testImpureAssignRef(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impure-assign-ref.php'], [ + [ + 'Possibly impure property assignment by reference in pure method ImpureAssignRef\HelloWorld::bar6().', + 49, + ], + ]); + } + + /** + * @dataProvider dataBug11207 + */ + public function testBug11207(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-11207.php'], []); + } + + public function dataBug11207(): array + { + return [ + [true], + [false], + ]; + } + +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-11207.php b/tests/PHPStan/Rules/Pure/data/bug-11207.php new file mode 100644 index 0000000000..e69bdb5573 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-11207.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug11207; + +final class FilterData +{ + /** + * @phpstan-pure + */ + private function __construct( + public ?int $type, + public bool $hasValue, + public mixed $value = null + ) { + } + + /** + * @param array{type?: int|numeric-string|null, value?: mixed} $data + * @phpstan-pure + */ + public static function fromArray(array $data): self + { + if (isset($data['type'])) { + if (!\is_int($data['type']) && (!\is_string($data['type']) || !is_numeric($data['type']))) { + throw new \InvalidArgumentException(sprintf( + 'The "type" parameter MUST be of type "integer" or "null", "%s" given.', + \gettype($data['type']) + )); + } + + $type = (int) $data['type']; + } else { + $type = null; + } + + return new self($type, \array_key_exists('value', $data), $data['value'] ?? null); + } +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-11361-pure.php b/tests/PHPStan/Rules/Pure/data/bug-11361-pure.php new file mode 100644 index 0000000000..f8a52fffe5 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-11361-pure.php @@ -0,0 +1,17 @@ += 8.1 + +namespace FirstClassCallablePureFunction; + +class Foo +{ + + /** + * @phpstan-pure + */ + function pureFunction() + { + + } + + /** + * @phpstan-impure + */ + function impureFunction() + { + echo ''; + } + + function voidFunction(): void + { + echo 'test'; + } + +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +/** + * @phpstan-pure + */ +function testThese(Foo $foo) +{ + $cb = $foo->pureFunction(...); + $cb(); + + $cb = $foo->impureFunction(...); + $cb(); + + $cb = $foo->voidFunction(...); + $cb(); + + $cb = pureFunction(...); + $cb(); + + $cb = impureFunction(...); + $cb(); + + $cb = voidFunction(...); + $cb(); + + callCallbackImmediately($cb); + + $cb = 'FirstClassCallablePureFunction\\pureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\impureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\voidFunction'; + $cb(); + + $cb = [$foo, 'pureFunction']; + $cb(); + + $cb = [$foo, 'impureFunction']; + $cb(); + + $cb = [$foo, 'voidFunction']; + $cb(); +} + +/** + * @phpstan-pure + * @return int + */ +function callCallbackImmediately(callable $cb): int +{ + return $cb(); +} diff --git a/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php new file mode 100644 index 0000000000..7aca03a4a4 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/impure-assign-ref.php @@ -0,0 +1,63 @@ += 7.4 + +namespace ImpureAssignRef; + +class HelloWorld +{ + public int $value = 0; + /** @var array */ + public array $arr = []; + /** @var array */ + public array $objectArr = []; + public static int $staticValue = 0; + /** @var array */ + public static array $staticArr = []; + + private function bar1(): void + { + $value = &$this->value; + $value = 1; + } + + private function bar2(): void + { + $value = &$this->arr[0]; + $value = 1; + } + + private function bar3(): void + { + $value = &self::$staticValue; + $value = 1; + } + + private function bar4(): void + { + $value = &self::$staticArr[0]; + $value = 1; + } + + private function bar5(self $other): void + { + $value = &$other->value; + $value = 1; + } + + /** @phpstan-pure */ + private function bar6(): int + { + $value = &$this->objectArr[0]->foo; + + return 1; + } + + public function foo(): void + { + $this->bar1(); + $this->bar2(); + $this->bar3(); + $this->bar4(); + $this->bar5(new self()); + $this->bar6(); + } +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-constructor.php b/tests/PHPStan/Rules/Pure/data/pure-constructor.php new file mode 100644 index 0000000000..71045fd3ed --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-constructor.php @@ -0,0 +1,51 @@ += 8.0 + +namespace PureConstructor; + +class Foo +{ + + private string $prop; + + public static $staticProp = 1; + + /** @phpstan-pure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + self::$staticProp++; + } + +} + +class Bar +{ + + private string $prop; + + /** @phpstan-impure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + } + +} + +class AssignOtherThanThis +{ + private int $i = 0; + + /** @phpstan-pure */ + public function __construct( + self $other, + ) + { + $other->i = 1; + } +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php new file mode 100644 index 0000000000..6a4bb319b3 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -0,0 +1,166 @@ +foo = 'test'; +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +function possiblyImpureFunction() +{ + +} + +/** + * @phpstan-pure + */ +function testThese(string $s, callable $cb) +{ + $s(); + $cb(); + pureFunction(); + impureFunction(); + voidFunction(); + possiblyImpureFunction(); + unknownFunction(); +} + +/** + * @phpstan-impure + */ +function actuallyPure() +{ + +} + +function voidFunctionThatThrows(): void +{ + if (rand(0, 1)) { + throw new \Exception(); + } +} + +function emptyVoidFunction(): void +{ + $a = 1 + 1; +} + +/** + * @phpstan-assert !null $a + */ +function emptyVoidFunctionWithAssertTag(?int $a): void +{ + +} + +/** + * @phpstan-pure + */ +function pureButAccessSuperGlobal(): int +{ + $a = $_POST['bla']; + $_POST['test'] = 1; + + return $_POST['test']; +} + +function emptyVoidFunctionWithByRefParameter(&$a): void +{ + +} + +/** + * @phpstan-pure + */ +function functionWithGlobal(): int +{ + global $db; + + return 1; +} + +/** + * @phpstan-pure + */ +function functionWithStaticVariable(): int +{ + static $v = 1; + + return $v; +} + +/** + * @phpstan-pure + * @param \Closure(): int $closure2 + */ +function callsClosures(\Closure $closure1, \Closure $closure2): int +{ + $closure1(); + return $closure2(); +} + +/** + * @phpstan-pure + * @param pure-callable $cb + * @param pure-Closure $closure + * @return int + */ +function callsPureCallableIdentifierTypeNode(callable $cb, \Closure $closure): int +{ + $cb(); + $closure(); +} + + +/** @phpstan-pure */ +function justContainsInlineHtml() +{ + ?> + + + + + + foo = 'test'; + } + + public function voidMethod(): void + { + echo '1'; + } + + /** + * @phpstan-impure + */ + public function impureVoidMethod(): void + { + echo ''; + } + + public function returningMethod(): int + { + + } + + /** + * @phpstan-pure + */ + public function pureReturningMethod(): int + { + + } + + /** + * @phpstan-impure + */ + public function impureReturningMethod(): int + { + echo ''; + } + + /** + * @phpstan-pure + */ + public function doFoo4() + { + $this->voidMethod(); + $this->impureVoidMethod(); + $this->returningMethod(); + $this->pureReturningMethod(); + $this->impureReturningMethod(); + $this->unknownMethod(); + } + + /** + * @phpstan-pure + */ + public function doFoo5() + { + self::voidMethod(); + self::impureVoidMethod(); + self::returningMethod(); + self::pureReturningMethod(); + self::impureReturningMethod(); + self::unknownMethod(); + } + + +} + +class PureConstructor +{ + + /** + * @phpstan-pure + */ + public function __construct() + { + + } + +} + +class ImpureConstructor +{ + + /** + * @phpstan-impure + */ + public function __construct() + { + echo ''; + } + +} + +class PossiblyImpureConstructor +{ + + public function __construct() + { + + } + +} + +class TestConstructors +{ + + /** + * @phpstan-pure + */ + public function doFoo(string $s) + { + new PureConstructor(); + new ImpureConstructor(); + new PossiblyImpureConstructor(); + new $s(); + } + +} + +class ActuallyPure +{ + + /** + * @phpstan-impure + */ + public function doFoo() + { + + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + public function pure(): int + { + echo 'test'; + return 1; + } + + public function impure(): int + { + return 1; + } + +} + +class ClassWithVoidMethods +{ + + public function voidFunctionThatThrows(): void + { + if (rand(0, 1)) { + throw new \Exception(); + } + } + + public function emptyVoidFunction(): void + { + + } + + protected function protectedEmptyVoidFunction(): void + { + + } + + private function privateEmptyVoidFunction(): void + { + $a = 1 + 1; + } + + private function setPostAndGet(array $post = [], array $get = []): void + { + $_POST = $post; + $_GET = $get; + } + + /** + * @phpstan-pure + */ + public function purePostGetAssign(array $post = [], array $get = []): int + { + $_POST = $post; + $_GET = $get; + + return 1; + } + +} + +class NoMagicMethods +{ + +} + +class PureMagicMethods +{ + + /** + * @phpstan-pure + */ + public function __toString(): string + { + return 'one'; + } + +} + +class MaybePureMagicMethods +{ + + public function __toString(): string + { + return 'one'; + } + +} + +class ImpureMagicMethods +{ + + /** + * @phpstan-impure + */ + public function __toString(): string + { + sleep(1); + return 'one'; + } + +} + +class TestMagicMethods +{ + + /** + * @phpstan-pure + */ + public function doFoo( + NoMagicMethods $no, + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + (string) $no; + (string) $pure; + (string) $maybe; + (string) $impure; + } + +} + +class NoConstructor +{ + +} + +class TestNoConstructor +{ + + /** + * @phpstan-pure + */ + public function doFoo(): int + { + new NoConstructor(); + + return 1; + } + +} + +class MaybeCallableFromUnion +{ + + /** + * @phpstan-pure + * @param callable|string $p + */ + public function doFoo($p): int + { + $p(); + + return 1; + } + +} + +class VoidMethods +{ + + private function doFoo(): void + { + + } + + private function doBar(): void + { + \PHPStan\dumpType(1); + } + + private function doBaz(): void + { + // nop + ; + + // nop + ; + + // nop + ; + } + +} + +class AssertingImpureVoidMethod +{ + + /** + * @param mixed $value + * @phpstan-assert array $value + * @phpstan-impure + */ + public function assertSth($value): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php index 4a37f5114b..b1c964fada 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Regex\RegexExpressionHelper; use function sprintf; use const PHP_VERSION_ID; @@ -15,7 +16,9 @@ class RegularExpressionPatternRuleTest extends RuleTestCase protected function getRule(): Rule { - return new RegularExpressionPatternRule(); + return new RegularExpressionPatternRule( + self::getContainer()->getByType(RegexExpressionHelper::class), + ); } public function testValidRegexPatternBefore73(): void @@ -115,6 +118,30 @@ public function testValidRegexPatternBefore73(): void '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, + ], ], ); } @@ -221,8 +248,85 @@ public function testValidRegexPatternAfter73(): void 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 43, ], + [ + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)', $messagePart), + 57, + ], + [ + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)', $messagePart), + 58, + ], + [ + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 7 in pattern: ~((?:.*)~', + 59, + ], + [ + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)nono', $messagePart), + 61, + ], + [ + sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok(?:.*)nope', $messagePart), + 62, + ], + [ + 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 7 in pattern: ~((?:.*)~', + 63, + ], ], ); } + /** + * @param list $errors + * @dataProvider dataArrayShapePatterns + */ + public function testArrayShapePatterns(string $file, array $errors): void + { + $this->analyse( + [$file], + $errors, + ); + } + + public function dataArrayShapePatterns(): iterable + { + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_all_shapes.php', + [], + ]; + + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_shapes.php', + [ + [ + "Regex pattern is invalid: Unknown modifier 'y' in pattern: /(foo)(bar)(baz)/xyz", + 124, + ], + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_shapes_php80.php', + [], + ]; + } + + if (PHP_VERSION_ID >= 80200) { + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_match_shapes_php82.php', + [], + ]; + } + + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_replace_callback_shapes.php', + [], + ]; + yield [ + __DIR__ . '/../../Analyser/nsrt/preg_replace_callback_shapes-php72.php', + [], + ]; + } + } diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php new file mode 100644 index 0000000000..38197c1aa6 --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php @@ -0,0 +1,98 @@ + + */ +class RegularExpressionQuotingRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RegularExpressionQuotingRule( + $this->createReflectionProvider(), + self::getContainer()->getByType(RegexExpressionHelper::class), + ); + } + + public function testRule(): void + { + $this->analyse( + [__DIR__ . '/data/preg-quote.php'], + [ + [ + 'Call to preg_quote() is missing delimiter & to be effective.', + 6, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 7, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 11, + ], + [ + 'Call to preg_quote() is missing delimiter & to be effective.', + 12, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 18, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 20, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 21, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 22, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 23, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 24, + ], + [ + 'Call to preg_quote() is missing delimiter parameter to be effective.', + 77, + ], + ], + ); + } + + public function testRulePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse( + [__DIR__ . '/data/preg-quote-php8.php'], + [ + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 6, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 7, + ], + ], + ); + } + +} diff --git a/tests/PHPStan/Rules/Regexp/data/preg-quote-php8.php b/tests/PHPStan/Rules/Regexp/data/preg-quote-php8.php new file mode 100644 index 0000000000..83e19cf03b --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/data/preg-quote-php8.php @@ -0,0 +1,13 @@ += 8.0 + +namespace PregQuotingPhp8; + +function doFoo(string $s, callable $cb): void { // errors + preg_split(subject: $s, pattern: '&' . preg_quote('&oops', '/') . 'pattern&'); + preg_split(subject: $s, pattern: '&' . preg_quote(delimiter: '/', str: '&oops') . 'pattern&'); +} + +function ok(string $s): void { // ok + preg_split(subject: $s, pattern: '&' . preg_quote('&oops', '&') . 'pattern&'); + preg_split(subject: $s, pattern: '&' . preg_quote(delimiter: '&', str: '&oops') . 'pattern&'); +} diff --git a/tests/PHPStan/Rules/Regexp/data/preg-quote.php b/tests/PHPStan/Rules/Regexp/data/preg-quote.php new file mode 100644 index 0000000000..5333d72f58 --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/data/preg-quote.php @@ -0,0 +1,96 @@ + + */ +class TooWideFunctionParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideFunctionParameterOutTypeRule(new TooWideParameterOutTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/too-wide-function-parameter-out.php'], [ + [ + 'Function TooWideFunctionParameterOut\doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 10, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Function TooWideFunctionParameterOut\doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 18, + ], + [ + 'Function TooWideFunctionParameterOut\doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 23, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Function TooWideFunctionParameterOut\bug10699() never assigns 20 to &$out so it can be removed from the @param-out type.', + 48, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 8b6438796d..b914db3141 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -44,6 +44,10 @@ public function testRule(): void 'Function TooWideFunctionReturnType\dolor6() never returns null so it can be removed from the return type.', 79, ], + [ + 'Function TooWideFunctionReturnType\conditionalType() never returns string so it can be removed from the return type.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..cc2ef739c8 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php @@ -0,0 +1,53 @@ + + */ +class TooWideMethodParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideMethodParameterOutTypeRule(new TooWideParameterOutTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/too-wide-method-parameter-out.php'], [ + [ + 'Method TooWideMethodParameterOut\Foo::doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 13, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\Foo::doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 21, + ], + [ + 'Method TooWideMethodParameterOut\Foo::doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 26, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\Foo::bug10699() never assigns 20 to &$out so it can be removed from the @param-out type.', + 37, + ], + ]); + } + + public function testBug10684(): void + { + $this->analyse([__DIR__ . '/data/bug-10684.php'], []); + } + + public function testBug10687(): void + { + $this->analyse([__DIR__ . '/data/bug-10687.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 0a6e138279..9e9588d219 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -48,6 +48,10 @@ public function testPrivate(): void 'Method TooWideMethodReturnType\Foo::dolor6() never returns null so it can be removed from the return type.', 86, ], + [ + 'Method TooWideMethodReturnType\ConditionalTypeClass::conditionalType() never returns string so it can be removed from the return type.', + 119, + ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php new file mode 100644 index 0000000000..1949d4d8b6 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php @@ -0,0 +1,41 @@ += 7.4 + +namespace Bug10684; + +abstract class HookBreaker extends Exception +{ + /** + * @return mixed + */ + public function getReturnValue() + { + return 1; + } +} + +class Foo +{ + /** @var \Closure(): void */ + protected \Closure $hook; + + /** + * @return mixed + */ + public function hook(HookBreaker &$brokenBy = null) + { + $brokenBy = null; + + $return = []; + if (mt_rand() === 0) { + try { + ($this->hook)(); + } catch (HookBreaker $e) { + $brokenBy = $e; + + return $e->getReturnValue(); + } + } + + return $return; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php new file mode 100644 index 0000000000..eb8f9c5f42 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php @@ -0,0 +1,20 @@ +analyse([__DIR__ . '/data/bug-10418.php'], []); } + public function testPassByReferenceIntoNotNullable(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/pass-by-reference-into-not-nullable.php'], [ + [ + 'Undefined variable: $three', + 32, + ], + ]); + } + + public function testBug10228(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-10228.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 960dad4d31..2c22dfd35a 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -422,7 +422,7 @@ public function testBug7776(): void $this->treatPhpDocTypesAsCertain = true; $this->strictUnnecessaryNullsafePropertyFetch = false; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-7776.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7776.php'], []); } public function testBug6008(): void @@ -463,7 +463,7 @@ public function testBug3985(): void $this->treatPhpDocTypesAsCertain = true; $this->strictUnnecessaryNullsafePropertyFetch = true; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-3985.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3985.php'], [ [ 'Variable $foo in isset() is never defined.', 13, diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 613770aead..4781fa1f18 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -368,4 +368,20 @@ public function testBug8084(): void $this->analyse([__DIR__ . '/data/bug-8084.php'], []); } + public function testBug10577(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-10577.php'], []); + } + + public function testBug10610(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-10610.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php new file mode 100644 index 0000000000..ed68b380b1 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -0,0 +1,67 @@ + + */ +class ParameterOutAssignedTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ParameterOutAssignedTypeRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, true, false), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parameter-out-assigned-type.php'], [ + [ + 'Parameter &$p @param-out type of function ParameterOutAssignedType\foo() expects int, string given.', + 10, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doFoo() expects int, string given.', + 21, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBar() expects string, int given.', + 29, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz() expects list, array<0|int<2, max>, int> given.', + 38, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz2() expects list, non-empty-list<\'str\'|int> given.', + 47, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, array, array, int>> given.', + 56, + ], + [ + 'Parameter &$p by-ref type of method ParameterOutAssignedType\Foo::doNoParamOut() expects string, int given.', + 61, + 'You can change the parameter out type with @param-out PHPDoc tag.', + ], + ]); + } + + public function testBug10699(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10699.php'], []); + } + + public function testBenevolentArrayKey(): void + { + $this->analyse([__DIR__ . '/data/benevolent-array-key.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php new file mode 100644 index 0000000000..26157f4ffb --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php @@ -0,0 +1,61 @@ + + */ +class ParameterOutExecutionEndTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ParameterOutExecutionEndTypeRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, true, false), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parameter-out-execution-end.php'], [ + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo2() expects string, string|null given.', + 21, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo2() expects string, string|null given.', + 23, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo3() expects string, string|null given.', + 34, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo4() expects string, string|null given.', + 47, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo6() expects int, string given.', + 69, + ], + [ + 'Parameter &$p @param-out type of function ParameterOutExecutionEnd\foo2() expects string, string|null given.', + 80, + ], + [ + 'Parameter &$p @param-out type of function ParameterOutExecutionEnd\foo2() expects string, string|null given.', + 82, + ], + ]); + } + + public function testBug11363(): void + { + $this->analyse([__DIR__ . '/data/bug-11363.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index f6897a5b9c..036d09c974 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -59,7 +59,7 @@ public function testBug4289(): void public function testBug5223(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-5223.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5223.php'], [ [ 'Cannot unset offset \'page\' on array{categoryKeys: array, tagNames: array}.', 20, @@ -88,7 +88,7 @@ public function testBug8113(): void public function testBug4565(): void { - $this->analyse([__DIR__ . '/../../Analyser/data/bug-4565.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4565.php'], []); } } diff --git a/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php b/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php new file mode 100644 index 0000000000..83be0c39f6 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php @@ -0,0 +1,53 @@ + $matches + * @param-out array> $matches + */ + public static function matchAllStrictGroups(array &$matches): int + { + $result = self::matchAll($matches); + + return $result; + } + + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAll(array &$matches): int + { + $matches = [['foo']]; + + return 1; + } +} + +class HelloWorld2 +{ + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAllStrictGroups(array &$matches): int + { + $result = self::matchAll($matches); + + return $result; + } + + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAll(array &$matches): int + { + $matches = [['foo']]; + + return 1; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10228.php b/tests/PHPStan/Rules/Variables/data/bug-10228.php new file mode 100644 index 0000000000..ef316285bc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10228.php @@ -0,0 +1,20 @@ + 'Test1', + '20' => 'Test2', + ]; + + + public function validate(string $value): void + { + $value = trim($value); + + if ($value === '') { + throw new \RuntimeException(); + } + + $value = self::MAP[$value] ?? $value; + + assertType("'Test1'|'Test2'", self::MAP[$value]); + + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10610.php b/tests/PHPStan/Rules/Variables/data/bug-10610.php new file mode 100644 index 0000000000..d56a2a8b07 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10610.php @@ -0,0 +1,87 @@ + [ + '19' => '582', + '26' => '689', + '56' => '817', + '52' => '1050', + '67' => '2923', + '78' => '4057', + '75' => '4078', + '54' => '4078', + '76' => '4079', + '77' => '4080', + '9' => '4080', + '46' => '4091', + '22' => '4111', + '48' => '4112', + '70' => '4113', + '42' => '4117', + '43' => '4118', + '6' => '4126', + '36' => '4129', + '13' => '4309', + '14' => '4904', + '5' => '5222', + '71' => '5223', + '73' => '5242', + '74' => '5250', + '24' => '5252', + '58' => '5255', + '35' => '5261', + '1' => '5264', + '20' => '5268', + '21' => '5269', + '31' => '5270', + '51' => '5271', + '55' => '5271', + '39' => '5274', + '50' => '5277', + '49' => '5278', + '11' => '5279', + '41' => '5279', + '44' => '5280', + '59' => '5281', + '60' => '5281', + '23' => '5281', + '72' => '5283', + '32' => '5283', + '8' => '5285', + '40' => '5285', + '12' => '5298', + '37' => '5305', + '65' => '5310', + '64' => '5310', + '57' => '5352', + '33' => '5364', + '25' => '5375', + '34' => '5460', + '45' => '7581', + '3' => '7624', + '53' => '7672', + '999' => '7953', + '69' => '7953', + '2' => '8206', + '7' => '9697', + ], + 'bar' => [ + '30' => 'Test3', + ], + ]; + + public function validate(string $k, string $value): void + { + $res = self::MAP[$k][$value] ?? ''; + + assertType("'1050'|'2923'|'4057'|'4078'|'4079'|'4080'|'4091'|'4111'|'4112'|'4113'|'4117'|'4118'|'4126'|'4129'|'4309'|'4904'|'5222'|'5223'|'5242'|'5250'|'5252'|'5255'|'5261'|'5264'|'5268'|'5269'|'5270'|'5271'|'5274'|'5277'|'5278'|'5279'|'5280'|'5281'|'5283'|'5285'|'5298'|'5305'|'5310'|'5352'|'5364'|'5375'|'5460'|'582'|'689'|'7581'|'7624'|'7672'|'7953'|'817'|'8206'|'9697'|'Test3'", self::MAP[$k][$value]); + + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-11363.php b/tests/PHPStan/Rules/Variables/data/bug-11363.php new file mode 100644 index 0000000000..72bf9b9968 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-11363.php @@ -0,0 +1,17 @@ +&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); unset($review['SurveyInvitation']['review']); - assertType("non-empty-array&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array)", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); } assertType('array', $review); if (array_key_exists('User', $review['Review'])) { @@ -42,7 +42,7 @@ function () { $review['User'] = $review['Review']['User']; assertType("hasOffsetValue('Review', array&hasOffset('User'))&hasOffsetValue('User', mixed)&non-empty-array", $review); unset($review['Review']['User']); - assertType("hasOffsetValue('Review', array)&hasOffsetValue('User', mixed)&non-empty-array", $review); + assertType("hasOffsetValue('Review', array)&hasOffsetValue('User', array)&non-empty-array", $review); } assertType("array&hasOffsetValue('Review', array)", $review); }; diff --git a/tests/PHPStan/Rules/Variables/data/parameter-out-assigned-type.php b/tests/PHPStan/Rules/Variables/data/parameter-out-assigned-type.php new file mode 100644 index 0000000000..f82a3267d8 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/parameter-out-assigned-type.php @@ -0,0 +1,69 @@ +doFoo($p); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz(&$p): void + { + unset($p[1]); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz2(&$p): void + { + $p[] = 'str'; + } + + /** + * @param list> $p + * @param-out list> $p + */ + function doBaz3(&$p): void + { + unset($p[1][2]); + } + + function doNoParamOut(string &$p): void + { + $p = 1; + } + + function doNoParamOut2(string &$p): void + { + $p = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php new file mode 100644 index 0000000000..6f55e987cc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php @@ -0,0 +1,106 @@ += 8.0 + +namespace PassByReferenceIntoNotNullable; + +class Foo +{ + + public function doFooNoType(&$test) + { + + } + + public function doFooMixedType(mixed &$test) + { + + } + + public function doFooIntType(int &$test) + { + + } + + public function doFooNullableType(?int &$test) + { + + } + + public function test() + { + $this->doFooNoType($one); + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} + +class FooPhpDocs +{ + + /** + * @param mixed $test + */ + public function doFooMixedType(&$test) + { + + } + + /** + * @param int $test + */ + public function doFooIntType(&$test) + { + + } + + /** + * @param int|null $test + */ + public function doFooNullableType(&$test) + { + + } + + public function test() + { + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} diff --git a/tests/PHPStan/Rules/WarningEmittingRuleTest.php b/tests/PHPStan/Rules/WarningEmittingRuleTest.php new file mode 100644 index 0000000000..6b0ac62cb8 --- /dev/null +++ b/tests/PHPStan/Rules/WarningEmittingRuleTest.php @@ -0,0 +1,53 @@ + + */ +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/Rules/data/empty-file.php b/tests/PHPStan/Rules/data/empty-file.php new file mode 100644 index 0000000000..60ac8c38d2 --- /dev/null +++ b/tests/PHPStan/Rules/data/empty-file.php @@ -0,0 +1,3 @@ +getByType(FileHelper::class); + yield [ __DIR__ . '/data/assert-certainty-missing-namespace.php', - 'Missing use statement for assertVariableCertainty() on line 8.', + sprintf( + 'Missing use statement for assertVariableCertainty() in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php'), + ), ]; yield [ __DIR__ . '/data/assert-native-type-missing-namespace.php', - 'Missing use statement for assertNativeType() on line 6.', + sprintf( + 'Missing use statement for assertNativeType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php'), + ), ]; yield [ __DIR__ . '/data/assert-type-missing-namespace.php', - 'Missing use statement for assertType() on line 6.', + sprintf( + 'Missing use statement for assertType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-type-missing-namespace.php'), + ), ]; yield [ __DIR__ . '/data/assert-certainty-wrong-namespace.php', - 'Function PHPStan\Testing\assertVariableCertainty imported with wrong namespace SomeWrong\Namespace\assertVariableCertainty called on line 9.', + sprintf( + 'Function PHPStan\Testing\assertVariableCertainty imported with wrong namespace SomeWrong\Namespace\assertVariableCertainty called in %s on line 9.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-certainty-wrong-namespace.php'), + ), ]; yield [ __DIR__ . '/data/assert-native-type-wrong-namespace.php', - 'Function PHPStan\Testing\assertNativeType imported with wrong namespace SomeWrong\Namespace\assertNativeType called on line 8.', + sprintf( + 'Function PHPStan\Testing\assertNativeType imported with wrong namespace SomeWrong\Namespace\assertNativeType called in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php'), + ), ]; yield [ __DIR__ . '/data/assert-type-wrong-namespace.php', - 'Function PHPStan\Testing\assertType imported with wrong namespace SomeWrong\Namespace\assertType called on line 8.', + sprintf( + 'Function PHPStan\Testing\assertType imported with wrong namespace SomeWrong\Namespace\assertType called in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-type-wrong-namespace.php'), + ), ]; yield [ __DIR__ . '/data/assert-certainty-case-insensitive.php', - 'Missing use statement for assertvariablecertainty() on line 8.', + sprintf( + 'Missing use statement for assertvariablecertainty() in %s on line 8.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php'), + ), ]; yield [ __DIR__ . '/data/assert-native-type-case-insensitive.php', - 'Missing use statement for assertNATIVEType() on line 6.', + sprintf( + 'Missing use statement for assertNATIVEType() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php'), + ), ]; yield [ __DIR__ . '/data/assert-type-case-insensitive.php', - 'Missing use statement for assertTYPe() on line 6.', + sprintf( + 'Missing use statement for assertTYPe() in %s on line 6.', + $fileHelper->normalizePath('tests/PHPStan/Testing/data/assert-type-case-insensitive.php'), + ), ]; } diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index ed3af63507..cd8ddce5bf 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -179,7 +179,7 @@ public function testDescribe( public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), diff --git a/tests/PHPStan/Type/BitwiseFlagHelperTest.php b/tests/PHPStan/Type/BitwiseFlagHelperTest.php index 49381b899b..f2136604c3 100644 --- a/tests/PHPStan/Type/BitwiseFlagHelperTest.php +++ b/tests/PHPStan/Type/BitwiseFlagHelperTest.php @@ -119,6 +119,8 @@ public function dataJsonExprContainsConst(): array /** * @dataProvider dataUnknownConstants * @dataProvider dataJsonExprContainsConst + * + * @param non-empty-string $constName */ public function testExprContainsConst(Expr $expr, string $constName, TrinaryLogic $expected): void { diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 1f6e0dc379..87d3bae274 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -50,6 +50,14 @@ public function dataIsSuperTypeOf(): array new CallableType([new NativeParameterReflection('foo', false, new MixedType(), PassedByReference::createNo(), false, null)], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new CallableType([ + new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection('bar', false, new StringType(), PassedByReference::createNo(), false, null), + ], new MixedType(), false), + new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), + TrinaryLogic::createMaybe(), + ], ]; } @@ -169,7 +177,7 @@ public function dataInferTemplateTypes(): array null, ); - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), @@ -338,6 +346,69 @@ public function dataAccepts(): array ]), TrinaryLogic::createYes(), ], + [ + new CallableType([ + new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection('bar', false, new StringType(), PassedByReference::createNo(), false, null), + ], new MixedType(), false), + new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], ]; } diff --git a/tests/PHPStan/Type/ClosureTypeTest.php b/tests/PHPStan/Type/ClosureTypeTest.php index f8d029048e..ccaaa4ca78 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -23,6 +23,11 @@ public function dataIsSuperTypeOf(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new ClosureType([], new MixedType(), false, null, null, null, [], [], []), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createMaybe(), + ], [ new ClosureType([], new UnionType([new IntegerType(), new StringType()]), false), new ClosureType([], new IntegerType(), false), diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 049139818e..727fd65d01 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -5,6 +5,8 @@ use Closure; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\ArrayType; use PHPStan\Type\CallableType; use PHPStan\Type\Generic\GenericClassStringType; @@ -12,8 +14,10 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -345,6 +349,63 @@ public function dataAccepts(): iterable ]), TrinaryLogic::createNo(), ]; + + yield [ + new ConstantArrayType([], []), + new NeverType(), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new NeverType(), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('test'), new StringType()), + ]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('test'), new StringType()), + ]), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new IntersectionType([ + new UnionType([new ArrayType(new MixedType(), new MixedType()), new IterableType(new MixedType(), new MixedType())]), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createMaybe(), + ]; } /** @@ -610,7 +671,7 @@ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, Trin public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 4acb72d280..976917737b 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -154,7 +154,13 @@ 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', (new ConstantStringType('0'))->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('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/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index 929ec087dc..1012234740 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -351,7 +351,7 @@ public function testAccepts( /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name, ?Type $bound = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $bound ?? new MixedType(), @@ -464,7 +464,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ /** @return array}> */ public function dataGetReferencedTypeArguments(): array { - $templateType = static fn (string $name, ?Type $bound = null): TemplateType => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $bound ?? new MixedType(), diff --git a/tests/PHPStan/Type/IterableTypeTest.php b/tests/PHPStan/Type/IterableTypeTest.php index 02a10d0dde..2094ebf589 100644 --- a/tests/PHPStan/Type/IterableTypeTest.php +++ b/tests/PHPStan/Type/IterableTypeTest.php @@ -185,7 +185,7 @@ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, Tri public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + $templateType = static fn ($name): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, new MixedType(), diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index 6ee710a8c1..5fe7d0c409 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -1059,6 +1059,53 @@ public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $ty ); } + public function dataSubstractedIsOffsetLegal(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new ObjectType(ArrayAccess::class), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectWithoutClassType(), + TrinaryLogic::createYes(), + ], + [ + new MixedType(), + new UnionType([ + new ObjectWithoutClassType(), + new StringType(), + ]), + TrinaryLogic::createYes(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsOffsetLegal + */ + public function testSubstractedIsOffsetLegal(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isOffsetAccessLegal(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isOffsetAccessLegal()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + public function dataSubtractedHasOffsetValueType(): array { return [ diff --git a/tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php b/tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php new file mode 100644 index 0000000000..7749b327f1 --- /dev/null +++ b/tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000000..9778b786e0 --- /dev/null +++ b/tests/PHPStan/Type/Php/data/curl-init-php-7.php @@ -0,0 +1,65 @@ + TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), $name, $bound, @@ -127,7 +127,7 @@ public function testAccepts( public function dataIsSuperTypeOf(): array { - $templateType = static fn (string $name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), $name, $bound, @@ -315,13 +315,12 @@ public function testIsSuperTypeOf( /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static fn (string $name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( + $templateType = static fn ($name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( TemplateTypeScope::createWithFunction($functionName ?? '_'), $name, $bound, TemplateTypeVariance::createInvariant(), ); - return [ 'simple' => [ new IntegerType(), diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 76b0074909..e521c3edba 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -15,7 +15,9 @@ use Iterator; use ObjectShapesAcceptance\ClassWithFooIntProperty; use PHPStan\Fixture\FinalClass; +use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -976,7 +978,7 @@ public function dataUnion(): iterable ]), ], IntersectionType::class, - 'array&hasOffset(\'foo\')', + 'array&hasOffsetValue(\'foo\', mixed)', ], [ [ @@ -2522,6 +2524,65 @@ public function dataUnion(): iterable ObjectWithoutClassType::class, 'object', ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ClosureType::createPure(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), + ], + UnionType::class, + '(Closure(): mixed)|(pure-Closure)', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + ClosureType::createPure(), + ], + ClosureType::class, + 'Closure(): mixed', + ]; + yield [ + [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantStringType('thing'), new ConstantStringType('bla')), + ]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('thing')), + ]), + ], + IntersectionType::class, + 'array&hasOffsetValue(\'thing\', mixed)', + ]; } /** @@ -4162,6 +4223,100 @@ public function dataIntersect(): iterable IntersectionType::class, 'array{a?: true, c?: true}&non-empty-array', ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ClosureType::createPure(), + ], + ClosureType::class, + 'pure-Closure', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + ClosureType::createPure(), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + ClosureType::createPure(), + ], + ClosureType::class, + 'pure-Closure', + ]; + + $xy = new ConstantArrayType([ + new ConstantIntegerType(0), + ], [ + new ConstantStringType('xy'), + ]); + $abxy = new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('ab'), + new ConstantStringType('xy'), + ], [2], [1]); + + yield [ + [ + new UnionType([ + new ConstantArrayType([], []), + $xy, + $abxy, + ]), + new UnionType([ + $xy, + $abxy, + ]), + ], + UnionType::class, + "array{'xy'}|array{0: 'ab', 1?: 'xy'}", + ]; + + yield [ + [ + new ConstantArrayType([], []), + new UnionType([ + $xy, + $abxy, + ]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ + [ + new ConstantArrayType([], []), + $abxy, + ], + NeverType::class, + '*NEVER*=implicit', + ]; } /** diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index 3e09804c39..cd0ee180cc 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -717,11 +717,13 @@ public function dataDescribe(): array new UnionType([new IntegerType(), new StringType()]), 'int|string', 'int|string', + 'int|string', ], [ new UnionType([new IntegerType(), new StringType(), new NullType()]), 'int|string|null', 'int|string|null', + 'int|string|null', ], [ new UnionType([ @@ -742,6 +744,7 @@ public function dataDescribe(): array new ConstantStringType('1'), ]), "1|2|2.2|10|'1'|'10'|'10aaa'|'11aaa'|'1aaa'|'2'|'2aaa'|'foo'|stdClass|true|null", + "1|2|2.2|10|'1'|'10'|'10aaa'|'11aaa'|'1aaa'|'2'|'2aaa'|'foo'|stdClass|true|null", 'float|int|stdClass|string|true|null', ], [ @@ -763,6 +766,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', 'array|string', ], [ @@ -784,6 +788,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', + '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', 'array|string', ], [ @@ -805,6 +810,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', + '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', 'array|string', ], [ @@ -825,6 +831,7 @@ public function dataDescribe(): array ]), ), 'array{int, bool, float}|array{string}', + 'array{int, bool, float}|array{string}', 'array', ], [ @@ -837,6 +844,7 @@ public function dataDescribe(): array ]), ), 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -848,6 +856,7 @@ public function dataDescribe(): array ]), ), 'int|numeric-string', + 'int|numeric-string', 'int|string', ], [ @@ -857,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>', ], [ TypeCombinator::union( @@ -868,6 +878,7 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo of int (class foo, parameter)|null', '(TFoo of int)|null', '(TFoo of int)|null', ], @@ -881,6 +892,7 @@ public function dataDescribe(): array ), new GenericClassStringType(new ObjectType('Abc')), ), + 'class-string|TFoo of int (class foo, parameter)', 'class-string|TFoo of int', 'class-string|TFoo of int', ], @@ -894,6 +906,7 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo (class foo, parameter)|null', 'TFoo|null', 'TFoo|null', ], @@ -912,9 +925,16 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo of TBar (class foo, parameter) (class foo, parameter)|null', '(TFoo of TBar)|null', '(TFoo of TBar)|null', ], + [ + new UnionType([new ObjectType('Foo'), new ObjectType('Foo')]), + 'Foo#1|Foo#2', + 'Foo', + 'Foo', + ], ]; } @@ -923,10 +943,12 @@ public function dataDescribe(): array */ public function testDescribe( Type $type, + string $expectedPreciseDescription, string $expectedValueDescription, string $expectedTypeOnlyDescription, ): void { + $this->assertSame($expectedPreciseDescription, $type->describe(VerbosityLevel::precise())); $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::value())); $this->assertSame($expectedTypeOnlyDescription, $type->describe(VerbosityLevel::typeOnly())); } diff --git a/tests/e2e/ResultCacheEndToEndTest.php b/tests/e2e/ResultCacheEndToEndTest.php index 3ba8efae97..1117af27b0 100644 --- a/tests/e2e/ResultCacheEndToEndTest.php +++ b/tests/e2e/ResultCacheEndToEndTest.php @@ -8,7 +8,6 @@ use PHPStan\File\FileReader; use PHPStan\File\SimpleRelativePathHelper; use PHPUnit\Framework\TestCase; -use function array_map; use function chdir; use function escapeshellarg; use function exec; @@ -93,6 +92,7 @@ public function testResultCache(): void 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']); @@ -117,6 +117,7 @@ public function testResultCacheDeleteFile(): void $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)); @@ -151,6 +152,7 @@ private function runPhpstan(int $expectedExitCode, string $phpstanConfigPath = _ try { $json = Json::decode($output, Json::FORCE_ARRAY); + $this->assertIsArray($json); } catch (JsonException $e) { $this->fail(sprintf('%s: %s', $e->getMessage(), $output)); } @@ -164,13 +166,22 @@ private function runPhpstan(int $expectedExitCode, string $phpstanConfigPath = _ /** * @param mixed[] $resultCache - * @return mixed[] + * @return array> */ private function transformResultCache(array $resultCache): array { $new = []; + $this->assertIsArray($resultCache['dependencies']); foreach ($resultCache['dependencies'] as $file => $data) { - $files = array_map(fn (string $file): string => $this->relativizePath($file), $data['dependentFiles']); + $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; }