diff --git a/.gitattributes b/.gitattributes index ed98b8a4c8a..ba520f3418f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ *.stub linguist-language=PHP tests/PHPStan/Command/ErrorFormatter/data/WindowsNewlines.php eol=crlf + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index f928b68c51f..cb5ff5f0fa4 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" paths: - 'src/**' - '.github/workflows/backward-compatibility.yml' diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 4a2a72ab9c1..d368a34dd17 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -8,7 +8,7 @@ on: - 'tests/bench/**' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'src/**' - '.github/workflows/bench.yml' diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml index 899c5b9b091..f634d4e9752 100644 --- a/.github/workflows/build-issue-bot.yml +++ b/.github/workflows/build-issue-bot.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/build-issue-bot.yml' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'issue-bot/**' - '.github/workflows/build-issue-bot.yml' diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml index 19e406007f9..2dd24c145b5 100644 --- a/.github/workflows/changelog-generator.yml +++ b/.github/workflows/changelog-generator.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/changelog-generator.yml' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'changelog-generator/**' - '.github/workflows/changelog-generator.yml' diff --git a/.github/workflows/claude-update-config-parameters-docs-on-change.yml b/.github/workflows/claude-update-config-parameters-docs-on-change.yml new file mode 100644 index 00000000000..1d1ec046e39 --- /dev/null +++ b/.github/workflows/claude-update-config-parameters-docs-on-change.yml @@ -0,0 +1,26 @@ +name: "Claude Update Config Parameters Docs On Change" + +on: + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/claude-update-config-parameters-docs-on-change.yml' + - 'conf/parametersSchema.neon' + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - name: Trigger Claude Update Config Parameters Docs + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-update-config-parameters-docs.yml --repo phpstan-bot/phpstan-src diff --git a/.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml b/.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml new file mode 100644 index 00000000000..63d7f87e088 --- /dev/null +++ b/.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml @@ -0,0 +1,26 @@ +name: "Claude Update PHPDoc Tags Docs On Change" + +on: + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml' + - 'src/PhpDoc/PhpDocNodeResolver.php' + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - name: Trigger Claude Update PHPDoc Tags Docs + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-update-phpdoc-tags-docs.yml --repo phpstan-bot/phpstan-src diff --git a/.github/workflows/claude-update-phpdoc-types-docs-on-change.yml b/.github/workflows/claude-update-phpdoc-types-docs-on-change.yml new file mode 100644 index 00000000000..f7f623896de --- /dev/null +++ b/.github/workflows/claude-update-phpdoc-types-docs-on-change.yml @@ -0,0 +1,26 @@ +name: "Claude Update PHPDoc Types Docs On Change" + +on: + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/claude-update-phpdoc-types-docs-on-change.yml' + - 'src/PhpDoc/TypeNodeResolver.php' + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - name: Trigger Claude Update PHPDoc Types Docs + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-update-phpdoc-types-docs.yml --repo phpstan-bot/phpstan-src diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9a44de55e92..c174980ac98 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' @@ -267,6 +267,12 @@ jobs: cd e2e/bug-11857 composer install ../../bin/phpstan + - script: | + cd e2e/in-trait + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") + ../bashunit -a contains 'FooTrait.php:10:Strict comparison using === between int<0, max> and false will always evaluate to false.' "$OUTPUT" + ../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Bar):18:Strict comparison using === between E2EInTrait\Bar and null will always evaluate to false.' "$OUTPUT" + ../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Foo):18:Strict comparison using === between E2EInTrait\Foo and null will always evaluate to false.' "$OUTPUT" - script: | cd e2e/result-cache-meta-extension composer install diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml index 5dd4964d5ee..94b083a9da5 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-workflows.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" permissions: {} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0dcd57fc111..a48cc8087d4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" concurrency: group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index abc8cc94166..98132502013 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -6,9 +6,9 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" tags: - - '2.1.*' + - '2.2.*' concurrency: group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests @@ -95,14 +95,14 @@ jobs: - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 env: - COMPOSER_ROOT_VERSION: "2.1.x-dev" + COMPOSER_ROOT_VERSION: "2.2.x-dev" - name: "Compile PHAR for checksum" working-directory: "compiler/build" run: "php ../box/vendor/bin/box compile --no-parallel --sort-compiled-files" env: PHAR_CHECKSUM: "1" - COMPOSER_ROOT_VERSION: "2.1.x-dev" + COMPOSER_ROOT_VERSION: "2.2.x-dev" - name: "Re-sign PHAR" run: "php compiler/build/resign.php tmp/phpstan.phar" @@ -134,25 +134,25 @@ jobs: integration-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} other-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} download-base-sha-phar: @@ -298,7 +298,7 @@ jobs: commit: name: "Commit PHAR" - if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.1.x' || startsWith(github.ref, 'refs/tags/'))" + if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.2.x' || startsWith(github.ref, 'refs/tags/'))" needs: compiler-tests runs-on: "ubuntu-latest" timeout-minutes: 60 @@ -325,7 +325,7 @@ jobs: repository: phpstan/phpstan path: phpstan-dist token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 2.1.x + ref: 2.2.x - name: "Get previous pushed dist commit" id: previous-commit diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index faa9329cc6c..4ca96ccde2f 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index a8c84e09814..442cd7347e5 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" permissions: contents: read diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 8162d58468e..2f79a0d3377 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -9,7 +9,7 @@ on: - 'apigen/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bdd01803733..ff3083cd87b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 59bcf6e7156..e249b3ce3e6 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -12,24 +12,12 @@ parameters: count: 1 path: ../src/Type/ClosureTypeFactory.php - - - message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php - - message: '#^Strict comparison using \=\=\= between int\<0, max\> and false will always evaluate to false\.$#' identifier: identical.alwaysFalse count: 1 path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - - message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' identifier: identical.alwaysFalse diff --git a/conf/config.neon b/conf/config.neon index cb8c20ad481..67a5301b571 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -82,6 +82,7 @@ parameters: reportWrongPhpDocTypeInVarTag: false reportAnyTypeWideningInVarTag: false reportNonIntStringArrayKey: false + reportUnsafeArrayStringKeyCasting: null reportPossiblyNonexistentGeneralArrayOffset: false reportPossiblyNonexistentConstantArrayOffset: false checkMissingOverrideMethodAttribute: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index bc79fe7c401..300ea5ac343 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -91,6 +91,7 @@ parametersSchema: reportWrongPhpDocTypeInVarTag: bool() reportAnyTypeWideningInVarTag: bool() reportNonIntStringArrayKey: bool() + reportUnsafeArrayStringKeyCasting: schema(string(), pattern('detect|prevent'), nullable()) reportPossiblyNonexistentGeneralArrayOffset: bool() reportPossiblyNonexistentConstantArrayOffset: bool() checkMissingOverrideMethodAttribute: bool() diff --git a/e2e/in-trait/phpstan.neon b/e2e/in-trait/phpstan.neon new file mode 100644 index 00000000000..c308dcf5421 --- /dev/null +++ b/e2e/in-trait/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/in-trait/src/Bar.php b/e2e/in-trait/src/Bar.php new file mode 100644 index 00000000000..a5a07b82067 --- /dev/null +++ b/e2e/in-trait/src/Bar.php @@ -0,0 +1,20 @@ +getSth() === null) { + + } + + if ($this->getSth2() === null) { + + } + } + +} diff --git a/issue-bot/playground.neon b/issue-bot/playground.neon index a252e3bac87..9fc2864ff3e 100644 --- a/issue-bot/playground.neon +++ b/issue-bot/playground.neon @@ -1,5 +1,7 @@ rules: + - PHPStan\Rules\Playground\ArrayDimCastRule - PHPStan\Rules\Playground\FunctionNeverRule + - PHPStan\Rules\Playground\LiteralArrayKeyCastRule - PHPStan\Rules\Playground\MethodNeverRule - PHPStan\Rules\Playground\NotAnalysedTraitRule - PHPStan\Rules\Playground\NoPhpCodeRule diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 15b47ac80e7..b2291373095 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -194,6 +194,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'checkTooWideReturnTypesInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, 'checkTooWideParameterOutInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, 'checkTooWideThrowTypesInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, + 'reportUnsafeArrayStringKeyCasting' => $options['reportUnsafeArrayStringKeyCasting'] ?? null, ]; $parameters['exceptions'] = [ 'implicitThrows' => $options['implicitThrows'] ?? true, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bb51fac85b5..b5a82f18ec0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -771,6 +771,12 @@ parameters: count: 1 path: src/Type/Accessory/AccessoryArrayListType.php + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryDecimalIntegerStringType.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType @@ -1710,7 +1716,7 @@ parameters: - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 5 + count: 6 path: src/Type/TypeCombinator.php - diff --git a/resources/constantToFunctionParameterMap.php b/resources/constantToFunctionParameterMap.php new file mode 100644 index 00000000000..6766e6be3f7 --- /dev/null +++ b/resources/constantToFunctionParameterMap.php @@ -0,0 +1,2546 @@ + 'single' | 'bitmask' + * 'constants' => list of constant names valid for this parameter + * 'exclusiveGroups' => (optional, bitmask only) groups of constants that are mutually exclusive + */ +return [ + + // ———————————————————————————————————————————— + // JSON + // ———————————————————————————————————————————— + + 'json_encode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_HEX_QUOT', + 'JSON_HEX_TAG', + 'JSON_HEX_AMP', + 'JSON_HEX_APOS', + 'JSON_NUMERIC_CHECK', + 'JSON_PRETTY_PRINT', + 'JSON_UNESCAPED_SLASHES', + 'JSON_FORCE_OBJECT', + 'JSON_PRESERVE_ZERO_FRACTION', + 'JSON_UNESCAPED_UNICODE', + 'JSON_PARTIAL_OUTPUT_ON_ERROR', + 'JSON_UNESCAPED_LINE_TERMINATORS', + 'JSON_THROW_ON_ERROR', + 'JSON_INVALID_UTF8_IGNORE', + 'JSON_INVALID_UTF8_SUBSTITUTE', + ], + ], + ], + + 'json_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_BIGINT_AS_STRING', + 'JSON_OBJECT_AS_ARRAY', + 'JSON_THROW_ON_ERROR', + 'JSON_INVALID_UTF8_IGNORE', + 'JSON_INVALID_UTF8_SUBSTITUTE', + ], + ], + ], + + 'json_validate' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_INVALID_UTF8_IGNORE', + ], + ], + ], + + // ———————————————————————————————————————————— + // PCRE + // ———————————————————————————————————————————— + + 'preg_match' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_OFFSET_CAPTURE', + 'PREG_UNMATCHED_AS_NULL', + ], + ], + ], + + 'preg_match_all' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_PATTERN_ORDER', + 'PREG_SET_ORDER', + 'PREG_OFFSET_CAPTURE', + 'PREG_UNMATCHED_AS_NULL', + ], + 'exclusiveGroups' => [ + ['PREG_PATTERN_ORDER', 'PREG_SET_ORDER'], + ], + ], + ], + + 'preg_split' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_SPLIT_NO_EMPTY', + 'PREG_SPLIT_DELIM_CAPTURE', + 'PREG_SPLIT_OFFSET_CAPTURE', + ], + ], + ], + + 'preg_grep' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'PREG_GREP_INVERT', + ], + ], + ], + + // ———————————————————————————————————————————— + // Sorting + // ———————————————————————————————————————————— + + 'sort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'rsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'asort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'arsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'ksort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'krsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'array_unique' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + ], + ], + ], + + // ———————————————————————————————————————————— + // Array functions + // ———————————————————————————————————————————— + + 'array_change_key_case' => [ + 'case' => [ + 'type' => 'single', + 'constants' => [ + 'CASE_LOWER', + 'CASE_UPPER', + ], + ], + ], + + 'array_filter' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'ARRAY_FILTER_USE_KEY', + 'ARRAY_FILTER_USE_BOTH', + ], + ], + ], + + 'count' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'COUNT_NORMAL', + 'COUNT_RECURSIVE', + ], + ], + ], + + 'extract' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'EXTR_OVERWRITE', + 'EXTR_SKIP', + 'EXTR_PREFIX_SAME', + 'EXTR_PREFIX_ALL', + 'EXTR_PREFIX_INVALID', + 'EXTR_IF_EXISTS', + 'EXTR_PREFIX_IF_EXISTS', + 'EXTR_REFS', + ], + 'exclusiveGroups' => [ + ['EXTR_OVERWRITE', 'EXTR_SKIP', 'EXTR_PREFIX_SAME', 'EXTR_PREFIX_ALL', 'EXTR_PREFIX_INVALID', 'EXTR_IF_EXISTS', 'EXTR_PREFIX_IF_EXISTS'], + ], + ], + ], + + // ———————————————————————————————————————————— + // HTML entities + // ———————————————————————————————————————————— + + 'htmlspecialchars' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'htmlentities' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'html_entity_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'htmlspecialchars_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + // ———————————————————————————————————————————— + // URL / Path + // ———————————————————————————————————————————— + + 'parse_url' => [ + 'component' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_URL_SCHEME', + 'PHP_URL_HOST', + 'PHP_URL_PORT', + 'PHP_URL_USER', + 'PHP_URL_PASS', + 'PHP_URL_PATH', + 'PHP_URL_QUERY', + 'PHP_URL_FRAGMENT', + ], + ], + ], + + 'pathinfo' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PATHINFO_DIRNAME', + 'PATHINFO_BASENAME', + 'PATHINFO_EXTENSION', + 'PATHINFO_FILENAME', + 'PATHINFO_ALL', + ], + ], + ], + + 'http_build_query' => [ + 'encoding_type' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_QUERY_RFC1738', + 'PHP_QUERY_RFC3986', + ], + ], + ], + + // ———————————————————————————————————————————— + // File operations + // ———————————————————————————————————————————— + + 'file_put_contents' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILE_USE_INCLUDE_PATH', + 'FILE_APPEND', + 'LOCK_EX', + ], + ], + ], + + 'file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILE_USE_INCLUDE_PATH', + 'FILE_IGNORE_NEW_LINES', + 'FILE_SKIP_EMPTY_LINES', + 'FILE_NO_DEFAULT_CONTEXT', + ], + ], + ], + + 'flock' => [ + 'operation' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOCK_SH', + 'LOCK_EX', + 'LOCK_UN', + 'LOCK_NB', + ], + 'exclusiveGroups' => [ + ['LOCK_SH', 'LOCK_EX', 'LOCK_UN'], + ], + ], + ], + + 'glob' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'GLOB_MARK', + 'GLOB_NOSORT', + 'GLOB_NOCHECK', + 'GLOB_NOESCAPE', + 'GLOB_BRACE', + 'GLOB_ONLYDIR', + 'GLOB_ERR', + ], + ], + ], + + 'fnmatch' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FNM_NOESCAPE', + 'FNM_PATHNAME', + 'FNM_PERIOD', + 'FNM_CASEFOLD', + ], + ], + ], + + 'scandir' => [ + 'sorting_order' => [ + 'type' => 'single', + 'constants' => [ + 'SCANDIR_SORT_ASCENDING', + 'SCANDIR_SORT_DESCENDING', + 'SCANDIR_SORT_NONE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Math + // ———————————————————————————————————————————— + + 'round' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_ROUND_HALF_UP', + 'PHP_ROUND_HALF_DOWN', + 'PHP_ROUND_HALF_EVEN', + 'PHP_ROUND_HALF_ODD', + ], + ], + ], + + // ———————————————————————————————————————————— + // Random + // ———————————————————————————————————————————— + + 'srand' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MT_RAND_MT19937', + 'MT_RAND_PHP', + ], + ], + ], + + 'mt_srand' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MT_RAND_MT19937', + 'MT_RAND_PHP', + ], + ], + ], + + // ———————————————————————————————————————————— + // Filter + // ———————————————————————————————————————————— + + 'filter_var' => [ + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'FILTER_VALIDATE_INT', + 'FILTER_VALIDATE_BOOLEAN', + 'FILTER_VALIDATE_FLOAT', + 'FILTER_VALIDATE_REGEXP', + 'FILTER_VALIDATE_DOMAIN', + 'FILTER_VALIDATE_URL', + 'FILTER_VALIDATE_EMAIL', + 'FILTER_VALIDATE_IP', + 'FILTER_VALIDATE_MAC', + 'FILTER_SANITIZE_STRING', + 'FILTER_SANITIZE_STRIPPED', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_SPECIAL_CHARS', + 'FILTER_SANITIZE_FULL_SPECIAL_CHARS', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_URL', + 'FILTER_SANITIZE_NUMBER_INT', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_ADD_SLASHES', + 'FILTER_UNSAFE_RAW', + 'FILTER_DEFAULT', + 'FILTER_CALLBACK', + ], + ], + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILTER_REQUIRE_SCALAR', + 'FILTER_REQUIRE_ARRAY', + 'FILTER_FORCE_ARRAY', + 'FILTER_NULL_ON_FAILURE', + 'FILTER_FLAG_NONE', + 'FILTER_FLAG_ALLOW_OCTAL', + 'FILTER_FLAG_ALLOW_HEX', + 'FILTER_FLAG_STRIP_LOW', + 'FILTER_FLAG_STRIP_HIGH', + 'FILTER_FLAG_STRIP_BACKTICK', + 'FILTER_FLAG_ENCODE_LOW', + 'FILTER_FLAG_ENCODE_HIGH', + 'FILTER_FLAG_ENCODE_AMP', + 'FILTER_FLAG_NO_ENCODE_QUOTES', + 'FILTER_FLAG_EMPTY_STRING_NULL', + 'FILTER_FLAG_ALLOW_FRACTION', + 'FILTER_FLAG_ALLOW_THOUSAND', + 'FILTER_FLAG_ALLOW_SCIENTIFIC', + 'FILTER_FLAG_PATH_REQUIRED', + 'FILTER_FLAG_QUERY_REQUIRED', + 'FILTER_FLAG_IPV4', + 'FILTER_FLAG_IPV6', + 'FILTER_FLAG_NO_RES_RANGE', + 'FILTER_FLAG_NO_PRIV_RANGE', + 'FILTER_FLAG_GLOBAL_RANGE', + 'FILTER_FLAG_HOSTNAME', + 'FILTER_FLAG_EMAIL_UNICODE', + ], + ], + ], + + 'filter_input' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'INPUT_POST', + 'INPUT_GET', + 'INPUT_COOKIE', + 'INPUT_ENV', + 'INPUT_SERVER', + ], + ], + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'FILTER_VALIDATE_INT', + 'FILTER_VALIDATE_BOOLEAN', + 'FILTER_VALIDATE_FLOAT', + 'FILTER_VALIDATE_REGEXP', + 'FILTER_VALIDATE_DOMAIN', + 'FILTER_VALIDATE_URL', + 'FILTER_VALIDATE_EMAIL', + 'FILTER_VALIDATE_IP', + 'FILTER_VALIDATE_MAC', + 'FILTER_SANITIZE_STRING', + 'FILTER_SANITIZE_STRIPPED', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_SPECIAL_CHARS', + 'FILTER_SANITIZE_FULL_SPECIAL_CHARS', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_URL', + 'FILTER_SANITIZE_NUMBER_INT', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_ADD_SLASHES', + 'FILTER_UNSAFE_RAW', + 'FILTER_DEFAULT', + 'FILTER_CALLBACK', + ], + ], + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILTER_REQUIRE_SCALAR', + 'FILTER_REQUIRE_ARRAY', + 'FILTER_FORCE_ARRAY', + 'FILTER_NULL_ON_FAILURE', + 'FILTER_FLAG_NONE', + 'FILTER_FLAG_ALLOW_OCTAL', + 'FILTER_FLAG_ALLOW_HEX', + 'FILTER_FLAG_STRIP_LOW', + 'FILTER_FLAG_STRIP_HIGH', + 'FILTER_FLAG_STRIP_BACKTICK', + 'FILTER_FLAG_ENCODE_LOW', + 'FILTER_FLAG_ENCODE_HIGH', + 'FILTER_FLAG_ENCODE_AMP', + 'FILTER_FLAG_NO_ENCODE_QUOTES', + 'FILTER_FLAG_EMPTY_STRING_NULL', + 'FILTER_FLAG_ALLOW_FRACTION', + 'FILTER_FLAG_ALLOW_THOUSAND', + 'FILTER_FLAG_ALLOW_SCIENTIFIC', + 'FILTER_FLAG_PATH_REQUIRED', + 'FILTER_FLAG_QUERY_REQUIRED', + 'FILTER_FLAG_IPV4', + 'FILTER_FLAG_IPV6', + 'FILTER_FLAG_NO_RES_RANGE', + 'FILTER_FLAG_NO_PRIV_RANGE', + 'FILTER_FLAG_GLOBAL_RANGE', + 'FILTER_FLAG_HOSTNAME', + 'FILTER_FLAG_EMAIL_UNICODE', + ], + ], + ], + + 'filter_input_array' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'INPUT_POST', + 'INPUT_GET', + 'INPUT_COOKIE', + 'INPUT_ENV', + 'INPUT_SERVER', + ], + ], + ], + + // ———————————————————————————————————————————— + // Password hashing + // ———————————————————————————————————————————— + + 'password_hash' => [ + 'algo' => [ + 'type' => 'single', + 'constants' => [ + 'PASSWORD_DEFAULT', + 'PASSWORD_BCRYPT', + 'PASSWORD_ARGON2I', + 'PASSWORD_ARGON2ID', + ], + ], + ], + + 'password_needs_rehash' => [ + 'algo' => [ + 'type' => 'single', + 'constants' => [ + 'PASSWORD_DEFAULT', + 'PASSWORD_BCRYPT', + 'PASSWORD_ARGON2I', + 'PASSWORD_ARGON2ID', + ], + ], + ], + + // ———————————————————————————————————————————— + // Error handling + // ———————————————————————————————————————————— + + 'error_reporting' => [ + 'error_level' => [ + 'type' => 'bitmask', + 'constants' => [ + 'E_ALL', + 'E_ERROR', + 'E_WARNING', + 'E_PARSE', + 'E_NOTICE', + 'E_STRICT', + 'E_RECOVERABLE_ERROR', + 'E_DEPRECATED', + 'E_CORE_ERROR', + 'E_CORE_WARNING', + 'E_COMPILE_ERROR', + 'E_COMPILE_WARNING', + 'E_USER_ERROR', + 'E_USER_WARNING', + 'E_USER_NOTICE', + 'E_USER_DEPRECATED', + ], + ], + ], + + 'trigger_error' => [ + 'error_level' => [ + 'type' => 'single', + 'constants' => [ + 'E_USER_NOTICE', + 'E_USER_WARNING', + 'E_USER_ERROR', + 'E_USER_DEPRECATED', + ], + ], + ], + + 'user_error' => [ + 'error_level' => [ + 'type' => 'single', + 'constants' => [ + 'E_USER_NOTICE', + 'E_USER_WARNING', + 'E_USER_ERROR', + 'E_USER_DEPRECATED', + ], + ], + ], + + // ———————————————————————————————————————————— + // Multibyte string + // ———————————————————————————————————————————— + + 'mb_convert_case' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MB_CASE_UPPER', + 'MB_CASE_LOWER', + 'MB_CASE_TITLE', + 'MB_CASE_FOLD', + 'MB_CASE_UPPER_SIMPLE', + 'MB_CASE_LOWER_SIMPLE', + 'MB_CASE_TITLE_SIMPLE', + 'MB_CASE_FOLD_SIMPLE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Fileinfo + // ———————————————————————————————————————————— + + 'finfo_file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Debug + // ———————————————————————————————————————————— + + 'debug_backtrace' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DEBUG_BACKTRACE_PROVIDE_OBJECT', + 'DEBUG_BACKTRACE_IGNORE_ARGS', + ], + ], + ], + + 'debug_print_backtrace' => [ + 'options' => [ + 'type' => 'single', + 'constants' => [ + 'DEBUG_BACKTRACE_IGNORE_ARGS', + ], + ], + ], + + // ———————————————————————————————————————————— + // Tokenizer + // ———————————————————————————————————————————— + + 'token_get_all' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'TOKEN_PARSE', + ], + ], + ], + + // cURL constants are excluded from this map because the constant lists + // are large and grow with each PHP/libcurl release, making them impractical + // to maintain without false positives. + + + 'image_type_to_extension' => [ + 'image_type' => [ + 'type' => 'single', + 'constants' => [ + 'IMAGETYPE_GIF', + 'IMAGETYPE_JPEG', + 'IMAGETYPE_PNG', + 'IMAGETYPE_SWF', + 'IMAGETYPE_PSD', + 'IMAGETYPE_BMP', + 'IMAGETYPE_WBMP', + 'IMAGETYPE_XBM', + 'IMAGETYPE_TIFF_II', + 'IMAGETYPE_TIFF_MM', + 'IMAGETYPE_ICO', + 'IMAGETYPE_WEBP', + 'IMAGETYPE_AVIF', + 'IMAGETYPE_JPC', + 'IMAGETYPE_JP2', + 'IMAGETYPE_JPX', + 'IMAGETYPE_JB2', + 'IMAGETYPE_SWC', + 'IMAGETYPE_IFF', + ], + ], + ], + + 'image_type_to_mime_type' => [ + 'image_type' => [ + 'type' => 'single', + 'constants' => [ + 'IMAGETYPE_GIF', + 'IMAGETYPE_JPEG', + 'IMAGETYPE_PNG', + 'IMAGETYPE_SWF', + 'IMAGETYPE_PSD', + 'IMAGETYPE_BMP', + 'IMAGETYPE_WBMP', + 'IMAGETYPE_XBM', + 'IMAGETYPE_TIFF_II', + 'IMAGETYPE_TIFF_MM', + 'IMAGETYPE_ICO', + 'IMAGETYPE_WEBP', + 'IMAGETYPE_AVIF', + 'IMAGETYPE_JPC', + 'IMAGETYPE_JP2', + 'IMAGETYPE_JPX', + 'IMAGETYPE_JB2', + 'IMAGETYPE_SWC', + 'IMAGETYPE_IFF', + ], + ], + ], + + // ———————————————————————————————————————————— + // GD image functions + // ———————————————————————————————————————————— + + 'imagecropauto' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_CROP_DEFAULT', + 'IMG_CROP_TRANSPARENT', + 'IMG_CROP_BLACK', + 'IMG_CROP_WHITE', + 'IMG_CROP_SIDES', + 'IMG_CROP_THRESHOLD', + ], + ], + ], + + 'imagelayereffect' => [ + 'effect' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_EFFECT_REPLACE', + 'IMG_EFFECT_ALPHABLEND', + 'IMG_EFFECT_NORMAL', + 'IMG_EFFECT_OVERLAY', + 'IMG_EFFECT_MULTIPLY', + ], + ], + ], + + 'imageflip' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_FLIP_HORIZONTAL', + 'IMG_FLIP_VERTICAL', + 'IMG_FLIP_BOTH', + ], + ], + ], + + 'imagefilter' => [ + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_FILTER_NEGATE', + 'IMG_FILTER_GRAYSCALE', + 'IMG_FILTER_BRIGHTNESS', + 'IMG_FILTER_CONTRAST', + 'IMG_FILTER_COLORIZE', + 'IMG_FILTER_EDGEDETECT', + 'IMG_FILTER_GAUSSIAN_BLUR', + 'IMG_FILTER_SELECTIVE_BLUR', + 'IMG_FILTER_EMBOSS', + 'IMG_FILTER_MEAN_REMOVAL', + 'IMG_FILTER_SMOOTH', + 'IMG_FILTER_PIXELATE', + 'IMG_FILTER_SCATTER', + ], + ], + ], + + // ———————————————————————————————————————————— + // Iconv + // ———————————————————————————————————————————— + + 'iconv_mime_decode' => [ + 'mode' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ICONV_MIME_DECODE_STRICT', + 'ICONV_MIME_DECODE_CONTINUE_ON_ERROR', + ], + ], + ], + + 'iconv_mime_decode_headers' => [ + 'mode' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ICONV_MIME_DECODE_STRICT', + 'ICONV_MIME_DECODE_CONTINUE_ON_ERROR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Output buffering + // ———————————————————————————————————————————— + + 'ob_start' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PHP_OUTPUT_HANDLER_CLEANABLE', + 'PHP_OUTPUT_HANDLER_FLUSHABLE', + 'PHP_OUTPUT_HANDLER_REMOVABLE', + 'PHP_OUTPUT_HANDLER_STDFLAGS', + ], + ], + ], + + // ———————————————————————————————————————————— + // Streams + // ———————————————————————————————————————————— + + 'stream_socket_client' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_CLIENT_CONNECT', + 'STREAM_CLIENT_ASYNC_CONNECT', + 'STREAM_CLIENT_PERSISTENT', + ], + ], + ], + + 'stream_socket_server' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_SERVER_BIND', + 'STREAM_SERVER_LISTEN', + ], + ], + ], + + 'stream_socket_recvfrom' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_OOB', + 'STREAM_PEEK', + ], + ], + ], + + 'stream_socket_sendto' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_OOB', + ], + ], + ], + + 'stream_wrapper_register' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'STREAM_IS_URL', + ], + ], + ], + + 'stream_socket_shutdown' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'STREAM_SHUT_RD', + 'STREAM_SHUT_WR', + 'STREAM_SHUT_RDWR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Syslog + // ———————————————————————————————————————————— + + 'openlog' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOG_CONS', + 'LOG_NDELAY', + 'LOG_ODELAY', + 'LOG_NOWAIT', + 'LOG_PERROR', + 'LOG_PID', + ], + ], + 'facility' => [ + 'type' => 'single', + 'constants' => [ + 'LOG_AUTH', + 'LOG_AUTHPRIV', + 'LOG_CRON', + 'LOG_DAEMON', + 'LOG_KERN', + 'LOG_LOCAL0', + 'LOG_LOCAL1', + 'LOG_LOCAL2', + 'LOG_LOCAL3', + 'LOG_LOCAL4', + 'LOG_LOCAL5', + 'LOG_LOCAL6', + 'LOG_LOCAL7', + 'LOG_LPR', + 'LOG_MAIL', + 'LOG_NEWS', + 'LOG_SYSLOG', + 'LOG_USER', + 'LOG_UUCP', + ], + ], + ], + + 'syslog' => [ + 'priority' => [ + 'type' => 'single', + 'constants' => [ + 'LOG_EMERG', + 'LOG_ALERT', + 'LOG_CRIT', + 'LOG_ERR', + 'LOG_WARNING', + 'LOG_NOTICE', + 'LOG_INFO', + 'LOG_DEBUG', + ], + ], + ], + + // ———————————————————————————————————————————— + // Sockets + // ———————————————————————————————————————————— + + 'socket_recv' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_PEEK', + 'MSG_WAITALL', + 'MSG_DONTWAIT', + ], + ], + ], + + 'socket_recvfrom' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_PEEK', + 'MSG_WAITALL', + 'MSG_DONTWAIT', + ], + ], + ], + + 'socket_send' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_EOR', + 'MSG_EOF', + 'MSG_DONTROUTE', + ], + ], + ], + + 'socket_sendto' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_EOR', + 'MSG_EOF', + 'MSG_DONTROUTE', + ], + ], + ], + + // ———————————————————————————————————————————— + // DNS + // ———————————————————————————————————————————— + + 'dns_get_record' => [ + 'type' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DNS_ANY', + 'DNS_ALL', + 'DNS_A', + 'DNS_AAAA', + 'DNS_CNAME', + 'DNS_HINFO', + 'DNS_MX', + 'DNS_NS', + 'DNS_PTR', + 'DNS_SOA', + 'DNS_SRV', + 'DNS_TXT', + 'DNS_NAPTR', + 'DNS_A6', + 'DNS_CAA', + ], + ], + ], + + // ———————————————————————————————————————————— + // FTP + // ———————————————————————————————————————————— + + 'ftp_get' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_fget' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_put' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_fput' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + // ———————————————————————————————————————————— + // IMAP + // ———————————————————————————————————————————— + + 'imap_close' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'CL_EXPUNGE', + ], + ], + ], + + // ———————————————————————————————————————————— + // OpenSSL + // ———————————————————————————————————————————— + + 'openssl_pkcs7_verify' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + ], + + 'openssl_pkcs7_sign' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + ], + + 'openssl_pkcs7_encrypt' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + 'cipher_algo' => [ + 'type' => 'single', + 'constants' => [ + 'OPENSSL_CIPHER_RC2_40', + 'OPENSSL_CIPHER_RC2_128', + 'OPENSSL_CIPHER_RC2_64', + 'OPENSSL_CIPHER_DES', + 'OPENSSL_CIPHER_3DES', + 'OPENSSL_CIPHER_AES_128_CBC', + 'OPENSSL_CIPHER_AES_192_CBC', + 'OPENSSL_CIPHER_AES_256_CBC', + ], + ], + ], + + // ———————————————————————————————————————————— + // IDN + // ———————————————————————————————————————————— + + 'idn_to_ascii' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'IDNA_DEFAULT', + 'IDNA_ALLOW_UNASSIGNED', + 'IDNA_CHECK_BIDI', + 'IDNA_CHECK_CONTEXTJ', + 'IDNA_NONTRANSITIONAL_TO_ASCII', + 'IDNA_NONTRANSITIONAL_TO_UNICODE', + 'IDNA_USE_STD3_RULES', + ], + ], + 'variant' => [ + 'type' => 'single', + 'constants' => [ + 'INTL_IDNA_VARIANT_UTS46', + 'INTL_IDNA_VARIANT_2003', + ], + ], + ], + + 'idn_to_utf8' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'IDNA_DEFAULT', + 'IDNA_ALLOW_UNASSIGNED', + 'IDNA_CHECK_BIDI', + 'IDNA_CHECK_CONTEXTJ', + 'IDNA_NONTRANSITIONAL_TO_ASCII', + 'IDNA_NONTRANSITIONAL_TO_UNICODE', + 'IDNA_USE_STD3_RULES', + ], + ], + 'variant' => [ + 'type' => 'single', + 'constants' => [ + 'INTL_IDNA_VARIANT_UTS46', + 'INTL_IDNA_VARIANT_2003', + ], + ], + ], + + // ———————————————————————————————————————————— + // String functions + // ———————————————————————————————————————————— + + 'str_pad' => [ + 'pad_type' => [ + 'type' => 'single', + 'constants' => [ + 'STR_PAD_RIGHT', + 'STR_PAD_LEFT', + 'STR_PAD_BOTH', + ], + ], + ], + + // ———————————————————————————————————————————— + // File seeking + // ———————————————————————————————————————————— + + 'fseek' => [ + 'whence' => [ + 'type' => 'single', + 'constants' => [ + 'SEEK_SET', + 'SEEK_CUR', + 'SEEK_END', + ], + ], + ], + + // ———————————————————————————————————————————— + // INI parsing + // ———————————————————————————————————————————— + + 'parse_ini_file' => [ + 'scanner_mode' => [ + 'type' => 'single', + 'constants' => [ + 'INI_SCANNER_NORMAL', + 'INI_SCANNER_RAW', + 'INI_SCANNER_TYPED', + ], + ], + ], + + 'parse_ini_string' => [ + 'scanner_mode' => [ + 'type' => 'single', + 'constants' => [ + 'INI_SCANNER_NORMAL', + 'INI_SCANNER_RAW', + 'INI_SCANNER_TYPED', + ], + ], + ], + + // ———————————————————————————————————————————— + // Message queues + // ———————————————————————————————————————————— + + 'msg_receive' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_IPC_NOWAIT', + 'MSG_EXCEPT', + 'MSG_NOERROR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Locale + // ———————————————————————————————————————————— + + 'setlocale' => [ + 'category' => [ + 'type' => 'single', + 'constants' => [ + 'LC_CTYPE', + 'LC_NUMERIC', + 'LC_TIME', + 'LC_COLLATE', + 'LC_MONETARY', + 'LC_MESSAGES', + 'LC_ALL', + ], + ], + ], + + // ———————————————————————————————————————————— + // libxml (functions) + // ———————————————————————————————————————————— + + 'simplexml_load_file' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'simplexml_load_string' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + // ———————————————————————————————————————————— + // mysqli (functions) + // ———————————————————————————————————————————— + + 'mysqli_begin_transaction' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_START_READ_ONLY', + 'MYSQLI_TRANS_START_READ_WRITE', + 'MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_START_READ_ONLY', 'MYSQLI_TRANS_START_READ_WRITE'], + ], + ], + ], + + 'mysqli_commit' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + 'mysqli_rollback' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + // ———————————————————————————————————————————— + // Methods with global constants + // ———————————————————————————————————————————— + + // finfo methods (FILEINFO_* global constants) + + 'finfo::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::buffer' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::set_flags' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + // SplFileObject methods (global constants) + + 'SplFileObject::flock' => [ + 'operation' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOCK_SH', + 'LOCK_EX', + 'LOCK_UN', + 'LOCK_NB', + ], + 'exclusiveGroups' => [ + ['LOCK_SH', 'LOCK_EX', 'LOCK_UN'], + ], + ], + ], + + 'SplFileObject::fseek' => [ + 'whence' => [ + 'type' => 'single', + 'constants' => [ + 'SEEK_SET', + 'SEEK_CUR', + 'SEEK_END', + ], + ], + ], + + // DOMDocument methods (LIBXML_* global constants) + + 'DOMDocument::load' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'DOMDocument::loadXML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'DOMDocument::loadHTML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + 'LIBXML_HTML_NOIMPLIED', + 'LIBXML_HTML_NODEFDTD', + ], + ], + ], + + 'DOMDocument::loadHTMLFile' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + 'LIBXML_HTML_NOIMPLIED', + 'LIBXML_HTML_NODEFDTD', + ], + ], + ], + + 'DOMDocument::save' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOEMPTYTAG', + ], + ], + ], + + 'DOMDocument::saveXML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOEMPTYTAG', + ], + ], + ], + + 'DOMDocument::schemaValidate' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_SCHEMA_CREATE', + ], + ], + ], + + 'DOMDocument::schemaValidateSource' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_SCHEMA_CREATE', + ], + ], + ], + + // XMLReader methods (LIBXML_* global constants) + + 'XMLReader::open' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'XMLReader::XML' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + // mysqli methods (global constants) + + 'mysqli::begin_transaction' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_START_READ_ONLY', + 'MYSQLI_TRANS_START_READ_WRITE', + 'MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_START_READ_ONLY', 'MYSQLI_TRANS_START_READ_WRITE'], + ], + ], + ], + + 'mysqli::commit' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + 'mysqli::rollback' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + // Collator methods (class constants) + + 'Collator::sort' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::SORT_REGULAR', + 'Collator::SORT_STRING', + 'Collator::SORT_NUMERIC', + ], + ], + ], + + 'Collator::asort' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::SORT_REGULAR', + 'Collator::SORT_STRING', + 'Collator::SORT_NUMERIC', + ], + ], + ], + + 'Collator::setAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::FRENCH_COLLATION', + 'Collator::ALTERNATE_HANDLING', + 'Collator::CASE_FIRST', + 'Collator::CASE_LEVEL', + 'Collator::NORMALIZATION_MODE', + 'Collator::STRENGTH', + 'Collator::HIRAGANA_QUATERNARY_MODE', + 'Collator::NUMERIC_COLLATION', + ], + ], + ], + + 'Collator::getAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::FRENCH_COLLATION', + 'Collator::ALTERNATE_HANDLING', + 'Collator::CASE_FIRST', + 'Collator::CASE_LEVEL', + 'Collator::NORMALIZATION_MODE', + 'Collator::STRENGTH', + 'Collator::HIRAGANA_QUATERNARY_MODE', + 'Collator::NUMERIC_COLLATION', + ], + ], + ], + + // ———————————————————————————————————————————— + // Methods with class constants + // ———————————————————————————————————————————— + + // PDO::setAttribute/getAttribute are excluded because PDO drivers add + // their own attribute constants (PGSQL_ATTR_*, MYSQL_ATTR_*, etc.) + + // PDOStatement + + 'PDOStatement::fetch' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + 'cursorOrientation' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_ORI_NEXT', + 'PDO::FETCH_ORI_PRIOR', + 'PDO::FETCH_ORI_FIRST', + 'PDO::FETCH_ORI_LAST', + 'PDO::FETCH_ORI_ABS', + 'PDO::FETCH_ORI_REL', + ], + ], + ], + + 'PDOStatement::fetchAll' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + ], + + 'PDOStatement::setFetchMode' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + ], + + 'PDOStatement::bindColumn' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + 'PDOStatement::bindParam' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + 'PDOStatement::bindValue' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + // ZipArchive + + 'ZipArchive::open' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ZipArchive::CREATE', + 'ZipArchive::EXCL', + 'ZipArchive::CHECKCONS', + 'ZipArchive::OVERWRITE', + 'ZipArchive::RDONLY', + ], + ], + ], + + 'ZipArchive::setCompressionName' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::CM_DEFAULT', + 'ZipArchive::CM_STORE', + 'ZipArchive::CM_SHRINK', + 'ZipArchive::CM_REDUCE_1', + 'ZipArchive::CM_REDUCE_2', + 'ZipArchive::CM_REDUCE_3', + 'ZipArchive::CM_REDUCE_4', + 'ZipArchive::CM_IMPLODE', + 'ZipArchive::CM_DEFLATE', + 'ZipArchive::CM_DEFLATE64', + 'ZipArchive::CM_PKWARE_IMPLODE', + 'ZipArchive::CM_BZIP2', + 'ZipArchive::CM_LZMA', + 'ZipArchive::CM_LZMA2', + 'ZipArchive::CM_ZSTD', + 'ZipArchive::CM_XZ', + ], + ], + ], + + 'ZipArchive::setCompressionIndex' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::CM_DEFAULT', + 'ZipArchive::CM_STORE', + 'ZipArchive::CM_SHRINK', + 'ZipArchive::CM_REDUCE_1', + 'ZipArchive::CM_REDUCE_2', + 'ZipArchive::CM_REDUCE_3', + 'ZipArchive::CM_REDUCE_4', + 'ZipArchive::CM_IMPLODE', + 'ZipArchive::CM_DEFLATE', + 'ZipArchive::CM_DEFLATE64', + 'ZipArchive::CM_PKWARE_IMPLODE', + 'ZipArchive::CM_BZIP2', + 'ZipArchive::CM_LZMA', + 'ZipArchive::CM_LZMA2', + 'ZipArchive::CM_ZSTD', + 'ZipArchive::CM_XZ', + ], + ], + ], + + 'ZipArchive::setEncryptionName' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::EM_NONE', + 'ZipArchive::EM_TRAD_PKWARE', + 'ZipArchive::EM_AES_128', + 'ZipArchive::EM_AES_192', + 'ZipArchive::EM_AES_256', + ], + ], + ], + + 'ZipArchive::setEncryptionIndex' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::EM_NONE', + 'ZipArchive::EM_TRAD_PKWARE', + 'ZipArchive::EM_AES_128', + 'ZipArchive::EM_AES_192', + 'ZipArchive::EM_AES_256', + ], + ], + ], + + // IntlDateFormatter + + 'IntlDateFormatter::__construct' => [ + 'dateType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + 'timeType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + ], + + 'IntlDateFormatter::create' => [ + 'dateType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + 'timeType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + ], + + // NumberFormatter + + 'NumberFormatter::__construct' => [ + 'style' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PATTERN_DECIMAL', + 'NumberFormatter::DECIMAL', + 'NumberFormatter::CURRENCY', + 'NumberFormatter::PERCENT', + 'NumberFormatter::SCIENTIFIC', + 'NumberFormatter::SPELLOUT', + 'NumberFormatter::ORDINAL', + 'NumberFormatter::DURATION', + 'NumberFormatter::PATTERN_RULEBASED', + 'NumberFormatter::IGNORE', + 'NumberFormatter::CURRENCY_ACCOUNTING', + 'NumberFormatter::DEFAULT_STYLE', + ], + ], + ], + + 'NumberFormatter::create' => [ + 'style' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PATTERN_DECIMAL', + 'NumberFormatter::DECIMAL', + 'NumberFormatter::CURRENCY', + 'NumberFormatter::PERCENT', + 'NumberFormatter::SCIENTIFIC', + 'NumberFormatter::SPELLOUT', + 'NumberFormatter::ORDINAL', + 'NumberFormatter::DURATION', + 'NumberFormatter::PATTERN_RULEBASED', + 'NumberFormatter::IGNORE', + 'NumberFormatter::CURRENCY_ACCOUNTING', + 'NumberFormatter::DEFAULT_STYLE', + ], + ], + ], + + 'NumberFormatter::format' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::TYPE_DEFAULT', + 'NumberFormatter::TYPE_INT32', + 'NumberFormatter::TYPE_INT64', + 'NumberFormatter::TYPE_DOUBLE', + 'NumberFormatter::TYPE_CURRENCY', + ], + ], + ], + + 'NumberFormatter::setAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PARSE_INT_ONLY', + 'NumberFormatter::GROUPING_USED', + 'NumberFormatter::DECIMAL_ALWAYS_SHOWN', + 'NumberFormatter::MAX_INTEGER_DIGITS', + 'NumberFormatter::MIN_INTEGER_DIGITS', + 'NumberFormatter::INTEGER_DIGITS', + 'NumberFormatter::MAX_FRACTION_DIGITS', + 'NumberFormatter::MIN_FRACTION_DIGITS', + 'NumberFormatter::FRACTION_DIGITS', + 'NumberFormatter::MULTIPLIER', + 'NumberFormatter::GROUPING_SIZE', + 'NumberFormatter::ROUNDING_MODE', + 'NumberFormatter::ROUNDING_INCREMENT', + 'NumberFormatter::FORMAT_WIDTH', + 'NumberFormatter::PADDING_POSITION', + 'NumberFormatter::SECONDARY_GROUPING_SIZE', + 'NumberFormatter::SIGNIFICANT_DIGITS_USED', + 'NumberFormatter::MIN_SIGNIFICANT_DIGITS', + 'NumberFormatter::MAX_SIGNIFICANT_DIGITS', + 'NumberFormatter::LENIENT_PARSE', + ], + ], + ], + + 'NumberFormatter::getAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PARSE_INT_ONLY', + 'NumberFormatter::GROUPING_USED', + 'NumberFormatter::DECIMAL_ALWAYS_SHOWN', + 'NumberFormatter::MAX_INTEGER_DIGITS', + 'NumberFormatter::MIN_INTEGER_DIGITS', + 'NumberFormatter::INTEGER_DIGITS', + 'NumberFormatter::MAX_FRACTION_DIGITS', + 'NumberFormatter::MIN_FRACTION_DIGITS', + 'NumberFormatter::FRACTION_DIGITS', + 'NumberFormatter::MULTIPLIER', + 'NumberFormatter::GROUPING_SIZE', + 'NumberFormatter::ROUNDING_MODE', + 'NumberFormatter::ROUNDING_INCREMENT', + 'NumberFormatter::FORMAT_WIDTH', + 'NumberFormatter::PADDING_POSITION', + 'NumberFormatter::SECONDARY_GROUPING_SIZE', + 'NumberFormatter::SIGNIFICANT_DIGITS_USED', + 'NumberFormatter::MIN_SIGNIFICANT_DIGITS', + 'NumberFormatter::MAX_SIGNIFICANT_DIGITS', + 'NumberFormatter::LENIENT_PARSE', + ], + ], + ], + + 'NumberFormatter::setTextAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::POSITIVE_PREFIX', + 'NumberFormatter::POSITIVE_SUFFIX', + 'NumberFormatter::NEGATIVE_PREFIX', + 'NumberFormatter::NEGATIVE_SUFFIX', + 'NumberFormatter::PADDING_CHARACTER', + 'NumberFormatter::CURRENCY_CODE', + 'NumberFormatter::DEFAULT_RULESET', + 'NumberFormatter::PUBLIC_RULESETS', + ], + ], + ], + + 'NumberFormatter::getTextAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::POSITIVE_PREFIX', + 'NumberFormatter::POSITIVE_SUFFIX', + 'NumberFormatter::NEGATIVE_PREFIX', + 'NumberFormatter::NEGATIVE_SUFFIX', + 'NumberFormatter::PADDING_CHARACTER', + 'NumberFormatter::CURRENCY_CODE', + 'NumberFormatter::DEFAULT_RULESET', + 'NumberFormatter::PUBLIC_RULESETS', + ], + ], + ], + + // SplPriorityQueue + + 'SplPriorityQueue::setExtractFlags' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'SplPriorityQueue::EXTR_BOTH', + 'SplPriorityQueue::EXTR_PRIORITY', + 'SplPriorityQueue::EXTR_DATA', + ], + ], + ], + + // FilesystemIterator / GlobIterator / RecursiveDirectoryIterator + + 'FilesystemIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'FilesystemIterator::setFlags' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'GlobIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'RecursiveDirectoryIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + // RecursiveIteratorIterator + + 'RecursiveIteratorIterator::__construct' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'RecursiveIteratorIterator::LEAVES_ONLY', + 'RecursiveIteratorIterator::SELF_FIRST', + 'RecursiveIteratorIterator::CHILD_FIRST', + ], + ], + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'RecursiveIteratorIterator::CATCH_GET_CHILD', + ], + ], + ], + + // DatePeriod + + 'DatePeriod::__construct' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DatePeriod::EXCLUDE_START_DATE', + 'DatePeriod::INCLUDE_END_DATE', + ], + ], + ], +]; diff --git a/resources/functionMap.php b/resources/functionMap.php index 7134875187a..71866bb604e 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -9284,7 +9284,7 @@ 'session_save_path' => ['string|false', 'newname='=>'string'], 'session_set_cookie_params' => ['bool', 'lifetime'=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], 'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:string,secure?:bool,httponly?:bool,samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], -'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(string):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'], +'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(int):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'], 'session_set_save_handler\'1' => ['bool', 'sessionhandler'=>'SessionHandlerInterface', 'register_shutdown='=>'bool'], 'session_start' => ['bool', 'options='=>'array'], 'session_status' => ['PHP_SESSION_NONE|PHP_SESSION_DISABLED|PHP_SESSION_ACTIVE'], diff --git a/src/Analyser/CollectedDataEmitter.php b/src/Analyser/CollectedDataEmitter.php new file mode 100644 index 00000000000..1f5be5f4def --- /dev/null +++ b/src/Analyser/CollectedDataEmitter.php @@ -0,0 +1,42 @@ +emitCollectedData(MyCollector::class, ['some', 'data']); + * ``` + * + * @api + */ +interface CollectedDataEmitter +{ + + /** + * @template TCollector of Collector + * @param class-string $collectorType + * @param template-type $data + */ + public function emitCollectedData(string $collectorType, mixed $data): void; + +} diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 9af5f2b297d..107db06f08a 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -106,6 +106,28 @@ public function changeTraitFilePath(string $newFilePath): self ); } + public function removeTraitContext(): self + { + if ($this->traitFilePath === null) { + throw new ShouldNotHappenException(); + } + + return new self( + $this->message, + $this->traitFilePath, + $this->line, + $this->canBeIgnored, + $this->filePath, + null, + $this->tip, + $this->nodeLine, + $this->nodeType, + $this->identifier, + $this->metadata, + $this->fixedErrorDiff, + ); + } + public function getTraitFilePath(): ?string { return $this->traitFilePath; diff --git a/src/Analyser/FileAnalyserCallback.php b/src/Analyser/FileAnalyserCallback.php index 304513f7f4e..07e5371a04e 100644 --- a/src/Analyser/FileAnalyserCallback.php +++ b/src/Analyser/FileAnalyserCallback.php @@ -11,6 +11,7 @@ use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\RootExportedNode; +use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InTraitNode; use PHPStan\Parser\Parser; @@ -77,9 +78,14 @@ public function __construct( public function __invoke(Node $node, Scope $scope): void { + if ($node instanceof EmitCollectedDataNode) { + $this->fileCollectedData[$scope->getFile()][$node->getCollectorType()][] = $node->getData(); + return; + } + $parserNodes = $this->parserNodes; - /** @var Scope&NodeCallbackInvoker $scope */ + /** @var Scope&NodeCallbackInvoker&CollectedDataEmitter $scope */ if ($node instanceof Node\Stmt\Trait_) { foreach (array_keys($this->linesToIgnore[$this->file] ?? []) as $lineToIgnore) { if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 122340cc27b..ce2fc2c3ca2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -23,7 +23,9 @@ use PhpParser\Node\Stmt\Function_; use PhpParser\NodeFinder; use PHPStan\Analyser\Traverser\TransformStaticTypeTraverser; +use PHPStan\Collectors\Collector; use PHPStan\DependencyInjection\Container; +use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; @@ -133,7 +135,7 @@ use const PHP_INT_MIN; use const PHP_VERSION_ID; -class MutatingScope implements Scope, NodeCallbackInvoker +class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter { public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; @@ -142,10 +144,10 @@ class MutatingScope implements Scope, NodeCallbackInvoker /** @var Type[] */ private array $resolvedTypes = []; - /** @var array */ + /** @var array */ private array $truthyScopes = []; - /** @var array */ + /** @var array */ private array $falseyScopes = []; private ?self $fiberScope = null; @@ -3142,6 +3144,9 @@ public function filterByFalseyValue(Expr $expr): self return $scope; } + /** + * @return static + */ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self { $typeSpecifications = []; @@ -3279,6 +3284,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } + /** @var static */ return $scope->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), @@ -4750,4 +4756,20 @@ public function invokeNodeCallback(Node $node): void $nodeCallback($node, $this); } + /** + * @template TNodeType of Node + * @template TValue + * @param class-string> $collectorType + * @param TValue $data + */ + public function emitCollectedData(string $collectorType, mixed $data): void + { + $nodeCallback = $this->nodeCallback; + if ($nodeCallback === null) { + throw new ShouldNotHappenException('Node callback is not present in this scope'); + } + + $nodeCallback(new EmitCollectedDataNode($collectorType, $data), $this); + } + } diff --git a/src/Analyser/RuleErrorTransformer.php b/src/Analyser/RuleErrorTransformer.php index 12ad5043201..4213d4c6314 100644 --- a/src/Analyser/RuleErrorTransformer.php +++ b/src/Analyser/RuleErrorTransformer.php @@ -22,6 +22,7 @@ use PHPStan\Rules\MetadataRuleError; use PHPStan\Rules\NonIgnorableRuleError; use PHPStan\Rules\RuleError; +use PHPStan\Rules\RuleErrors\TransformedRuleError; use PHPStan\Rules\TipRuleError; use PHPStan\ShouldNotHappenException; use SebastianBergmann\Diff\Differ; @@ -55,6 +56,10 @@ public function transform( Node $node, ): Error { + if ($ruleError instanceof TransformedRuleError) { + return $ruleError->getError(); + } + $line = $node->getStartLine(); $canBeIgnored = true; $fileName = $scope->getFileDescription(); diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 6f616acd802..8e78a30d4c2 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -307,6 +307,8 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool; * if-branch of `if ($x instanceof Foo)`. * * Uses the TypeSpecifier internally to determine type narrowing. + * + * @return static */ public function filterByTruthyValue(Expr $expr): self; @@ -316,6 +318,8 @@ public function filterByTruthyValue(Expr $expr): self; * The opposite of filterByTruthyValue(). Given `$x instanceof Foo`, returns * a scope where $x is known NOT to be of type Foo. This is the scope used * in the else-branch of `if ($x instanceof Foo)`. + * + * @return static */ public function filterByFalseyValue(Expr $expr): self; diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index cac88d0e39b..6e8f0a385e7 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -202,6 +202,7 @@ public static function postInitializeContainer(Container $container): void $container->getService('typeSpecifier'); BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); + ReportUnsafeArrayStringKeyCastingToggle::setLevel($container->getParameter('reportUnsafeArrayStringKeyCasting')); } public function getCurrentWorkingDirectory(): string diff --git a/src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php b/src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php new file mode 100644 index 00000000000..e2f13563fec --- /dev/null +++ b/src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php @@ -0,0 +1,34 @@ +> $collectorType + * @param TValue $data + */ + public function __construct( + private string $collectorType, + private mixed $data, + ) + { + parent::__construct([]); + } + + /** + * @return class-string> + */ + public function getCollectorType(): string + { + return $this->collectorType; + } + + /** + * @return TValue + */ + public function getData(): mixed + { + return $this->data; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_EmitCollectedDataNode'; + } + + /** + * @return list + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 7a42ff77292..b96f2cbe093 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -10,7 +10,9 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; @@ -47,6 +49,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -105,6 +108,7 @@ use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\TypeAliasResolverProvider; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\ValueOfType; @@ -127,6 +131,9 @@ use function strtolower; use function substr; +/** + * @phpstan-import-type Level from ReportUnsafeArrayStringKeyCastingToggle as ReportUnsafeArrayStringKeyCastingLevel + */ #[AutowiredService] final class TypeNodeResolver { @@ -134,12 +141,17 @@ final class TypeNodeResolver /** @var array */ private array $genericTypeResolvingStack = []; + /** + * @param ReportUnsafeArrayStringKeyCastingLevel $reportUnsafeArrayStringKeyCasting + */ public function __construct( private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private TypeAliasResolverProvider $typeAliasResolverProvider, private ConstantResolver $constantResolver, private InitializerExprTypeResolver $initializerExprTypeResolver, + #[AutowiredParameter] + private ?string $reportUnsafeArrayStringKeyCasting, ) { } @@ -235,6 +247,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'string': return new StringType(); + case 'decimal-int-string': + return new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]); + + case 'non-decimal-int-string': + return new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + case 'lowercase-string': return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); @@ -652,7 +673,7 @@ private function resolveConditionalTypeForParameterNode(ConditionalTypeForParame private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type { $itemType = $this->resolve($typeNode->type, $nameScope); - return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $itemType); + return new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $itemType); } private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type @@ -677,9 +698,23 @@ static function (string $variance): TemplateTypeVariance { if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) { if (count($genericTypes) === 1) { // array - $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); + $arrayType = new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array - $keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([ + $originalKey = $genericTypes[0]; + if ($this->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + $originalKey = TypeTraverser::map($originalKey, static function (Type $type, callable $traverse) { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof StringType) { + return TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)); + } + + return $type; + }); + } + $keyType = TypeCombinator::intersect($originalKey->toArrayKey(), new UnionType([ new IntegerType(), new StringType(), ]))->toArrayKey(); diff --git a/src/Reflection/AllowedConstantsResult.php b/src/Reflection/AllowedConstantsResult.php new file mode 100644 index 00000000000..8dd9f315a4d --- /dev/null +++ b/src/Reflection/AllowedConstantsResult.php @@ -0,0 +1,55 @@ + $disallowedConstants + * @param list> $violatedExclusiveGroups + */ + public function __construct( + private array $disallowedConstants, + private array $violatedExclusiveGroups, + private bool $bitmaskNotAllowed, + ) + { + } + + public function isOk(): bool + { + return $this->disallowedConstants === [] && $this->violatedExclusiveGroups === [] && !$this->bitmaskNotAllowed; + } + + public function isBitmaskNotAllowed(): bool + { + return $this->bitmaskNotAllowed; + } + + /** + * @return list + */ + public function getDisallowedConstants(): array + { + return $this->disallowedConstants; + } + + /** + * @return list> + */ + public function getViolatedExclusiveGroups(): array + { + return $this->violatedExclusiveGroups; + } + +} diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index b01a6db6ff8..93941cf0698 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,7 +2,9 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -80,4 +82,14 @@ public function getAttributes(): array return []; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return null; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + return new AllowedConstantsResult([], [], false); + } + } diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index b514bfa1a93..c7d4bac0df4 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -444,6 +444,7 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn array_map(static fn (BetterReflectionAttribute $betterReflectionAttribute) => ReflectionAttributeFactory::create($betterReflectionAttribute), $constantReflection->getAttributes()), InitializerExprContext::fromGlobalConstant($constantReflection), ), + $constantReflection->isInternal(), ); } diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index 0cbe1eb1db2..1f643c28a33 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -20,6 +20,7 @@ public function __construct( private TrinaryLogic $isDeprecated, private ?string $deprecatedDescription, private array $attributes, + private bool $internal, ) { } @@ -29,6 +30,16 @@ public function getName(): string return $this->name; } + public function describe(): string + { + return $this->name; + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->internal); + } + public function getValueType(): Type { return $this->valueType; diff --git a/src/Reflection/ConstantReflection.php b/src/Reflection/ConstantReflection.php index 01aea117ea2..34fceef8467 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -20,6 +20,10 @@ interface ConstantReflection public function getName(): string; + public function describe(): string; + + public function isBuiltin(): TrinaryLogic; + public function getValueType(): Type; public function isDeprecated(): TrinaryLogic; diff --git a/src/Reflection/Dummy/DummyClassConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php index 768c5bdf275..8436abf76c0 100644 --- a/src/Reflection/Dummy/DummyClassConstantReflection.php +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -12,6 +12,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use stdClass; +use function sprintf; final class DummyClassConstantReflection implements ClassConstantReflection { @@ -62,6 +63,16 @@ public function getName(): string return $this->name; } + public function describe(): string + { + return sprintf('%s::%s', $this->getDeclaringClass()->getDisplayName(), $this->name); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getDeclaringClass()->isBuiltin()); + } + public function getValueType(): Type { return new MixedType(); diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index 890b0493469..1ccd1d4b935 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -29,4 +29,11 @@ public function getClosureThisType(): ?Type; */ public function getAttributes(): array; + public function getAllowedConstants(): ?ParameterAllowedConstants; + + /** + * @param list $constants Global and/or class constant reflections + */ + public function checkAllowedConstants(array $constants): AllowedConstantsResult; + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index c72afeedbb5..1ceb4d66669 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -108,6 +108,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc TrinaryLogic::createMaybe(), null, [], + null, ), $parameters), $parametersAcceptor->isVariadic(), $returnType, diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index 00e2ea1a99e..5539d9132a1 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -28,6 +30,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { } @@ -97,4 +100,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/ParameterAllowedConstants.php b/src/Reflection/ParameterAllowedConstants.php new file mode 100644 index 00000000000..c784e3c29af --- /dev/null +++ b/src/Reflection/ParameterAllowedConstants.php @@ -0,0 +1,98 @@ + $constants + * @param list> $exclusiveGroups + */ + public function __construct( + private string $type, + private array $constants, + private array $exclusiveGroups, + ) + { + } + + public function isBitmask(): bool + { + return $this->type === 'bitmask'; + } + + /** + * @return list> + */ + public function getExclusiveGroups(): array + { + return $this->exclusiveGroups; + } + + /** + * @param list $constants + */ + public function check(array $constants): AllowedConstantsResult + { + $bitmaskNotAllowed = !$this->isBitmask() && count($constants) > 1; + + $disallowed = []; + $names = []; + + foreach ($constants as $constant) { + if ($constant->isBuiltin()->no()) { + continue; + } + + $name = $constant->describe(); + $names[] = $name; + + if (in_array($name, $this->constants, true)) { + continue; + } + + $disallowed[] = $constant; + } + + $violated = []; + if ($this->isBitmask()) { + foreach ($this->exclusiveGroups as $group) { + $matched = []; + foreach ($names as $name) { + if (!in_array($name, $group, true)) { + continue; + } + + $matched[] = $name; + } + + if (count($matched) < 2) { + continue; + } + + $violated[] = $matched; + } + } + + return new AllowedConstantsResult($disallowed, $violated, $bitmaskNotAllowed); + } + +} diff --git a/src/Reflection/ParameterAllowedConstantsMapProvider.php b/src/Reflection/ParameterAllowedConstantsMapProvider.php new file mode 100644 index 00000000000..22c0a9fd31e --- /dev/null +++ b/src/Reflection/ParameterAllowedConstantsMapProvider.php @@ -0,0 +1,50 @@ +, exclusiveGroups?: list>}>>|null */ + private ?array $map = null; + + public function getForFunctionParameter(string $functionName, string $parameterName): ?ParameterAllowedConstants + { + return $this->get($functionName, $parameterName); + } + + public function getForMethodParameter(string $className, string $methodName, string $parameterName): ?ParameterAllowedConstants + { + return $this->get($className . '::' . $methodName, $parameterName); + } + + private function get(string $key, string $parameterName): ?ParameterAllowedConstants + { + $map = $this->getMap(); + + if (!isset($map[$key][$parameterName])) { + return null; + } + + /** @var array{type: 'single'|'bitmask', constants: list, exclusiveGroups?: list>} $config */ + $config = $map[$key][$parameterName]; + + return new ParameterAllowedConstants( + $config['type'], + $config['constants'], + $config['exclusiveGroups'] ?? [], + ); + } + + /** + * @return array, exclusiveGroups?: list>}>> + */ + private function getMap(): array + { + return $this->map ??= require __DIR__ . '/../../resources/constantToFunctionParameterMap.php'; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index b4c9b3a3821..608f3fb93d1 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -771,6 +771,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $parameter instanceof ExtendedParameterReflection ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(), $parameter instanceof ExtendedParameterReflection ? $parameter->getClosureThisType() : null, $parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [], + $parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null, ); continue; } @@ -830,6 +831,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $immediatelyInvokedCallable, $closureThisType, $attributes, + null, ); if ($isVariadic) { @@ -928,6 +930,7 @@ private static function wrapParameter(ParameterReflection $parameter): ExtendedP TrinaryLogic::createMaybe(), null, [], + null, ); } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index b15ec9401b9..41c278f08dd 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -98,6 +98,7 @@ public function getVariants(): array TrinaryLogic::createMaybe(), null, [], + null, ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index 731a848fe60..8303f1084f4 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -59,6 +59,7 @@ public function getVariants(): array TrinaryLogic::createNo(), null, [], + null, ), ], false, diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index 19a917e0a17..69a19ccbf3a 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -28,6 +30,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -68,4 +71,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index fccce2fa772..f5d2f09b717 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -31,6 +31,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\FunctionSignature; use PHPStan\Reflection\SignatureMap\ParameterSignature; @@ -100,6 +101,7 @@ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private FileTypeMapper $fileTypeMapper, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private bool $inferPrivatePropertyTypeFromConstructor, ) { @@ -723,7 +725,7 @@ private function createMethod( } } } - $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($declaringClassName, $methodReflection->getName(), $methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); } } @@ -973,6 +975,8 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla * @param array $closureThisParameters */ private function createNativeMethodVariant( + string $declaringClassName, + string $methodName, FunctionSignature $methodSignature, array $phpDocParameterTypes, ?Type $phpDocReturnType, @@ -1027,6 +1031,7 @@ private function createNativeMethodVariant( $immediatelyInvoked, $closureThisType, [], + $this->allowedConstantsMapProvider->getForMethodParameter($declaringClassName, $methodName, $parameterSignature->getName()), ); } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 7dbd7ca3c47..2dcb0c7b870 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\FunctionReflectionFactory; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; @@ -44,6 +45,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionFunction $reflection, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -127,6 +129,7 @@ private function getParameters(): array $immediatelyInvokedCallable, $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + $this->allowedConstantsMapProvider->getForFunctionParameter(strtolower($this->reflection->getName()), $reflection->getName()), ); }, $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 34f28635007..d24532340d4 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -19,6 +19,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodPrototypeReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; @@ -70,6 +71,7 @@ public function __construct( private ReflectionMethod $reflection, private ReflectionProvider $reflectionProvider, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -226,6 +228,7 @@ private function getParameters(): array $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(), $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null, $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + $this->allowedConstantsMapProvider->getForMethodParameter($this->declaringClass->getName(), $this->reflection->getName(), $reflection->getName()), ), $this->reflection->getParameters()); } @@ -411,6 +414,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->reflection, $this->reflectionProvider, $this->attributeReflectionFactory, + $this->allowedConstantsMapProvider, $this->templateTypeMap, $this->phpDocParameterTypes, $phpDocType, @@ -444,6 +448,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $this->reflection, $this->reflectionProvider, $this->attributeReflectionFactory, + $this->allowedConstantsMapProvider, $this->templateTypeMap, $phpDocParameterTypes, $this->phpDocReturnType, diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index f048ea71006..7061d7f63e9 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -113,4 +115,14 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return null; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + return new AllowedConstantsResult([], [], false); + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 8469f7bef44..17b55295159 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -3,11 +3,13 @@ namespace PHPStan\Reflection\Php; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -34,6 +36,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { } @@ -143,4 +146,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php index d0b69f5eedc..c565feb0811 100644 --- a/src/Reflection/RealClassClassConstantReflection.php +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -9,6 +9,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use function sprintf; final class RealClassClassConstantReflection implements ClassConstantReflection { @@ -39,6 +40,16 @@ public function getName(): string return $this->reflection->getName(); } + public function describe(): string + { + return sprintf('%s::%s', $this->declaringClass->getDisplayName(), $this->getName()); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->isBuiltin()); + } + public function getFileName(): ?string { return $this->declaringClass->getFileName(); diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index 21108d658ef..cd57c8db9d0 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -121,6 +121,7 @@ function (ExtendedParameterReflection $param): ExtendedParameterReflection { $param->isImmediatelyInvokedCallable(), $closureThisType, $param->getAttributes(), + $param->getAllowedConstants(), ); }, $this->parametersAcceptor->getParameters(), diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index a2f12f6daad..efb3b508db3 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeFunctionReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -41,6 +42,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, ) { } @@ -107,13 +109,14 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $acceptsNamedArguments = $phpDoc->acceptsNamedArguments(); } + $allowedConstantsMapProvider = $this->allowedConstantsMapProvider; $variantsByType = ['positional' => []]; foreach ($functionSignaturesResult as $signatureType => $functionSignatures) { foreach ($functionSignatures ?? [] as $functionSignature) { $variantsByType[$signatureType][] = new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): ExtendedNativeParameterReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc, $lowerCasedFunctionName, $allowedConstantsMapProvider): ExtendedNativeParameterReflection { $type = $parameterSignature->getType(); $phpDocType = null; @@ -144,6 +147,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $immediatelyInvokedCallable, $closureThisType, [], + $allowedConstantsMapProvider->getForFunctionParameter($lowerCasedFunctionName, $parameterSignature->getName()), ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 1f183844d53..026d3c36fb2 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -118,6 +118,7 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection { $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, $parameter->getAttributes(), + $parameter->getAllowedConstants(), ); }, $acceptor->getParameters(), diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index 68d3200bec0..8198ea1f954 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -105,6 +105,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $acceptor->getParameters(), ), diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index d028d80d04b..7711a799580 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -77,6 +77,7 @@ public function getVariants(): array TrinaryLogic::createMaybe(), null, [], + null, ), $variant->getParameters()), $variant->isVariadic(), $variant->getReturnType(), diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 36b3c6faa61..ee47292ed5c 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -5,6 +5,8 @@ use Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\New_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -36,7 +38,7 @@ public function __construct( * @return list */ public function check( - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, array $attrGroups, int $requiredTarget, string $targetName, @@ -157,6 +159,10 @@ public function check( 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', '%s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', + 'Constants %s cannot be combined for %s of attribute class ' . $attributeClassName . ' constructor.', + 'Combining constants with | is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', + null, ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 197987ddf1c..9ceea3ce4f1 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; @@ -30,7 +32,7 @@ public function getNodeType(): string return InClassNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $classReflection = $node->getClassReflection(); diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index 3beaf3d0ea3..a6c0506443f 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; @@ -25,7 +27,7 @@ public function getNodeType(): string return Node\Stmt\ClassConst::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index 2adbba15a26..8dbee61149e 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -3,10 +3,14 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; +use PHPStan\Rules\Comparison\PossiblyImpureTipHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -29,6 +33,8 @@ final class ImpossibleInstanceOfRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, + private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -44,7 +50,7 @@ public function getNodeType(): string return Node\Expr\Instanceof_::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if ($node->class instanceof Node\Name) { $className = $scope->resolveName($node->class); @@ -74,40 +80,48 @@ public function processNode(Node $node, Scope $scope): array $instanceofType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$instanceofType instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { - return $ruleErrorBuilder; + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { - return $ruleErrorBuilder; + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } if (!$this->treatPhpDocTypesAsCertainTip) { - return $ruleErrorBuilder; + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } - return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + $ruleErrorBuilder = $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); }; if (!$instanceofType->getValue()) { $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Instanceof between %s and %s will always evaluate to false.', - $exprType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), - )))->identifier('instanceof.alwaysFalse')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and %s will always evaluate to false.', + $exprType->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), + )))->identifier('instanceof.alwaysFalse')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -123,7 +137,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('instanceof.alwaysTrue'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index e92e18a03d4..296bb78954a 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\Container; @@ -58,7 +60,7 @@ public function getNodeType(): string return New_::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; foreach ($this->getClassNames($node, $scope) as [$class, $isName]) { @@ -71,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array * @param Node\Expr\New_ $node * @return list */ - private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array + private function checkClassName(string $class, bool $isName, Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $lowercasedClass = strtolower($class); $messages = []; @@ -269,6 +271,10 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', '%s of class ' . $classDisplayName . ' constructor contains unresolvable type.', 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of class ' . $classDisplayName . ' constructor.', + 'Constants %s cannot be combined for %s of class ' . $classDisplayName . ' constructor.', + 'Combining constants with | is not allowed for %s of class ' . $classDisplayName . ' constructor.', + null, )); } diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 15546825909..60e88caa527 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class BooleanAndConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -41,7 +44,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $errors = []; @@ -49,6 +52,8 @@ public function processNode( $nodeText = $originalNode->getOperatorSigil(); $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; + $isInTrait = $scope->isInTrait(); + $hasLeftOrRightError = false; if ($leftType instanceof ConstantBooleanType) { $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { @@ -80,8 +85,18 @@ public function processNode( if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } $rightScope = $node->getRightScope(); @@ -123,11 +138,21 @@ public function processNode( if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } - if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) { + if (count($errors) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { @@ -161,8 +186,17 @@ public function processNode( $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode, $nodeType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index 0f62704ebdb..fe786dd3c18 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -22,6 +24,7 @@ final class BooleanNotConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->expr); @@ -74,12 +77,17 @@ public function processNode( $errorBuilder->identifier(sprintf('booleanNot.always%s', $exprType->getValue() ? 'False' : 'True')); - return [ - $errorBuilder->build(), - ]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->expr, !$exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->expr); return []; } diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 8d2e1b86107..cc9fc93efa1 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class BooleanOrConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -41,7 +44,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $originalNode = $node->getOriginalNode(); @@ -49,6 +52,8 @@ public function processNode( $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; + $isInTrait = $scope->isInTrait(); + $hasLeftOrRightError = false; if ($leftType instanceof ConstantBooleanType) { $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { @@ -80,8 +85,18 @@ public function processNode( if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $messages[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); + } else { + $messages[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } $rightScope = $node->getRightScope(); @@ -123,11 +138,21 @@ public function processNode( if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $messages[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); + } else { + $messages[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } - if (count($messages) === 0 && !$scope->isInFirstLevelStatement()) { + if (count($messages) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { @@ -161,8 +186,17 @@ public function processNode( $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); - $messages[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode, $nodeType->getValue(), $ruleError); + } else { + $messages[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } } diff --git a/src/Rules/Comparison/ConstantConditionInTraitCollector.php b/src/Rules/Comparison/ConstantConditionInTraitCollector.php new file mode 100644 index 00000000000..7e2f75990d6 --- /dev/null +++ b/src/Rules/Comparison/ConstantConditionInTraitCollector.php @@ -0,0 +1,28 @@ +>, trait-string, string, null}|array{class-string>, trait-string, string, bool, Error|array}> + */ +final class ConstantConditionInTraitCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Comparison/ConstantConditionInTraitHelper.php b/src/Rules/Comparison/ConstantConditionInTraitHelper.php new file mode 100644 index 00000000000..31f70c70cc6 --- /dev/null +++ b/src/Rules/Comparison/ConstantConditionInTraitHelper.php @@ -0,0 +1,81 @@ +> $ruleName + */ + public function emitNoError( + string $ruleName, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + if (!$scope->isInTrait()) { + return; + } + + $exprString = sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); + $scope->emitCollectedData(ConstantConditionInTraitCollector::class, [ + $ruleName, + $scope->getTraitReflection()->getName(), + $exprString, + null, + ]); + } + + /** + * @param class-string> $ruleName + */ + public function emitError( + string $ruleName, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + bool $value, + RuleError $ruleError, + ): void + { + if ($ruleError instanceof FixableNodeRuleError) { + throw new ShouldNotHappenException('Fixable errors are not supported by ConstantConditionInTraitHelper.'); + } + + if (!$scope->isInTrait()) { + return; + } + + $exprString = sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); + $scope->emitCollectedData(ConstantConditionInTraitCollector::class, [ + $ruleName, + $scope->getTraitReflection()->getName(), + $exprString, + $value, + $this->ruleErrorTransformer->transform($ruleError, $scope, [], $expr), + ]); + } + +} diff --git a/src/Rules/Comparison/ConstantConditionInTraitRule.php b/src/Rules/Comparison/ConstantConditionInTraitRule.php new file mode 100644 index 00000000000..fd595520e12 --- /dev/null +++ b/src/Rules/Comparison/ConstantConditionInTraitRule.php @@ -0,0 +1,95 @@ + + */ +#[RegisteredRule(level: 0)] +final class ConstantConditionInTraitRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errorsByRuleTraitExprValue = []; + foreach ($node->get(ConstantConditionInTraitCollector::class) as $fileData) { + foreach ($fileData as $data) { + $ruleName = $data[0]; + $traitName = $data[1]; + $exprString = $data[2]; + $value = $data[3]; + $valueKey = var_export($value, true); + if ($data[3] === null) { + $errorsByRuleTraitExprValue[$ruleName][$traitName][$exprString][$valueKey][] = null; + // no error reported + continue; + } + + $error = $data[4]; + $errorsByRuleTraitExprValue[$ruleName][$traitName][$exprString][$valueKey][] = $error; + } + } + + $transformedErrors = []; + foreach ($errorsByRuleTraitExprValue as $ruleData) { + foreach ($ruleData as $traitData) { + foreach ($traitData as $valueData) { + if (count($valueData) > 1) { + continue; + } + + $uniquedErrors = []; + foreach ($valueData as $errors) { + foreach ($errors as $errorObject) { + if ($errorObject === null) { + continue; + } + if (is_array($errorObject)) { + $errorObject = Error::decode($errorObject); + } + + $message = $errorObject->getMessage(); + $uniquedErrors[$message] = $errorObject; + } + } + + $uniquedErrors = array_values($uniquedErrors); + if (count($uniquedErrors) === 0) { + continue; + } + + if (count($uniquedErrors) === 1) { + // report directly in trait, no "in context of" + $transformedErrors[] = new TransformedRuleError($uniquedErrors[0]->removeTraitContext()); + continue; + } + + // report each error in its context + foreach ($uniquedErrors as $uniquedError) { + $transformedErrors[] = new TransformedRuleError($uniquedError); + } + } + } + } + + return $transformedErrors; + } + +} diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php index dde16af74d6..d872f60a016 100644 --- a/src/Rules/Comparison/ConstantLooseComparisonRule.php +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class ConstantLooseComparisonRule implements Rule public function __construct( private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -36,7 +39,7 @@ public function getNodeType(): string return Node\Expr\BinaryOp::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node instanceof Node\Expr\BinaryOp\Equal && !$node instanceof Node\Expr\BinaryOp\NotEqual) { return []; @@ -44,6 +47,7 @@ public function processNode(Node $node, Scope $scope): array $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$nodeType->isTrue()->yes() && !$nodeType->isFalse()->yes()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -66,18 +70,23 @@ public function processNode(Node $node, Scope $scope): array }; if ($nodeType->isFalse()->yes()) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Loose comparison using %s between %s and %s will always evaluate to false.', - $node->getOperatorSigil(), - $scope->getType($node->left)->describe(VerbosityLevel::value()), - $scope->getType($node->right)->describe(VerbosityLevel::value()), - )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Loose comparison using %s between %s and %s will always evaluate to false.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -93,7 +102,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual')); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 973d49295eb..bff27fcfe20 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -25,6 +27,7 @@ final class DoWhileLoopConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -38,23 +41,26 @@ public function getNodeType(): string return DoWhileLoopConditionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $exprType = $this->helper->getBooleanType($scope, $node->getCond()); if ($exprType instanceof ConstantBooleanType) { if ($exprType->getValue()) { if ($node->hasYield()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if (!$statement instanceof Continue_) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } if (!$statement->num instanceof Int_) { continue; } if ($statement->num->value > 1) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } } @@ -62,6 +68,7 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if ($statement instanceof Break_) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } } @@ -85,17 +92,22 @@ public function processNode(Node $node, Scope $scope): array return $this->possiblyImpureTipHelper->addTip($scope, $node->getCond(), $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Do-while loop condition is always %s.', - $exprType->getValue() ? 'true' : 'false', - ))) - ->line($node->getCond()->getStartLine()) - ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) - ->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Do-while loop condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + ))) + ->line($node->getCond()->getStartLine()) + ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) + ->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->getCond(), $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 10df54ef12c..22c19dd1ec0 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -22,6 +24,7 @@ final class ElseIfConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -75,10 +78,17 @@ public function processNode( $errorBuilder->identifier(sprintf('elseif.always%s', $exprType->getValue() ? 'True' : 'False')); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index 19ee79df134..a1eb712e653 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class IfConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -36,7 +39,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -59,16 +62,21 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $node->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'If condition is always %s.', - $exprType->getValue() ? 'true' : 'false', - ))) - ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) - ->line($node->cond->getStartLine())->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'If condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) + ->line($node->cond->getStartLine())->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 61e3b57c303..8b3d6919c4c 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class ImpossibleCheckTypeFunctionCallRule implements Rule public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -36,7 +39,7 @@ public function getNodeType(): string return Node\Expr\FuncCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Name) { return []; @@ -45,6 +48,7 @@ public function processNode(Node $node, Scope $scope): array $functionName = (string) $node->name; $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -67,17 +71,22 @@ public function processNode(Node $node, Scope $scope): array }; if (!$isAlways) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to function %s()%s will always evaluate to false.', - $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->identifier('function.impossibleType')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to function %s()%s will always evaluate to false.', + $functionName, + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->identifier('function.impossibleType')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -92,7 +101,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('function.alreadyNarrowedType'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index bc8284d1111..650f15d5a69 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class ImpossibleCheckTypeMethodCallRule implements Rule public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string return Node\Expr\MethodCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Identifier) { return []; @@ -47,6 +50,7 @@ public function processNode(Node $node, Scope $scope): array $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -70,18 +74,23 @@ public function processNode(Node $node, Scope $scope): array if (!$isAlways) { $method = $this->getMethod($node->var, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to method %s::%s()%s will always evaluate to false.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->identifier('method.impossibleType')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to method %s::%s()%s will always evaluate to false.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->identifier('method.impossibleType')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -98,7 +107,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('method.alreadyNarrowedType'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } private function getMethod( diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 3c24b381762..3b41e6221a0 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class ImpossibleCheckTypeStaticMethodCallRule implements Rule public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string return Node\Expr\StaticCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Identifier) { return []; @@ -47,6 +50,7 @@ public function processNode(Node $node, Scope $scope): array $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -71,18 +75,23 @@ public function processNode(Node $node, Scope $scope): array if (!$isAlways) { $method = $this->getMethod($node->class, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to static method %s::%s()%s will always evaluate to false.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->identifier('staticMethod.impossibleType')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to static method %s::%s()%s will always evaluate to false.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->identifier('staticMethod.impossibleType')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -99,7 +108,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('staticMethod.alreadyNarrowedType'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } /** diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php index 3618a9f9643..16589f28886 100644 --- a/src/Rules/Comparison/LogicalXorConstantConditionRule.php +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp\LogicalXor; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -23,6 +25,7 @@ final class LogicalXorConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -38,9 +41,10 @@ public function getNodeType(): string return LogicalXor::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; + $isInTrait = $scope->isInTrait(); $leftType = $this->helper->getBooleanType($scope, $node->left); if ($leftType instanceof ConstantBooleanType) { $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { @@ -72,8 +76,17 @@ public function processNode(Node $node, Scope $scope): array if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->left, $leftType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->left); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->left); } $rightType = $this->helper->getBooleanType($scope, $node->right); @@ -110,8 +123,17 @@ public function processNode(Node $node, Scope $scope): array if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->right, $rightType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->right); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->right); } return $errors; diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index 665b7af1e34..d89fe2dd87c 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -31,6 +33,7 @@ final class MatchExpressionRule implements Rule public function __construct( private ConstantConditionRuleHelper $constantConditionRuleHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) @@ -42,7 +45,7 @@ public function getNodeType(): string return MatchExpressionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $matchCondition = $node->getCondition(); $matchConditionType = $scope->getType($matchCondition); @@ -71,6 +74,7 @@ public function processNode(Node $node, Scope $scope): array $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } if ($armConditionResult->getValue()) { @@ -80,6 +84,7 @@ public function processNode(Node $node, Scope $scope): array if (!$this->treatPhpDocTypesAsCertain) { $armConditionNativeResult = $armConditionScope->getNativeType($armConditionExpr); if (!$armConditionNativeResult instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } if ($armConditionNativeResult->getValue()) { @@ -90,6 +95,7 @@ public function processNode(Node $node, Scope $scope): array if ($matchConditionType instanceof ConstantBooleanType) { $armConditionStandaloneResult = $this->constantConditionRuleHelper->getBooleanType($armConditionScope, $armCondition->getCondition()); if (!$armConditionStandaloneResult instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } } @@ -102,11 +108,17 @@ public function processNode(Node $node, Scope $scope): array $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), ))->line($armLine)->identifier('match.alwaysFalse'); $this->possiblyImpureTipHelper->addTip($armConditionScope, $armConditionExpr, $errorBuilder); - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $armConditionExpr, false, $ruleError); + } else { + $errors[] = $ruleError; + } continue; } if ($i === $armsCount - 1) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } @@ -120,7 +132,12 @@ public function processNode(Node $node, Scope $scope): array ->identifier('match.alwaysTrue') ->tip('Remove remaining cases below this one and this error will disappear too.'); $this->possiblyImpureTipHelper->addTip($armConditionScope, $armConditionExpr, $errorBuilder); - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $armConditionExpr, true, $ruleError); + } else { + $errors[] = $ruleError; + } } } diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index 5cd0ab417e3..392e702f497 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class NumberComparisonOperatorsConstantConditionRule implements Rule public function __construct( private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -39,7 +42,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { if ( @@ -88,17 +91,22 @@ public function processNode( throw new ShouldNotHappenException(); } - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Comparison operation "%s" between %s and %s is always %s.', - $node->getOperatorSigil(), - $scope->getType($node->left)->describe(VerbosityLevel::value()), - $scope->getType($node->right)->describe(VerbosityLevel::value()), - $exprType->getValue() ? 'true' : 'false', - )))->identifier(sprintf('%s.always%s', $nodeType, $exprType->getValue() ? 'True' : 'False'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Comparison operation "%s" between %s and %s is always %s.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + $exprType->getValue() ? 'true' : 'false', + )))->identifier(sprintf('%s.always%s', $nodeType, $exprType->getValue() ? 'True' : 'False'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index b8b0ca09db2..dc2889aa170 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -28,6 +30,7 @@ final class StrictComparisonOfDifferentTypesRule implements Rule public function __construct( private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -43,7 +46,7 @@ public function getNodeType(): string return Node\Expr\BinaryOp::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); @@ -59,6 +62,7 @@ public function processNode(Node $node, Scope $scope): array $nodeType = $nodeTypeResult->type; if (!$nodeType instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -116,18 +120,26 @@ public function processNode(Node $node, Scope $scope): array } if (!$nodeType->getValue()) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Strict comparison using %s between %s and %s will always evaluate to false.', - $node->getOperatorSigil(), - $leftType->describe($verbosity), - $rightType->describe($verbosity), - )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Strict comparison using %s between %s and %s will always evaluate to false.', + $node->getOperatorSigil(), + $leftType->describe($verbosity), + $rightType->describe($verbosity), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + return []; + } return []; } @@ -150,10 +162,13 @@ public function processNode(Node $node, Scope $scope): array } $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical')); + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } - return [ - $errorBuilder->build(), - ]; + return [$ruleError]; } } diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index 273405ee36b..ddb606965c9 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class TernaryOperatorConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -36,7 +39,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -58,14 +61,19 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $node->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Ternary operator condition is always %s.', - $exprType->getValue() ? 'true' : 'false', - )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Ternary operator condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index 77b9b7543b5..d6bd7479dc8 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Stmt\While_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class WhileLoopAlwaysFalseConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -36,7 +39,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -59,13 +62,18 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $node->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) - ->identifier('while.alwaysFalse') - ->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) + ->identifier('while.alwaysFalse') + ->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, false, $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index bfc50e3e48a..ef942cfe0cf 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -25,6 +27,7 @@ final class WhileLoopAlwaysTrueConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -40,7 +43,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { foreach ($node->getExitPoints() as $exitPoint) { @@ -70,12 +73,14 @@ public function processNode( $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); if ($exprType->isTrue()->yes()) { if ($node->hasYield()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); return []; } $ref = $scope->getFunction() ?? $scope->getAnonymousFunctionReflection(); if ($ref !== null && $ref->getReturnType() instanceof NeverType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); return []; } @@ -97,13 +102,18 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $originalNode->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) - ->identifier('while.alwaysTrue') - ->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) + ->identifier('while.alwaysTrue') + ->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->cond, true, $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); return []; } diff --git a/src/Rules/Constants/ConstantAttributesRule.php b/src/Rules/Constants/ConstantAttributesRule.php index f944a7981ee..f076769bf9b 100644 --- a/src/Rules/Constants/ConstantAttributesRule.php +++ b/src/Rules/Constants/ConstantAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; @@ -31,7 +33,7 @@ public function getNodeType(): string return Node\Stmt\Const_::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if ($node->attrGroups === []) { return []; diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php index f6489f2e871..b0e670af317 100644 --- a/src/Rules/EnumCases/EnumCaseAttributesRule.php +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; @@ -25,7 +27,7 @@ public function getNodeType(): string return Node\Stmt\EnumCase::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 4308efe56c3..05ba58a069b 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -4,15 +4,19 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ResolvedFunctionVariant; +use PHPStan\Rules\Methods\NamedArgumentParameterMethodCallsCollector; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; @@ -31,11 +35,13 @@ use function array_fill; use function array_key_exists; use function array_last; +use function array_merge; use function count; use function implode; use function in_array; use function is_int; use function is_string; +use function lcfirst; use function max; use function sprintf; @@ -63,11 +69,12 @@ public function __construct( /** * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType + * @param array{class-string, string}|null $renamedNamedArgumentParameterData * @return list */ public function check( ParametersAcceptor $parametersAcceptor, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, bool $isBuiltin, Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, string $nodeType, @@ -87,6 +94,10 @@ public function check( string $unresolvableReturnTypeMessage, string $unresolvableParameterTypeMessage, string $namedArgumentMessage, + string $invalidConstantMessage, + string $exclusiveConstantsMessage, + string $bitmaskNotAllowedMessage, + ?array $renamedNamedArgumentParameterData, ): array { if ($funcCall instanceof Node\Expr\MethodCall || $funcCall instanceof Node\Expr\StaticCall || $funcCall instanceof Node\Expr\FuncCall) { @@ -97,7 +108,14 @@ public function check( $functionParametersMinCount = 0; $functionParametersMaxCount = 0; + $allowedConstantsTypes = []; foreach ($parametersAcceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getAllowedConstants() !== null + ) { + $allowedConstantsTypes[] = $parameter->getType(); + } if (!$parameter->isOptional()) { $functionParametersMinCount++; } @@ -105,6 +123,11 @@ public function check( $functionParametersMaxCount++; } + $allowedConstantsType = null; + if (count($allowedConstantsTypes) > 0) { + $allowedConstantsType = TypeCombinator::union(...$allowedConstantsTypes); + } + if ($parametersAcceptor->isVariadic()) { $functionParametersMaxCount = -1; } @@ -354,6 +377,11 @@ public function check( ->build(); } } + } elseif ($argumentName !== null && $renamedNamedArgumentParameterData !== null) { + $scope->emitCollectedData(NamedArgumentParameterMethodCallsCollector::class, array_merge( + $renamedNamedArgumentParameterData, + [$parameter->getName(), $argumentLine], + )); } if ($this->checkArgumentTypes) { @@ -410,6 +438,61 @@ public function check( ->line($argumentLine) ->build(); } + + if ( + $parameter instanceof ExtendedParameterReflection + && $scope->getPhpVersion()->supportsNamedArguments()->yes() + ) { + $constantReflections = $this->resolveConstantReflections($argumentValue, $scope); + if ($constantReflections !== null) { + if ($parameter->getAllowedConstants() !== null) { + $result = $parameter->checkAllowedConstants($constantReflections); + foreach ($result->getDisallowedConstants() as $disallowedConstant) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invalidConstantMessage, + $disallowedConstant->describe(), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.invalidConstant') + ->line($argumentLine) + ->build(); + } + foreach ($result->getViolatedExclusiveGroups() as $group) { + $errors[] = RuleErrorBuilder::message(sprintf( + $exclusiveConstantsMessage, + implode(', ', $group), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.exclusiveConstants') + ->line($argumentLine) + ->build(); + } + if ($result->isBitmaskNotAllowed()) { + $errors[] = RuleErrorBuilder::message(sprintf( + $bitmaskNotAllowedMessage, + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.bitmaskNotAllowed') + ->line($argumentLine) + ->build(); + } + } elseif ($isBuiltin && $allowedConstantsType !== null && $allowedConstantsType->isSuperTypeOf($parameterType)->yes()) { + foreach ($constantReflections as $constantReflection) { + if ($constantReflection->isBuiltin()->no()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + $invalidConstantMessage, + $constantReflection->describe(), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.invalidConstant') + ->line($argumentLine) + ->build(); + } + } + } + } } if ( @@ -705,6 +788,58 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu return implode(' ', $parts); } + /** + * @return list|null Null when the expression is not a constant or bitmask of constants + */ + private function resolveConstantReflections(Expr $expr, Scope $scope): ?array + { + if ($expr instanceof Expr\ConstFetch) { + $lowerName = $expr->name->toLowerString(); + if (in_array($lowerName, ['null', 'true', 'false'], true)) { + return null; + } + + if (!$this->reflectionProvider->hasConstant($expr->name, $scope)) { + return null; + } + + return [$this->reflectionProvider->getConstant($expr->name, $scope)]; + } + + if ($expr instanceof Expr\ClassConstFetch) { + if (!$expr->class instanceof Node\Name) { + return null; + } + if (!$expr->name instanceof Node\Identifier) { + return null; + } + + $className = $scope->resolveName($expr->class); + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstant($expr->name->name)) { + return null; + } + + return [$classReflection->getConstant($expr->name->name)]; + } + + if ($expr instanceof Expr\BinaryOp\BitwiseOr) { + $left = $this->resolveConstantReflections($expr->left, $scope); + $right = $this->resolveConstantReflections($expr->right, $scope); + if ($left === null || $right === null) { + return null; + } + + return [...$left, ...$right]; + } + + return null; + } + private function callReturnsByReference(Expr $expr, Scope $scope): bool { if ($expr instanceof Node\Expr\MethodCall) { diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index 092758eaad9..862583c6c55 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InArrowFunctionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 4ab0af4b014..85c33490b1b 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Functions; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -47,7 +49,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { if (!$node->name instanceof Node\Expr) { @@ -139,6 +141,10 @@ public function processNode( 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $callableDescription . '.', + 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', + 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', + null, ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 39f6f7cfeac..f01a081fdae 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; @@ -28,7 +30,7 @@ public function getNodeType(): string return FuncCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!($node->name instanceof Node\Name)) { return []; @@ -68,6 +70,10 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to function ' . $functionName . ' contains unresolvable type.', '%s of function ' . $functionName . ' contains unresolvable type.', 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of function ' . $functionName . '.', + 'Constants %s cannot be combined for %s of function ' . $functionName . '.', + 'Combining constants with | is not allowed for %s of function ' . $functionName . '.', + null, ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 3757f826fcd..cfc8f1955be 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -5,6 +5,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; @@ -33,7 +35,7 @@ public function getNodeType(): string return FuncCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Name) { return []; @@ -92,6 +94,10 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $callableDescription . '.', + 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', + 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', + null, ); } diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index d9dd348f9c3..54ee5218644 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClosureNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InClosureNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index a7b6547cb01..605982c5865 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InFunctionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index ad67abb22b7..c04f2452bbb 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; @@ -25,7 +27,7 @@ public function getNodeType(): string return Node\Param::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $targetName = 'parameter'; $targetType = Attribute::TARGET_PARAMETER; diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 1f042288f0e..51881bfbea2 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Scalar\String_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; @@ -34,7 +36,7 @@ public function getNodeType(): string return MethodCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; if ($node->name instanceof Node\Identifier) { @@ -62,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function processSingleMethodCall(Scope $scope, MethodCall $node, string $methodName): array + private function processSingleMethodCall(Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, MethodCall $node, string $methodName): array { [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var, $node->name); if ($methodReflection === null) { @@ -99,6 +101,13 @@ private function processSingleMethodCall(Scope $scope, MethodCall $node, string 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', '%s of method ' . $messagesMethodName . ' contains unresolvable type.', 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of method ' . $messagesMethodName . '.', + 'Constants %s cannot be combined for %s of method ' . $messagesMethodName . '.', + 'Combining constants with | is not allowed for %s of method ' . $messagesMethodName . '.', + !$methodReflection->isPrivate() && !$declaringClass->isFinal() ? [ + $declaringClass->getName(), + $methodReflection->getName(), + ] : null, )); } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 166ad74aced..a275d3fb731 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Scalar\String_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; @@ -35,7 +37,7 @@ public function getNodeType(): string return StaticCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; if ($node->name instanceof Node\Identifier) { @@ -63,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function processSingleMethodCall(Scope $scope, StaticCall $node, string $methodName): array + private function processSingleMethodCall(Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, StaticCall $node, string $methodName): array { [$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class, $node->name); if ($method === null) { @@ -108,6 +110,10 @@ private function processSingleMethodCall(Scope $scope, StaticCall $node, string 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', '%s of ' . $lowercasedMethodName . ' contains unresolvable type.', $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $lowercasedMethodName . '.', + 'Constants %s cannot be combined for %s of ' . $lowercasedMethodName . '.', + 'Combining constants with | is not allowed for %s of ' . $lowercasedMethodName . '.', + null, )); return $errors; diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php index 5eace25f3a6..e3eb6d1513d 100644 --- a/src/Rules/Methods/ConsistentConstructorRule.php +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; @@ -29,7 +31,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $method = $node->getMethodReflection(); if (strtolower($method->getName()) !== '__construct') { @@ -47,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array } return array_merge( - $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true), + $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, $scope, true), $this->methodVisibilityComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method), ); } diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index 56bb6016a1b..ecec4569c95 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php b/src/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php new file mode 100644 index 00000000000..55d7e9d8ad1 --- /dev/null +++ b/src/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php @@ -0,0 +1,78 @@ + + */ +#[RegisteredRule(level: 0)] +final class MethodCallWithPossiblyRenamedNamedArgumentRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, NodeCallbackInvoker&Scope&CollectedDataEmitter $scope): array + { + $calls = []; + foreach ($node->get(NamedArgumentParameterMethodCallsCollector::class) as $file => $data) { + foreach ($data as [$declaringClassName, $methodName, $parameterName, $callLine]) { + $calls[$declaringClassName][$methodName][$parameterName][] = [$file, $callLine]; + } + } + + $errors = []; + foreach ($node->get(OverridingMethodRenamesParameterCollector::class) as $data) { + foreach ($data as [$prototypeDeclaringClassName, $methodName, $methodDeclaringClassName, $prototypeParameterName, $methodParameterName]) { + if (!array_key_exists($prototypeDeclaringClassName, $calls)) { + continue; + } + + $prototypeClassCalls = $calls[$prototypeDeclaringClassName]; + if (!array_key_exists($methodName, $prototypeClassCalls)) { + continue; + } + + $prototypeMethodCalls = $prototypeClassCalls[$methodName]; + if (!array_key_exists($prototypeParameterName, $prototypeMethodCalls)) { + continue; + } + + if (!array_key_exists($prototypeParameterName, $prototypeMethodCalls)) { + continue; + } + + $callsWithParameter = $prototypeMethodCalls[$prototypeParameterName]; + foreach ($callsWithParameter as [$file, $line]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() uses named argument for parameter $%s, but %s renames it to $%s.', + $prototypeDeclaringClassName, + $methodName, + $prototypeParameterName, + $methodDeclaringClassName, + $methodParameterName, + ))->identifier('argument.parameterRenamedInSubtype') + ->file($file) + ->line($line) + ->build(); + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Methods/MethodParameterComparisonHelper.php b/src/Rules/Methods/MethodParameterComparisonHelper.php index 41a9c985cfb..c86a76cacfc 100644 --- a/src/Rules/Methods/MethodParameterComparisonHelper.php +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -2,6 +2,9 @@ namespace PHPStan\Rules\Methods; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; +use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassReflection; @@ -33,7 +36,13 @@ public function __construct(private PhpVersion $phpVersion) /** * @return list */ - public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method, bool $ignorable): array + public function compare( + ExtendedMethodReflection $prototype, + ClassReflection $prototypeDeclaringClass, + PhpMethodFromParserNodeReflection $method, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + bool $ignorable, + ): array { /** @var list $messages */ $messages = []; @@ -64,6 +73,17 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr } $methodParameter = $methodParameters[$i]; + if ($prototype->acceptsNamedArguments()->yes()) { + if ($prototypeParameter->getName() !== $methodParameter->getName()) { + $scope->emitCollectedData(OverridingMethodRenamesParameterCollector::class, [ + $prototypeDeclaringClass->getName(), + $prototype->getName(), + $method->getDeclaringClass()->getName(), + $prototypeParameter->getName(), + $methodParameter->getName(), + ]); + } + } if ($prototypeParameter->passedByReference()->no()) { if (!$methodParameter->passedByReference()->no()) { $error = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php b/src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php new file mode 100644 index 00000000000..6da034b7617 --- /dev/null +++ b/src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php @@ -0,0 +1,26 @@ + + */ +final class NamedArgumentParameterMethodCallsCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Methods/OverridingMethodRenamesParameterCollector.php b/src/Rules/Methods/OverridingMethodRenamesParameterCollector.php new file mode 100644 index 00000000000..d076548882b --- /dev/null +++ b/src/Rules/Methods/OverridingMethodRenamesParameterCollector.php @@ -0,0 +1,26 @@ + + */ +final class OverridingMethodRenamesParameterCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 2058b64931f..b1185a7008a 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Attribute; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -51,7 +52,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $method = $node->getMethodReflection(); $prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName()); @@ -227,7 +228,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array } } - $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, false)); + $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, $scope, false)); if (!$prototypeVariant instanceof ExtendedFunctionVariant) { return $this->addErrors($messages, $node, $scope); @@ -331,7 +332,7 @@ private function filterOverrideAttribute(array $attrGroups): array private function addErrors( array $errors, InClassMethodNode $classMethod, - Scope&NodeCallbackInvoker $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { if (count($errors) > 0) { diff --git a/src/Rules/Playground/ArrayDimCastRule.php b/src/Rules/Playground/ArrayDimCastRule.php new file mode 100644 index 00000000000..0f16a2d4616 --- /dev/null +++ b/src/Rules/Playground/ArrayDimCastRule.php @@ -0,0 +1,63 @@ + + */ +final class ArrayDimCastRule implements Rule +{ + + public function getNodeType(): string + { + return ArrayDimFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->dim === null) { + return []; + } + + $varType = $scope->getType($node->var); + if ($varType->isArray()->no()) { + return []; + } + + $dimType = $scope->getType($node->dim); + if (!$dimType->isConstantScalarValue()->yes()) { + return []; + } + + $constantScalars = $dimType->getConstantScalarTypes(); + $errors = []; + foreach ($constantScalars as $constantScalar) { + $arrayKeyType = $constantScalar->toArrayKey(); + if ($arrayKeyType->equals($constantScalar)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Key %s (%s) will be cast to %s (%s) in the array access.', + $constantScalar->describe(VerbosityLevel::value()), + $constantScalar->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::typeOnly()), + $arrayKeyType->describe(VerbosityLevel::value()), + $arrayKeyType->describe(VerbosityLevel::typeOnly()), + ))->identifier('phpstanPlayground.arrayDimFetchCast') + ->tip('Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/LiteralArrayKeyCastRule.php b/src/Rules/Playground/LiteralArrayKeyCastRule.php new file mode 100644 index 00000000000..807754d35cd --- /dev/null +++ b/src/Rules/Playground/LiteralArrayKeyCastRule.php @@ -0,0 +1,61 @@ + + */ +final class LiteralArrayKeyCastRule implements Rule +{ + + public function getNodeType(): string + { + return Array_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + foreach ($node->items as $item) { + if ($item->key === null) { + continue; + } + + $keyType = $scope->getType($item->key); + if (!$keyType->isConstantScalarValue()->yes()) { + continue; + } + + $constantScalars = $keyType->getConstantScalarTypes(); + foreach ($constantScalars as $constantScalar) { + $arrayKeyType = $constantScalar->toArrayKey(); + if ($arrayKeyType->equals($constantScalar)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Key %s (%s) will be cast to %s (%s) in the array.', + $constantScalar->describe(VerbosityLevel::value()), + $constantScalar->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::typeOnly()), + $arrayKeyType->describe(VerbosityLevel::value()), + $arrayKeyType->describe(VerbosityLevel::typeOnly()), + ))->identifier('phpstanPlayground.arrayKeyCast') + ->tip('Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe') + ->line($item->getStartLine()) + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/PromoteParameterRule.php b/src/Rules/Playground/PromoteParameterRule.php index 8dc1d329165..d01b77c530e 100644 --- a/src/Rules/Playground/PromoteParameterRule.php +++ b/src/Rules/Playground/PromoteParameterRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Playground; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Container; @@ -88,7 +89,7 @@ private function getOriginalRule(): ?Rule return $this->originalRule = $originalRule; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if ($this->parameterValue) { return []; diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index 4ce2080d863..c6dbc29b452 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; @@ -32,7 +34,7 @@ public function getNodeType(): string return ClassPropertyNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$this->phpVersion->supportsOverrideAttributeOnProperty()) { $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php index 2eb1c11f604..79e48aa03fa 100644 --- a/src/Rules/Properties/PropertyHookAttributesRule.php +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InPropertyHookNode; @@ -29,7 +31,7 @@ public function getNodeType(): string return InPropertyHookNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $attrGroups = $node->getOriginalNode()->attrGroups; $errors = $this->attributesCheck->check( diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php index a92a6172737..02f45dbbb69 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use function sprintf; final class RewrittenDeclaringClassClassConstantReflection implements ClassConstantReflection { @@ -89,6 +90,16 @@ public function getName(): string return $this->constantReflection->getName(); } + public function describe(): string + { + return sprintf('%s::%s', $this->getDeclaringClass()->getDisplayName(), $this->getName()); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getDeclaringClass()->isBuiltin()); + } + public function getValueType(): Type { return $this->constantReflection->getValueType(); diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index 03a5a047b03..fd1d3333e50 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; @@ -35,6 +36,6 @@ public function getNodeType(): string; * @param TNodeType $node * @return list */ - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array; + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array; } diff --git a/src/Rules/RuleErrors/TransformedRuleError.php b/src/Rules/RuleErrors/TransformedRuleError.php new file mode 100644 index 00000000000..0a69d7387fc --- /dev/null +++ b/src/Rules/RuleErrors/TransformedRuleError.php @@ -0,0 +1,39 @@ +error; + } + + public function getIdentifier(): string + { + $identifier = $this->error->getIdentifier(); + if ($identifier === null) { + throw new ShouldNotHappenException(); + } + + return $identifier; + } + + public function getMessage(): string + { + return $this->error->getMessage(); + } + +} diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php index 8006203180d..7d6c6fd6e75 100644 --- a/src/Rules/Traits/TraitAttributesRule.php +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InTraitNode; @@ -32,7 +34,7 @@ public function getNodeType(): string return InTraitNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$this->phpVersion->supportsDeprecatedTraits()) { if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('Deprecated')) > 0) { diff --git a/src/Testing/CompositeRule.php b/src/Testing/CompositeRule.php index c83fb047b52..269ed259cc4 100644 --- a/src/Testing/CompositeRule.php +++ b/src/Testing/CompositeRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Testing; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\Rules\DirectRegistry; @@ -37,7 +38,7 @@ public function getNodeType(): string return Node::class; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; diff --git a/src/Testing/DelayedRule.php b/src/Testing/DelayedRule.php index 27b35cb2f40..17909e3721b 100644 --- a/src/Testing/DelayedRule.php +++ b/src/Testing/DelayedRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Testing; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\Rules\DirectRegistry; @@ -43,7 +44,7 @@ public function getDelayedErrors(): array return $this->errors; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $nodeType = get_class($node); foreach ($this->registry->getRules($nodeType) as $rule) { diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index ff3536f4735..9e2bd72230b 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -385,6 +385,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php new file mode 100644 index 00000000000..c83657bbd56 --- /dev/null +++ b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php @@ -0,0 +1,457 @@ +isDecimalIntegerString(); + + if ( + $type->isString()->yes() + && ($this->inverse ? $isDecimalIntegerString->no() : $isDecimalIntegerString->yes()) + ) { + return AcceptsResult::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + $result = $type->isString()->and($this->inverse ? $isDecimalIntegerString->negate() : $isDecimalIntegerString); + + return new AcceptsResult($result, []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + $isDecimalIntegerString = $type->isDecimalIntegerString(); + $result = $type->isString()->and($this->inverse ? $isDecimalIntegerString->negate() : $isDecimalIntegerString); + + return new IsSuperTypeOfResult($result, []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + if ( + ( + $otherType instanceof AccessoryNumericStringType + || $otherType instanceof AccessoryLowercaseStringType + || $otherType instanceof AccessoryUppercaseStringType + ) + && !$this->inverse + ) { + return IsSuperTypeOfResult::createYes(); + } + + $otherTypeResult = $otherType->isString()->and($this->inverse ? $otherType->isDecimalIntegerString()->negate() : $otherType->isDecimalIntegerString()); + + return new IsSuperTypeOfResult( + $otherTypeResult->and($otherType->equals($this) ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()), + [], + ); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self && $this->inverse === $type->inverse; + } + + public function describe(VerbosityLevel $level): string + { + return $this->inverse ? 'non-decimal-int-string' : 'decimal-int-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + return $this; + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + if ($this->inverse) { + return new UnionType([ + $this->toInteger(), + $this->toFloat(), + ]); + } + + return $this->toInteger(); + } + + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + + public function toBoolean(): BooleanType + { + return $this->isNonFalsyString()->negate()->toBooleanType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + [1], + isList: TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + if ($this->inverse) { + return $this; + } + + return new IntegerType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isCallable(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + } + + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + { + if ($this->inverse) { + return [new TrivialParametersAcceptor()]; + } + + throw new ShouldNotHappenException(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); + } + + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(!$this->inverse); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); + } + + public function isUppercaseString(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + + if ($type->isString()->yes()) { + if ($this->inverse) { + if ($type->isDecimalIntegerString()->yes()) { + return new ConstantBooleanType(false); + } + } elseif ($type->isDecimalIntegerString()->no()) { + return new ConstantBooleanType(false); + } + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->inverse ? 'non-decimal-int-string' : 'decimal-int-string'); + } + + public function hasTemplateOrLateResolvableType(): bool + { + return false; + } + +} diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index da1abf5e370..f0e9b7c033b 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -290,6 +290,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 2e5ca831461..826b6f706b5 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 2499084ba4c..1238abc7dcb 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 9f2eebdbd2d..ffc8f2b9b4b 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -290,6 +290,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 88a811bc1be..d0a7d65ac0d 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Accessory; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -209,12 +210,20 @@ public function toArray(): Type public function toArrayKey(): Type { + $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); + if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + new UnionType([ + new IntegerType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + ]); + } + return new UnionType([ new IntegerType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]), + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), ]); } @@ -287,6 +296,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index a85c74745be..683b6dec981 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index b6757fefb62..20256f2731f 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -295,6 +295,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 66872dc2f3e..0a16fb5801f 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -383,6 +383,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 688da67695a..fa3980c769b 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -372,6 +372,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 4956e879926..847bddeccfc 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -349,6 +349,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 288caefdc63..8a82601946f 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; @@ -12,6 +13,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -32,6 +34,7 @@ use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_merge; use function count; +use function in_array; use function sprintf; /** @api */ @@ -47,14 +50,16 @@ class ArrayType implements Type private Type $keyType; + private ?Type $cachedIterableKeyType = null; + /** @api */ public function __construct(Type $keyType, private Type $itemType) { - if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { + if (in_array($keyType->describe(VerbosityLevel::value()), ['(int|string)', '(int|non-decimal-int-string)'], true)) { $keyType = new MixedType(); } if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) { - $keyType = new UnionType([new StringType(), new IntegerType()]); + $keyType = (new UnionType([new StringType(), new IntegerType()]))->toArrayKey(); } $this->keyType = $keyType; @@ -198,15 +203,44 @@ public function getArraySize(): Type public function getIterableKeyType(): Type { + if ($this->cachedIterableKeyType !== null) { + return $this->cachedIterableKeyType; + } $keyType = $this->keyType; if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) { - return new BenevolentUnionType([new IntegerType(), new StringType()]); + $keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(); } if ($keyType instanceof StrictMixedType) { - return new BenevolentUnionType([new IntegerType(), new StringType()]); + $keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(); + } + + $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); + if ($level === null) { + return $this->cachedIterableKeyType = $keyType; + } + + if ($level === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + return $this->cachedIterableKeyType = $keyType; + } + + if ($level !== ReportUnsafeArrayStringKeyCastingToggle::DETECT) { // @phpstan-ignore notIdentical.alwaysFalse + throw new ShouldNotHappenException(); } - return $keyType; + return $this->cachedIterableKeyType = TypeTraverser::map($keyType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + if ($type->isString()->yes() && !$type->isDecimalIntegerString()->no()) { + return TypeCombinator::union( + new IntegerType(), + TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)), + ); + } + + return $type; + }); } public function getFirstIterableKeyType(): Type diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index adad99a6331..7b86e77fa0b 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -688,6 +688,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index c5dae4f1958..55f24671a20 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -46,7 +46,12 @@ public function isString(): TrinaryLogic public function isNumericString(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); + } + + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function isNonEmptyString(): TrinaryLogic diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 23ee9dc162e..4bdc31184e6 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -791,6 +791,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 40d0773c48d..a3b06036c7f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -329,6 +329,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createFromBoolean(is_numeric($this->getValue())); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean((string) (int) $this->value === $this->value); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->getValue() !== ''); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 5df57fe7955..b792753083e 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -216,6 +216,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index d4ed8baf373..e989ac91996 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -132,7 +132,7 @@ public function subtract(Type $typeToRemove): Type public function getTypeWithoutSubtractedType(): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + if (!$bound instanceof SubtractableType) { return $this; } @@ -149,7 +149,7 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + if (!$bound instanceof SubtractableType) { return $this; } @@ -166,7 +166,7 @@ public function changeSubtractedType(?Type $subtractedType): Type public function getSubtractedType(): ?Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + if (!$bound instanceof SubtractableType) { return null; } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 10482dae047..e58fb538f40 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -27,6 +27,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -406,6 +407,7 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType + || $type instanceof AccessoryDecimalIntegerStringType ) { if ( ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType) @@ -805,6 +807,11 @@ public function isNumericString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString()); + } + public function isNonEmptyString(): TrinaryLogic { if ($this->isCallable()->yes() && $this->isString()->yes()) { @@ -1321,6 +1328,10 @@ public function toArray(): Type public function toArrayKey(): Type { + if ($this->isDecimalIntegerString()->yes()) { + return new IntegerType(); + } + if ($this->isNumericString()->yes()) { return TypeCombinator::union( new IntegerType(), @@ -1504,6 +1515,7 @@ public function toPhpDocNode(): TypeNode || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType + || $type instanceof AccessoryDecimalIntegerStringType ) { if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 2cf46b754e9..8911cbdf49f 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -367,6 +367,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index 5435c540ff0..9697954fae3 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -124,6 +124,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 0c1892e01eb..ce5634548af 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -20,6 +20,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -938,6 +939,22 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $decimalIntegerString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + + if ($this->subtractedType->isSuperTypeOf($decimalIntegerString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index cf1d90f7a1d..a14f405d674 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -509,6 +509,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 5c7730ee9f7..42915cb2025 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -285,6 +285,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index e129e52f73f..b0e9f09869a 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1300,6 +1300,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 682bc77d300..c9b2fb225b3 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -653,6 +653,11 @@ public function isNumericString(): TrinaryLogic return $this->getStaticObjectType()->isNumericString(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->getStaticObjectType()->isDecimalIntegerString(); + } + public function isNonEmptyString(): TrinaryLogic { return $this->getStaticObjectType()->isNonEmptyString(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index af20367941f..f85dcba9c0e 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -286,6 +286,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 730869022fc..e549f4ba826 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,12 +2,14 @@ namespace PHPStan\Type; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -177,7 +179,22 @@ public function toArray(): Type public function toArrayKey(): Type { - return $this; + $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); + if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + return $this; + } + + $isDecimalIntString = $this->isDecimalIntegerString(); + if ($isDecimalIntString->no()) { + return $this; + } elseif ($isDecimalIntString->yes()) { + return new IntegerType(); + } + + return new UnionType([ + new IntegerType(), + TypeCombinator::intersect($this, new AccessoryDecimalIntegerStringType(inverse: true)), + ]); } public function toCoercedArgumentType(bool $strictTypes): Type @@ -232,6 +249,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php index a019125c3f0..0a73a101626 100644 --- a/src/Type/Traits/ArrayTypeTrait.php +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -140,6 +140,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 4b0dacddd72..5d171b751b4 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -494,6 +494,11 @@ public function isNumericString(): TrinaryLogic return $this->resolve()->isNumericString(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->resolve()->isDecimalIntegerString(); + } + public function isNonEmptyString(): TrinaryLogic { return $this->resolve()->isNonEmptyString(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 51a4922f43f..3dc499e2f31 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -225,6 +225,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 9af6fcf203c..0f759655af6 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -392,6 +392,17 @@ public function isString(): TrinaryLogic; public function isNumericString(): TrinaryLogic; + /** + * When isDecimalIntegerString() returns yes(), the type + * is guaranteed to be cast to an integer in an array key. + * Examples of constant values covered by this type: "0", "1", "1234", "-1" + * + * When isDecimalIntegerString() returns no(), the type represents strings containing non-decimal integers and other text. + * These are guaranteed to stay as string in an array key. + * Examples of constant values covered by this type: "+1", "00", "18E+3", "1.2", "1,3", "foo" + */ + public function isDecimalIntegerString(): TrinaryLogic; + public function isNonEmptyString(): TrinaryLogic; /** diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index b4544a86627..36526c4fa65 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -4,6 +4,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryType; @@ -27,6 +28,7 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateUnionType; use function array_fill; +use function array_filter; use function array_key_exists; use function array_key_first; use function array_merge; @@ -631,6 +633,24 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array } } + // numeric-string | non-decimal-int-string → string (preserving common accessories) + // Works because decimal-int-string ⊂ numeric-string, so together they cover all strings + if ($a->isString()->yes() && $b->isString()->yes()) { + $decimalIntString = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]); + if ($b->isDecimalIntegerString()->no()) { + $bBase = self::removeDecimalIntStringAccessory($b); + if ($bBase->isSuperTypeOf($a)->yes() && $a->isSuperTypeOf($decimalIntString)->yes()) { + return [null, $bBase]; + } + } + if ($a->isDecimalIntegerString()->no()) { + $aBase = self::removeDecimalIntStringAccessory($a); + if ($aBase->isSuperTypeOf($b)->yes() && $b->isSuperTypeOf($decimalIntString)->yes()) { + return [$aBase, null]; + } + } + } + return null; } @@ -650,6 +670,18 @@ private static function getAccessoryCaseStringTypes(Type $type): array return $accessory; } + private static function removeDecimalIntStringAccessory(Type $type): Type + { + if (!$type instanceof IntersectionType) { + return $type; + } + + return self::intersect(...array_filter( + $type->getTypes(), + static fn (Type $t): bool => !$t instanceof AccessoryDecimalIntegerStringType, + )); + } + private static function unionWithSubtractedType( Type $type, ?Type $subtractedType, diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index fda47810cc3..89e643bc293 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -705,6 +705,11 @@ public function isNumericString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString()); + } + public function isNonEmptyString(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 32be9683a81..73513641eef 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -156,6 +157,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType + || $type instanceof AccessoryDecimalIntegerStringType || $type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType ) { diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index ca864245e67..f84c733abc5 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -189,6 +189,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php new file mode 100644 index 00000000000..02412d59406 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php @@ -0,0 +1,45 @@ + + */ +class ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [ + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 33, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 39, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php new file mode 100644 index 00000000000..1ad96508af6 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php @@ -0,0 +1,40 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php new file mode 100644 index 00000000000..846ab7512a5 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php @@ -0,0 +1,53 @@ + + */ +class ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [ + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doFoo() expects array, non-empty-array given.', + 31, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 33, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doFoo() expects array, non-empty-array given.', + 37, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 39, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingPrevent.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php new file mode 100644 index 00000000000..0f8b64b4b51 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php @@ -0,0 +1,40 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingPrevent.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php new file mode 100644 index 00000000000..429a6a438ae --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php @@ -0,0 +1,34 @@ + + */ +class ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [ + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 33, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php new file mode 100644 index 00000000000..d04e42a4990 --- /dev/null +++ b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php @@ -0,0 +1,42 @@ + $a */ + public function doFoo(array $a): void + { + + } + + /** @param array $a */ + public function doBar(array $a): void + { + + } + + /** @param array $a */ + public function doBaz(array $a): void + { + + } + + public function doTest(string $s): void + { + $a = [$s => new stdClass()]; + $this->doFoo($a); + $this->doBar($a); + $this->doBaz($a); + + $b = []; + $b[$s] = new stdClass(); + $this->doFoo($b); + $this->doBar($b); + $this->doBaz($b); + } + +} diff --git a/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php new file mode 100644 index 00000000000..df88c4437db --- /dev/null +++ b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php @@ -0,0 +1,91 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('(int|non-decimal-int-string)', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} + +class FooNonDecimalIntString +{ + + /** + * @param array $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + /** @param non-decimal-int-string $s */ + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} diff --git a/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php new file mode 100644 index 00000000000..163a996bd25 --- /dev/null +++ b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php @@ -0,0 +1,91 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('(int|non-decimal-int-string)', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} + +class FooNonDecimalIntString +{ + + /** + * @param array $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + /** @param non-decimal-int-string $s */ + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/decimal-int-string.php b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php new file mode 100644 index 00000000000..fe5c5fdd529 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php @@ -0,0 +1,58 @@ + 1]; + assertType('non-empty-array', $a); + + assertType('bool', (bool) $s); + + assertType('int', $s + $s); + } + + /** + * @param non-decimal-int-string $s + */ + public function doBar(string $s): void + { + assertType('non-decimal-int-string' ,$s); + $a = [$s => 1]; + assertType('non-empty-array', $a); + + assertType('bool', (bool) $s); + + assertType('float|int', $s + $s); + } + + public function doBaz(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + + /** + * @param non-decimal-int-string $s + */ + public function emptyStringIsNonDecimal(string $s): void + { + if ($s === '') { + assertType("''", $s); // '' is a valid non-decimal-int-string + } + } + +} diff --git a/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon new file mode 100644 index 00000000000..1ea800ca917 --- /dev/null +++ b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon @@ -0,0 +1,2 @@ +parameters: + reportUnsafeArrayStringKeyCasting: detect diff --git a/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon new file mode 100644 index 00000000000..f35820e8667 --- /dev/null +++ b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon @@ -0,0 +1,2 @@ +parameters: + reportUnsafeArrayStringKeyCasting: prevent diff --git a/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php b/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php new file mode 100644 index 00000000000..99a4041d6c7 --- /dev/null +++ b/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php @@ -0,0 +1,162 @@ += 8.0')] +class ConstantToFunctionParameterMapTest extends PHPStanTestCase +{ + + public function testMapIsValid(): void + { + $map = require __DIR__ . '/../../../resources/constantToFunctionParameterMap.php'; + $this->assertIsArray($map); + + $reflectionProvider = self::createReflectionProvider(); + + foreach ($map as $entry => $parameters) { + $this->assertIsString($entry, 'Entry key must be a string.'); + $this->assertIsArray($parameters, sprintf('Parameters for %s must be an array.', $entry)); + + if (str_contains($entry, '::')) { + // Method entry: Class::method + [$className, $methodName] = explode('::', $entry, 2); + + $this->assertTrue( + $reflectionProvider->hasClass($className), + sprintf('Class %s not found in reflection (from %s).', $className, $entry), + ); + + $classReflection = $reflectionProvider->getClass($className); + $this->assertTrue( + $classReflection->hasMethod($methodName), + sprintf('Method %s not found in reflection.', $entry), + ); + + $methodReflection = $classReflection->getNativeMethod($methodName); + $variants = $methodReflection->getVariants(); + $this->assertNotEmpty($variants, sprintf('Method %s has no variants.', $entry)); + + $reflectionParameters = $variants[0]->getParameters(); + } else { + $this->assertNotSame('', $entry); + // Function entry + $nameNode = new Name($entry); + $this->assertTrue( + $reflectionProvider->hasFunction($nameNode, null), + sprintf('Function %s() not found in reflection.', $entry), + ); + + $functionReflection = $reflectionProvider->getFunction($nameNode, null); + $variants = $functionReflection->getVariants(); + $this->assertNotEmpty($variants, sprintf('Function %s() has no variants.', $entry)); + + $reflectionParameters = $variants[0]->getParameters(); + } + + $reflectionParameterNames = []; + foreach ($reflectionParameters as $reflectionParameter) { + $reflectionParameterNames[] = $reflectionParameter->getName(); + } + + foreach ($parameters as $parameterName => $config) { + $this->assertIsString($parameterName, sprintf('Parameter name for %s must be a string.', $entry)); + $this->assertContains( + $parameterName, + $reflectionParameterNames, + sprintf( + 'Parameter $%s not found in %s. Available parameters: $%s', + $parameterName, + $entry, + implode(', $', $reflectionParameterNames), + ), + ); + + $this->assertIsArray($config, sprintf('Config for %s($%s) must be an array.', $entry, $parameterName)); + $this->assertArrayHasKey('type', $config, sprintf('Missing "type" key for %s($%s).', $entry, $parameterName)); + $this->assertContains($config['type'], ['single', 'bitmask'], sprintf('Invalid type "%s" for %s($%s).', $config['type'], $entry, $parameterName)); + $this->assertArrayHasKey('constants', $config, sprintf('Missing "constants" key for %s($%s).', $entry, $parameterName)); + $this->assertIsArray($config['constants'], sprintf('Constants for %s($%s) must be an array.', $entry, $parameterName)); + $this->assertNotEmpty($config['constants'], sprintf('Constants for %s($%s) must not be empty.', $entry, $parameterName)); + + foreach ($config['constants'] as $constantName) { + $this->assertIsString($constantName, sprintf('Constant name for %s($%s) must be a string.', $entry, $parameterName)); + + if (str_contains($constantName, '::')) { + // Class constant: Class::CONSTANT + [$constClassName, $constName] = explode('::', $constantName, 2); + $this->assertTrue( + $reflectionProvider->hasClass($constClassName), + sprintf('Class %s not found in reflection (constant %s used in %s($%s)).', $constClassName, $constantName, $entry, $parameterName), + ); + $constClassReflection = $reflectionProvider->getClass($constClassName); + $this->assertTrue( + $constClassReflection->hasConstant($constName), + sprintf('Constant %s not found in reflection (used in %s($%s)).', $constantName, $entry, $parameterName), + ); + } else { + $this->assertNotSame('', $constantName); + // Global constant + $constantNameNode = new Name($constantName); + $this->assertTrue( + $reflectionProvider->hasConstant($constantNameNode, null), + sprintf('Constant %s (used in %s($%s)) not found in reflection.', $constantName, $entry, $parameterName), + ); + } + } + + $allowedKeys = ['type', 'constants', 'exclusiveGroups']; + foreach (array_keys($config) as $key) { + $this->assertContains($key, $allowedKeys, sprintf('Unknown key "%s" in config for %s($%s).', $key, $entry, $parameterName)); + } + + if (!isset($config['exclusiveGroups'])) { + continue; + } + + $this->assertSame('bitmask', $config['type'], sprintf('exclusiveGroups only makes sense for bitmask type in %s($%s).', $entry, $parameterName)); + $this->assertIsArray($config['exclusiveGroups']); + + foreach ($config['exclusiveGroups'] as $groupIndex => $group) { + $this->assertIsArray($group, sprintf('Exclusive group #%d for %s($%s) must be an array.', $groupIndex, $entry, $parameterName)); + $this->assertGreaterThanOrEqual(2, count($group), sprintf('Exclusive group #%d for %s($%s) must have at least 2 constants.', $groupIndex, $entry, $parameterName)); + + foreach ($group as $constantName) { + $this->assertContains( + $constantName, + $config['constants'], + sprintf( + 'Constant %s in exclusive group #%d for %s($%s) is not in the constants list.', + $constantName, + $groupIndex, + $entry, + $parameterName, + ), + ); + } + } + } + } + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/constantToFunctionParameterMap.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php b/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php new file mode 100644 index 00000000000..89f135dc78d --- /dev/null +++ b/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php @@ -0,0 +1,285 @@ += 8.0')] +class ParameterAllowedConstantsTest extends PHPStanTestCase +{ + + public function testJsonEncodeFlagsAllowsJsonConstant(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $flagsParam->checkAllowedConstants([$jsonThrowOnError]); + $this->assertTrue($result->isOk()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + $this->assertSame('SORT_REGULAR', $result->getDisallowedConstants()[0]->getName()); + } + + public function testJsonDecodeDoesNotAllowEncodeOnlyConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_decode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[3]; + + $this->assertSame('flags', $flagsParam->getName()); + + $jsonPrettyPrint = $reflectionProvider->getConstant(new Name('JSON_PRETTY_PRINT'), null); + $result = $flagsParam->checkAllowedConstants([$jsonPrettyPrint]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $flagsParam->checkAllowedConstants([$jsonThrowOnError]); + $this->assertTrue($result->isOk()); + } + + public function testSortFlagsExclusiveGroups(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + + $config = $flagsParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertTrue($config->isBitmask()); + $this->assertCount(1, $config->getExclusiveGroups()); + $this->assertSame( + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + $config->getExclusiveGroups()[0], + ); + + $sortFlagCase = $reflectionProvider->getConstant(new Name('SORT_FLAG_CASE'), null); + $result = $flagsParam->checkAllowedConstants([$sortFlagCase]); + $this->assertTrue($result->isOk()); + } + + public function testHtmlspecialcharsMultipleExclusiveGroups(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('htmlspecialchars'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + + $config = $flagsParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertCount(2, $config->getExclusiveGroups()); + $this->assertSame(['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], $config->getExclusiveGroups()[0]); + $this->assertSame(['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], $config->getExclusiveGroups()[1]); + } + + public function testSingleTypeParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('round'), null); + $modeParam = $function->getVariants()[0]->getParameters()[2]; + + $this->assertSame('mode', $modeParam->getName()); + + $config = $modeParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertFalse($config->isBitmask()); + $this->assertSame([], $config->getExclusiveGroups()); + + $halfUp = $reflectionProvider->getConstant(new Name('PHP_ROUND_HALF_UP'), null); + $result = $modeParam->checkAllowedConstants([$halfUp]); + $this->assertTrue($result->isOk()); + } + + public function testUnmappedParameterReturnsOk(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('strlen'), null); + $param = $function->getVariants()[0]->getParameters()[0]; + + $this->assertNull($param->getAllowedConstants()); + + $anyConstant = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $param->checkAllowedConstants([$anyConstant]); + $this->assertTrue($result->isOk()); + } + + public function testMethodWithGlobalConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass('finfo'); + $method = $class->getNativeMethod('file'); + $flagsParam = $method->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $fileinfoMime = $reflectionProvider->getConstant(new Name('FILEINFO_MIME'), null); + $result = $flagsParam->checkAllowedConstants([$fileinfoMime]); + $this->assertTrue($result->isOk()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testMethodWithClassConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass('PDOStatement'); + $method = $class->getNativeMethod('fetch'); + $modeParam = $method->getVariants()[0]->getParameters()[0]; + + $this->assertSame('mode', $modeParam->getName()); + $this->assertNotNull($modeParam->getAllowedConstants()); + $this->assertFalse($modeParam->getAllowedConstants()->isBitmask()); + + $pdoClass = $reflectionProvider->getClass('PDO'); + + $fetchAssoc = $pdoClass->getConstant('FETCH_ASSOC'); + $result = $modeParam->checkAllowedConstants([$fetchAssoc]); + $this->assertTrue($result->isOk()); + + $attrErrmode = $pdoClass->getConstant('ATTR_ERRMODE'); + $result = $modeParam->checkAllowedConstants([$attrErrmode]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testClassConstantNotAllowedWhenGlobalConstantsExpected(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $pdoClass = $reflectionProvider->getClass('PDO'); + $fetchAssoc = $pdoClass->getConstant('FETCH_ASSOC'); + + $result = $flagsParam->checkAllowedConstants([$fetchAssoc]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testViolatedExclusiveGroupsSortFlags(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + $sortString = $reflectionProvider->getConstant(new Name('SORT_STRING'), null); + $sortFlagCase = $reflectionProvider->getConstant(new Name('SORT_FLAG_CASE'), null); + + // Two mutually exclusive sort types + $result = $flagsParam->checkAllowedConstants([$sortNumeric, $sortString]); + $this->assertFalse($result->isOk()); + $this->assertSame([], $result->getDisallowedConstants()); + $this->assertCount(1, $result->getViolatedExclusiveGroups()); + $this->assertSame(['SORT_NUMERIC', 'SORT_STRING'], $result->getViolatedExclusiveGroups()[0]); + + // Sort type + modifier is fine + $result = $flagsParam->checkAllowedConstants([$sortString, $sortFlagCase]); + $this->assertTrue($result->isOk()); + } + + public function testViolatedExclusiveGroupsHtmlEntities(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('htmlspecialchars'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $entQuotes = $reflectionProvider->getConstant(new Name('ENT_QUOTES'), null); + $entNoquotes = $reflectionProvider->getConstant(new Name('ENT_NOQUOTES'), null); + $entHtml401 = $reflectionProvider->getConstant(new Name('ENT_HTML401'), null); + $entHtml5 = $reflectionProvider->getConstant(new Name('ENT_HTML5'), null); + $entSubstitute = $reflectionProvider->getConstant(new Name('ENT_SUBSTITUTE'), null); + + // Violates both exclusive groups + $result = $flagsParam->checkAllowedConstants([$entQuotes, $entNoquotes, $entHtml401, $entHtml5]); + $this->assertFalse($result->isOk()); + $this->assertSame([], $result->getDisallowedConstants()); + $this->assertCount(2, $result->getViolatedExclusiveGroups()); + $this->assertSame(['ENT_QUOTES', 'ENT_NOQUOTES'], $result->getViolatedExclusiveGroups()[0]); + $this->assertSame(['ENT_HTML401', 'ENT_HTML5'], $result->getViolatedExclusiveGroups()[1]); + + // One from each group is fine + $result = $flagsParam->checkAllowedConstants([$entQuotes, $entHtml5, $entSubstitute]); + $this->assertTrue($result->isOk()); + } + + public function testBitmaskNotAllowedOnSingleParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('array_unique'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertFalse($flagsParam->getAllowedConstants()->isBitmask()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + + // Single constant is fine + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertTrue($result->isOk()); + $this->assertFalse($result->isBitmaskNotAllowed()); + + // Bitmask on single-value parameter is not allowed + $result = $flagsParam->checkAllowedConstants([$sortRegular, $sortNumeric]); + $this->assertFalse($result->isOk()); + $this->assertTrue($result->isBitmaskNotAllowed()); + } + + public function testBitmaskAllowedOnBitmaskParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $prettyPrint = $reflectionProvider->getConstant(new Name('JSON_PRETTY_PRINT'), null); + $unescaped = $reflectionProvider->getConstant(new Name('JSON_UNESCAPED_SLASHES'), null); + + $result = $flagsParam->checkAllowedConstants([$prettyPrint, $unescaped]); + $this->assertTrue($result->isOk()); + $this->assertFalse($result->isBitmaskNotAllowed()); + } + + public function testBothDisallowedAndExclusiveViolation(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + $sortString = $reflectionProvider->getConstant(new Name('SORT_STRING'), null); + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + + // Wrong constant AND exclusive group violation + $result = $flagsParam->checkAllowedConstants([$sortNumeric, $sortString, $jsonThrowOnError]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + $this->assertSame('JSON_THROW_ON_ERROR', $result->getDisallowedConstants()[0]->getName()); + $this->assertCount(1, $result->getViolatedExclusiveGroups()); + $this->assertSame(['SORT_NUMERIC', 'SORT_STRING'], $result->getViolatedExclusiveGroups()[0]); + } + +} diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 1926cb56e53..b00993697a6 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -92,6 +92,7 @@ public static function dataSelectFromTypes(): Generator $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType(), $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $datePeriodConstructorVariants[0]->getParameters()), false, new VoidType(), @@ -123,6 +124,7 @@ public static function dataSelectFromTypes(): Generator $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType(), $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $datePeriodConstructorVariants[1]->getParameters()), false, new VoidType(), diff --git a/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon b/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon new file mode 100644 index 00000000000..72ae924610a --- /dev/null +++ b/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80500 diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index fcc567c51d9..6dc06a51159 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; +use PHPStan\Rules\Comparison\ConstantConditionInTraitRule; +use PHPStan\Rules\Comparison\PossiblyImpureTipHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -11,7 +15,7 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleInstanceOfRuleTest extends RuleTestCase { @@ -33,12 +37,18 @@ protected function getRule(): Rule discoveringSymbolsTip: true, ); - return new ImpossibleInstanceOfRule( - $ruleLevelHelper, - treatPhpDocTypesAsCertain: $this->treatPhpDocTypesAsCertain, - reportAlwaysTrueInLastCondition: $this->reportAlwaysTrueInLastCondition, - treatPhpDocTypesAsCertainTip: true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleInstanceOfRule( + $ruleLevelHelper, + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + treatPhpDocTypesAsCertain: $this->treatPhpDocTypesAsCertain, + reportAlwaysTrueInLastCondition: $this->reportAlwaysTrueInLastCondition, + treatPhpDocTypesAsCertainTip: true, + ), + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -536,6 +546,18 @@ public function testBug10036(): void ]); } + public function testBug10353(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10353.php'], []); + } + + public function testBug12267(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12267.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testNewIsAlwaysFinalClass(): void { @@ -596,4 +618,42 @@ public function testBug13975(string $file): void $this->analyse([$file], []); } + public function testPossiblyImpureTip(): void + { + $this->treatPhpDocTypesAsCertain = true; + $learnMore = ' Learn more: https://phpstan.org/blog/remembering-and-forgetting-returned-values'; + $this->analyse([__DIR__ . '/data/possibly-impure-instanceof-tip.php'], [ + // maybe-impure: tip expected + [ + 'Instanceof between PossiblyImpureInstanceofTip\Cat and PossiblyImpureInstanceofTip\Cat will always evaluate to true.', + 41, + 'If PossiblyImpureInstanceofTip\Holder::maybeImpureMethod() is impure, add @phpstan-impure PHPDoc tag above its declaration.' . $learnMore, + ], + // pure: no tip, error explained by type + [ + 'Instanceof between PossiblyImpureInstanceofTip\Cat and PossiblyImpureInstanceofTip\Cat will always evaluate to true.', + 53, + ], + // impure: no error - $holder invalidated + ]); + } + + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/impossible-instanceof-in-trait.php'], [ + [ + 'Instanceof between ImpossibleInstanceofInTrait\Cat and stdClass will always evaluate to false.', + 25, + $tipText, + ], + [ + 'Instanceof between ImpossibleInstanceofInTrait\Dog and stdClass will always evaluate to false.', + 25, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 841dde57810..ce313cafc4c 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -472,6 +472,7 @@ public function testBug9946(): void $this->analyse([__DIR__ . '/data/bug-9946.php'], []); } + #[RequiresPhp('< 8.0')] public function testBug10324(): void { $this->analyse([__DIR__ . '/data/bug-10324.php'], [ @@ -482,6 +483,21 @@ public function testBug10324(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug10324On80(): void + { + $this->analyse([__DIR__ . '/data/bug-10324.php'], [ + [ + 'Constant RecursiveIteratorIterator::CHILD_FIRST is not allowed for parameter #3 $flags of class RecursiveIteratorIterator constructor.', + 23, + ], + [ + 'Parameter #3 $flags of class RecursiveIteratorIterator constructor expects 0|16, 2 given.', + 23, + ], + ]); + } + public function testPhpstanInternalClass(): void { $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; @@ -635,6 +651,20 @@ public function testBug11006(): void } #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckInstantiation(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check-instantiation.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #1 $flags of class finfo constructor.', + 12, + ], + [ + 'Constant IntlDateFormatter::GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.', + 18, + ], + ]); + } + public function testBug14138(): void { $this->analyse([__DIR__ . '/data/bug-14138.php'], [ diff --git a/tests/PHPStan/Rules/Classes/data/bug-10353.php b/tests/PHPStan/Rules/Classes/data/bug-10353.php new file mode 100644 index 00000000000..bf4305af417 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10353.php @@ -0,0 +1,37 @@ +test(); + } +} + +class OtherClass +{ + use Foo; + + function bar(): string + { + return $this->test(); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-12267.php b/tests/PHPStan/Rules/Classes/data/bug-12267.php new file mode 100644 index 00000000000..8317471d39d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12267.php @@ -0,0 +1,53 @@ + */ + protected function getFileExistsHelpBlock(string $field): array + { + if (!($this->model instanceof A11yPhase)) { + return []; + } + + return []; + } +} + +/** + * @extends Form + */ +class EditA11yPhaseForm extends Form +{ + use ContainsA11yPhaseResultFields; +} + +/** + * @extends Form + */ +class SubmitA11yAuditPhaseForm extends Form +{ + use ContainsA11yPhaseResultFields; +} diff --git a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php new file mode 100644 index 00000000000..256c7639938 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php @@ -0,0 +1,18 @@ +animal instanceof Dog) { + + } + } + + public function doFoo2(): void + { + // always false + if ($this->animal instanceof \stdClass) { + + } + } + +} + +class Foo +{ + + /** @use FooTrait */ + use FooTrait; + + /** @var Dog */ + protected $animal; + +} + +class FooAnother +{ + + /** @use FooTrait */ + use FooTrait; + + /** @var Cat */ + protected $animal; + +} diff --git a/tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php b/tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php new file mode 100644 index 00000000000..bac1fbcce67 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php @@ -0,0 +1,69 @@ +getAnimal() instanceof Cat) { + $holder->maybeImpureMethod(); + + // tip expected: maybeImpureMethod() might have changed the object + if ($holder->getAnimal() instanceof Cat) { + return; + } + } +} + +function testPure(Holder $holder): void +{ + if ($holder->getAnimal() instanceof Cat) { + $holder->pureMethod(); + + // no tip - pureMethod() cannot change anything + if ($holder->getAnimal() instanceof Cat) { + return; + } + } +} + +function testImpure(Holder $holder): void +{ + if ($holder->getAnimal() instanceof Cat) { + $holder->impureMethod(); + + // no error - $holder invalidated by impure call + if ($holder->getAnimal() instanceof Cat) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/CollectedDataEmitterRule.php b/tests/PHPStan/Rules/CollectedDataEmitterRule.php new file mode 100644 index 00000000000..f8a08ca1b6e --- /dev/null +++ b/tests/PHPStan/Rules/CollectedDataEmitterRule.php @@ -0,0 +1,33 @@ + + */ +final class CollectedDataEmitterRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, NodeCallbackInvoker&Scope&CollectedDataEmitter $scope): array + { + // same implementation as DummyCollector, but is actually a rule! + if (!$node->name instanceof Node\Identifier) { + return []; + } + + $scope->emitCollectedData(DummyCollector::class, $node->name->toString()); + + return []; + } + +} diff --git a/tests/PHPStan/Rules/CollectedDataEmitterTest.php b/tests/PHPStan/Rules/CollectedDataEmitterTest.php new file mode 100644 index 00000000000..99c642e4579 --- /dev/null +++ b/tests/PHPStan/Rules/CollectedDataEmitterTest.php @@ -0,0 +1,33 @@ + + */ +class CollectedDataEmitterTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new CollectedDataEmitterRule(), + new DummyCollectorRule(), + ]); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/dummy-collector.php'], [ + [ + '2× doFoo, 2× doBar', + 5, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index b332d01adb8..f235ec0c7e4 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class BooleanAndConstantConditionRuleTest extends RuleTestCase { @@ -18,21 +19,26 @@ class BooleanAndConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new BooleanAndConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanAndConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -446,4 +452,16 @@ public function testBug8555(): void $this->analyse([__DIR__ . '/data/bug-8555.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/boolean-and-in-trait.php'], [ + [ + 'Left side of && is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 3cf3f2a8bbc..fae6233ed77 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class BooleanNotConstantConditionRuleTest extends RuleTestCase { @@ -18,21 +19,26 @@ class BooleanNotConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new BooleanNotConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanNotConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -230,10 +236,27 @@ public function testBug5984(): void $this->analyse([__DIR__ . '/data/bug-5984.php'], []); } + public function testBug12267(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12267.php'], []); + } + public function testBug6702(): void { $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6702.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/boolean-not-in-trait.php'], [ + [ + 'Negated boolean expression is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index cc628575f79..241e9be4540 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class BooleanOrConstantConditionRuleTest extends RuleTestCase { @@ -19,21 +20,26 @@ class BooleanOrConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new BooleanOrConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanOrConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -384,4 +390,16 @@ public function testBug10305(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/boolean-or-in-trait.php'], [ + [ + 'Left side of || is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index bb6aa370ac4..9d69da03b4c 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -10,7 +11,7 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ConstantLooseComparisonRuleTest extends RuleTestCase { @@ -21,12 +22,17 @@ class ConstantLooseComparisonRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ConstantLooseComparisonRule( - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new ConstantLooseComparisonRule( + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -248,4 +254,15 @@ public function testBug13098(): void $this->analyse([__DIR__ . '/data/bug-13098.php'], []); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/loose-comparison-in-trait.php'], [ + [ + 'Loose comparison using == between 1 and null will always evaluate to false.', + 19, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 97b3d705baf..8c8e736a579 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -3,30 +3,37 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class DoWhileLoopConstantConditionRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new DoWhileLoopConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new DoWhileLoopConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testBug6189(): void @@ -76,4 +83,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/do-while-in-trait.php'], [ + [ + 'Do-while loop condition is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 594de979a59..5575663828d 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ElseIfConstantConditionRuleTest extends RuleTestCase { @@ -19,21 +20,26 @@ class ElseIfConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ElseIfConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ElseIfConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -152,4 +158,16 @@ public function testBug6947(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/elseif-condition-in-trait.php'], [ + [ + 'Elseif condition is always false.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index d1d32ffe259..e6761845bc0 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class IfConstantConditionRuleTest extends RuleTestCase { @@ -16,20 +17,25 @@ class IfConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new IfConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new IfConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -231,6 +237,17 @@ public function testBug4284(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/if-condition-in-trait.php'], [ + [ + 'If condition is always true.', + 19, + ], + ]); + } + public function testBug6822(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index cbb3ed25a77..3964901b989 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -13,7 +14,7 @@ use function count; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { @@ -24,18 +25,23 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ImpossibleCheckTypeFunctionCallRule( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [stdClass::class], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [stdClass::class], + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -1141,6 +1147,51 @@ public function testBug13628(): void $this->analyse([__DIR__ . '/data/bug-13628.php'], []); } + public function testBug13023(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13023.php'], []); + } + + public function testBug9095(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9095.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7599(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7599.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug13474(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13474.php'], []); + } + + public function testBug13687(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13687.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12798(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12798.php'], []); + } + + public function testBug4570(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4570.php'], []); + } + public function testBug9666(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; @@ -1220,6 +1271,17 @@ public function testBug13799(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-function-call-in-trait.php'], [ + [ + 'Call to function is_string() with int will always evaluate to false.', + 19, + ], + ]); + } + public function testBug12063(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php index 0190415a989..3871e50298b 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule true, ), new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true, false, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 178b4148958..8066975bf92 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule true, ), new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true, false, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 382dbc2f28a..4e9a371d730 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase { @@ -19,18 +20,23 @@ class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase public function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -303,6 +309,17 @@ public function testBug10337(): void $this->analyse([__DIR__ . '/data/bug-10337.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-method-call-in-trait.php'], [ + [ + 'Call to method ImpossibleMethodCallInTrait\TypeChecker::isString() with int will always evaluate to false.', + 30, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index f53b87d0077..075b14d4ae6 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase { @@ -19,18 +20,23 @@ class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase public function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -168,6 +174,17 @@ public function testBug13566(): void $this->analyse([__DIR__ . '/data/bug-13566.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-static-method-call-in-trait.php'], [ + [ + 'Call to static method ImpossibleStaticMethodCallInTrait\TypeChecker::isString() with int will always evaluate to false.', + 28, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php index 14b97daacaa..c68f739131a 100644 --- a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -3,31 +3,38 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule as TRule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class LogicalXorConstantConditionRuleTest extends RuleTestCase { protected function getRule(): TRule { - return new LogicalXorConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new LogicalXorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + false, + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - false, - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -71,4 +78,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/logical-xor-in-trait.php'], [ + [ + 'Left side of xor is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 1f7880fc2a9..4cf9ce6bfd2 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class MatchExpressionRuleTest extends RuleTestCase { @@ -16,19 +17,24 @@ class MatchExpressionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MatchExpressionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new MatchExpressionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -490,4 +496,16 @@ public function testBug11310(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/match-in-trait.php'], [ + [ + 'Match arm comparison between true and false is always false.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 5400e3edee6..3822a2df11c 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase { @@ -19,11 +20,16 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NumberComparisonOperatorsConstantConditionRule( - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new NumberComparisonOperatorsConstantConditionRule( + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + ), + new ConstantConditionInTraitRule(), + ]); } protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool @@ -298,4 +304,15 @@ public function testBug12163(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/number-comparison-in-trait.php'], [ + [ + 'Comparison operation ">" between 1 and 0 is always true.', + 19, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 3c928fefd1c..a1b892613d0 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -11,7 +12,7 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase { @@ -24,13 +25,18 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new StrictComparisonOfDifferentTypesRule( - self::getContainer()->getByType(RicherScopeGetTypeHelper::class), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -1047,6 +1053,22 @@ public function testBug3761(): void $this->analyse([__DIR__ . '/data/bug-3761.php'], []); } + public function testBug8060(): void + { + $this->analyse([__DIR__ . '/data/bug-8060.php'], []); + } + + #[RequiresPhp('>= 8.2')] + public function testBug9515(): void + { + $this->analyse([__DIR__ . '/data/bug-9515.php'], []); + } + + public function testBug4121(): void + { + $this->analyse([__DIR__ . '/data/bug-4121.php'], []); + } + public function testBug13208(): void { $this->analyse([__DIR__ . '/data/bug-13208.php'], []); @@ -1168,6 +1190,16 @@ public function testPossiblyImpureTip(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/strict-comparison-in-trait.php'], [ + [ + 'Strict comparison using !== between string and null will always evaluate to true.', + 19, + ], + ]); + } + public function testBug11054(): void { $this->analyse([__DIR__ . '/data/bug-11054.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index 300484a89b7..3bd4459d290 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -3,10 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class TernaryOperatorConstantConditionRuleTest extends RuleTestCase { @@ -15,20 +17,25 @@ class TernaryOperatorConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TernaryOperatorConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new TernaryOperatorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -100,10 +107,28 @@ public function testBug7580(): void $this->analyse([__DIR__ . '/data/bug-7580.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug11949(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11949.php'], []); + } + public function testBug3370(): void { $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3370.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/ternary-in-trait.php'], [ + [ + 'Ternary operator condition is always true.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index e84fdb4d566..e4356732de2 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -3,30 +3,37 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class WhileLoopAlwaysFalseConditionRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new WhileLoopAlwaysFalseConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new WhileLoopAlwaysFalseConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -44,4 +51,15 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/while-false-in-trait.php'], [ + [ + 'While loop condition is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index 31f3abbf5e5..ce2f3549cc3 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -3,31 +3,37 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class WhileLoopAlwaysTrueConditionRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new WhileLoopAlwaysTrueConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new WhileLoopAlwaysTrueConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -74,4 +80,14 @@ public function testBug6189(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/while-true-in-trait.php'], [ + [ + 'While loop condition is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php b/tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php new file mode 100644 index 00000000000..a82755c5b3d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php @@ -0,0 +1,58 @@ +doBar() && rand(0, 1)) { + + } + } + + public function doFoo2() + { + // left side: always constant + if ($this->doBar2() && rand(0, 1)) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php b/tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php new file mode 100644 index 00000000000..a51454f5cc9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2() + { + // always constant (negation of always-truthy is always false) + if (!$this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php b/tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php new file mode 100644 index 00000000000..acb20bfb2c1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php @@ -0,0 +1,58 @@ +doBar() || rand(0, 1)) { + + } + } + + public function doFoo2() + { + // left side: always constant + if ($this->doBar2() || rand(0, 1)) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11949.php b/tests/PHPStan/Rules/Comparison/data/bug-11949.php new file mode 100644 index 00000000000..4de1ea766b3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11949.php @@ -0,0 +1,68 @@ += 8.1 + +namespace Bug11949; + +function trans(string $key): string +{ + return $key; +} + +trait EnumString +{ + + /** @var array */ + static protected ?array $_translatedValues; + + static public function getValueIndex(string $value): int + { + return ($i = array_search($value, self::NAMES)) === false ? -1 : $i; + } + + /** @return array */ + static public function getTranslatedValues(): array + { + return self::$_translatedValues ??= array_map(static::getTranslatedValue(...), array_combine(self::NAMES, self::NAMES)); + } + + static public function getTranslatedValue(string $value): string + { + return self::TRANSLATION ? trans(self::TRANSLATION . $value) : $value; + } + +} + +abstract class UserStatus +{ + + use EnumString; + + const ACTIVE = 'active'; + const PENDING = 'pending'; + const BLOCKED = 'blocked'; + + protected const NAMES = [ + self::ACTIVE, + self::PENDING, + self::BLOCKED, + ]; + + protected const TRANSLATION = 'users.statuses.'; + +} + +abstract class SystemCheckStatus +{ + + use EnumString; + + const SUCCESS = 'success'; + const FAILURE = 'failure'; + + protected const NAMES = [ + self::SUCCESS, + self::FAILURE, + ]; + + protected const TRANSLATION = ''; + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12267.php b/tests/PHPStan/Rules/Comparison/data/bug-12267.php new file mode 100644 index 00000000000..4d300a9b4eb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12267.php @@ -0,0 +1,43 @@ +model) { + return; + } + + echo $this->model; + } +} + +class Class1 +{ + /** @use PrintSomething */ + use PrintSomething; + + public function what(): void + { + $this->printIt(); + } +} + +class Class2 +{ + /** @use PrintSomething<\Exception> */ + use PrintSomething; + + public function what(): void + { + $this->printIt(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12798.php b/tests/PHPStan/Rules/Comparison/data/bug-12798.php new file mode 100644 index 00000000000..d6d29084b56 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12798.php @@ -0,0 +1,53 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug12798; + +interface Colorable +{ + public function color(): string; +} + +trait HasColors +{ + /** @return array */ + public static function colors(): array + { + /** @phpstan-ignore return.type */ + return array_reduce(self::cases(), function (array $colors, self $case) { + $key = is_subclass_of($case, \BackedEnum::class) ? $case->value : $case->name; + $color = is_subclass_of($case, Colorable::class) ? $case->color() : 'gray'; + + $colors[$key] = $color; + return $colors; + }, []); + } +} + +enum AlertLevelBacked: int implements Colorable +{ + use HasColors; + + case Low = 1; + case Medium = 2; + case Critical = 3; + + public function color(): string + { + return match ($this) { + self::Low => 'green', + self::Medium => 'yellow', + self::Critical => 'red', + }; + } +} + +enum AlertLevel +{ + use HasColors; + + case Low; + case Medium; + case Critical; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php new file mode 100644 index 00000000000..dae307df47d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug13474; + +/** + * @template TValue of mixed + */ +interface ModelInterface +{ + /** + * @return TValue + */ + public function getValue(): mixed; +} + +/** + * @implements ModelInterface + */ +class ModelA implements ModelInterface +{ + #[\Override] + public function getValue(): int + { + return 0; + } +} + +/** + * @implements ModelInterface + */ +class ModelB implements ModelInterface +{ + #[\Override] + public function getValue(): string + { + return 'foo'; + } +} + +/** + * @template T of ModelInterface + */ +trait ModelTrait +{ + /** + * @return T + */ + abstract function model(): ModelInterface; + + /** + * @return template-type + */ + public function getValue(): mixed + { + return $this->model()->getValue(); + } + + public function test(): void + { + if (is_string($this->getValue())) { + echo 'string'; + return; + } + + echo 'other'; + } +} + +class TestA +{ + /** @use ModelTrait */ + use ModelTrait; + + #[\Override] + function model(): ModelA + { + return new ModelA(); + } +} + +class TestB +{ + /** @use ModelTrait */ + use ModelTrait; + + #[\Override] + function model(): ModelB + { + return new ModelB(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13687.php b/tests/PHPStan/Rules/Comparison/data/bug-13687.php new file mode 100644 index 00000000000..0ccb02a4263 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13687.php @@ -0,0 +1,34 @@ +bar(); + } + + if (property_exists($this, 'baz')) { + $a = $this->baz; + } + } +} + +class A +{ + use MyTrait; + + public string $baz = 'baz'; +} + +class B +{ + use MyTrait; + + public function bar(): void + { + echo 'bar'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4121.php b/tests/PHPStan/Rules/Comparison/data/bug-4121.php new file mode 100644 index 00000000000..a790dbe7f51 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4121.php @@ -0,0 +1,23 @@ + 'abc', + 'valueToFetch' => '123', + ]; +} + +final class SecondConsumer +{ + use MyLogic; + + private const MY_CONST_ARRAY = [ + 'someValue' => 'abc', + 'someOtherValue' => '123', + ]; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7599.php b/tests/PHPStan/Rules/Comparison/data/bug-7599.php new file mode 100644 index 00000000000..37210e72415 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7599.php @@ -0,0 +1,41 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7599; + +trait TraitForEnum +{ + /** + * @return array + */ + public static function fooMethod(): array + { + return array_map( + fn(self $enum): string => method_exists($enum, 'barMethod') + ? $enum->barMethod() + : $enum->name, + static::cases() + ); + } +} + +enum TestEnum: string +{ + use TraitForEnum; + + case Foo = 'foo'; + case Bar = 'bar'; +} + +enum SecondEnum: string +{ + use TraitForEnum; + + case Baz = 'baz'; + + public function barMethod(): string + { + return 'blah'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8060.php b/tests/PHPStan/Rules/Comparison/data/bug-8060.php new file mode 100644 index 00000000000..b762a452ba5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8060.php @@ -0,0 +1,39 @@ +getAnything(); + + if ($anything !== null) { + return; + } + + echo 'foo'; + } + + abstract protected function getAnything(): ?string; +} + +class Example +{ + use ExampleTrait; + + protected function getAnything(): string + { + return 'foo'; + } +} + +class Example2 +{ + use ExampleTrait; + + protected function getAnything(): ?string + { + return 'foo'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9095.php b/tests/PHPStan/Rules/Comparison/data/bug-9095.php new file mode 100644 index 00000000000..32fa108496f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9095.php @@ -0,0 +1,34 @@ +bar(); + } +} + +class EmptyClass +{ + use SomeTrait; +} + +trait SomeTrait +{ + public function bar(): void + { + if (property_exists($this, 'message')) { + if (!is_string($this->message)) { + return; + } + + echo $this->message . "\n"; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9515.php b/tests/PHPStan/Rules/Comparison/data/bug-9515.php new file mode 100644 index 00000000000..07667db002f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9515.php @@ -0,0 +1,41 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug9515; + +trait Foo +{ + abstract public function getFoo(): ?string; + + public function getName(): string + { + $str = 'Hello'; + + if ($this->getFoo() !== null) { + $str .= ' World'; + } + + return $str; + } +} + +class Bar +{ + use Foo; + + public function getFoo(): string + { + return "Bar"; + } +} + +class Zar +{ + use Foo; + + public function getFoo(): null + { + return null; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php b/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php new file mode 100644 index 00000000000..05c5fb89007 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php @@ -0,0 +1,56 @@ += 8.2 + +namespace DoWhileInTrait; + +trait FooTrait +{ + + public function doFoo() + { + // sometimes constant, sometimes not + do { + } while ($this->doBar()); + } + + public function doFoo2() + { + // always falsy + do { + } while ($this->doBar2()); + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php b/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php new file mode 100644 index 00000000000..0ec93827730 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php @@ -0,0 +1,62 @@ += 8.2 + +namespace ElseIfConditionInTrait; + +trait FooTrait +{ + + public function doFoo() + { + $x = rand(0, 1); + // sometimes falsy, sometimes not + if ($x) { + } elseif ($this->doBar()) { + + } + } + + public function doFoo2() + { + $x = rand(0, 1); + // always falsy + if ($x) { + } elseif ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php b/tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php new file mode 100644 index 00000000000..9065569d00e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2() + { + // always truthy + if ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php b/tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php new file mode 100644 index 00000000000..9b1123c1181 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php @@ -0,0 +1,59 @@ +doBar())) { + + } + } + + public function doFoo2() + { + // always false + if (is_string($this->doBar2())) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): int + { + + } + + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** @return int|string */ + public function doBar() + { + + } + + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php new file mode 100644 index 00000000000..86c0de8769a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php @@ -0,0 +1,70 @@ +isString($this->doBar())) { + + } + } + + public function doFoo2() + { + $checker = new TypeChecker(); + // always false + if ($checker->isString($this->doBar2())) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): int + { + + } + + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** @return int|string */ + public function doBar() + { + + } + + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php new file mode 100644 index 00000000000..b11bf5ba709 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php @@ -0,0 +1,68 @@ +doBar())) { + + } + } + + public function doFoo2() + { + // always false + if (TypeChecker::isString($this->doBar2())) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): int + { + + } + + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** @return int|string */ + public function doBar() + { + + } + + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php b/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php new file mode 100644 index 00000000000..9a9c140b35b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php @@ -0,0 +1,58 @@ += 8.2 + +namespace LogicalXorInTrait; + +trait FooTrait +{ + + public function doFoo() + { + // left side: sometimes constant, sometimes not + if ($this->doBar() xor rand(0, 1)) { + + } + } + + public function doFoo2() + { + // left side: always constant (always false) + if ($this->doBar2() xor rand(0, 1)) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php b/tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php new file mode 100644 index 00000000000..9456460a62f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php @@ -0,0 +1,61 @@ +doBar() == null) { + + } + } + + public function doFoo2() + { + // always false + if ($this->doBar2() == null) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + /** @return 1 */ + public function doBar(): int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/match-in-trait.php b/tests/PHPStan/Rules/Comparison/data/match-in-trait.php new file mode 100644 index 00000000000..bf257ff0550 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/match-in-trait.php @@ -0,0 +1,60 @@ += 8.2 + +namespace MatchInTrait; + +trait FooTrait +{ + + public function doFoo() + { + // sometimes constant, sometimes not + match (true) { + $this->doBar() => 'yes', + default => 'no', + }; + } + + public function doFoo2() + { + // always false + match (true) { + $this->doBar2() => 'yes', + default => 'no', + }; + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): false + { + + } + + public function doBar2(): false + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): bool + { + + } + + public function doBar2(): false + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php b/tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php new file mode 100644 index 00000000000..be62971b3e0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php @@ -0,0 +1,61 @@ +doBar() > 0) { + + } + } + + public function doFoo2() + { + // always constant + if ($this->doBar2() > 0) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + /** @return 1 */ + public function doBar(): int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php new file mode 100644 index 00000000000..ee3b08ecd11 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php @@ -0,0 +1,58 @@ +doBar() !== null) { + + } + } + + public function doFoo2() + { + // always not nullable + if ($this->doBar2() !== null) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): string + { + + } + + public function doBar2(): string + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?string + { + + } + + public function doBar2(): string + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php b/tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php new file mode 100644 index 00000000000..5c0376725cc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php @@ -0,0 +1,54 @@ +doBar() ? 'yes' : 'no'; + } + + public function doFoo2() + { + // always truthy + $x = $this->doBar2() ? 'yes' : 'no'; + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php b/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php new file mode 100644 index 00000000000..a5538629436 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php @@ -0,0 +1,58 @@ += 8.2 + +namespace WhileFalseInTrait; + +trait FooTrait +{ + + public function doFoo() + { + // sometimes falsy, sometimes not + while ($this->doBar()) { + + } + } + + public function doFoo2() + { + // always falsy + while ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php b/tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php new file mode 100644 index 00000000000..0ea5164fa8b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2(): void + { + // always truthy + while ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 72f0f27e21e..db49ecb7661 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -364,6 +364,18 @@ public function testPipeOperator(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckCallables(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/constant-parameter-check-callables.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of closure.', + 10, + ], + ]); + } + public function testBug4608(): void { $this->analyse([__DIR__ . '/data/bug-4608-callables.php'], [ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index ef5e6865769..34a506402f0 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1605,9 +1605,14 @@ public function testBenevolentSuperglobalKeys(): void $this->analyse([__DIR__ . '/data/benevolent-superglobal-keys.php'], []); } + #[RequiresPhp('>= 8.0')] public function testFileParams(): void { $this->analyse([__DIR__ . '/data/file.php'], [ + [ + 'Constant FILE_APPEND is not allowed for parameter #2 $flags of function file.', + 16, + ], [ 'Parameter #2 $flags of function file expects 0|1|2|3|4|5|6|7|16|17|18|19|20|21|22|23, 8 given.', 16, @@ -1615,9 +1620,14 @@ public function testFileParams(): void ]); } + #[RequiresPhp('>= 8.0')] public function testFlockParams(): void { $this->analyse([__DIR__ . '/data/flock.php'], [ + [ + 'Constant FILE_APPEND is not allowed for parameter #2 $operation of function flock.', + 45, + ], [ 'Parameter #2 $operation of function flock expects int<0, 7>, 8 given.', 45, @@ -1633,6 +1643,10 @@ public function testJsonValidate(): void 'Parameter #2 $depth of function json_validate expects int<1, max>, 0 given.', 6, ], + [ + 'Constant JSON_BIGINT_AS_STRING is not allowed for parameter #3 $flags of function json_validate.', + 7, + ], [ 'Parameter #3 $flags of function json_validate expects 0|1048576, 2 given.', 7, @@ -2817,6 +2831,76 @@ public function testBug14312b(): void $this->analyse([__DIR__ . '/data/bug-14312b.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheck(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of function json_encode.', + 12, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of function json_encode.', + 21, + ], + [ + 'Constants SORT_NUMERIC, SORT_STRING cannot be combined for parameter #2 $flags of function sort.', + 27, + ], + [ + 'Constants SORT_NUMERIC, SORT_STRING cannot be combined for parameter #2 $flags of function sort.', + 30, + ], + [ + 'Constants ENT_QUOTES, ENT_NOQUOTES cannot be combined for parameter #2 $flags of function htmlspecialchars.', + 33, + ], + [ + 'Constants ENT_HTML401, ENT_HTML5 cannot be combined for parameter #2 $flags of function htmlspecialchars.', + 33, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $filter of function filter_var.', + 39, + ], + [ + 'Constant JSON_PRETTY_PRINT is not allowed for parameter #4 $flags of function json_decode.', + 51, + ], + [ + 'Constants LOCK_SH, LOCK_EX cannot be combined for parameter #2 $operation of function flock.', + 54, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter $flags of function json_encode.', + 70, + ], + [ + 'Combining constants with | is not allowed for parameter #2 $flags of function array_unique.', + 76, + ], + [ + 'Combining constants with | is not allowed for parameter #2 $filter of function filter_var.', + 79, + ], + [ + 'Constant JSON_THROW_ON_ERROR is not allowed for parameter #3 $depth of function json_decode.', + 99, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12850(): void + { + $this->analyse([__DIR__ . '/data/bug-12850.php'], [ + [ + 'Constants LOCK_EX, LOCK_SH cannot be combined for parameter #2 $operation of function flock.', + 9, + ], + ]); + } + public function testBug4608(): void { $paramName = PHP_VERSION_ID >= 80000 ? 'callback' : 'function'; @@ -2824,6 +2908,7 @@ public function testBug4608(): void [ sprintf("Parameter #1 \$%s of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", $paramName), 11, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index 95468a44174..37cfc4f1679 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -183,4 +183,15 @@ public function testNoNamedArguments(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckCallUserFunc(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check-call-user-func.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of callable passed to call_user_func().', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12850.php b/tests/PHPStan/Rules/Functions/data/bug-12850.php new file mode 100644 index 00000000000..fa8a41ab5f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12850.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ConstantParameterCheckCallUserFunc; + +// call_user_func with correct constant +call_user_func('json_encode', [], JSON_PRETTY_PRINT); + +// call_user_func with wrong constant +call_user_func('json_encode', [], SORT_REGULAR); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php new file mode 100644 index 00000000000..6acb0ba1876 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php @@ -0,0 +1,10 @@ += 8.0')] + public function testConstantParameterCheckMethods(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/constant-parameter-check-methods.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of method finfo::file().', + 10, + ], + [ + 'Constant PDO::ATTR_ERRMODE is not allowed for parameter #1 $mode of method PDOStatement::fetch().', + 17, + ], + [ + 'Constant Collator::FRENCH_COLLATION is not allowed for parameter #2 $flags of method Collator::sort().', + 25, + ], + ]); + } + public function testBug11073(): void { $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-11073.php'], []); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index d7dc2805345..59fd39b5eff 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1009,4 +1009,20 @@ public function testPipeOperator(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckStatic(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/constant-parameter-check-static.php'], [ + [ + 'Constant IntlDateFormatter::GREGORIAN is not allowed for parameter #2 $dateType of static method IntlDateFormatter::create().', + 9, + ], + [ + 'Constant NumberFormatter::TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php new file mode 100644 index 00000000000..b5ecc3f37c4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php @@ -0,0 +1,70 @@ + + */ +#[RequiresPhp('>= 8.0')] +class MethodCallWithPossiblyRenamedNamedArgumentRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, checkNullables: true, checkThisOnly: false, checkUnionTypes: true, checkExplicitMixed: true, checkImplicitMixed: false, checkBenevolentUnionTypes: false, discoveringSymbolsTip: true); + $phpVersion = self::getContainer()->getByType(PhpVersion::class); + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + + // @phpstan-ignore argument.type + return new CompositeRule([ + new CallMethodsRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true), + ), + new OverridingMethodRule( + $phpVersion, + new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), true, true), + false, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), + false, + ), + new MethodCallWithPossiblyRenamedNamedArgumentRule(), + ]); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/named-argument-renamed-parameter.php'], [ + [ + 'Call to NamedArgumentRenamedParameter\Foo::doFoo() uses named argument for parameter $a, but NamedArgumentRenamedParameter\Bar renames it to $b.', + 25, + ], + ]); + } + + public function testBug7434(): void + { + $this->analyse([__DIR__ . '/data/bug-7434.php'], [ + [ + 'Call to Bug7434\Contract::method() uses named argument for parameter $val, but Bug7434\ImplementationWithDifferentName renames it to $wrong.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7434.php b/tests/PHPStan/Rules/Methods/data/bug-7434.php new file mode 100644 index 00000000000..be1750fdfbc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7434.php @@ -0,0 +1,29 @@ +method(val: 'string'); +} diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php new file mode 100644 index 00000000000..7ecae9f3f89 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php @@ -0,0 +1,25 @@ +file('test.txt', FILEINFO_MIME_TYPE); + +// finfo::file - wrong constant +$finfo->file('test.txt', SORT_REGULAR); + +// PDOStatement::fetch - correct class constant +/** @var \PDOStatement $stmt */ +$stmt->fetch(\PDO::FETCH_ASSOC); + +// PDOStatement::fetch - wrong class constant +$stmt->fetch(\PDO::ATTR_ERRMODE); + +// Collator::sort - correct class constant +/** @var \Collator $collator */ +$arr = []; +$collator->sort($arr, \Collator::SORT_STRING); + +// Collator::sort - wrong class constant +$collator->sort($arr, \Collator::FRENCH_COLLATION); diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php new file mode 100644 index 00000000000..16b7298f55c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php @@ -0,0 +1,15 @@ += 8.0 + +declare(strict_types = 1); + +namespace NamedArgumentRenamedParameter; + +interface Foo +{ + + public function doFoo(string $a): void; + +} + +class Bar implements Foo +{ + + public function doFoo(string $b): void + { + + } + +} + +function (Foo $foo): void { + $foo->doFoo(a: 'a'); +}; diff --git a/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php b/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php new file mode 100644 index 00000000000..f0ca0570744 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php @@ -0,0 +1,61 @@ + + */ +final class ArrayDimCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ArrayDimCastRule(); + } + + public function testRule(): void + { + $tip = 'Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe'; + $this->analyse([__DIR__ . '/data/array-dim-fetch-cast.php'], [ + [ + "Key '1' (string) will be cast to 1 (int) in the array access.", + 13, + $tip, + ], + [ + "Key null (null) will be cast to '' (string) in the array access.", + 14, + $tip, + ], + [ + 'Key 2.5 (float) will be cast to 2 (int) in the array access.', + 15, + $tip, + ], + [ + 'Key true (bool) will be cast to 1 (int) in the array access.', + 17, + $tip, + ], + [ + 'Key false (bool) will be cast to 0 (int) in the array access.', + 18, + $tip, + ], + [ + "Key '10' (string) will be cast to 10 (int) in the array access.", + 20, + $tip, + ], + [ + "Key '1' (string) will be cast to 1 (int) in the array access.", + 26, + $tip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php b/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php new file mode 100644 index 00000000000..898e12a7321 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php @@ -0,0 +1,56 @@ + + */ +final class LiteralArrayKeyCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new LiteralArrayKeyCastRule(); + } + + public function testRule(): void + { + $tip = 'Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe'; + $this->analyse([__DIR__ . '/data/literal-array-key-cast.php'], [ + [ + "Key '1' (string) will be cast to 1 (int) in the array.", + 14, + $tip, + ], + [ + "Key null (null) will be cast to '' (string) in the array.", + 15, + $tip, + ], + [ + 'Key 2.5 (float) will be cast to 2 (int) in the array.', + 16, + $tip, + ], + [ + 'Key true (bool) will be cast to 1 (int) in the array.', + 18, + $tip, + ], + [ + 'Key false (bool) will be cast to 0 (int) in the array.', + 19, + $tip, + ], + [ + "Key '10' (string) will be cast to 10 (int) in the array.", + 21, + $tip, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php b/tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php new file mode 100644 index 00000000000..20dfa5a7021 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php @@ -0,0 +1,29 @@ + 1, + '+1' => 2, + '1' => 3, // cast to 1 + null => 4, // cast to '' + 2.5 => 5, // cast to 2 + '1.2' => 6, + true => 7, // cast to 1 + false => 8, // cast to 0 + '08' => 9, + $partiallyCast => 10, // one part of the union is cast to 10 + ]; + } + +} diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index d07d72d48af..801b2d1772e 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -185,4 +185,63 @@ public function testSetInvalidValue(): void $this->assertInstanceOf(ErrorType::class, $result); } + public static function dataIsDecimalIntegerString(): iterable + { + yield [ + '0', + TrinaryLogic::createYes(), + ]; + yield [ + '1', + TrinaryLogic::createYes(), + ]; + yield [ + '1234', + TrinaryLogic::createYes(), + ]; + yield [ + '-1', + TrinaryLogic::createYes(), + ]; + yield [ + '+1', + TrinaryLogic::createNo(), + ]; + yield [ + '00', + TrinaryLogic::createNo(), + ]; + yield [ + '01', + TrinaryLogic::createNo(), + ]; + yield [ + '18E+3', + TrinaryLogic::createNo(), + ]; + yield [ + '1.2', + TrinaryLogic::createNo(), + ]; + yield [ + '1,3', + TrinaryLogic::createNo(), + ]; + yield [ + 'foo', + TrinaryLogic::createNo(), + ]; + yield [ + '1foo', + TrinaryLogic::createNo(), + ]; + } + + #[DataProvider('dataIsDecimalIntegerString')] + public function testIsDecimalIntegerString(string $value, TrinaryLogic $expected): void + { + $type = new ConstantStringType($value); + $this->assertSame($expected->describe(), $type->isDecimalIntegerString()->describe()); + } + } diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index e3ed23eb46b..a10aa80ee49 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -8,6 +8,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; @@ -747,6 +748,30 @@ public static function dataDescribe(): iterable VerbosityLevel::precise(), 'uppercase-string', ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), + VerbosityLevel::typeOnly(), + 'string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + VerbosityLevel::value(), + 'decimal-int-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), + VerbosityLevel::value(), + 'non-decimal-int-string', + ]; } #[DataProvider('dataDescribe')] diff --git a/tests/PHPStan/Type/StringTypeTest.php b/tests/PHPStan/Type/StringTypeTest.php index 3be9e03240a..205f6a81c0b 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -5,6 +5,7 @@ use Exception; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; @@ -173,12 +174,87 @@ public static function dataAccepts(): iterable )->toArgument(), TrinaryLogic::createYes(), ]; + + $decimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + $nonDecimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + + yield [ + $decimalIntString, + new ConstantStringType('1'), + TrinaryLogic::createYes(), + ]; + yield [ + $decimalIntString, + $decimalIntString, + TrinaryLogic::createYes(), + ]; + yield [ + $decimalIntString, + $nonDecimalIntString, + TrinaryLogic::createNo(), + ]; + yield [ + $nonDecimalIntString, + $decimalIntString, + TrinaryLogic::createNo(), + ]; + yield [ + $decimalIntString, + new StringType(), + TrinaryLogic::createMaybe(), + ]; + yield [ + $decimalIntString, + new ConstantStringType('10'), + TrinaryLogic::createYes(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('10'), + TrinaryLogic::createNo(), + ]; + yield [ + $decimalIntString, + new ConstantStringType('foo'), + TrinaryLogic::createNo(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('foo'), + TrinaryLogic::createYes(), + ]; + yield [ + $nonDecimalIntString, + new StringType(), + TrinaryLogic::createMaybe(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('1'), + TrinaryLogic::createNo(), + ]; + yield [ + $decimalIntString, + new ConstantStringType('+1'), + TrinaryLogic::createNo(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('+1'), + TrinaryLogic::createYes(), + ]; } #[DataProvider('dataAccepts')] public function testAccepts(Type $stringType, Type $otherType, TrinaryLogic $expectedResult): void { - $this->assertInstanceOf(StringType::class, $stringType); + $this->assertSame('Yes', $stringType->isString()->describe()); $actualResult = $stringType->accepts($otherType, true)->result; $this->assertSame( diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 6c51dbcd9b1..6a04347b13d 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -20,6 +20,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -2821,6 +2822,76 @@ public static function dataUnion(): iterable ObjectType::class, $nonFinalClass->getDisplayName(), ]; + + $decimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + $nonDecimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + + yield [ + [ + $decimalIntString, + new StringType(), + ], + StringType::class, + 'string', + ]; + yield [ + [ + $nonDecimalIntString, + new StringType(), + ], + StringType::class, + 'string', + ]; + yield [ + [ + $decimalIntString, + $nonDecimalIntString, + ], + StringType::class, + 'string', + ]; + + yield [ + [ + $decimalIntString, + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + ], + IntersectionType::class, + 'numeric-string', + ]; + + yield [ + [ + $nonDecimalIntString, + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + ], + StringType::class, + 'string', + ]; + + yield [ + [ + $decimalIntString, + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string', + ]; + + yield [ + [ + $nonDecimalIntString, + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ]; } /** @@ -4933,6 +5004,67 @@ public static function dataIntersect(): iterable TemplateIntersectionType::class, 'T of Countable&Iterator (function a(), parameter)', ]; + + yield [ + [ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(), + ], + IntersectionType::class, + 'decimal-int-string', + ]; + + yield [ + [ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ], + IntersectionType::class, + 'non-decimal-int-string&numeric-string', + ]; + + $decimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + $nonDecimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + + yield [ + [ + $decimalIntString, + $nonDecimalIntString, + ], + NeverType::class, + '*NEVER*=implicit', + ]; + + yield [ + [ + $decimalIntString, + new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + ]), + ], + IntersectionType::class, + 'decimal-int-string', + ]; + yield [ + [ + $nonDecimalIntString, + new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + ]), + ], + IntersectionType::class, + 'lowercase-string&non-decimal-int-string', + ]; } /** diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 3cf9e5f3fa6..fcc6c6d0cf9 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -6,6 +6,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -522,6 +523,16 @@ public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable new ConstantFloatType(-0.0), '-0.0', ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + 'decimal-int-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), + 'non-decimal-int-string', + ]; } #[DataProvider('dataToPhpDocNodeWithoutCheckingEquals')]