Compare commits

...

119 Commits
6.3.2 ... main

Author SHA1 Message Date
Sebastian Bergmann 642e2bbda7
Introduce Exporter::hasCustomRepresentationFor() for checking whether an object exporter provides the representation for an object 2026-08-01 09:45:25 +02:00
Sebastian Bergmann 1ff285c572
Consult custom object exporter when exporting a value into a single-line string 2026-08-01 09:27:34 +02:00
Sebastian Bergmann 60d658c5b5
Fix formatting 2026-08-01 09:14:52 +02:00
Sebastian Bergmann 58c91cd888
Document that exporting a value can throw ObjectNotSupportedException 2026-08-01 09:13:57 +02:00
Sebastian Bergmann 792150d027
Accept any object exporter, not just a chain, in Exporter's constructor 2026-08-01 09:11:45 +02:00
Sebastian Bergmann b5aff00a52
Update documentation 2026-08-01 09:09:00 +02:00
Sebastian Bergmann 067b4fde58
Ask custom object exporter for representation of enum before using default representation 2026-08-01 08:57:29 +02:00
Sebastian Bergmann b9ec7d93fe
Prevent duplicate array references and endless recursion when a custom object exporter exports nested values 2026-08-01 08:49:42 +02:00
Sebastian Bergmann 5f9dc382bf
Ask custom object exporter for representation before consulting recursion context 2026-07-30 07:56:57 +02:00
renovate[bot] 7e531fa24d Update dependency tomasvotruba/type-coverage to ^2.3.0 2026-07-30 04:57:56 +00:00
renovate[bot] 4f08958e05 Update dependency phpstan/phpstan to ^2.2.7 2026-07-30 01:28:08 +00:00
renovate[bot] d3ff5fec0e Update dependency phpstan/phpstan to ^2.2.6 2026-07-27 02:14:17 +00:00
renovate[bot] d3cc5ab80b Update actions/checkout action to v7.0.1 2026-07-25 21:50:59 +00:00
renovate[bot] 2f54d7b25f Update dependency phpstan/phpstan-strict-rules to ^2.0.12 2026-07-19 16:09:14 +00:00
Sebastian Bergmann 3f33ba4c67
Introduce ObjectExporter interface and ObjectExporterChain for customizing how objects are exported 2026-07-13 13:48:57 +02:00
Sebastian Bergmann cfaa77c750
Prepare release 2026-07-13 13:35:11 +02:00
renovate[bot] 020a76a804 Update dependency phpunit/phpunit to ^13.2.4 2026-07-08 15:56:21 +00:00
renovate[bot] 80e37628a9 Update dependency phpunit/phpunit to ^13.2.3 2026-07-06 20:51:12 +00:00
renovate[bot] 4864aa2dac Update dependency phpstan/phpstan to ^2.2.5 2026-07-05 13:09:34 +00:00
renovate[bot] 33eb9b136b Update dependency phpstan/phpstan to ^2.2.4 2026-07-03 15:41:22 +00:00
renovate[bot] 17e1967798 Update dependency phpstan/phpstan to ^2.2.3 2026-07-01 00:30:26 +00:00
renovate[bot] 2187505ead Update dependency phpunit/phpunit to ^13.2.2 2026-06-30 19:29:27 +00:00
renovate[bot] 38122ad449 Update actions/cache action to v6.1.0 2026-06-27 02:01:59 +00:00
renovate[bot] 6218f433e8 Update github-actions 2026-06-23 20:53:54 +00:00
renovate[bot] 1b6cb53e74 Update github-actions to v7 2026-06-22 08:13:07 +02:00
renovate[bot] 380e0104f7 Update dependency phpunit/phpunit to ^13.2.1 2026-06-16 05:56:13 +00:00
renovate[bot] f202473cb1 Update github-actions to v2.37.2 2026-06-08 21:49:52 +00:00
renovate[bot] 39cb211d31 Update dependency tomasvotruba/type-coverage to ^2.2.2 2026-06-07 17:38:35 +00:00
renovate[bot] 862a07a930 Update github-actions to v7 2026-06-07 13:15:21 +00:00
renovate[bot] 183b82cc23 Update github-actions to v6.0.2 2026-06-07 08:28:19 +00:00
renovate[bot] 7f308faee2 Update PHPStan packages to ^2.2.2 2026-06-05 15:11:51 +00:00
renovate[bot] a2fd7eabd9 Update dependency phpunit/phpunit to ^13.2.0 2026-06-05 10:43:04 +00:00
renovate[bot] 94a4f8ce9b Update github-actions to v1.0.1 2026-06-04 13:51:03 +00:00
renovate[bot] 40dc091991 Update dependency phpunit/phpunit to ^13.1.14 2026-06-04 13:50:46 +00:00
renovate[bot] 4f8268a02a Update github-actions to v6.0.3 2026-06-02 22:55:11 +00:00
renovate[bot] 8839e93dba Update dependency phpstan/phpstan to ^2.2.1 2026-05-29 00:05:35 +00:00
renovate[bot] a97aec49ff Update dependency phpunit/phpunit to ^13.1.13 2026-05-27 21:27:14 +00:00
renovate[bot] e43b042d47 Update dependency tomasvotruba/type-coverage to ^2.2.1 2026-05-27 05:53:35 +00:00
Sebastian Bergmann 830d67d80f
Also automerge minor updates 2026-05-27 07:51:02 +02:00
renovate[bot] 570a3312f9 Update dependency phpstan/phpstan to ^2.1.56 2026-05-27 02:40:02 +00:00
renovate[bot] 94572e37dc Update dependency phpunit/phpunit to ^13.1.12 2026-05-25 22:15:14 +00:00
Sebastian Bergmann b54c4ddf23
Treat lone LF followed by lone CR as two separate line endings 2026-05-23 06:27:19 +02:00
renovate[bot] fb73a7e90f Update dependency phpunit/phpunit to ^13.1.11 2026-05-21 18:38:36 +00:00
Sebastian Bergmann c0d29a945f
Prepare release 2026-05-21 13:50:56 +02:00
Sebastian Bergmann 78d76615cd
Bump 2026-05-21 13:48:12 +02:00
Sebastian Bergmann 1f94fb33ae
Update ChangeLog 2026-05-21 13:47:24 +02:00
Sebastian Bergmann 4a11e3aea7
Add tests 2026-05-21 13:39:57 +02:00
Sebastian Bergmann c3c99d6629
Make binary string output readable for mostly-printable values 2026-05-20 08:16:59 +02:00
Sebastian Bergmann 41cdd17bf0
Closes #57 2026-05-20 07:48:57 +02:00
Sebastian Bergmann 08b965bdef
Merge branch '7.0'
* 7.0:
  Fix package name
2026-05-20 06:53:00 +02:00
Sebastian Bergmann 7028549592
Fix package name 2026-05-20 06:52:54 +02:00
Sebastian Bergmann 9b54f1afab
Prepare release 2026-05-20 06:38:47 +02:00
Sebastian Bergmann 8baf36f516
Merge branch '7.0'
* 7.0:
  Prepare release
2026-05-20 06:37:56 +02:00
Sebastian Bergmann c5e21b5de6
Prepare release 2026-05-20 06:37:17 +02:00
Sebastian Bergmann 3825c739b1
Merge branch '7.0'
* 7.0:
  Closes #90
  Update github-actions
  Update codecov/codecov-action digest to e79a696
  Update dependency phpstan/phpstan to ^2.1.55
2026-05-20 06:35:46 +02:00
Sebastian Bergmann d5f1896389
Closes #90 2026-05-20 06:34:14 +02:00
renovate[bot] 99dffe1f0c Update github-actions 2026-05-19 09:54:58 +00:00
renovate[bot] de8ee89f26 Update github-actions 2026-05-19 09:54:36 +00:00
Sebastian Bergmann 59eac409ce
More work on Renovate configuration 2026-05-19 07:03:06 +02:00
renovate[bot] 33ad39f854 Update codecov/codecov-action digest to e79a696 2026-05-19 06:16:29 +02:00
renovate[bot] cb6a7c4ba1 Update codecov/codecov-action digest to e79a696 2026-05-19 06:16:24 +02:00
Sebastian Bergmann 4d6693391a
Use tagged version 2026-05-18 17:44:37 +02:00
renovate[bot] 1193484136 Update composer/api-surface-check digest to c630883 2026-05-18 16:10:19 +02:00
renovate[bot] 916a6e14f3 Update dependency phpstan/phpstan to ^2.1.55 2026-05-18 15:00:51 +02:00
renovate[bot] 24d760418a Update dependency phpstan/phpstan to ^2.1.55 2026-05-18 15:00:49 +02:00
Sebastian Bergmann 249d162f1a
Merge branch '7.0'
* 7.0:
  Harden workflows
  Update github-actions to v6
  Update dependency phpunit/phpunit to ^12.5.25
  Update dependency sebastian/recursion-context to ^7.0.1
  Pin dependencies
2026-05-17 08:35:28 +02:00
Sebastian Bergmann 78c7e55c00
Harden workflows 2026-05-17 08:34:52 +02:00
renovate[bot] 7e38e043ea Update github-actions to v6 2026-05-16 18:25:12 +02:00
renovate[bot] ad1c70765c Update dependency phpunit/phpunit to ^12.5.25 2026-05-16 17:03:30 +02:00
renovate[bot] e90dba91d9 Update dependency sebastian/recursion-context to ^7.0.1 2026-05-16 17:03:24 +02:00
renovate[bot] baa9a4396f Pin dependencies 2026-05-16 10:43:17 +02:00
renovate[bot] 894227552c Update github-actions to v6 2026-05-16 10:42:12 +02:00
Sebastian Bergmann c204fd5a4f
Ignore renovate.json for export 2026-05-16 06:58:38 +02:00
renovate[bot] bf162ac591 Update dependency phpunit/phpunit to ^13.1.10 2026-05-16 06:46:13 +02:00
renovate[bot] b396512ed8 Pin dependencies 2026-05-16 06:40:18 +02:00
Sebastian Bergmann a26cf06434
Configure Renovate 2026-05-16 06:25:47 +02:00
Sebastian Bergmann 8d0e9c4c32
Merge branch '7.0'
* 7.0:
  Update tools
  Do not use Phive to manage Composer
2026-05-13 13:32:02 +02:00
Sebastian Bergmann 46fbf4d175
Update tools 2026-05-13 13:31:29 +02:00
Sebastian Bergmann 150ed9dd55
Do not use Phive to manage Composer 2026-05-13 13:31:16 +02:00
Sebastian Bergmann b11215d971
Merge branch '7.0'
* 7.0:
  Harden workflows
2026-05-13 08:23:02 +02:00
Sebastian Bergmann d26874f911
Harden workflows 2026-05-13 08:22:56 +02:00
Sebastian Bergmann e8c1311f17
Set up API surface check for pull requests 2026-05-01 08:39:35 +02:00
Sebastian Bergmann 439f0be9b9
Update security policy 2026-05-01 08:33:37 +02:00
Sebastian Bergmann 9cee180ebe
Prepare release 2026-04-15 14:38:05 +02:00
USAMI Kenta 4d141ea52d Avoid int casts when exporting floats
Avoid runtime warnings for large float values by removing the
int-cast-based check and using a simpler string-based check instead.
2026-04-15 14:35:39 +02:00
Sebastian Bergmann 40801a527c
Prepare release 2026-04-10 14:56:23 +02:00
Sebastian Bergmann 3367e85396
Explicitly handle NAN, INF, and -INF 2026-04-10 14:32:24 +02:00
Sebastian Bergmann 9daa802412
Merge branch '7.0'
* 7.0:
  Do not interpolate GitHub Actions expressions in shell scripts
2026-04-09 07:19:02 +02:00
Sebastian Bergmann 2308f4f1ce
Do not interpolate GitHub Actions expressions in shell scripts 2026-04-09 07:19:00 +02:00
Sebastian Bergmann dc31f1f8e0
Prepare release 2026-02-06 05:44:28 +01:00
Sebastian Bergmann abc7f44aa8
Refactor 2026-01-01 09:52:33 +01:00
Sebastian Bergmann 968ad065d7
Remove skip condition that is no longer needed 2025-12-31 17:45:07 +01:00
Sebastian Bergmann 2f6cbc5b83
Start development of next major version 2025-12-31 17:07:52 +01:00
Sebastian Bergmann e0408a38b0
Bump copyright year 2025-12-31 14:59:57 +01:00
Sebastian Bergmann a5d484ef4f
Update GitHub Actions 2025-12-31 14:59:49 +01:00
Sebastian Bergmann 69a57cf8d4
Update tools 2025-12-31 14:59:27 +01:00
Sebastian Bergmann d4d9bba3c5
Merge branch '6.3'
* 6.3:
  Sync with CI pipeline configuration for PHPUnit
  Update tools
2025-09-26 13:25:33 +02:00
Sebastian Bergmann c939676885
Merge branch '5.1' into 6.3
* 5.1:
  Sync with CI pipeline configuration for PHPUnit
  Update tools
2025-09-26 13:23:57 +02:00
Sebastian Bergmann 674f8e4778
Sync with CI pipeline configuration for PHPUnit
* Remove superfluous double-quotes
* Configure timeout for jobs
* Do not fetch tags
* Cache Composer's cache directory
* Introduce PHP_VERSION variable
* Introduce separate job for code coverage
* Test with PHP 8.6
2025-09-26 13:20:41 +02:00
Sebastian Bergmann e15d3843bb
Update tools 2025-09-26 13:20:08 +02:00
Sebastian Bergmann 016951ae10
Prepare release 2025-09-24 08:16:11 +02:00
Sebastian Bergmann 6eefe702f2
Merge branch '6.3'
* 6.3:
  Prepare release
  Prepare release
  Prepare release
  Suppress warning
  Suppress warning
  Use SplObjectStorage::offsetSet() instead of deprecated SplObjectStorage::attach()
2025-09-24 08:15:29 +02:00
Sebastian Bergmann b759164a8e
Prepare release 2025-09-22 07:39:29 +02:00
Sebastian Bergmann 5c10a78583
Merge branch '6.3'
* 6.3:
  Prepare release
  Ignore error
  Use SplObjectStorage::offsetSet() instead of deprecated SplObjectStorage::attach()
  Suppress warning
  Update tools
  Prepare release
  Use SplObjectStorage::offsetSet() instead of deprecated SplObjectStorage::attach()
  Suppress warning
  Prepare release
  Test with more versions
  Do not cache Composer dependencies
  Suppress warning
  Do not use implicitly nullable parameters and prepare release
  Do not use implicitly nullable parameters and prepare release
2025-09-22 07:36:56 +02:00
Sebastian Bergmann 0ef7d66753
Update codecov.io configuration 2025-05-25 08:40:56 +02:00
Sebastian Bergmann e935ca93a8
Remove superfluous double-quotes 2025-05-25 08:40:45 +02:00
Sebastian Bergmann a7adaa1c3b
Sync with CONTRIBUTING.md and CODE_OF_CONDUCT.md from sebastianbergmann/phpunit 2025-05-21 17:25:43 +02:00
Sebastian Bergmann 7426ded03b
Add thanks.dev 2025-04-13 09:26:46 +02:00
Sebastian Bergmann d4c79356c6
Add Liberapay and Tidelift 2025-04-13 08:57:57 +02:00
Sebastian Bergmann def9f76157
Declare class final 2025-03-17 17:17:26 +01:00
Sebastian Bergmann cb499f5074
Ignore cast.useless error 2025-03-17 17:17:26 +01:00
Sebastian Bergmann a3bb1f01c1
Assert that the element exists 2025-03-17 17:17:26 +01:00
Sebastian Bergmann 515a304e30
Use stricter comparison 2025-03-17 17:17:26 +01:00
Sebastian Bergmann 1bd365aa5f
* Install PHPStan using Composer in order to be able to use phpstan/phpstan-strict-rules and tomasvotruba/type-coverage
* Configure phpstan/phpstan-strict-rules, tomasvotruba/type-coverage, and ergebnis/phpstan-rules
* Update PHPStan configuration
* Put Phive binary under version control and manage it through "ant update-tools"
2025-03-17 17:17:21 +01:00
Sebastian Bergmann ab05de4ae0
Merge branch '6.3' 2025-03-17 17:04:23 +01:00
Sebastian Bergmann da1133d8d6
Merge branch '6.3' 2025-03-11 08:14:44 +01:00
Sebastian Bergmann 76432aafc5
Prepare release 2025-02-07 05:56:42 +01:00
Sebastian Bergmann 11b6f654a9
Update PHPUnit configuration 2025-01-18 09:13:29 +01:00
Sebastian Bergmann 17e38c7a60
Start development of next major version 2025-01-16 09:54:00 +01:00
407 changed files with 32411 additions and 282 deletions

1
.gitattributes vendored
View File

@ -8,6 +8,7 @@
/infection.json5.dist export-ignore
/phpstan.neon export-ignore
/phpunit.xml export-ignore
/renovate.json export-ignore
/tests export-ignore
/tools export-ignore
/tools/* binary

28
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,28 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic
addresses, without explicit permission
* Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at sebastian@phpunit.de. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View File

@ -7,7 +7,10 @@ We look forward to your contributions! Here are some examples how you can contri
* [Report a bug](https://github.com/sebastianbergmann/exporter/issues/new)
* [Send a pull request to fix a bug](https://github.com/sebastianbergmann/exporter/pulls)
Please do not send pull requests that expand the scope of this project (see below).
## We have a Code of Conduct
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
## Any contributions you make will be under the BSD-3-Clause License
@ -15,6 +18,24 @@ Please do not send pull requests that expand the scope of this project (see belo
When you submit code changes, your submissions are understood to be under the same [BSD-3-Clause License](https://github.com/sebastianbergmann/exporter/blob/main/LICENSE) that covers the project. By contributing to this project, you agree that your contributions will be licensed under its BSD-3-Clause License.
### Do Not Violate Copyright
Only submit a pull request with your own original code. Do NOT submit a pull request containing code which you have largely copied from
another project, unless you wrote the respective code yourself.
Open Source does not mean that copyright does not apply. Copyright infringements will not be tolerated and can lead to you being banned from this project and repository.
### Do Not Submit AI-Generated Pull Requests
The same goes for (largely) AI-generated pull requests. These are not welcome as they will be based on copyrighted code from others
without accreditation and without taking the license of the original code into account, let alone getting permission
for the use of the code or for re-licensing.
Aside from that, the experience is that AI-generated pull requests will be incorrect 100% of the time and cost reviewers too much time.
Submitting a (largely) AI-generated pull request will lead to you being banned from this project and repository.
## Write bug reports with detail, background, and sample code
[This is an example](https://github.com/sebastianbergmann/phpunit/issues/4376) of a bug report I wrote, and I think it's not too bad.
@ -29,13 +50,17 @@ In your bug report, please provide the following:
* What actually happens
* Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
Please do not report a bug for a version of this library that is no longer supported. Please do not report a bug if you are using a version of PHP that is not supported by the version of this library you are using.
The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. Support for this library follows the [support for the version of PHPUnit that uses a specific version of this library](https://phpunit.de/supported-versions.html).
Please post code and output as text ([using proper markup](https://guides.github.com/features/mastering-markdown/)). Do not post screenshots of code or output.
## Workflow for Pull Requests
1. Fork the repository.
2. Create your branch from the oldest branch that is affected by the bug you plan to fix.
2. Create your branch from `main` if you plan to implement new functionality or change existing code significantly; create your branch from the oldest branch that is affected by the bug if you plan to fix a bug.
3. Implement your change and add tests for it.
4. Ensure the test suite passes.
5. Ensure the code complies with our coding guidelines (see below).
@ -45,6 +70,12 @@ Please make sure you have [set up your username and email address](https://git-s
We encourage you to [sign your Git commits with your GPG key](https://docs.github.com/en/github/authenticating-to-github/signing-commits).
Pull requests for bug fixes must be made for the oldest branch that is supported (see above). Pull requests for new features must be based on the `main` branch.
We are trying to keep backwards compatibility breaks to an absolute minimum. Please take this into account when proposing changes.
Due to time constraints, we are not always able to respond as quickly as we would like. Please do not take delays personal and feel free to remind us if you feel that we forgot to respond.
## Development

3
.github/FUNDING.yml vendored
View File

@ -1 +1,4 @@
github: sebastianbergmann
liberapay: sebastianbergmann
thanks_dev: u/gh/sebastianbergmann
tidelift: "packagist/sebastian/exporter"

View File

@ -0,0 +1,26 @@
# https://github.com/marketplace/actions/api-surface-check-for-php
on:
pull_request:
paths-ignore:
- ".editorconfig"
- "LICENSE"
- "**.md"
- "**.png"
- "**.svg"
- "tests/**"
name: "API Surface Check"
permissions: {}
concurrency:
group: api-surface-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
signal:
name: Signal
runs-on: ubuntu-latest
steps:
- run: "true"

View File

@ -0,0 +1,27 @@
# https://github.com/marketplace/actions/api-surface-check-for-php
on:
# zizmor: ignore[dangerous-triggers] workflow_run is required to comment on
# PRs from forks; this workflow only runs a pinned action and does not check
# out PR-controlled code.
workflow_run:
workflows: ["API Surface Check"]
types: [completed]
name: "API Surface Comment"
permissions:
contents: read
concurrency:
group: api-surface-comment-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
check-api-surface:
name: Check API Surface
runs-on: ubuntu-latest
permissions:
pull-requests: write # required to post the API surface diff comment on the PR
steps:
- uses: composer/api-surface-check@bba9f1cb940df6e02271b1b6158679ac0b3f39d3 # 1.0.1

243
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,243 @@
# https://docs.github.com/en/actions
on:
- pull_request
- push
name: CI
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COMPOSER_ROOT_VERSION: 8.2.x-dev
PHP_VERSION: 8.4
permissions:
contents: read
jobs:
coding-guidelines:
name: Coding Guidelines
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Use local branch
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
BRANCH="$HEAD_REF"
else
BRANCH="$REF_NAME"
fi
git branch -D "$BRANCH" 2>/dev/null || true
git branch "$BRANCH" HEAD
git checkout "$BRANCH"
- name: Install PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ env.PHP_VERSION }}
coverage: none
tools: none
- name: Run PHP-CS-Fixer
run: ./tools/php-cs-fixer fix --dry-run --show-progress=dots --using-cache=no --verbose
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Use local branch
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
BRANCH="$HEAD_REF"
else
BRANCH="$REF_NAME"
fi
git branch -D "$BRANCH" 2>/dev/null || true
git branch "$BRANCH" HEAD
git checkout "$BRANCH"
- name: Install PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ env.PHP_VERSION }}
coverage: none
tools: none
- name: Install dependencies with Composer
run: ./tools/composer update --no-interaction --no-ansi --no-progress
- name: Run PHPStan
run: ./tools/phpstan analyse --no-progress --error-format=github
tests:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
fail-fast: false
matrix:
php-version:
- 8.4
- 8.5
- 8.6
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Use local branch
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
BRANCH="$HEAD_REF"
else
BRANCH="$REF_NAME"
fi
git branch -D "$BRANCH" 2>/dev/null || true
git branch "$BRANCH" HEAD
git checkout "$BRANCH"
- name: Install PHP with extensions
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php-version }}
coverage: none
tools: none
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: |
echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- name: Cache Composer cache directory
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies with Composer
run: ./tools/composer update --no-ansi --no-interaction --no-progress
- name: Run tests with PHPUnit
run: vendor/bin/phpunit
code-coverage:
name: Code Coverage
needs:
- tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Use local branch
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
BRANCH="$HEAD_REF"
else
BRANCH="$REF_NAME"
fi
git branch -D "$BRANCH" 2>/dev/null || true
git branch "$BRANCH" HEAD
git checkout "$BRANCH"
- name: Install PHP with extensions
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php-version }}
coverage: xdebug
tools: none
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: |
echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- name: Cache Composer cache directory
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies with Composer
run: ./tools/composer update --no-ansi --no-interaction --no-progress
- name: Collect code coverage with PHPUnit
run: vendor/bin/phpunit --log-junit test-results.xml --coverage-openclover=code-coverage.xml
- name: Upload test results to Codecov.io
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
disable_search: true
files: ./test-results.xml
- name: Upload code coverage data to Codecov.io
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: coverage
disable_search: true
files: ./code-coverage.xml

View File

@ -1,94 +0,0 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
on:
- "pull_request"
- "push"
name: "CI"
env:
COMPOSER_ROOT_VERSION: "6.3.x-dev"
permissions:
contents: read
jobs:
coding-guidelines:
name: Coding Guidelines
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
- name: Run PHP-CS-Fixer
run: ./tools/php-cs-fixer fix --dry-run --show-progress=dots --using-cache=no --verbose
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
- name: Install dependencies with Composer
run: ./tools/composer update --no-interaction --no-ansi --no-progress
- name: Run PHPStan
run: ./tools/phpstan analyse --no-progress --error-format=github
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version:
- "8.2"
- "8.3"
- "8.4"
- "8.5"
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
- name: "Install PHP with extensions"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
coverage: "xdebug"
- name: "Install dependencies with Composer"
run: "./tools/composer update --no-ansi --no-interaction --no-progress"
- name: "Run tests with PHPUnit"
run: "vendor/bin/phpunit --log-junit junit.xml --coverage-clover=coverage.xml"
- name: Upload test results to Codecov.io
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload code coverage data to Codecov.io
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -7,6 +7,13 @@ on:
name: Release
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
release:
name: Release
@ -14,14 +21,16 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
contents: write # required to create GitHub releases
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: 8.3
coverage: none
@ -29,15 +38,12 @@ jobs:
tools: none
- name: Determine tag
run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV"
- name: Parse ChangeLog
run: build/scripts/extract-release-notes.php ${{ env.RELEASE_TAG }} > release-notes.md
run: build/scripts/extract-release-notes.php "${RELEASE_TAG}" > release-notes.md
- name: Create release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.RELEASE_TAG }}
name: sebastian/exporter ${{ env.RELEASE_TAG }}
bodyFile: release-notes.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create "${RELEASE_TAG}" --title "sebastian/exporter ${RELEASE_TAG}" --notes-file release-notes.md

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="php-cs-fixer" version="^3.71" installed="3.87.2" location="./tools/php-cs-fixer" copy="true"/>
<phar name="composer" version="^2.8" installed="2.8.12" location="./tools/composer" copy="true"/>
<phar name="php-cs-fixer" version="^3.71" installed="3.95.1" location="./tools/php-cs-fixer" copy="true"/>
<phar name="infection" version="^0.29" installed="0.29.14" location="./tools/infection" copy="true"/>
<phar name="phpstan" version="^2.1" installed="2.1.28" location="./tools/phpstan" copy="true"/>
</phive>

View File

@ -2,28 +2,82 @@
All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
## [6.3.2] - 2025-09-24
## [8.2.0] - 2026-08-07
### Added
* `ObjectExporter` interface, `ObjectExporterChain`, and `ExportContext` for customizing how objects are exported (an object exporter can be passed to `Exporter`'s constructor and is consulted before the default representation of an object is used)
* `Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` use the representation provided by an object exporter as well; it is collapsed to a single line and shortened when it is longer than the configured maximum length
* `Exporter::hasCustomRepresentationFor()` for checking whether an object exporter provides the representation for an object
## [8.1.1] - 2026-07-13
### Fixed
* A lone `\n` (LF) followed by a lone `\r` (CR) was incorrectly treated as a single `\n\r` line ending when exporting a string; these are now treated as two separate line endings
## [8.1.0] - 2026-05-21
### Changed
* [#57](https://github.com/sebastianbergmann/exporter/issues/57): Preserve `SplObjectStorage` iterator position instead of resetting to first element
* [#91](https://github.com/sebastianbergmann/exporter/pull/91): Make binary string output readable for mostly-printable values
## [8.0.3] - 2026-05-20
### Fixed
* [#90](https://github.com/sebastianbergmann/exporter/issues/90): `Exporter::toArray()` silently drops a private property that is redeclared in a derived class
## [8.0.2] - 2026-04-15
* [#74](https://github.com/sebastianbergmann/exporter/pull/74): Int cast warning when exporting large floats
## [8.0.1] - 2026-04-10
### Changed
* Explicitly handle `NAN`, `INF`, and `-INF` in `exportFloat()`
## [8.0.0] - 2026-02-06
### Removed
* This component is no longer supported on PHP 8.3
## [7.0.3] - 2026-05-20
### Fixed
* [#90](https://github.com/sebastianbergmann/exporter/issues/90): `Exporter::toArray()` silently drops a private property that is redeclared in a derived class
## [7.0.2] - 2025-09-24
### Changed
* Suppress `unexpected NAN value was coerced to string` warning triggered on PHP 8.5
## [6.3.1] - 2025-09-22
## [7.0.1] - 2025-09-22
### Changed
* Suppress `not representable as an int, cast occurred` warning triggered on PHP 8.5
## [6.3.0] - 2024-12-05
## [7.0.0] - 2025-02-07
### Added
### Removed
* Optional constructor argument to control maximum string length
* This component is no longer supported on PHP 8.2
### Deprecated
* Optional argument for `shortenedRecursiveExport()` and `shortenedExport()` to control maximum string length
[6.3.2]: https://github.com/sebastianbergmann/exporter/compare/6.3.1...6.3.2
[6.3.1]: https://github.com/sebastianbergmann/exporter/compare/6.3.0...6.3.1
[6.3.0]: https://github.com/sebastianbergmann/exporter/compare/6.2.0...6.3.0
[8.2.0]: https://github.com/sebastianbergmann/exporter/compare/8.1.1...main
[8.1.1]: https://github.com/sebastianbergmann/exporter/compare/8.1.0...8.1.1
[8.1.0]: https://github.com/sebastianbergmann/exporter/compare/8.0.3...8.1.0
[8.0.3]: https://github.com/sebastianbergmann/exporter/compare/8.0.2...8.0.3
[8.0.2]: https://github.com/sebastianbergmann/exporter/compare/8.0.1...8.0.2
[8.0.1]: https://github.com/sebastianbergmann/exporter/compare/8.0.0...8.0.1
[8.0.0]: https://github.com/sebastianbergmann/exporter/compare/7.0...8.0.0
[7.0.3]: https://github.com/sebastianbergmann/exporter/compare/7.0.2...7.0
[7.0.3]: https://github.com/sebastianbergmann/exporter/compare/7.0.2...7.0.3
[7.0.2]: https://github.com/sebastianbergmann/exporter/compare/7.0.1...7.0.2
[7.0.1]: https://github.com/sebastianbergmann/exporter/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/sebastianbergmann/exporter/compare/6.3...7.0.0

View File

@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2002-2025, Sebastian Bergmann
Copyright (c) 2002-2026, Sebastian Bergmann
All rights reserved.
Redistribution and use in source and binary forms, with or without

256
README.md
View File

@ -25,19 +25,19 @@ composer require --dev sebastian/exporter
Exporting:
```php
<?php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
/*
Exception Object &0000000078de0f0d000000002003a261 (
'message' => ''
'string' => ''
'code' => 0
'file' => '/home/sebastianbergmann/test.php'
'line' => 34
'previous' => null
Exception Object #4 (
'message' => '',
'string' => '',
'code' => 0,
'file' => '/home/sebastianbergmann/test.php',
'line' => 34,
'previous' => null,
)
*/
@ -49,7 +49,7 @@ print $exporter->export(new Exception);
Exporting simple types:
```php
<?php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
@ -85,64 +85,66 @@ print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5));
Exporting complex types:
```php
<?php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
/*
Array &0 (
0 => Array &1 (
0 => 1
1 => 2
2 => 3
)
1 => Array &2 (
0 => ''
1 => 0
2 => false
)
)
Array &0 [
0 => Array &1 [
0 => 1,
1 => 2,
2 => 3,
],
1 => Array &2 [
0 => '',
1 => 0,
2 => false,
],
]
*/
print $exporter->export(array(array(1,2,3), array("",0,FALSE)));
print $exporter->export([[1, 2, 3], ['', 0, false]]);
/*
Array &0 (
'self' => Array &1 (
'self' => Array &1
)
)
Array &0 [
'self' => Array &1 [
'self' => Array &1,
],
]
*/
$array = array();
$array = [];
$array['self'] = &$array;
print $exporter->export($array);
/*
stdClass Object &0000000003a66dcc0000000025e723e2 (
'self' => stdClass Object &0000000003a66dcc0000000025e723e2
stdClass Object #4 (
'self' => stdClass Object #4,
)
*/
$obj = new stdClass();
$obj = new stdClass;
$obj->self = $obj;
print $exporter->export($obj);
```
Compact exports:
```php
<?php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter;
// Array ()
print $exporter->shortenedExport(array());
// []
print $exporter->shortenedExport([]);
// Array (...)
print $exporter->shortenedExport(array(1,2,3,4,5));
// [...]
print $exporter->shortenedExport([1, 2, 3, 4, 5]);
// stdClass Object ()
print $exporter->shortenedExport(new stdClass);
@ -150,7 +152,7 @@ print $exporter->shortenedExport(new stdClass);
// Exception Object (...)
print $exporter->shortenedExport(new Exception);
// this\nis\na\nsuper\nlong\nstring\nt...\nspace
// 'this\nis\na\nsuper\nlong\nstr...nspace'
print $exporter->shortenedExport(
<<<LONG_STRING
this
@ -173,3 +175,181 @@ space
LONG_STRING
);
```
## Customizing How Objects Are Exported
By default, an object is exported property by property. Implement the `ObjectExporter` interface to control how objects of a type are represented:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\ExportContext;
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporter;
final class MoneyExporter implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Money;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Money);
return sprintf('Money (%d %s)', $object->amount(), $object->currency());
}
}
```
An object exporter is registered by passing it to `Exporter`'s constructor. It is consulted before the default representation of an object is used:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter(objectExporter: new MoneyExporter);
// Money (1999 EUR)
print $exporter->export(new Money(1999, 'EUR'));
/*
stdClass Object #6 (
'net' => Money (1999 EUR),
'gross' => Money (2379 EUR),
)
*/
$price = new stdClass;
$price->net = new Money(1999, 'EUR');
$price->gross = new Money(2379, 'EUR');
print $exporter->export($price);
```
`ObjectExporterChain` composes multiple object exporters into a single one. They are consulted in the order in which they are composed into the chain and the first one whose `handles()` method returns `true` is asked for the representation:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporterChain;
$exporter = new Exporter(
objectExporter: new ObjectExporterChain(
[
new BasketExporter,
new MoneyExporter,
],
),
);
```
Enums are objects and are therefore passed to the object exporters as well. When no object exporter handles an object, the default representation is used for it.
### Repeated Occurrences
When the same object occurs more than once, the default representation is only used for the first occurrence; subsequent occurrences are replaced with a reference to the object:
```php
/*
Array &0 [
0 => Money Object #8 (
'amount' => 1999,
'currency' => 'EUR',
),
1 => Money Object #8,
]
*/
$money = new Money(1999, 'EUR');
print (new Exporter)->export([$money, $money]);
```
An object exporter is responsible for the entire representation of the object it handles. Every occurrence of such an object is therefore exported by it:
```php
/*
Array &0 [
0 => Money (1999 EUR),
1 => Money (1999 EUR),
]
*/
print $exporter->export([$money, $money]);
```
The exception is an object that is (indirectly) nested in itself: it cannot be exported this way without recursing infinitely and is replaced with a reference to the object.
### Exporting Nested Values
An object exporter may use the `Exporter` it is given to export values that are nested in the object it handles. The `ExportContext` it is given must be passed on when it does. Otherwise, the export of these nested values starts over with an empty context and, for instance, assigns references to arrays that are already in use elsewhere in the same export:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\ExportContext;
use SebastianBergmann\Exporter\Exporter;
use SebastianBergmann\Exporter\ObjectExporter;
final class BasketExporter implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Basket;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Basket);
return 'Basket ' . $exporter->export($object->items(), $indentation, $context);
}
}
```
```php
/*
Array &0 [
'basket' => Basket Array &1 [
0 => Money (1999 EUR),
],
]
*/
print $exporter->export(['basket' => new Basket([new Money(1999, 'EUR')])]);
```
### Compact Exports
`Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` consult object exporters as well. The representation an object exporter provides is collapsed to a single line and shortened when it is longer than the configured maximum length:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter(objectExporter: new MoneyExporter);
// Money (1999 EUR)
print $exporter->shortenedExport(new Money(1999, 'EUR'));
```
Bear in mind that these representations are used where a short, single-line, and stable string is required. The representation of a data set is built using `Exporter::shortenedRecursiveExport()`, for instance, and is part of the name of a test that uses a data provider. An object exporter should therefore provide a representation that is compact and that does not change from one export to the next.
### Custom Representations
`Exporter::hasCustomRepresentationFor()` tells whether an object exporter provides the representation for an object. This is meant for code that renders objects itself and wants to use the representation an object exporter provides when there is one:
```php
<?php declare(strict_types=1);
use SebastianBergmann\Exporter\Exporter;
$exporter = new Exporter(objectExporter: new MoneyExporter);
// true
var_dump($exporter->hasCustomRepresentationFor(new Money(1999, 'EUR')));
// false
var_dump($exporter->hasCustomRepresentationFor(new stdClass));
```
Bear in mind that an object that is (indirectly) nested in itself is replaced with a reference to the object instead of being exported by an object exporter again.

View File

@ -1,30 +1,10 @@
# Security Policy
If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure.
The library that is developed in this repository is a first-party dependency of [PHPUnit](https://github.com/sebastianbergmann/phpunit).
[PHPUnit's Security Policy](https://github.com/sebastianbergmann/phpunit/security/policy) applies to this library as well.
If you believe you have found a security vulnerability in this library, please report it to me through coordinated disclosure.
Use my [PGP key](https://sebastian-bergmann.de/gpg.asc) for encrypted email, for example when your report includes proof-of-concept exploits against third-party systems.
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please email `sebastian@phpunit.de`.
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
* The type of issue
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Web Context
The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit.
The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes.
If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context.
Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes.

View File

@ -17,10 +17,34 @@
</target>
<target name="update-tools">
<exec executable="phive" taskname="phive">
<exec executable="${basedir}/tools/phive">
<arg value="--no-progress"/>
<arg value="self-update"/>
</exec>
<exec executable="${basedir}/tools/phive">
<arg value="--no-progress"/>
<arg value="update"/>
<arg value="--force-accept-unsigned"/>
</exec>
<exec executable="${basedir}/tools/composer" taskname="composer">
<arg value="self-update"/>
<arg value="--no-interaction"/>
<arg value="--no-progress"/>
<arg value="--no-ansi"/>
</exec>
<exec executable="${basedir}/tools/composer" dir="${basedir}/tools/.phpstan" taskname="composer">
<arg value="update"/>
<arg value="--no-interaction"/>
<arg value="--no-progress"/>
<arg value="--no-ansi"/>
</exec>
<exec executable="${basedir}/tools/composer" dir="${basedir}/tools/.phpstan" taskname="composer">
<arg value="bump"/>
<arg value="--no-interaction"/>
<arg value="--no-ansi"/>
</exec>
</target>
</project>

View File

@ -32,19 +32,20 @@
},
"config": {
"platform": {
"php": "8.2.0"
"php": "8.4.1"
},
"optimize-autoloader": true,
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php": ">=8.4",
"ext-mbstring": "*",
"sebastian/recursion-context": "^6.0"
"sebastian/recursion-context": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^11.3"
"phpunit/phpunit": "^13.3"
},
"autoload": {
"classmap": [
@ -58,7 +59,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "6.3-dev"
"dev-main": "8.2-dev"
}
}
}

View File

@ -3,3 +3,46 @@ parameters:
paths:
- src
- tests
checkTooWideReturnTypesInProtectedAndPublicMethods: true
reportAlwaysTrueInLastCondition: true
reportPossiblyNonexistentConstantArrayOffset: true
reportPossiblyNonexistentGeneralArrayOffset: true
treatPhpDocTypesAsCertain: false
strictRules:
allRules: false
booleansInConditions: true
closureUsesThis: true
disallowedBacktick: true
disallowedEmpty: true
disallowedImplicitArrayCreation: true
disallowedLooseComparison: true
disallowedShortTernary: true
illegalConstructorMethodCall: true
matchingInheritedMethodNames: true
noVariableVariables: true
numericOperandsInArithmeticOperators: true
overwriteVariablesWithLoop: true
requireParentConstructorCall: true
strictArrayFilter: true
strictFunctionCalls: true
switchConditionsMatchingType: true
uselessCast: true
ergebnis:
allRules: false
final:
enabled: true
privateInFinalClass:
enabled: true
type_coverage:
declare: 100
return: 100
param: 100
property: 100
constant: 100
includes:
- phar://phpstan.phar/conf/bleedingEdge.neon

View File

@ -1,10 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
shortenArraysForExportThreshold="10"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"

61
renovate.json Normal file
View File

@ -0,0 +1,61 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"helpers:pinGitHubActionDigestsToSemver",
":configMigration",
":maintainLockFilesWeekly"
],
"timezone": "Europe/Berlin",
"baseBranchPatterns": [
"main",
"/^\\d+\\.\\d+$/"
],
"rebaseWhen": "conflicted",
"prHourlyLimit": 2,
"prConcurrentLimit": 10,
"dependencyDashboard": true,
"labels": ["type/dependencies"],
"platformAutomerge": true,
"osvVulnerabilityAlerts": true,
"dependencyDashboardOSVVulnerabilitySummary": "all",
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["type/security", "type/dependencies"]
},
"lockFileMaintenance": {
"enabled": true,
"automerge": true,
"schedule": ["before 6am on monday"]
},
"packageRules": [
{
"description": "Group GitHub Actions updates per branch into one PR, automerge",
"matchManagers": ["github-actions"],
"groupName": "github-actions",
"automerge": true
},
{
"description": "Automerge minor and patch updates after they have settled",
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"description": "Do not update the PHP platform requirement in composer.json",
"matchManagers": ["composer"],
"matchDepNames": ["php"],
"enabled": false
},
{
"description": "Do not suggest major updates for Composer dependencies",
"matchManagers": ["composer"],
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"description": "Bump composer.json ranges when updating dependencies",
"matchManagers": ["composer"],
"rangeStrategy": "bump"
}
]
}

82
src/ExportContext.php Normal file
View File

@ -0,0 +1,82 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use SebastianBergmann\RecursionContext\Context as RecursionContext;
use SplObjectStorage;
/**
* Carries the state that is shared by all steps of a single export operation.
*
* An implementation of ObjectExporter that exports values nested in the object
* it handles must pass the instance of this class it is given to the Exporter
* it delegates to. Otherwise, the export of these nested values starts over
* with an empty context and, for instance, assigns references to arrays that
* are already in use elsewhere in the same export.
*/
final class ExportContext
{
private RecursionContext $recursionContext;
/**
* @var SplObjectStorage<object, null>
*/
private SplObjectStorage $exportedByObjectExporter;
public function __construct()
{
$this->recursionContext = new RecursionContext;
$this->exportedByObjectExporter = new SplObjectStorage;
}
/**
* @template T of array|object
*
* @param T $value
*
* @param-out T $value
*/
public function add(array|object &$value): int
{
return $this->recursionContext->add($value);
}
/**
* @template T of array|object
*
* @param T $value
*
* @param-out T $value
*/
public function contains(array|object &$value): false|int
{
return $this->recursionContext->contains($value);
}
/**
* Whether the export of an object by an ObjectExporter is in progress.
*
* This is the case when the object is (indirectly) nested in itself.
*/
public function isBeingExportedByObjectExporter(object $object): bool
{
return $this->exportedByObjectExporter->offsetExists($object);
}
public function beginExportByObjectExporter(object $object): void
{
$this->exportedByObjectExporter->offsetSet($object);
}
public function endExportByObjectExporter(object $object): void
{
$this->exportedByObjectExporter->offsetUnset($object);
}
}

View File

@ -21,16 +21,24 @@ use function ini_set;
use function is_array;
use function is_bool;
use function is_float;
use function is_infinite;
use function is_nan;
use function is_object;
use function is_resource;
use function is_string;
use function mb_strlen;
use function mb_substr;
use function ord;
use function preg_match;
use function preg_match_all;
use function preg_replace_callback;
use function spl_object_id;
use function sprintf;
use function str_contains;
use function str_repeat;
use function str_replace;
use function strlen;
use function strpbrk;
use function strtr;
use function var_export;
use BackedEnum;
@ -53,15 +61,17 @@ final readonly class Exporter
* @var positive-int
*/
private int $maxLengthForStrings;
private ?ObjectExporter $objectExporter;
/**
* @param non-negative-int $shortenArraysLongerThan
* @param positive-int $maxLengthForStrings
*/
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40)
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporter $objectExporter = null)
{
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
$this->maxLengthForStrings = $maxLengthForStrings;
$this->objectExporter = $objectExporter;
}
/**
@ -76,15 +86,23 @@ final readonly class Exporter
* - Strings are always quoted with single quotes
* - Carriage returns and newlines are normalized to \n
* - Recursion and repeated rendering is treated properly
*
* An implementation of ObjectExporter must pass the ExportContext it is
* given to this method when it exports values that are nested in the
* object it handles.
*
* @throws ObjectNotSupportedException
*/
public function export(mixed $value, int $indentation = 0): string
public function export(mixed $value, int $indentation = 0, ?ExportContext $context = null): string
{
return $this->recursiveExport($value, $indentation);
return $this->recursiveExport($value, $indentation, $context);
}
/**
* @param array<mixed> $data
* @param positive-int $maxLengthForStrings
*
* @throws ObjectNotSupportedException
*/
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
{
@ -92,7 +110,7 @@ final readonly class Exporter
$maxLengthForStrings = $this->maxLengthForStrings;
}
if (!$processed) {
if ($processed === null) {
$processed = new RecursionContext;
}
@ -118,7 +136,13 @@ final readonly class Exporter
* Newlines are replaced by the visible string '\n'.
* Contents of arrays and objects (if any) are replaced by '...'.
*
* The representation a custom object exporter provides for an object is
* used, but it is collapsed to a single line and shortened when it is
* longer than $maxLengthForStrings.
*
* @param positive-int $maxLengthForStrings
*
* @throws ObjectNotSupportedException
*/
public function shortenedExport(mixed $value, int $maxLengthForStrings = 40): string
{
@ -127,13 +151,20 @@ final readonly class Exporter
}
if (is_string($value)) {
$string = str_replace("\n", '', $this->exportString($value));
return $this->shorten($this->exportString($value), $maxLengthForStrings);
}
if (mb_strlen($string) > $maxLengthForStrings) {
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
}
if ($this->objectExporter !== null &&
is_object($value) &&
$this->objectExporter->handles($value)) {
$context = new ExportContext;
return $string;
$context->beginExportByObjectExporter($value);
return $this->shorten(
$this->objectExporter->export($value, $this, 0, $context),
$maxLengthForStrings,
);
}
if ($value instanceof BackedEnum) {
@ -171,6 +202,26 @@ final readonly class Exporter
return $this->export($value);
}
/**
* Returns whether a custom object exporter provides the representation
* for an object.
*
* This is intended for code that renders objects itself and wants to use
* the representation a custom object exporter provides when there is one.
*
* Bear in mind that an object that is (indirectly) nested in itself is
* replaced with a reference to the object instead of being exported by a
* custom object exporter again.
*/
public function hasCustomRepresentationFor(object $object): bool
{
if ($this->objectExporter === null) {
return false;
}
return $this->objectExporter->handles($object);
}
/**
* Converts an object to an array containing all of its private, protected
* and public properties.
@ -183,29 +234,43 @@ final readonly class Exporter
return (array) $value;
}
$array = [];
$properties = (array) $value;
$shadowed = $this->shadowedPropertyNames($properties);
$array = [];
foreach ($properties as $key => $val) {
$key = (string) $key;
foreach ((array) $value as $key => $val) {
// Exception traces commonly reference hundreds to thousands of
// objects currently loaded in memory. Including them in the result
// has a severe negative performance impact.
if ("\0Error\0trace" === $key || "\0Exception\0trace" === $key) {
if ($key === "\0Error\0trace" || $key === "\0Exception\0trace") {
continue;
}
// properties are transformed to keys in the following way:
// private $propertyName => "\0ClassName\0propertyName"
// protected $propertyName => "\0*\0propertyName"
// public $propertyName => "propertyName"
if (preg_match('/\0.+\0(.+)/', (string) $key, $matches)) {
$key = $matches[1];
}
// See https://github.com/php/php-src/commit/5721132
if ($key === "\0gcdata") {
continue;
}
// Properties are transformed to keys in the following way:
// private $propertyName => "\0DeclaringClassName\0propertyName"
// protected $propertyName => "\0*\0propertyName"
// public $propertyName => "propertyName"
//
// A private property that is redeclared in a derived class and the
// private property of the same name that it shadows both exist,
// independently of each other. To keep one from overwriting the
// other, the name of a shadowed private property is prefixed with
// the name of the class that declares it.
if (preg_match('/^\0([^\0]+)\0([^\0]+)$/', $key, $matches) === 1) {
if ($matches[1] !== '*' && isset($shadowed[$matches[2]])) {
$key = $matches[1] . '::' . $matches[2];
} else {
$key = $matches[2];
}
}
$array[$key] = $val;
}
@ -213,6 +278,12 @@ final readonly class Exporter
// above (fast) mechanism nor with reflection in Zend.
// Format the output similarly to print_r() in this case
if ($value instanceof SplObjectStorage) {
$key = null;
if ($value->valid()) {
$key = $value->key();
}
foreach ($value as $_value) {
$array['Object #' . spl_object_id($_value)] = [
'obj' => $_value,
@ -220,7 +291,9 @@ final readonly class Exporter
];
}
$value->rewind();
if ($key !== null) {
$value->seek($key);
}
}
return $array;
@ -236,15 +309,54 @@ final readonly class Exporter
if (!$value instanceof stdClass) {
// using ReflectionClass prevents initialization of potential lazy objects
return count((new ReflectionClass($value))->getProperties());
return count(new ReflectionClass($value)->getProperties());
}
return count((new ReflectionObject($value))->getProperties());
return count(new ReflectionObject($value)->getProperties());
}
/**
* Returns, as keys of the returned array, the names of properties that
* are declared more than once in the inheritance chain of an object.
*
* This can only happen when a derived class redeclares a private property
* that one of its parent classes also declares.
*
* @param array<array-key, mixed> $properties
*
* @return array<string, true>
*/
private function shadowedPropertyNames(array $properties): array
{
$seen = [];
$shadowed = [];
foreach ($properties as $key => $unused) {
$key = (string) $key;
if ($key === "\0Error\0trace" || $key === "\0Exception\0trace" || $key === "\0gcdata") {
continue;
}
if (preg_match('/^\0[^\0]+\0([^\0]+)$/', $key, $matches) === 1) {
$key = $matches[1];
}
if (isset($seen[$key])) {
$shadowed[$key] = true;
}
$seen[$key] = true;
}
return $shadowed;
}
/**
* @param array<mixed> $data
* @param positive-int $maxLengthForStrings
*
* @throws ObjectNotSupportedException
*/
private function shortenedCountedRecursiveExport(array &$data, RecursionContext $processed, int &$counter, int $maxLengthForStrings): string
{
@ -262,7 +374,7 @@ final readonly class Exporter
}
if (is_array($value)) {
assert(is_array($data[$key]) || is_object($data[$key]));
assert(isset($data[$key]) && (is_array($data[$key]) || is_object($data[$key])));
if ($processed->contains($data[$key]) !== false) {
$result[] = '*RECURSION*';
@ -281,7 +393,10 @@ final readonly class Exporter
return implode(', ', $result);
}
private function recursiveExport(mixed &$value, int $indentation = 0, ?RecursionContext $processed = null): string
/**
* @throws ObjectNotSupportedException
*/
private function recursiveExport(mixed &$value, int $indentation = 0, ?ExportContext $context = null): string
{
if ($value === null) {
return 'null';
@ -302,60 +417,68 @@ final readonly class Exporter
if (is_resource($value)) {
return sprintf(
'resource(%d) of type (%s)',
/** @phpstan-ignore cast.useless */
(int) $value,
get_resource_type($value),
);
}
if ($value instanceof BackedEnum) {
return sprintf(
'%s Enum #%d (%s, %s)',
$value::class,
spl_object_id($value),
$value->name,
$this->export($value->value),
);
}
if ($value instanceof UnitEnum) {
return sprintf(
'%s Enum #%d (%s)',
$value::class,
spl_object_id($value),
$value->name,
);
}
if (is_string($value)) {
return $this->exportString($value);
}
if (!$processed) {
$processed = new RecursionContext;
if ($context === null) {
$context = new ExportContext;
}
if (is_array($value)) {
return $this->exportArray($value, $processed, $indentation);
return $this->exportArray($value, $context, $indentation);
}
if (is_object($value)) {
return $this->exportObject($value, $processed, $indentation);
return $this->exportObject($value, $context, $indentation);
}
return var_export($value, true);
}
/**
* Collapses a representation to a single line and shortens it when it is
* longer than $maxLengthForStrings.
*
* @param positive-int $maxLengthForStrings
*/
private function shorten(string $string, int $maxLengthForStrings): string
{
$string = str_replace(["\r", "\n"], '', $string);
if (mb_strlen($string) > $maxLengthForStrings) {
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
}
return $string;
}
private function exportFloat(float $value): string
{
if (is_nan($value)) {
return 'NAN';
}
if (is_infinite($value)) {
return $value > 0 ? 'INF' : '-INF';
}
$precisionBackup = ini_get('precision');
ini_set('precision', '-1');
$valueAsString = @(string) $value;
$valueAsString = (string) $value;
ini_set('precision', $precisionBackup);
if ((string) @(int) $value === $valueAsString) {
// Add '.0' only if decimals and scientific notation are absent.
if (strpbrk($valueAsString, '.E') === false) {
return $valueAsString . '.0';
}
@ -365,34 +488,58 @@ final readonly class Exporter
private function exportString(string $value): string
{
// Match for most non-printable chars somewhat taking multibyte chars into account
if (preg_match('/[^\x09-\x0d\x1b\x20-\xff]/', $value)) {
$unprintableCount = preg_match_all('/[^\x09-\x0d\x1b\x20-\xff]/', $value);
if ($unprintableCount === false || $unprintableCount === 0) {
return "'" .
strtr(
$value,
[
"\r\n" => '\r\n' . "\n",
"\r" => '\r' . "\n",
"\n" => '\n' . "\n",
],
) .
"'";
}
// A NUL byte or a high ratio of unprintable bytes signals truly
// binary data; keep the compact hex dump in those cases.
if (str_contains($value, "\x00") || ($unprintableCount / strlen($value)) > 0.3) {
return 'Binary String: 0x' . bin2hex($value);
}
return "'" .
strtr(
// Mostly printable: keep printable bytes visible and escape only
// the offending ones inline using PHP-style \xNN escapes.
return 'Binary String: "' .
preg_replace_callback(
'/[\x00-\x1f\x7f"\\\\]/',
static fn (array $m): string => match ($m[0]) {
"\t" => '\t',
"\n" => '\n',
"\r" => '\r',
'"' => '\"',
'\\' => '\\\\',
default => sprintf('\x%02x', ord($m[0])),
},
$value,
[
"\r\n" => '\r\n' . "\n",
"\n\r" => '\n\r' . "\n",
"\r" => '\r' . "\n",
"\n" => '\n' . "\n",
],
) .
"'";
'"';
}
/**
* @param array<mixed> $value
*
* @throws ObjectNotSupportedException
*/
private function exportArray(array &$value, RecursionContext $processed, int $indentation): string
private function exportArray(array &$value, ExportContext $context, int $indentation): string
{
if (($key = $processed->contains($value)) !== false) {
if (($key = $context->contains($value)) !== false) {
return 'Array &' . $key;
}
$array = $value;
$key = $processed->add($value);
$key = $context->add($value);
$values = '';
if (count($array) > 0) {
@ -405,7 +552,7 @@ final readonly class Exporter
$this->recursiveExport($k, $indentation)
. ' => ' .
/** @phpstan-ignore offsetAccess.invalidOffset */
$this->recursiveExport($value[$k], $indentation + 1, $processed)
$this->recursiveExport($value[$k], $indentation + 1, $context)
. ",\n";
}
@ -415,15 +562,68 @@ final readonly class Exporter
return 'Array &' . (string) $key . ' [' . $values . ']';
}
private function exportObject(object $value, RecursionContext $processed, int $indentation): string
/**
* @throws ObjectNotSupportedException
*/
private function exportObject(object $value, ExportContext $context, int $indentation): string
{
$class = $value::class;
if ($processed->contains($value) !== false) {
if ($this->objectExporter !== null) {
// An object that is (indirectly) nested in itself cannot be
// exported by a custom object exporter without recursing
// infinitely and is therefore replaced with a reference to the
// object.
if ($context->isBeingExportedByObjectExporter($value)) {
return $class . ' Object #' . spl_object_id($value);
}
// A custom object exporter is responsible for the entire
// representation of the object it handles. Therefore, it is asked
// for that representation before the recursion context is
// consulted: every occurrence of such an object is exported the
// same way instead of repeated occurrences being replaced with a
// reference to the object.
if ($this->objectExporter->handles($value)) {
$context->beginExportByObjectExporter($value);
try {
return $this->objectExporter->export($value, $this, $indentation, $context);
} finally {
$context->endExportByObjectExporter($value);
}
}
}
// Enums are handled after a custom object exporter has been consulted
// so that the representation of an enum can be customized, but before
// the recursion context is consulted because an enum case is a
// singleton for which a reference to a previous occurrence would be
// less informative than the representation itself.
if ($value instanceof BackedEnum) {
return sprintf(
'%s Enum #%d (%s, %s)',
$class,
spl_object_id($value),
$value->name,
$this->export($value->value),
);
}
if ($value instanceof UnitEnum) {
return sprintf(
'%s Enum #%d (%s)',
$class,
spl_object_id($value),
$value->name,
);
}
if ($context->contains($value) !== false) {
return $class . ' Object #' . spl_object_id($value);
}
$processed->add($value);
$context->add($value);
$array = $this->toArray($value);
$buffer = '';
@ -437,7 +637,7 @@ final readonly class Exporter
. ' ' .
$this->recursiveExport($k, $indentation)
. ' => ' .
$this->recursiveExport($v, $indentation + 1, $processed)
$this->recursiveExport($v, $indentation + 1, $context)
. ",\n";
}

25
src/ObjectExporter.php Normal file
View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
interface ObjectExporter
{
public function handles(object $object): bool;
/**
* Exports an object this object exporter handles.
*
* The ExportContext must be passed on to Exporter::export() when values
* that are nested in the object are exported using $exporter.
*
* @throws ObjectNotSupportedException
*/
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string;
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use function array_any;
final readonly class ObjectExporterChain implements ObjectExporter
{
/**
* @var non-empty-list<ObjectExporter>
*/
private array $exporters;
/**
* @param non-empty-list<ObjectExporter> $exporters
*/
public function __construct(array $exporters)
{
$this->exporters = $exporters;
}
public function handles(object $object): bool
{
return array_any(
$this->exporters,
static fn (ObjectExporter $exporter) => $exporter->handles($object),
);
}
/**
* @throws ObjectNotSupportedException
*/
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
foreach ($this->exporters as $objectExporter) {
if ($objectExporter->handles($object)) {
return $objectExporter->export($object, $exporter, $indentation, $context);
}
}
throw new ObjectNotSupportedException;
}
}

View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use Throwable;
interface Exception extends Throwable
{
}

View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use RuntimeException;
final class ObjectNotSupportedException extends RuntimeException implements Exception
{
}

View File

@ -21,7 +21,6 @@ use function is_resource;
use function is_string;
use function mb_internal_encoding;
use function mb_language;
use function method_exists;
use function preg_replace;
use function range;
use function str_repeat;
@ -29,9 +28,9 @@ use Error;
use Exception;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use SebastianBergmann\RecursionContext\Context;
@ -39,6 +38,8 @@ use SplObjectStorage;
use stdClass;
#[CoversClass(Exporter::class)]
#[UsesClass(ExportContext::class)]
#[UsesClass(ObjectExporterChain::class)]
#[Small]
final class ExporterTest extends TestCase
{
@ -89,6 +90,7 @@ final class ExporterTest extends TestCase
'float 1 - 2 / 3' => [1 - 2 / 3, '0.33333333333333337', 0],
'float 5.5E+123' => [5.5E+123, '5.5E+123', 0],
'float 5.5E-123' => [5.5E-123, '5.5E-123', 0],
'float 9.2233720368547758E+18' => [9.2233720368547758E+18, '9.223372036854776E+18', 0],
'float NAN' => [NAN, 'NAN', 0],
'float INF' => [INF, 'INF', 0],
'float -INF' => [-INF, '-INF', 0],
@ -146,7 +148,9 @@ Array &0 [
EOF,
2,
],
// \n\r and \r is converted to \n
// A real newline is inserted after each line ending so the
// escaped text wraps readably; \r\n stays one break while a lone
// \n followed by a lone \r are two separate line endings.
'export multiline text' => ["this\nis\na\nvery\nvery\nvery\nvery\nvery\nvery\rlong\n\rtext",
<<<'EOF'
'this\n
@ -158,8 +162,27 @@ very\n
very\n
very\n
very\r
long\n\r
long\n
\r
text'
EOF,
0,
],
// A lone \n (Unix) followed by a lone \r (old Mac) are two
// distinct line endings and must each produce their own break.
'lone newline followed by lone carriage return' => ["a\n\rb",
<<<'EOF'
'a\n
\r
b'
EOF,
0,
],
// \r\n (Windows) is a single line ending and stays one break.
'carriage return followed by newline' => ["a\r\nb",
<<<'EOF'
'a\r\n
b'
EOF,
0,
],
@ -181,7 +204,8 @@ very\n
very\n
very\n
very\r
long\n\r
long\n
\r
text',
'object' => stdClass Object #%d (
'foo' => 'bar',
@ -231,7 +255,7 @@ EOF,
0,
],
[
implode('', array_map('chr', range(0x0E, 0x1F))),
implode('', array_map(chr(...), range(0x0E, 0x1F))),
'Binary String: 0x0e0f101112131415161718191a1b1c1d1e1f',
0,
],
@ -240,6 +264,41 @@ EOF,
'Binary String: 0x0009',
0,
],
'mostly printable string with a single control byte' => [
'Hello' . chr(0x01) . 'World',
'Binary String: "Hello\x01World"',
0,
],
'mostly printable string with several control bytes' => [
'BEGIN' . chr(0x01) . 'DATA' . chr(0x1F) . 'END',
'Binary String: "BEGIN\x01DATA\x1fEND"',
0,
],
'mostly printable string with embedded tab' => [
'Tab' . chr(0x09) . 'Sep' . chr(0x01),
'Binary String: "Tab\tSep\x01"',
0,
],
'mostly printable string with embedded newline' => [
'Line1' . chr(0x0A) . 'Line2' . chr(0x01),
'Binary String: "Line1\nLine2\x01"',
0,
],
'mostly printable string with embedded carriage return' => [
'A' . chr(0x0D) . 'B' . chr(0x01),
'Binary String: "A\rB\x01"',
0,
],
'mixed string with embedded double quote and backslash' => [
'a"b\\c' . chr(0x07) . 'd',
'Binary String: "a\"b\\\\c\x07d"',
0,
],
'mixed string preserves UTF-8 high bytes' => [
'café' . chr(0x01),
'Binary String: "café\x01"',
0,
],
[
'',
"''",
@ -351,9 +410,9 @@ EOF,
public static function provideNonBinaryMultibyteStrings(): array
{
return [
[implode('', array_map('chr', range(0x09, 0x0D))), 9],
[implode('', array_map('chr', range(0x20, 0x7F))), 96],
[implode('', array_map('chr', range(0x80, 0xFF))), 128],
[implode('', array_map(chr(...), range(0x09, 0x0D))), 9],
[implode('', array_map(chr(...), range(0x20, 0x7F))), 96],
[implode('', array_map(chr(...), range(0x80, 0xFF))), 128],
];
}
@ -434,7 +493,8 @@ very\n
very\n
very\n
very\r
long\n\r
long\n
\r
text',
'object' => stdClass Object #%d (
'foo' => 'bar',
@ -459,7 +519,8 @@ very\n
very\n
very\n
very\r
long\n\r
long\n
\r
text',
'object' => stdClass Object #%d,
'objectagain' => stdClass Object #%d,
@ -485,7 +546,7 @@ EOF;
{
$this->assertSame(
$expected,
$this->trimNewline((new Exporter(0, $maxLengthForStrings))->shortenedExport($value)),
$this->trimNewline(new Exporter(0, $maxLengthForStrings)->shortenedExport($value)),
);
$this->assertSame(
@ -544,6 +605,19 @@ EOF;
$this->assertEquals([], (new Exporter)->toArray((object) $array));
}
public function testPrivatePropertyRedeclaredInDerivedClassIsNotOverwritten(): void
{
$this->assertSame(
[
ParentClassWithPrivateProperty::class . '::property' => 'parent value',
ChildClassWithPrivateProperty::class . '::property' => 'child value',
],
(new Exporter)->toArray(
new ChildClassWithPrivateProperty('parent value', 'child value'),
),
);
}
/**
* @param array<mixed> $value
* @param non-negative-int $limit
@ -551,7 +625,7 @@ EOF;
#[DataProvider('shortenedRecursiveExportProvider')]
public function testShortenedRecursiveExport(array $value, string $expected, int $limit): void
{
$this->assertEquals($expected, (new Exporter($limit))->shortenedRecursiveExport($value));
$this->assertEquals($expected, new Exporter($limit)->shortenedRecursiveExport($value));
}
public function testShortenedRecursiveOccurredRecursion(): void
@ -567,14 +641,10 @@ EOF;
$this->assertEquals('*RECURSION*', (new Exporter)->shortenedRecursiveExport($value, processed: $context));
}
#[RequiresPhp('^8.4')]
public function testShortenedExportDoesNotInitializeLazyObject(): void
{
$reflector = new ReflectionClass(ExampleClass::class);
assert(method_exists($reflector, 'newLazyProxy'));
assert(method_exists($reflector, 'isUninitializedLazyObject'));
$object = $reflector->newLazyProxy(static fn () => new ExampleClass('bar'));
(new Exporter)->shortenedExport($object, 10);
@ -582,6 +652,428 @@ EOF;
$this->assertTrue($reflector->isUninitializedLazyObject($object));
}
public function testObjectCanBeExportedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesEveryObject,
],
),
);
$this->assertSame(
'stdClass (indentation: 0)',
$exporter->export(new stdClass),
);
}
public function testObjectCanBeExportedByCustomObjectExporterThatIsNotComposedIntoChain(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesEveryObject,
);
$this->assertSame(
'stdClass (indentation: 0)',
$exporter->export(new stdClass),
);
}
public function testObjectNestedInArrayCanBeExportedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesEveryObject,
],
),
);
$this->assertSame(
<<<'EOT'
Array &0 [
0 => stdClass (indentation: 1),
]
EOT,
$exporter->export([new stdClass]),
);
}
public function testObjectNestedInObjectCanBeExportedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
],
),
);
$object = new stdClass;
$object->nested = new ExampleClass('bar');
$this->assertStringMatchesFormat(
<<<'EOT'
stdClass Object #%d (
'nested' => SebastianBergmann\Exporter\ExampleClass handled by custom exporter,
)
EOT,
$exporter->export($object),
);
}
public function testEveryOccurrenceOfObjectHandledByCustomObjectExporterIsExportedByThatObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesEveryObject,
],
),
);
$object = new stdClass;
$this->assertSame(
<<<'EOT'
Array &0 [
0 => stdClass (indentation: 1),
1 => stdClass (indentation: 1),
]
EOT,
$exporter->export([$object, $object]),
);
}
public function testRepeatedOccurrenceOfObjectNotHandledByCustomObjectExporterIsReplacedWithReferenceToObject(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
],
),
);
$object = new stdClass;
$this->assertStringMatchesFormat(
<<<'EOT'
Array &0 [
0 => stdClass Object #%d (),
1 => stdClass Object #%d,
]
EOT,
$exporter->export([$object, $object]),
);
}
public function testDefaultExportIsUsedWhenNoCustomObjectExporterHandlesObject(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
],
),
);
$this->assertStringMatchesFormat(
'stdClass Object #%d ()',
$exporter->export(new stdClass),
);
}
public function testEnumCanBeExportedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesEveryObject,
],
),
);
$this->assertSame(
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
$exporter->export(ExampleEnum::Value),
);
$this->assertSame(
'SebastianBergmann\Exporter\ExampleStringBackedEnum (indentation: 0)',
$exporter->export(ExampleStringBackedEnum::Value),
);
}
public function testDefaultExportIsUsedForEnumWhenNoCustomObjectExporterHandlesIt(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
],
),
);
$this->assertStringMatchesFormat(
'SebastianBergmann\Exporter\ExampleEnum Enum #%d (Value)',
$exporter->export(ExampleEnum::Value),
);
$this->assertStringMatchesFormat(
'SebastianBergmann\Exporter\ExampleStringBackedEnum Enum #%d (Value, \'value\')',
$exporter->export(ExampleStringBackedEnum::Value),
);
}
public function testShortenedExportUsesRepresentationProvidedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesEveryObject,
);
$this->assertSame(
'stdClass (indentation: 0)',
$exporter->shortenedExport(new stdClass),
);
$this->assertSame(
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
$exporter->shortenedExport(ExampleEnum::Value, 80),
);
}
public function testShortenedExportCollapsesRepresentationProvidedByCustomObjectExporterToSingleLine(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatReturnsGivenRepresentation("first\nsecond"),
);
$this->assertSame(
'firstsecond',
$exporter->shortenedExport(new stdClass),
);
}
public function testShortenedExportShortensRepresentationProvidedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatReturnsGivenRepresentation(str_repeat('a', 41)),
);
$this->assertSame(
str_repeat('a', 30) . '...' . str_repeat('a', 7),
$exporter->shortenedExport(new stdClass),
);
}
public function testShortenedRecursiveExportUsesRepresentationProvidedByCustomObjectExporter(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesEveryObject,
);
$data = [new stdClass, 'foo'];
$this->assertSame(
"stdClass (indentation: 0), 'foo'",
$exporter->shortenedRecursiveExport($data),
);
}
public function testShortenedExportDoesNotRecurseInfinitelyForObjectNestedInItself(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatExportsNestedValues,
);
$node = new Node;
$node->children = [$node];
$this->assertStringMatchesFormat(
'Node(Array &0 [ 0 => SebastianBergmann\Exporter\Node Object #%d,])',
$exporter->shortenedExport($node, 200),
);
}
public function testShortenedExportUsesDefaultRepresentationWhenNoCustomObjectExporterHandlesObject(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterThatHandlesNoObject,
);
$this->assertSame(
'stdClass Object ()',
$exporter->shortenedExport(new stdClass),
);
}
public function testKnowsThatCustomObjectExporterProvidesRepresentationForObjectItHandles(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
],
),
);
$this->assertTrue($exporter->hasCustomRepresentationFor(new ExampleClass('bar')));
}
public function testKnowsThatCustomObjectExporterDoesNotProvideRepresentationForObjectItDoesNotHandle(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
],
),
);
$this->assertFalse($exporter->hasCustomRepresentationFor(new stdClass));
}
public function testKnowsThatNoCustomObjectExporterProvidesRepresentationForObjectWhenNoneIsConfigured(): void
{
$this->assertFalse((new Exporter)->hasCustomRepresentationFor(new stdClass));
}
public function testCustomObjectExporterCanExportValuesNestedInObjectItHandles(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$this->assertSame(
<<<'EOT'
Node(Array &0 [
'key' => 'value',
])
EOT,
$exporter->export(new Node(['key' => 'value'])),
);
}
public function testArrayExportedByCustomObjectExporterDoesNotReuseReferenceToOtherArray(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$this->assertSame(
<<<'EOT'
Array &0 [
0 => Node(Array &1 [
'key' => 'value',
]),
]
EOT,
$exporter->export([new Node(['key' => 'value'])]),
);
}
public function testObjectNestedInItselfIsNotExportedByCustomObjectExporterAgain(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$node = new Node;
$node->children = [$node];
$this->assertStringMatchesFormat(
<<<'EOT'
Node(Array &0 [
0 => SebastianBergmann\Exporter\Node Object #%d,
])
EOT,
$exporter->export($node),
);
}
public function testObjectsNestedInEachOtherAreNotExportedByCustomObjectExporterAgain(): void
{
$exporter = new Exporter(
0,
40,
new ObjectExporterChain(
[
new ObjectExporterThatExportsNestedValues,
],
),
);
$first = new Node;
$second = new Node;
$first->children = [$second];
$second->children = [$first];
$this->assertStringMatchesFormat(
<<<'EOT'
Node(Array &0 [
0 => Node(Array &1 [
0 => SebastianBergmann\Exporter\Node Object #%d,
]),
])
EOT,
$exporter->export($first),
);
}
private function trimNewline(string $string): string
{
return (string) preg_replace('/[ ]*\n/', "\n", $string);

View File

@ -0,0 +1,80 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use stdClass;
#[CoversClass(ObjectExporterChain::class)]
#[UsesClass(Exporter::class)]
#[UsesClass(ExportContext::class)]
#[Small]
final class ObjectExporterChainTest extends TestCase
{
public function testHandlesObjectWhenAtLeastOneComposedObjectExporterHandlesIt(): void
{
$chain = new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
new ObjectExporterThatHandlesEveryObject,
],
);
$this->assertTrue($chain->handles(new stdClass));
}
public function testDoesNotHandleObjectWhenNoComposedObjectExporterHandlesIt(): void
{
$chain = new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
],
);
$this->assertFalse($chain->handles(new stdClass));
}
public function testDelegatesExportingToFirstComposedObjectExporterThatHandlesObject(): void
{
$chain = new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
new ObjectExporterThatHandlesObjectsOfSpecificType(stdClass::class),
new ObjectExporterThatHandlesEveryObject,
],
);
$this->assertSame(
'stdClass handled by custom exporter',
$chain->export(new stdClass, new Exporter, 0, new ExportContext),
);
$this->assertSame(
ExampleClass::class . ' (indentation: 0)',
$chain->export(new ExampleClass('foo'), new Exporter, 0, new ExportContext),
);
}
public function testCannotExportObjectWhenNoComposedObjectExporterHandlesIt(): void
{
$chain = new ObjectExporterChain(
[
new ObjectExporterThatHandlesNoObject,
],
);
$this->expectException(ObjectNotSupportedException::class);
$chain->export(new stdClass, new Exporter, 0, new ExportContext);
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
final class ChildClassWithPrivateProperty extends ParentClassWithPrivateProperty
{
/**
* @phpstan-ignore property.onlyWritten
*/
private string $property;
public function __construct(string $parentProperty, string $childProperty)
{
parent::__construct($parentProperty);
$this->property = $childProperty;
}
}

View File

@ -12,7 +12,7 @@ namespace SebastianBergmann\Exporter;
/*
* Helper to test export of enums.
*/
class ExampleClass
final class ExampleClass
{
/**
* @phpstan-ignore property.onlyWritten

29
tests/_fixture/Node.php Normal file
View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
/*
* Helper to test export of objects that are nested in objects.
*/
final class Node
{
/**
* @var array<mixed>
*/
public array $children;
/**
* @param array<mixed> $children
*/
public function __construct(array $children = [])
{
$this->children = $children;
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use function assert;
use function sprintf;
final readonly class ObjectExporterThatExportsNestedValues implements ObjectExporter
{
public function handles(object $object): bool
{
return $object instanceof Node;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
assert($object instanceof Node);
return sprintf(
'Node(%s)',
$exporter->export($object->children, $indentation, $context),
);
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use function sprintf;
final readonly class ObjectExporterThatHandlesEveryObject implements ObjectExporter
{
public function handles(object $object): bool
{
return true;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
return sprintf(
'%s (indentation: %d)',
$object::class,
$indentation,
);
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
final readonly class ObjectExporterThatHandlesNoObject implements ObjectExporter
{
public function handles(object $object): bool
{
return false;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
throw new ObjectNotSupportedException;
}
}

View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
use function sprintf;
final readonly class ObjectExporterThatHandlesObjectsOfSpecificType implements ObjectExporter
{
/**
* @var class-string
*/
private string $type;
/**
* @param class-string $type
*/
public function __construct(string $type)
{
$this->type = $type;
}
public function handles(object $object): bool
{
return $object instanceof $this->type;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
return sprintf(
'%s handled by custom exporter',
$object::class,
);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
final readonly class ObjectExporterThatReturnsGivenRepresentation implements ObjectExporter
{
private string $representation;
public function __construct(string $representation)
{
$this->representation = $representation;
}
public function handles(object $object): bool
{
return true;
}
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
{
return $this->representation;
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/exporter.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Exporter;
abstract class ParentClassWithPrivateProperty
{
/**
* @phpstan-ignore property.onlyWritten
*/
private string $property;
public function __construct(string $property)
{
$this->property = $property;
}
}

View File

@ -0,0 +1,14 @@
{
"require-dev": {
"phpstan/phpstan": "^2.2.7",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-strict-rules": "^2.0.12",
"tomasvotruba/type-coverage": "^2.3.0",
"ergebnis/phpstan-rules": "^2.13.1"
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
}
}

392
tools/.phpstan/composer.lock generated Normal file
View File

@ -0,0 +1,392 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3579343022b2a28c7195fe30b1752bfb",
"packages": [],
"packages-dev": [
{
"name": "ergebnis/phpstan-rules",
"version": "2.13.1",
"source": {
"type": "git",
"url": "https://github.com/ergebnis/phpstan-rules.git",
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/f69db86b98595c34fc1f61c89fe3b380141aa519",
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"phpstan/phpstan": "^2.1.35"
},
"require-dev": {
"codeception/codeception": "^4.0.0 || ^5.0.0",
"doctrine/orm": "^2.20.0 || ^3.3.0",
"ergebnis/composer-normalize": "^2.49.0",
"ergebnis/license": "^2.7.0",
"ergebnis/php-cs-fixer-config": "^6.59.0",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
"fakerphp/faker": "^1.24.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpunit/phpunit": "^9.6.21",
"psr/container": "^2.0.2",
"symfony/finder": "^5.4.45",
"symfony/process": "^5.4.47"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"Ergebnis\\PHPStan\\Rules\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andreas Möller",
"email": "am@localheinz.com",
"homepage": "https://localheinz.com"
}
],
"description": "Provides rules for phpstan/phpstan.",
"homepage": "https://github.com/ergebnis/phpstan-rules",
"keywords": [
"PHPStan",
"phpstan-rules"
],
"support": {
"issues": "https://github.com/ergebnis/phpstan-rules/issues",
"security": "https://github.com/ergebnis/phpstan-rules/blob/main/.github/SECURITY.md",
"source": "https://github.com/ergebnis/phpstan-rules"
},
"time": "2026-01-27T17:13:06+00:00"
},
{
"name": "phpstan/extension-installer",
"version": "1.4.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/extension-installer.git",
"reference": "85e90b3942d06b2326fba0403ec24fe912372936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
"reference": "85e90b3942d06b2326fba0403ec24fe912372936",
"shasum": ""
},
"require": {
"composer-plugin-api": "^2.0",
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.9.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2.0",
"phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
},
"type": "composer-plugin",
"extra": {
"class": "PHPStan\\ExtensionInstaller\\Plugin"
},
"autoload": {
"psr-4": {
"PHPStan\\ExtensionInstaller\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Composer plugin for automatic installation of PHPStan extensions",
"keywords": [
"dev",
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/extension-installer/issues",
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
},
"time": "2024-09-04T20:21:43+00:00"
},
{
"name": "phpstan/phpstan",
"version": "2.2.7",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
"reference": "692db47b9dddb0487934e5236e77d48594aef921",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ondřej Mirtes"
},
{
"name": "Markus Staab"
},
{
"name": "Vincent Langlet"
}
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"time": "2026-07-29T17:39:32+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "2.0.12",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b",
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.52"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6",
"shipmonk/name-collision-detector": "^2.1"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Extra strict and opinionated rules for PHPStan",
"keywords": [
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12"
},
"time": "2026-07-19T07:24:06+00:00"
},
{
"name": "tomasvotruba/type-coverage",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/TomasVotruba/type-coverage.git",
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"shasum": ""
},
"require": {
"php": "^8.4",
"phpstan/phpstan": "^2.2",
"webmozart/assert": "^1.11 || ^2.1"
},
"require-dev": {
"phpstan/extension-installer": "^1.4",
"phpunit/phpunit": "^13.2",
"rector/jack": "^1.0",
"rector/rector": "^2.5",
"shipmonk/composer-dependency-analyser": "^1.8",
"symfony/dom-crawler": "^8.1",
"symplify/easy-coding-standard": "^13.2",
"tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"config/extension.neon",
"packages/type-perfect/config/extension.neon"
]
}
},
"autoload": {
"psr-4": {
"Rector\\TypePerfect\\": "packages/type-perfect/src",
"TomasVotruba\\TypeCoverage\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Measure type coverage of your project",
"keywords": [
"phpstan-extension",
"static analysis"
],
"support": {
"issues": "https://github.com/TomasVotruba/type-coverage/issues",
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
},
"funding": [
{
"url": "https://www.paypal.me/rectorphp",
"type": "custom"
},
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2026-07-29T20:32:37+00:00"
},
{
"name": "webmozart/assert",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^8.2"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"type": "library",
"extra": {
"psalm": {
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
},
"branch-alias": {
"dev-master": "2.0-dev",
"dev-feature/2-0": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.4.1"
},
"time": "2026-06-15T15:31:57+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

22
tools/.phpstan/vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitf9e7218f71d5874b5632927df4f72bd7::getLoader();

119
tools/.phpstan/vendor/bin/phpstan vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpstan/phpstan/phpstan)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan');
}
}
return include __DIR__ . '/..'.'/phpstan/phpstan/phpstan';

119
tools/.phpstan/vendor/bin/phpstan.phar vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpstan/phpstan/phpstan.phar)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar');
}
}
return include __DIR__ . '/..'.'/phpstan/phpstan/phpstan.phar';

View File

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
tools/.phpstan/vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,63 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Nette\\ArgumentOutOfRangeException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\DeprecatedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\DirectoryNotFoundException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\FileNotFoundException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\HtmlStringable' => $vendorDir . '/nette/utils/src/HtmlStringable.php',
'Nette\\IOException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\InvalidArgumentException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\InvalidStateException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\Iterators\\CachingIterator' => $vendorDir . '/nette/utils/src/Iterators/CachingIterator.php',
'Nette\\Iterators\\Mapper' => $vendorDir . '/nette/utils/src/Iterators/Mapper.php',
'Nette\\Localization\\ITranslator' => $vendorDir . '/nette/utils/src/compatibility.php',
'Nette\\Localization\\Translator' => $vendorDir . '/nette/utils/src/Translator.php',
'Nette\\MemberAccessException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\NotImplementedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\NotSupportedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\OutOfRangeException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\ShouldNotHappenException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\SmartObject' => $vendorDir . '/nette/utils/src/SmartObject.php',
'Nette\\StaticClass' => $vendorDir . '/nette/utils/src/StaticClass.php',
'Nette\\UnexpectedValueException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\Utils\\ArrayHash' => $vendorDir . '/nette/utils/src/Utils/ArrayHash.php',
'Nette\\Utils\\ArrayList' => $vendorDir . '/nette/utils/src/Utils/ArrayList.php',
'Nette\\Utils\\Arrays' => $vendorDir . '/nette/utils/src/Utils/Arrays.php',
'Nette\\Utils\\AssertionException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Callback' => $vendorDir . '/nette/utils/src/Utils/Callback.php',
'Nette\\Utils\\DateTime' => $vendorDir . '/nette/utils/src/Utils/DateTime.php',
'Nette\\Utils\\FileInfo' => $vendorDir . '/nette/utils/src/Utils/FileInfo.php',
'Nette\\Utils\\FileSystem' => $vendorDir . '/nette/utils/src/Utils/FileSystem.php',
'Nette\\Utils\\Finder' => $vendorDir . '/nette/utils/src/Utils/Finder.php',
'Nette\\Utils\\Floats' => $vendorDir . '/nette/utils/src/Utils/Floats.php',
'Nette\\Utils\\Helpers' => $vendorDir . '/nette/utils/src/Utils/Helpers.php',
'Nette\\Utils\\Html' => $vendorDir . '/nette/utils/src/Utils/Html.php',
'Nette\\Utils\\IHtmlString' => $vendorDir . '/nette/utils/src/compatibility.php',
'Nette\\Utils\\Image' => $vendorDir . '/nette/utils/src/Utils/Image.php',
'Nette\\Utils\\ImageColor' => $vendorDir . '/nette/utils/src/Utils/ImageColor.php',
'Nette\\Utils\\ImageException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ImageType' => $vendorDir . '/nette/utils/src/Utils/ImageType.php',
'Nette\\Utils\\Iterables' => $vendorDir . '/nette/utils/src/Utils/Iterables.php',
'Nette\\Utils\\Json' => $vendorDir . '/nette/utils/src/Utils/Json.php',
'Nette\\Utils\\JsonException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => $vendorDir . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\Paginator' => $vendorDir . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Process' => $vendorDir . '/nette/utils/src/Utils/Process.php',
'Nette\\Utils\\ProcessFailedException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ProcessTimeoutException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Random' => $vendorDir . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => $vendorDir . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => $vendorDir . '/nette/utils/src/Utils/ReflectionMethod.php',
'Nette\\Utils\\RegexpException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Strings' => $vendorDir . '/nette/utils/src/Utils/Strings.php',
'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php',
'Nette\\Utils\\UnknownImageFileException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Validators' => $vendorDir . '/nette/utils/src/Utils/Validators.php',
);

View File

@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'9b38cf48e83f5d8f60375221cd213eee' => $vendorDir . '/phpstan/phpstan/bootstrap.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,14 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'TomasVotruba\\TypeCoverage\\' => array($vendorDir . '/tomasvotruba/type-coverage/src'),
'PHPStan\\ExtensionInstaller\\' => array($vendorDir . '/phpstan/extension-installer/src'),
'PHPStan\\' => array($vendorDir . '/phpstan/phpstan-strict-rules/src'),
'Nette\\' => array($vendorDir . '/nette/utils/src'),
'Ergebnis\\PHPStan\\Rules\\' => array($vendorDir . '/ergebnis/phpstan-rules/src'),
);

View File

@ -0,0 +1,48 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitf9e7218f71d5874b5632927df4f72bd7
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitf9e7218f71d5874b5632927df4f72bd7', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitf9e7218f71d5874b5632927df4f72bd7', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

View File

@ -0,0 +1,122 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7
{
public static $files = array (
'9b38cf48e83f5d8f60375221cd213eee' => __DIR__ . '/..' . '/phpstan/phpstan/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'T' =>
array (
'TomasVotruba\\TypeCoverage\\' => 26,
),
'P' =>
array (
'PHPStan\\ExtensionInstaller\\' => 27,
'PHPStan\\' => 8,
),
'N' =>
array (
'Nette\\' => 6,
),
'E' =>
array (
'Ergebnis\\PHPStan\\Rules\\' => 23,
),
);
public static $prefixDirsPsr4 = array (
'TomasVotruba\\TypeCoverage\\' =>
array (
0 => __DIR__ . '/..' . '/tomasvotruba/type-coverage/src',
),
'PHPStan\\ExtensionInstaller\\' =>
array (
0 => __DIR__ . '/..' . '/phpstan/extension-installer/src',
),
'PHPStan\\' =>
array (
0 => __DIR__ . '/..' . '/phpstan/phpstan-strict-rules/src',
),
'Nette\\' =>
array (
0 => __DIR__ . '/..' . '/nette/utils/src',
),
'Ergebnis\\PHPStan\\Rules\\' =>
array (
0 => __DIR__ . '/..' . '/ergebnis/phpstan-rules/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Nette\\ArgumentOutOfRangeException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\DeprecatedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\DirectoryNotFoundException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\FileNotFoundException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\HtmlStringable' => __DIR__ . '/..' . '/nette/utils/src/HtmlStringable.php',
'Nette\\IOException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\InvalidArgumentException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\InvalidStateException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\Iterators\\CachingIterator' => __DIR__ . '/..' . '/nette/utils/src/Iterators/CachingIterator.php',
'Nette\\Iterators\\Mapper' => __DIR__ . '/..' . '/nette/utils/src/Iterators/Mapper.php',
'Nette\\Localization\\ITranslator' => __DIR__ . '/..' . '/nette/utils/src/compatibility.php',
'Nette\\Localization\\Translator' => __DIR__ . '/..' . '/nette/utils/src/Translator.php',
'Nette\\MemberAccessException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\NotImplementedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\NotSupportedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\OutOfRangeException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\ShouldNotHappenException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\SmartObject' => __DIR__ . '/..' . '/nette/utils/src/SmartObject.php',
'Nette\\StaticClass' => __DIR__ . '/..' . '/nette/utils/src/StaticClass.php',
'Nette\\UnexpectedValueException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\Utils\\ArrayHash' => __DIR__ . '/..' . '/nette/utils/src/Utils/ArrayHash.php',
'Nette\\Utils\\ArrayList' => __DIR__ . '/..' . '/nette/utils/src/Utils/ArrayList.php',
'Nette\\Utils\\Arrays' => __DIR__ . '/..' . '/nette/utils/src/Utils/Arrays.php',
'Nette\\Utils\\AssertionException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Callback' => __DIR__ . '/..' . '/nette/utils/src/Utils/Callback.php',
'Nette\\Utils\\DateTime' => __DIR__ . '/..' . '/nette/utils/src/Utils/DateTime.php',
'Nette\\Utils\\FileInfo' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileInfo.php',
'Nette\\Utils\\FileSystem' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileSystem.php',
'Nette\\Utils\\Finder' => __DIR__ . '/..' . '/nette/utils/src/Utils/Finder.php',
'Nette\\Utils\\Floats' => __DIR__ . '/..' . '/nette/utils/src/Utils/Floats.php',
'Nette\\Utils\\Helpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/Helpers.php',
'Nette\\Utils\\Html' => __DIR__ . '/..' . '/nette/utils/src/Utils/Html.php',
'Nette\\Utils\\IHtmlString' => __DIR__ . '/..' . '/nette/utils/src/compatibility.php',
'Nette\\Utils\\Image' => __DIR__ . '/..' . '/nette/utils/src/Utils/Image.php',
'Nette\\Utils\\ImageColor' => __DIR__ . '/..' . '/nette/utils/src/Utils/ImageColor.php',
'Nette\\Utils\\ImageException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ImageType' => __DIR__ . '/..' . '/nette/utils/src/Utils/ImageType.php',
'Nette\\Utils\\Iterables' => __DIR__ . '/..' . '/nette/utils/src/Utils/Iterables.php',
'Nette\\Utils\\Json' => __DIR__ . '/..' . '/nette/utils/src/Utils/Json.php',
'Nette\\Utils\\JsonException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\Paginator' => __DIR__ . '/..' . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Process' => __DIR__ . '/..' . '/nette/utils/src/Utils/Process.php',
'Nette\\Utils\\ProcessFailedException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ProcessTimeoutException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Random' => __DIR__ . '/..' . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => __DIR__ . '/..' . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => __DIR__ . '/..' . '/nette/utils/src/Utils/ReflectionMethod.php',
'Nette\\Utils\\RegexpException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Strings' => __DIR__ . '/..' . '/nette/utils/src/Utils/Strings.php',
'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php',
'Nette\\Utils\\UnknownImageFileException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Validators' => __DIR__ . '/..' . '/nette/utils/src/Utils/Validators.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7::$classMap;
}, null, ClassLoader::class);
}
}

View File

@ -0,0 +1,404 @@
{
"packages": [
{
"name": "ergebnis/phpstan-rules",
"version": "2.13.1",
"version_normalized": "2.13.1.0",
"source": {
"type": "git",
"url": "https://github.com/ergebnis/phpstan-rules.git",
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/f69db86b98595c34fc1f61c89fe3b380141aa519",
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"phpstan/phpstan": "^2.1.35"
},
"require-dev": {
"codeception/codeception": "^4.0.0 || ^5.0.0",
"doctrine/orm": "^2.20.0 || ^3.3.0",
"ergebnis/composer-normalize": "^2.49.0",
"ergebnis/license": "^2.7.0",
"ergebnis/php-cs-fixer-config": "^6.59.0",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
"fakerphp/faker": "^1.24.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpunit/phpunit": "^9.6.21",
"psr/container": "^2.0.2",
"symfony/finder": "^5.4.45",
"symfony/process": "^5.4.47"
},
"time": "2026-01-27T17:13:06+00:00",
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Ergebnis\\PHPStan\\Rules\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andreas Möller",
"email": "am@localheinz.com",
"homepage": "https://localheinz.com"
}
],
"description": "Provides rules for phpstan/phpstan.",
"homepage": "https://github.com/ergebnis/phpstan-rules",
"keywords": [
"PHPStan",
"phpstan-rules"
],
"support": {
"issues": "https://github.com/ergebnis/phpstan-rules/issues",
"security": "https://github.com/ergebnis/phpstan-rules/blob/main/.github/SECURITY.md",
"source": "https://github.com/ergebnis/phpstan-rules"
},
"install-path": "../ergebnis/phpstan-rules"
},
{
"name": "phpstan/extension-installer",
"version": "1.4.3",
"version_normalized": "1.4.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/extension-installer.git",
"reference": "85e90b3942d06b2326fba0403ec24fe912372936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
"reference": "85e90b3942d06b2326fba0403ec24fe912372936",
"shasum": ""
},
"require": {
"composer-plugin-api": "^2.0",
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.9.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2.0",
"phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
},
"time": "2024-09-04T20:21:43+00:00",
"type": "composer-plugin",
"extra": {
"class": "PHPStan\\ExtensionInstaller\\Plugin"
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"PHPStan\\ExtensionInstaller\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Composer plugin for automatic installation of PHPStan extensions",
"keywords": [
"dev",
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/extension-installer/issues",
"source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
},
"install-path": "../phpstan/extension-installer"
},
{
"name": "phpstan/phpstan",
"version": "2.2.7",
"version_normalized": "2.2.7.0",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
"reference": "692db47b9dddb0487934e5236e77d48594aef921",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"time": "2026-07-29T17:39:32+00:00",
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ondřej Mirtes"
},
{
"name": "Markus Staab"
},
{
"name": "Vincent Langlet"
}
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"docs": "https://phpstan.org/user-guide/getting-started",
"forum": "https://github.com/phpstan/phpstan/discussions",
"issues": "https://github.com/phpstan/phpstan/issues",
"security": "https://github.com/phpstan/phpstan/security/policy",
"source": "https://github.com/phpstan/phpstan-src"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
}
],
"install-path": "../phpstan/phpstan"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "2.0.12",
"version_normalized": "2.0.12.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b",
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.52"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6",
"shipmonk/name-collision-detector": "^2.1"
},
"time": "2026-07-19T07:24:06+00:00",
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Extra strict and opinionated rules for PHPStan",
"keywords": [
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12"
},
"install-path": "../phpstan/phpstan-strict-rules"
},
{
"name": "tomasvotruba/type-coverage",
"version": "2.3.0",
"version_normalized": "2.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/TomasVotruba/type-coverage.git",
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"shasum": ""
},
"require": {
"php": "^8.4",
"phpstan/phpstan": "^2.2",
"webmozart/assert": "^1.11 || ^2.1"
},
"require-dev": {
"phpstan/extension-installer": "^1.4",
"phpunit/phpunit": "^13.2",
"rector/jack": "^1.0",
"rector/rector": "^2.5",
"shipmonk/composer-dependency-analyser": "^1.8",
"symfony/dom-crawler": "^8.1",
"symplify/easy-coding-standard": "^13.2",
"tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12"
},
"time": "2026-07-29T20:32:37+00:00",
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"config/extension.neon",
"packages/type-perfect/config/extension.neon"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Rector\\TypePerfect\\": "packages/type-perfect/src",
"TomasVotruba\\TypeCoverage\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Measure type coverage of your project",
"keywords": [
"phpstan-extension",
"static analysis"
],
"support": {
"issues": "https://github.com/TomasVotruba/type-coverage/issues",
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
},
"funding": [
{
"url": "https://www.paypal.me/rectorphp",
"type": "custom"
},
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"install-path": "../tomasvotruba/type-coverage"
},
{
"name": "webmozart/assert",
"version": "2.4.1",
"version_normalized": "2.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^8.2"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"time": "2026-06-15T15:31:57+00:00",
"type": "library",
"extra": {
"psalm": {
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
},
"branch-alias": {
"dev-master": "2.0-dev",
"dev-feature/2-0": "2.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.4.1"
},
"install-path": "../webmozart/assert"
}
],
"dev": true,
"dev-package-names": [
"ergebnis/phpstan-rules",
"phpstan/extension-installer",
"phpstan/phpstan",
"phpstan/phpstan-strict-rules",
"tomasvotruba/type-coverage",
"webmozart/assert"
]
}

View File

@ -0,0 +1,77 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'ergebnis/phpstan-rules' => array(
'pretty_version' => '2.13.1',
'version' => '2.13.1.0',
'reference' => 'f69db86b98595c34fc1f61c89fe3b380141aa519',
'type' => 'phpstan-extension',
'install_path' => __DIR__ . '/../ergebnis/phpstan-rules',
'aliases' => array(),
'dev_requirement' => true,
),
'phpstan/extension-installer' => array(
'pretty_version' => '1.4.3',
'version' => '1.4.3.0',
'reference' => '85e90b3942d06b2326fba0403ec24fe912372936',
'type' => 'composer-plugin',
'install_path' => __DIR__ . '/../phpstan/extension-installer',
'aliases' => array(),
'dev_requirement' => true,
),
'phpstan/phpstan' => array(
'pretty_version' => '2.2.7',
'version' => '2.2.7.0',
'reference' => '692db47b9dddb0487934e5236e77d48594aef921',
'type' => 'library',
'install_path' => __DIR__ . '/../phpstan/phpstan',
'aliases' => array(),
'dev_requirement' => true,
),
'phpstan/phpstan-strict-rules' => array(
'pretty_version' => '2.0.12',
'version' => '2.0.12.0',
'reference' => '2bc5ae19ae965663b62ac907ee6342c3903ec93b',
'type' => 'phpstan-extension',
'install_path' => __DIR__ . '/../phpstan/phpstan-strict-rules',
'aliases' => array(),
'dev_requirement' => true,
),
'tomasvotruba/type-coverage' => array(
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'reference' => '7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644',
'type' => 'phpstan-extension',
'install_path' => __DIR__ . '/../tomasvotruba/type-coverage',
'aliases' => array(),
'dev_requirement' => true,
),
'webmozart/assert' => array(
'pretty_version' => '2.4.1',
'version' => '2.4.1.0',
'reference' => '2ccb7c2e821038c03a3e6e1700c570c158c55f70',
'type' => 'library',
'install_path' => __DIR__ . '/../webmozart/assert',
'aliases' => array(),
'dev_requirement' => true,
),
),
);

View File

@ -0,0 +1,775 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
For a full diff see [`2.13.1...main`][2.13.1...main].
## [`2.13.1`][2.13.1]
For a full diff see [`2.13.0...2.13.1`][2.13.0...2.13.1].
### Fixed
- Adjusted `Files\NoPhpstanIgnoreRule` to traverse into all nodes ([#1011]), by [@localheinz]
## [`2.13.0`][2.13.0]
For a full diff see [`2.12.0...2.13.0`][2.12.0...2.13.0].
### Added
- Added `Files\NoPhpstanIgnoreRule`, which reports an error when a `@phpstan-ignore`, `@phpstan-ignore-line`, or `@phpstan-ignore-next-line` tag is used to suppress an error ([#1009]), by [@localheinz]
### Changed
- Required `phpstan/phpstan:^2.1.35` ([#1010]), by [@localheinz]
## [`2.12.0`][2.12.0]
For a full diff see [`2.11.0...2.12.0`][2.11.0...2.12.0].
### Added
- Added support for PHP 8.5 ([#977]), by [@localheinz]
## [`2.11.0`][2.11.0]
For a full diff see [`2.10.5...2.11.0`][2.10.5...2.11.0].
### Changed
- Allowed installation on PHP 8.5 ([#972]), by [@localheinz]
## [`2.10.5`][2.10.5]
For a full diff see [`2.10.4...2.10.5`][2.10.4...2.10.5].
### Fixed
- Adjusted `Methods\NoNamedArgumentRule` to handle calls to constructors of variable class names ([#957]), by [@localheinz]
- Adjusted `Methods\NoNamedArgumentRule` to describe known calls only ([#958]), by [@localheinz]
## [`2.10.4`][2.10.4]
For a full diff see [`2.10.3...2.10.4`][2.10.3...2.10.4].
### Fixed
- Adjusted `Methods\NoNamedArgumentRule` to handle static calls on variable expressions ([#947]), by [@localheinz]
- Adjusted `Methods\NoNamedArgumentRule` to handle calls on invokables ([#948]), by [@localheinz]
- Adjusted `Methods\NoNamedArgumentRule` to handle calls on callables assigned to properties ([#949]), by [@localheinz]
- Adjusted `Methods\NoNamedArgumentRule` to handle all other calls with generic error message ([#951]), by [@localheinz]
## [`2.10.3`][2.10.3]
For a full diff see [`2.10.2...2.10.3`][2.10.2...2.10.3].
### Fixed
- Adjusted `Methods\InvokeParentHookMethodRule` to ignore comments ([#944]), by [@localheinz]
## [`2.10.2`][2.10.2]
For a full diff see [`2.10.1...2.10.2`][2.10.1...2.10.2].
### Fixed
- Renamed error identifier for `Methods\InvokeParentHookMethodRule` ([#943]), by [@localheinz]
## [`2.10.1`][2.10.1]
For a full diff see [`2.10.0...2.10.1`][2.10.0...2.10.1].
### Fixed
- Fixed schema for configuration of `Methods\InvokeParentHookMethodRule` ([#940]), by [@localheinz]
## [`2.10.0`][2.10.0]
For a full diff see [`2.9.0...2.10.0`][2.9.0...2.10.0].
### Added
- Added `Methods\InvokeParentHookMethodRule`, which reports an error when a hook method that overrides a hook method in a parent class does not invoke the overridden hook method in the expected order ([#939]), by [@localheinz]
## [`2.9.0`][2.9.0]
For a full diff see [`2.8.0...2.9.0`][2.8.0...2.9.0].
### Added
- Added `CallLikes\NoNamedArgumentRule`, which reports an error when an anonymous function, a function, or a method is invoked using a named argument ([#914]), by [@localheinz]
### Changed
- Required `phpstan/phpstan:^2.1.8` ([#938]), by [@localheinz]
## [`2.8.0`][2.8.0]
For a full diff see [`2.7.0...2.8.0`][2.7.0...2.8.0].
### Added
- Added `allRules` parameter to allow disabling and enabling all rules ([#913]), by [@localheinz]
- Added `Expressions\NoAssignByReferenceRule`, which reports an error when a variable is assigned by reference ([#914]), by [@localheinz]
## [`2.7.0`][2.7.0]
For a full diff see [`2.6.1...2.7.0`][2.6.1...2.7.0].
### Added
- Added `Closures\NoParameterPassedByReferenceRule`, `Functions\NoParameterPassedByReferenceRule`, `Methods\NoParameterPassedByReferenceRule`, which report an error when a closure, a function, or a method has a parameter that is passed by reference ([#911]), by [@localheinz]
- Added `Functions\NoReturnByReferenceRule` and `Methods\NoReturnByReferenceRule`, which report an error when a function or a method returns by reference ([#912]), by [@localheinz]
## [`2.6.1`][2.6.1]
For a full diff see [`2.6.0...2.6.1`][2.6.0...2.6.1].
### Fixed
- Adjusted `Methods\NoParameterWithNullableTypeDeclarationRule` to use the appropriate error identifier ([#902]), by [@manuelkiessling]
## [`2.6.0`][2.6.0]
For a full diff see [`2.5.2...2.6.0`][2.5.2...2.6.0].
### Added
- Added support for `phpstan/phpstan:^2.0.0` ([#873]), by [@localheinz]
## [`2.5.2`][2.5.2]
For a full diff see [`2.5.1...2.5.2`][2.5.1...2.5.2].
### Fixed
- Adjusted `Closures\NoNullableReturnTypeDeclarationRule`, `Closures\NoParameterWithNullableTypeDeclarationRule`, `Functions\NoNullableReturnTypeDeclarationRule`, `Functions\NoParameterWithNullableTypeDeclarationRule`, `Methods\NoNullableReturnTypeDeclarationRule`, `Methods\NoParameterWithNullableTypeDeclarationRule` to detect cases where `null` is referenced with incorrect case or relative to the root namespace ([#897]), by [@localheinz]
## [`2.5.1`][2.5.1]
For a full diff see [`2.5.0...2.5.1`][2.5.0...2.5.1].
### Fixed
- Returned rule with error identifier ([#882]), by [@localheinz]
- Adjusted `Methods\FinalInAbstractClassRule` to ignore Doctrine embeddables and entities ([#396]), by [@localheinz]
- Adjusted `Expressions\NoCompactRule` to detect usages of `compact()` with incorrect case ([#889]), by [@localheinz]
- Adjusted `Methods\PrivateInFinalClassRule` to use more appropriate message when detecting a `protected` method in an anonymous class ([#890]), by [@localheinz]
- Adjusted `Methods\PrivateInFinalClassRule` to ignore `protected` methods from traits ([#891]), by [@localheinz]
- Adjusted `Methods\PrivateInFinalClassRule` to ignore `protected` methods with `phpunit/phpunit` attributes requiring methods to be `protected` ([#863]), by [@cosmastech]
- Adjusted `Methods\PrivateInFinalClassRule` to ignore `protected` methods with `phpunit/phpunit` annotations requiring methods to be `protected` ([#895]), by [@cosmastech]
## [`2.5.0`][2.5.0]
For a full diff see [`2.4.0...2.5.0`][2.4.0...2.5.0].
### Added
- Added rule error identifiers ([#875]), by [@localheinz]
- Added support for PHP 8.0 ([#877]), by [@localheinz]
- Added support for PHP 7.4 ([#880]), by [@localheinz]
### Changed
- Removed dependency on `nikic/php-parser` ([#878]), by [@localheinz]
## [`2.4.0`][2.4.0]
For a full diff see [`2.3.0...2.4.0`][2.3.0...2.4.0].
### Added
- Added support for PHP 8.4 ([#872]), by [@localheinz]
## [`2.3.0`][2.3.0]
For a full diff see [`2.2.0...2.3.0`][2.2.0...2.3.0].
### Changed
- Allowed installation on PHP 8.4 ([#862]), by [@localheinz]
## [`2.2.0`][2.2.0]
For a full diff see [`2.1.0...2.2.0`][2.1.0...2.2.0].
### Changed
- Allowed installation of `nikic/php-parser:^5.0.0` ([#735]), by [@localheinz]
## [`2.1.0`][2.1.0]
For a full diff see [`2.0.0...2.1.0`][2.0.0...2.1.0].
### Changed
- Dropped support for PHP 8.0 ([#567]), by [@localheinz]
- Added support for PHP 8.3 ([#604]), by [@nunomaduro]
## [`2.0.0`][2.0.0]
For a full diff see [`1.0.0...2.0.0`][1.0.0...2.0.0].
### Added
- Added `methodsAllowedToUseContainerTypeDeclarations` parameter to allow configuring a list of method names that are allowed to have container parameter type declarations ([#541), by [@localheinz]
- Allowed disabling rules ([#542), by [@localheinz]
- Added support for nullable union types ([#543), by [@localheinz]
### Changed
- Dropped support for PHP 7.2 ([#496]), by [@localheinz]
- Dropped support for PHP 7.3 ([#498]), by [@localheinz]
- Dropped support for PHP 7.4 ([#499]), by [@localheinz]
- Added support for PHP 8.2 ([#540]), by [@localheinz]
### Removed
- Removed `Expressions\NoEmptyRule` ([#525]), by [@enumag]
## [`1.0.0`][1.0.0]
For a full diff see [`0.15.3...1.0.0`][0.15.3...1.0.0].
### Changed
- Added support for `phpstan/phpstan:^1.0.0` and dropped support for non-stable versions of `phpstan/phpstan` ([#381]), by [@rpkamp]
### Fixed
- Adjusted `Classes\FinalRule` to not report an error when a non-final class has a `Doctrinbe\ORM\Mapping\Entity` attribute ([#395]), by [@localheinz]
## [`0.15.3`][0.15.3]
For a full diff see [`0.15.2...0.15.3`][0.15.2...0.15.3].
### Changed
- Allow installation with PHP 8.0 ([#294]), by [@localheinz]
## [`0.15.2`][0.15.2]
For a full diff see [`0.15.1...0.15.2`][0.15.1...0.15.2].
### Changed
- Dropped support for PHP 7.1 ([#259]), by [@localheinz]
## [`0.15.1`][0.15.1]
For a full diff see [`0.15.0...0.15.1`][0.15.0...0.15.1].
### Changed
- Adjusted `Methods\FinalInAbstractClass` rule to allow non-`final` `public` constructors in abstract classes ([#248]), by [@Slamdunk]
## [`0.15.0`][0.15.0]
For a full diff see [`0.14.4...0.15.0`][0.14.4...0.15.0].
### Added
- Added `Classes\PHPUnit\Framework\TestCaseWithSuffixRule`, which reports an error when a concrete class extending `PHPUnit\Framework\TestCase` does not have a `Test` suffix ([#225]), by [@localheinz]
## [`0.14.4`][0.14.4]
For a full diff see [`0.14.3...0.14.4`][0.14.3...0.14.4].
### Fixed
- Ignored classes with `@ORM\Mapping\Entity` annotations in `FinalRule` ([#202]), by [@localheinz]
## [`0.14.3`][0.14.3]
For a full diff see [`0.14.2...0.14.3`][0.14.2...0.14.3].
### Fixed
- Ignored first line in `DeclareStrictTypesRule` when it is a shebang ([#186]), by [@Great-Antique]
## [`0.14.2`][0.14.2]
For a full diff see [`0.14.1...0.14.2`][0.14.1...0.14.2].
### Fixed
- Brought back support for PHP 7.1 ([#166]), by [@localheinz]
## [`0.14.1`][0.14.1]
For a full diff see [`0.14.0...0.14.1`][0.14.0...0.14.1].
### Fixed
- Removed an inappropriate `replace` configuration from `composer.json` ([#161]), by [@localheinz]
## [`0.14.0`][0.14.0]
For a full diff see [`0.13.0...0.14.0`][0.13.0...0.14.0].
### Changed
- Allowed installation of `phpstan/phpstan:~0.12.0` ([#147]), by [@localheinz]
- Renamed vendor namespace `Localheinz` to `Ergebnis` after move to [@ergebnis] ([#157]), by [@localheinz]
Run
```sh
composer remove localheinz/phpstan-rules
```
and
```sh
composer require ergebnis/phpstan-rules
```
to update.
Run
```sh
find . -type f -exec sed -i '.bak' 's/Localheinz\\PHPStan/Ergebnis\\PHPStan/g' {} \;
```
to replace occurrences of `Localheinz\PHPStan` with `Ergebnis\PHPStan`.
Run
```sh
find -type f -name '*.bak' -delete
```
to delete backup files created in the previous step.
- Moved parameters into `ergebnis` section to prevent conflicts with global parameters ([#158]), by [@localheinz]
Where previously `phpstan.neon` looked like the following
```neon
parameters:
allowAbstractClasses: true
classesAllowedToBeExtended: []
classesNotRequiredToBeAbstractOrFinal: []
interfacesImplementedByContainers:
- Psr\Container\ContainerInterface
```
these parameters now need to be moved into an `ergebnis` section:
```diff
parameters:
- allowAbstractClasses: true
- classesAllowedToBeExtended: []
- classesNotRequiredToBeAbstractOrFinal: []
- interfacesImplementedByContainers:
- - Psr\Container\ContainerInterface
+ ergebnis:
+ allowAbstractClasses: true
+ classesAllowedToBeExtended: []
+ classesNotRequiredToBeAbstractOrFinal: []
+ interfacesImplementedByContainers:
+ - Psr\Container\ContainerInterface
```
### Fixed
- Dropped support for PHP 7.1 ([#141]), by [@localheinz]
## [`0.13.0`][0.13.0]
For a full diff see [`0.12.2...0.13.0`][0.12.2...0.13.0].
### Added
- Added `Methods\PrivateInFinalClassRule` which reports an error when a method in a `final` class is `protected` when it could be `private` ([#126]), by [@localheinz]
## [`0.12.2`][0.12.2]
For a full diff see [`0.12.1...0.12.2`][0.12.1...0.12.2].
### Fixed
- Started ignoring interfaces from analysis by `Methods\FinalInAbstractClassRule` to avoid inappropriate errors ([#132]), by [@localheinz]
## [`0.12.1`][0.12.1]
For a full diff see [`0.12.0...0.12.1`][0.12.0...0.12.1].
### Fixed
- Started resolving class name in type declaration before attempting to analyze it in the `Methods\NoParameterWithContainerTypeDeclarationRule` to avoid errors where class `self` is not found ([#128]), by [@localheinz]
## [`0.12.0`][0.12.0]
For a full diff see [`0.11.0...0.12.0`][0.11.0...0.12.0].
### Added
- Added `Methods\NoParameterWithContainerTypeDeclarationRule`, which reports an error when a method has a type declaration that corresponds to a known dependency injection container or service locator ([#122]), by [@localheinz]
- Added `Methods\FinalInAbstractClassRule`, which reports an error when a concrete `public` or `protected` method in an `abstract` class is not `final` ([#123]), by [@localheinz]
## [`0.11.0`][0.11.0]
For a full diff see [`0.10.0...0.11.0`][0.10.0...0.11.0].
### Added
- Added `Files\DeclareStrictTypesRule`, which reports an error when a PHP file does not have a `declare(strict_types=1)` declaration ([#79]
- Added `Expressions\NoEmptyRule`, which reports an error when the language construct `empty()` is used ([#110]), by [@localheinz]
- Added `Expressions\NoEvalRule`, which reports an error when the language construct `eval()` is used ([#112]), by [@localheinz]
- Added `Expressions\NoErrorSuppressionRule`, which reports an error when `@` is used to suppress errors ([#113]), by [@localheinz]
- Added `Expressions\NoCompactRule`, which reports an error when the function `compact()` is used ([#116]), by [@localheinz]
- Added `Statements\NoSwitchRule`, which reports an error when the statement `switch()` is used ([#117]), by [@localheinz]
### Changed
- Require at least `nikic/php-parser:^4.2.3` ([#102]), by [@localheinz]
- Require at least `phpstan/phpstan:~0.11.15` ([#103]), by [@localheinz]
## [`0.10.0`][0.10.0]
For a full diff see [`0.9.1...0.10.0`][0.9.1...0.10.0].
### Changed
- Require at least `phpstan/phpstan:~0.11.7` ([#91]), by [@localheinz]
### Fixed
- Added missing `parametersSchema` configuration to `rules.neon`, which is required for use with `bleedingEdge.neon`, see [`phpstan/phpstan@54a125d`](https://github.com/phpstan/phpstan/commit/54a125df47fa097b792cb9a3e70c49f753f66b85) ([#93]), by [@localheinz]
*
## [`0.9.1`][0.9.1]
For a full diff see [`0.9.0...0.9.1`][0.9.0...0.9.1].
### Changed
- Allow usage with [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer) ([#89]), by [@localheinz]
## [`0.9.0`][0.9.0]
For a full diff see [`0.8.1...0.9.0`][0.8.1...0.9.0].
### Changed
- Adjusted `Classes\FinalRule` to ignore Doctrine entities when they are annotated ([#84]), by [@localheinz]
## [`0.8.1`][0.8.1]
For a full diff see [`0.8.0...0.8.1`][0.8.0...0.8.1].
### Fixed
- Actually enable `Expressions\NoIssetRule` ([#83]), by [@localheinz]
## [`0.8.0`][0.8.0]
For a full diff see [`0.7.1...0.8.0`][0.7.1...0.8.0].
### Added
- Added `Expressions\NoIssetRule`, which reports an error when the language construct `isset()` is used ([#81]), by [@localheinz]
## [`0.7.1`][0.7.1]
For a full diff see [`0.7.0...0.7.1`][0.7.0...0.7.1].
### Changed
- Configured `Classes\NoExtendsRule` to allow a set of default classes to be extended ([#73]), by [@localheinz]
## [`0.7.0`][0.7.0]
For a full diff see [`0.6.0...0.7.0`][0.6.0...0.7.0].
### Added
- Added `Classes\NoExtendsRule`, which reports an error when a class extends a class that is not allowed to be extended ([#68]), by [@localheinz]
## [`0.6.0`][0.6.0]
For a full diff see [`0.5.0...0.6.0`][0.5.0...0.6.0].
### Added
- Allow installation with `phpstan/phpstan:~0.11.0` ([#65]), by [@localheinz]
## [`0.5.0`][0.5.0]
For a full diff see [`0.4.0...0.5.0`][0.4.0...0.5.0].
### Added
- Added `Methods\NoConstructorParameterWithDefaultValueRule`, which reports an error when a constructor of an anonymous class or a class has a parameter with a default value ([#45]), by [@localheinz]
- Added parameters `$allowAbstractClasses` and `$classesNotRequiredToBeAbstractOrFinal` to allow configuration of `Classes`FinalRule` ([#51]), by [@localheinz]
### Removed
- Removed `Classes\AbstractOrFinalRule` after merging behaviour into `Classes\FinalRule` ([#51]), by [@localheinz]
- Removed default values from constructor of `Classes\FinalRule` ([#53]), by [@localheinz]
## [`0.4.0`][0.4.0]
For a full diff see [`0.3.0...0.4.0`][0.3.0...0.4.0]
### Changed
- Removed double-quotes from error messages to be more consistent with error messages generated by `phpstan/phstan` ([#39]), by [@localheinz]
- Modified wording and placement of method, function, and parameter names in error messages to be more consistent with error messages generated by `phpstan/phstan` ([#42]), by [@localheinz]
## [`0.3.0`][0.3.0]
For a full diff see [`0.2.0...0.3.0`][0.2.0...0.3.0]
### Added
- Added `Functions\NoNullableReturnTypeDeclarationRule`, which reports an error when a function has a nullable return type declaration, and `Methods\NoNullableReturnTypeDeclarationRule`, which reports an error when a method declared in an anonymous class, a class, or an interface has a nullable return type declaration ([#16]), by [@localheinz]
- Added `Closures\NoParameterWithNullDefaultValueRule`, which reports an error when a closure has a parameter with `null` as default value ([#26]), by [@localheinz]
- Added `Closures\NoNullableReturnTypeDeclarationRule`, which reports an error when a closure has a nullable return type declaration ([#29]), by [@localheinz]
- Added `Functions\NoParameterWithNullDefaultValueRule`, which reports an error when a function has a parameter with `null` as default value ([#31]), by [@localheinz]
- Added `Methods\NoParameterWithNullDefaultValueRule`, which reports an error when a method declared in an anonymous class, a class, or an interface has a parameter with `null` as default value ([#32]), by [@localheinz]
- Added `Closures\NoParameterWithNullableTypeDeclarationRule`, which reports an error when a closure has a parameter with a nullable type declaration ([#33]), by [@localheinz]
- Added `Functions\NoParameterWithNullableTypeDeclarationRule`, which reports an error when a function has a parameter with a nullable type declaration ([#34]), by [@localheinz]
- Added `Methods\NoParameterWithNullableTypeDeclarationRule`, which reports an error when a method declared in an anonymous class, a class, or an interface has a parameter with a nullable type declaration ([#35]), by [@localheinz]
- Extracted `rules.neon`, so you can easily enable all rules by including it in your `phpstan.neon` ([#37]), by [@localheinz]
## [`0.2.0`][0.2.0]
For a full diff see [`0.1.0...0.2.0`][0.1.0...0.2.0]
### Added
- Added `Classes\FinalRule`, which reports an error when a non-anonymous class is not `final`, ([#4]), by [@localheinz]
### Changed
- Added an `$excludeClassNames` argument to the constructors of `Classes\AbstractOrFinalRule` and `Classes\FinalRule` to allow whitelisting of classes, ([#11]), by [@localheinz]
## [`0.1.0`][0.1.0]
For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
### Added
- Added `Classes\AbstractOrFinalRule`, which reports an error when a non-anonymous class is neither `abstract` nor `final`, ([#1]), by [@localheinz]
[0.1.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.1.0
[0.2.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.2.0
[0.3.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.3.0
[0.4.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.4.0
[0.5.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.5.0
[0.6.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.6.0
[0.7.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.7.0
[0.7.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.7.1
[0.8.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.8.0
[0.8.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.8.1
[0.9.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.9.0
[0.9.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.9.1
[0.10.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.10.0
[0.11.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.11.0
[0.12.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.12.0
[0.12.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.12.1
[0.12.2]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.12.2
[0.13.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.13.0
[0.14.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.14.0
[0.14.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.14.1
[0.14.2]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.14.2
[0.14.3]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.14.3
[0.14.4]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.14.4
[0.15.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.15.0
[0.15.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.15.1
[0.15.2]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.15.2
[0.15.3]: https://github.com/ergebnis/phpstan-rules/releases/tag/0.15.3
[1.0.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/1.0.0
[2.0.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.0.0
[2.1.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.1.0
[2.2.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.2.0
[2.3.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.3.0
[2.4.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.4.0
[2.5.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.5.0
[2.5.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.5.1
[2.5.2]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.5.2
[2.6.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.6.0
[2.6.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.6.1
[2.7.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.7.0
[2.8.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.8.0
[2.9.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.9.0
[2.10.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.0
[2.10.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.1
[2.10.2]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.2
[2.10.3]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.3
[2.10.4]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.4
[2.10.5]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.5
[2.11.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.11.0
[2.12.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.12.0
[2.13.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.13.0
[2.13.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.13.1
[362c7ea...0.1.0]: https://github.com/ergebnis/phpstan-rules/compare/362c7ea...0.1.0
[0.1.0...0.2.0]: https://github.com/ergebnis/phpstan-rules/compare/0.1.0...0.2.0
[0.2.0...0.3.0]: https://github.com/ergebnis/phpstan-rules/compare/0.2.0...0.3.0
[0.3.0...0.4.0]: https://github.com/ergebnis/phpstan-rules/compare/0.3.0...0.4.0
[0.4.0...0.5.0]: https://github.com/ergebnis/phpstan-rules/compare/0.4.0...0.5.0
[0.5.0...0.6.0]: https://github.com/ergebnis/phpstan-rules/compare/0.5.0...0.6.0
[0.6.0...0.7.0]: https://github.com/ergebnis/phpstan-rules/compare/0.6.0...0.7.0
[0.7.0...0.7.1]: https://github.com/ergebnis/phpstan-rules/compare/0.7.0...0.7.1
[0.7.1...0.8.0]: https://github.com/ergebnis/phpstan-rules/compare/0.7.1...0.8.0
[0.8.0...0.8.1]: https://github.com/ergebnis/phpstan-rules/compare/0.8.0...0.8.1
[0.8.1...0.9.0]: https://github.com/ergebnis/phpstan-rules/compare/0.8.1...0.9.0
[0.9.0...0.9.1]: https://github.com/ergebnis/phpstan-rules/compare/0.9.0...0.9.1
[0.9.1...0.10.0]: https://github.com/ergebnis/phpstan-rules/compare/0.9.1...0.10.0
[0.10.0...0.11.0]: https://github.com/ergebnis/phpstan-rules/compare/0.10.0...0.11.0
[0.11.0...0.12.0]: https://github.com/ergebnis/phpstan-rules/compare/0.11.0...0.12.0
[0.12.0...0.12.1]: https://github.com/ergebnis/phpstan-rules/compare/0.12.0...0.12.1
[0.12.1...0.12.2]: https://github.com/ergebnis/phpstan-rules/compare/0.12.1...0.12.2
[0.12.2...0.13.0]: https://github.com/ergebnis/phpstan-rules/compare/0.12.2...0.13.0
[0.13.0...0.14.0]: https://github.com/ergebnis/phpstan-rules/compare/0.13.0...0.14.0
[0.14.0...0.14.1]: https://github.com/ergebnis/phpstan-rules/compare/0.14.0...0.14.1
[0.14.1...0.14.2]: https://github.com/ergebnis/phpstan-rules/compare/0.14.1...0.14.2
[0.14.2...0.14.3]: https://github.com/ergebnis/phpstan-rules/compare/0.14.2...0.14.3
[0.14.3...0.14.4]: https://github.com/ergebnis/phpstan-rules/compare/0.14.3...0.14.4
[0.14.4...0.15.0]: https://github.com/ergebnis/phpstan-rules/compare/0.14.4...0.15.0
[0.15.0...0.15.1]: https://github.com/ergebnis/phpstan-rules/compare/0.15.0...0.15.1
[0.15.1...0.15.2]: https://github.com/ergebnis/phpstan-rules/compare/0.15.1...0.15.2
[0.15.2...0.15.3]: https://github.com/ergebnis/phpstan-rules/compare/0.15.2...0.15.3
[0.15.3...1.0.0]: https://github.com/ergebnis/phpstan-rules/compare/0.15.3...1.0.0
[1.0.0...2.0.0]: https://github.com/ergebnis/phpstan-rules/compare/1.0.0...2.0.0
[2.0.0...2.1.0]: https://github.com/ergebnis/phpstan-rules/compare/2.0.0...2.1.0
[2.1.0...2.2.0]: https://github.com/ergebnis/phpstan-rules/compare/2.1.0...2.2.0
[2.2.0...2.3.0]: https://github.com/ergebnis/phpstan-rules/compare/2.2.0...2.3.0
[2.3.0...2.4.0]: https://github.com/ergebnis/phpstan-rules/compare/2.3.0...2.4.0
[2.4.0...2.5.0]: https://github.com/ergebnis/phpstan-rules/compare/2.4.0...2.5.0
[2.5.0...2.5.1]: https://github.com/ergebnis/phpstan-rules/compare/2.5.0...2.5.1
[2.5.1...2.5.2]: https://github.com/ergebnis/phpstan-rules/compare/2.5.1...2.5.2
[2.5.2...2.6.0]: https://github.com/ergebnis/phpstan-rules/compare/2.5.2...2.6.0
[2.6.0...2.6.1]: https://github.com/ergebnis/phpstan-rules/compare/2.6.0...2.6.1
[2.6.1...2.7.0]: https://github.com/ergebnis/phpstan-rules/compare/2.6.1...2.7.0
[2.7.0...2.8.0]: https://github.com/ergebnis/phpstan-rules/compare/2.7.0...2.8.0
[2.8.0...2.9.0]: https://github.com/ergebnis/phpstan-rules/compare/2.8.0...2.9.0
[2.9.0...2.10.0]: https://github.com/ergebnis/phpstan-rules/compare/2.9.0...2.10.0
[2.10.0...2.10.1]: https://github.com/ergebnis/phpstan-rules/compare/2.10.0...2.10.1
[2.10.1...2.10.2]: https://github.com/ergebnis/phpstan-rules/compare/2.10.1...2.10.2
[2.10.2...2.10.3]: https://github.com/ergebnis/phpstan-rules/compare/2.10.2...2.10.3
[2.10.3...2.10.4]: https://github.com/ergebnis/phpstan-rules/compare/2.10.3...2.10.4
[2.10.4...2.10.5]: https://github.com/ergebnis/phpstan-rules/compare/2.10.4...2.10.5
[2.10.5...2.11.0]: https://github.com/ergebnis/phpstan-rules/compare/2.10.5...2.11.0
[2.11.0...2.12.0]: https://github.com/ergebnis/phpstan-rules/compare/2.11.0...2.12.0
[2.12.0...2.13.0]: https://github.com/ergebnis/phpstan-rules/compare/2.12.0...2.13.0
[2.13.0...2.13.1]: https://github.com/ergebnis/phpstan-rules/compare/2.13.0...2.13.1
[2.13.1...main]: https://github.com/ergebnis/phpstan-rules/compare/2.13.1...main
[#1]: https://github.com/ergebnis/phpstan-rules/pull/1
[#4]: https://github.com/ergebnis/phpstan-rules/pull/4
[#11]: https://github.com/ergebnis/phpstan-rules/pull/11
[#16]: https://github.com/ergebnis/phpstan-rules/pull/16
[#26]: https://github.com/ergebnis/phpstan-rules/pull/26
[#29]: https://github.com/ergebnis/phpstan-rules/pull/29
[#31]: https://github.com/ergebnis/phpstan-rules/pull/31
[#32]: https://github.com/ergebnis/phpstan-rules/pull/32
[#33]: https://github.com/ergebnis/phpstan-rules/pull/33
[#34]: https://github.com/ergebnis/phpstan-rules/pull/34
[#35]: https://github.com/ergebnis/phpstan-rules/pull/35
[#37]: https://github.com/ergebnis/phpstan-rules/pull/37
[#39]: https://github.com/ergebnis/phpstan-rules/pull/39
[#42]: https://github.com/ergebnis/phpstan-rules/pull/42
[#45]: https://github.com/ergebnis/phpstan-rules/pull/45
[#51]: https://github.com/ergebnis/phpstan-rules/pull/51
[#53]: https://github.com/ergebnis/phpstan-rules/pull/53
[#65]: https://github.com/ergebnis/phpstan-rules/pull/65
[#68]: https://github.com/ergebnis/phpstan-rules/pull/68
[#73]: https://github.com/ergebnis/phpstan-rules/pull/73
[#79]: https://github.com/ergebnis/phpstan-rules/pull/79
[#81]: https://github.com/ergebnis/phpstan-rules/pull/81
[#83]: https://github.com/ergebnis/phpstan-rules/pull/83
[#84]: https://github.com/ergebnis/phpstan-rules/pull/84
[#89]: https://github.com/ergebnis/phpstan-rules/pull/89
[#91]: https://github.com/ergebnis/phpstan-rules/pull/91
[#93]: https://github.com/ergebnis/phpstan-rules/pull/93
[#102]: https://github.com/ergebnis/phpstan-rules/pull/102
[#103]: https://github.com/ergebnis/phpstan-rules/pull/103
[#110]: https://github.com/ergebnis/phpstan-rules/pull/110
[#112]: https://github.com/ergebnis/phpstan-rules/pull/112
[#113]: https://github.com/ergebnis/phpstan-rules/pull/113
[#116]: https://github.com/ergebnis/phpstan-rules/pull/116
[#117]: https://github.com/ergebnis/phpstan-rules/pull/117
[#122]: https://github.com/ergebnis/phpstan-rules/pull/122
[#123]: https://github.com/ergebnis/phpstan-rules/pull/123
[#126]: https://github.com/ergebnis/phpstan-rules/pull/126
[#128]: https://github.com/ergebnis/phpstan-rules/pull/128
[#132]: https://github.com/ergebnis/phpstan-rules/pull/132
[#141]: https://github.com/ergebnis/phpstan-rules/pull/141
[#147]: https://github.com/ergebnis/phpstan-rules/pull/147
[#157]: https://github.com/ergebnis/phpstan-rules/pull/157
[#158]: https://github.com/ergebnis/phpstan-rules/pull/158
[#161]: https://github.com/ergebnis/phpstan-rules/pull/161
[#166]: https://github.com/ergebnis/phpstan-rules/pull/166
[#186]: https://github.com/ergebnis/phpstan-rules/pull/186
[#202]: https://github.com/ergebnis/phpstan-rules/pull/202
[#225]: https://github.com/ergebnis/phpstan-rules/pull/225
[#248]: https://github.com/ergebnis/phpstan-rules/pull/248
[#259]: https://github.com/ergebnis/phpstan-rules/pull/259
[#294]: https://github.com/ergebnis/phpstan-rules/pull/294
[#381]: https://github.com/ergebnis/phpstan-rules/pull/381
[#395]: https://github.com/ergebnis/phpstan-rules/pull/395
[#396]: https://github.com/ergebnis/phpstan-rules/pull/396
[#496]: https://github.com/ergebnis/phpstan-rules/pull/496
[#498]: https://github.com/ergebnis/phpstan-rules/pull/498
[#499]: https://github.com/ergebnis/phpstan-rules/pull/498
[#525]: https://github.com/ergebnis/phpstan-rules/pull/525
[#540]: https://github.com/ergebnis/phpstan-rules/pull/540
[#541]: https://github.com/ergebnis/phpstan-rules/pull/541
[#542]: https://github.com/ergebnis/phpstan-rules/pull/542
[#543]: https://github.com/ergebnis/phpstan-rules/pull/543
[#567]: https://github.com/ergebnis/phpstan-rules/pull/567
[#735]: https://github.com/ergebnis/phpstan-rules/pull/735
[#862]: https://github.com/ergebnis/phpstan-rules/pull/862
[#863]: https://github.com/ergebnis/phpstan-rules/pull/863
[#872]: https://github.com/ergebnis/phpstan-rules/pull/872
[#873]: https://github.com/ergebnis/phpstan-rules/pull/873
[#875]: https://github.com/ergebnis/phpstan-rules/pull/875
[#877]: https://github.com/ergebnis/phpstan-rules/pull/877
[#878]: https://github.com/ergebnis/phpstan-rules/pull/878
[#880]: https://github.com/ergebnis/phpstan-rules/pull/880
[#882]: https://github.com/ergebnis/phpstan-rules/pull/882
[#889]: https://github.com/ergebnis/phpstan-rules/pull/889
[#890]: https://github.com/ergebnis/phpstan-rules/pull/890
[#891]: https://github.com/ergebnis/phpstan-rules/pull/891
[#895]: https://github.com/ergebnis/phpstan-rules/pull/895
[#897]: https://github.com/ergebnis/phpstan-rules/pull/897
[#902]: https://github.com/ergebnis/phpstan-rules/pull/902
[#911]: https://github.com/ergebnis/phpstan-rules/pull/911
[#912]: https://github.com/ergebnis/phpstan-rules/pull/912
[#913]: https://github.com/ergebnis/phpstan-rules/pull/913
[#914]: https://github.com/ergebnis/phpstan-rules/pull/914
[#938]: https://github.com/ergebnis/phpstan-rules/pull/938
[#939]: https://github.com/ergebnis/phpstan-rules/pull/939
[#940]: https://github.com/ergebnis/phpstan-rules/pull/940
[#943]: https://github.com/ergebnis/phpstan-rules/pull/943
[#944]: https://github.com/ergebnis/phpstan-rules/pull/944
[#947]: https://github.com/ergebnis/phpstan-rules/pull/947
[#948]: https://github.com/ergebnis/phpstan-rules/pull/948
[#949]: https://github.com/ergebnis/phpstan-rules/pull/949
[#951]: https://github.com/ergebnis/phpstan-rules/pull/951
[#957]: https://github.com/ergebnis/phpstan-rules/pull/957
[#958]: https://github.com/ergebnis/phpstan-rules/pull/958
[#972]: https://github.com/ergebnis/phpstan-rules/pull/972
[#977]: https://github.com/ergebnis/phpstan-rules/pull/977
[#1009]: https://github.com/ergebnis/phpstan-rules/pull/1009
[#1010]: https://github.com/ergebnis/phpstan-rules/pull/1010
[#1011]: https://github.com/ergebnis/phpstan-rules/pull/1011
[@cosmastech]: https://github.com/cosmastech
[@enumag]: https://github.com/enumag
[@ergebnis]: https://github.com/ergebnis
[@Great-Antique]: https://github.com/Great-Antique
[@localheinz]: https://github.com/localheinz
[@manuelkiessling]: https://github.com/manuelkiessling
[@nunomaduro]: https://github.com/nunomaduro
[@rpkamp]: https://github.com/rpkamp
[@Slamdunk]: https://github.com/Slamdunk

View File

@ -0,0 +1,16 @@
# The MIT License (MIT)
Copyright (c) 2018-2026 Andreas Möller
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the _Software_), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED **AS IS**, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,776 @@
# phpstan-rules
[![Integrate](https://github.com/ergebnis/phpstan-rules/workflows/Integrate/badge.svg)](https://github.com/ergebnis/phpstan-rules/actions)
[![Merge](https://github.com/ergebnis/phpstan-rules/workflows/Merge/badge.svg)](https://github.com/ergebnis/phpstan-rules/actions)
[![Release](https://github.com/ergebnis/phpstan-rules/workflows/Release/badge.svg)](https://github.com/ergebnis/phpstan-rules/actions)
[![Renew](https://github.com/ergebnis/phpstan-rules/workflows/Renew/badge.svg)](https://github.com/ergebnis/phpstan-rules/actions)
[![Code Coverage](https://codecov.io/gh/ergebnis/phpstan-rules/branch/main/graph/badge.svg)](https://codecov.io/gh/ergebnis/phpstan-rules)
[![Latest Stable Version](https://poser.pugx.org/ergebnis/phpstan-rules/v/stable)](https://packagist.org/packages/ergebnis/phpstan-rules)
[![Total Downloads](https://poser.pugx.org/ergebnis/phpstan-rules/downloads)](https://packagist.org/packages/ergebnis/phpstan-rules)
[![Monthly Downloads](http://poser.pugx.org/ergebnis/phpstan-rules/d/monthly)](https://packagist.org/packages/ergebnis/phpstan-rules)
This project provides a [`composer`](https://getcomposer.org) package with rules for [`phpstan/phpstan`](https://github.com/phpstan/phpstan).
## Installation
Run
```sh
composer require --dev ergebnis/phpstan-rules
```
## Usage
All of the [rules](https://github.com/ergebnis/phpstan-rules#rules) provided (and used) by this library are included in [`rules.neon`](rules.neon).
When you are using [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), `rules.neon` will be automatically included.
Otherwise you need to include `rules.neon` in your `phpstan.neon`:
```neon
includes:
- vendor/ergebnis/phpstan-rules/rules.neon
```
:bulb: You probably want to use these rules on top of the rules provided by:
- [`phpstan/phpstan`](https://github.com/phpstan/phpstan)
- [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules)
- [`phpstan/phpstan-strict-rules`](https://github.com/phpstan/phpstan-strict-rules)
## Rules
This package provides the following rules for use with [`phpstan/phpstan`](https://github.com/phpstan/phpstan):
- [`Ergebnis\PHPStan\Rules\CallLikes\NoNamedArgumentRule`](https://github.com/ergebnis/phpstan-rules#calllikesnonamedargumentrule)
- [`Ergebnis\PHPStan\Rules\Classes\FinalRule`](https://github.com/ergebnis/phpstan-rules#classesfinalrule)
- [`Ergebnis\PHPStan\Rules\Classes\NoExtendsRule`](https://github.com/ergebnis/phpstan-rules#classesnoextendsrule)
- [`Ergebnis\PHPStan\Rules\Classes\PHPUnit\Framework\TestCaseWithSuffixRule`](https://github.com/ergebnis/phpstan-rules#classesphpunitframeworktestcasewithsuffixrule)
- [`Ergebnis\PHPStan\Rules\Closures\NoNullableReturnTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#closuresnonullablereturntypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Closures\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#closuresnoparameterpassedbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Closures\NoParameterWithNullableTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#closuresnoparameterwithnullabletypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Closures\NoParameterWithNullDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#closuresnoparameterwithnulldefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoAssignByReferenceRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoassignbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoCompactRule`](https://github.com/ergebnis/phpstan-rules#expressionsnocompactrule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoErrorSuppressionRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoerrorsuppressionrule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoEvalRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoevalrule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoIssetRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoissetrule)
- [`Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule`](https://github.com/ergebnis/phpstan-rules#filesdeclarestricttypesrule)
- [`Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule`](https://github.com/ergebnis/phpstan-rules#filesnophpstanignorerule)
- [`Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#functionsnonullablereturntypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterpassedbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullableTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterwithnullabletypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterwithnulldefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Functions\NoReturnByReferenceRule`](https://github.com/ergebnis/phpstan-rules#functionsnoreturnbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Methods\FinalInAbstractClassRule`](https://github.com/ergebnis/phpstan-rules#methodsfinalinabstractclassrule)
- [`Ergebnis\PHPStan\Rules\Methods\InvokeParentHookMethodRule`](https://github.com/ergebnis/phpstan-rules#methodsinvokeparenthookmethodrule)
- [`Ergebnis\PHPStan\Rules\Methods\NoConstructorParameterWithDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#methodsnoconstructorparameterwithdefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Methods\NoNullableReturnTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#methodsnonullablereturntypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Methods\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#methodsnoparameterpassedbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Methods\NoParameterWithContainerTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#methodsnoparameterwithcontainertypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullableTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#methodsnoparameterwithnullabletypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#methodsnoparameterwithnulldefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Methods\NoReturnByReferenceRule`](https://github.com/ergebnis/phpstan-rules#methodsnoreturnbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Methods\PrivateInFinalClassRule`](https://github.com/ergebnis/phpstan-rules#methodsprivateinfinalclassrule)
- [`Ergebnis\PHPStan\Rules\Statements\NoSwitchRule`](https://github.com/ergebnis/phpstan-rules#statementsnoswitchrule)
### CallLikes
#### `CallLikes\NoNamedArgumentRule`
This rule reports an error when an anonymous function, a function, or a method is invoked using a [named argument](https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noNamedArgument:
enabled: false
```
### Classes
#### `Classes\FinalRule`
This rule reports an error when a non-anonymous class is not `final`.
:bulb: This rule ignores classes that
- use `@Entity`, `@ORM\Entity`, or `@ORM\Mapping\Entity` annotations
- use `Doctrine\ORM\Mapping\Entity` attributes
on the class level.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
final:
enabled: false
```
##### Disallowing `abstract` classes
By default, this rule allows to declare `abstract` classes.
You can set the `allowAbstractClasses` parameter to `false` to disallow abstract classes.
```neon
parameters:
ergebnis:
final:
allowAbstractClasses: false
```
##### Excluding classes from inspection
You can set the `classesNotRequiredToBeAbstractOrFinal` parameter to a list of class names that you want to exclude from inspection.
```neon
parameters:
ergebnis:
final:
classesNotRequiredToBeAbstractOrFinal:
- Foo\Bar\NeitherAbstractNorFinal
- Bar\Baz\NeitherAbstractNorFinal
```
#### `Classes\NoExtendsRule`
This rule reports an error when a class extends another class.
##### Defaults
By default, this rule allows the following classes to be extended:
- [`PHPUnit\Framework\TestCase`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php)
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noExtends:
enabled: false
```
##### Allowing classes to be extended
You can set the `classesAllowedToBeExtended` parameter to a list of class names that you want to allow to be extended.
```neon
parameters:
ergebnis:
noExtends:
classesAllowedToBeExtended:
- Ergebnis\PHPStan\Rules\Test\Integration\AbstractTestCase
- Ergebnis\PHPStan\Rules\Test\Integration\AbstractTestCase
```
#### `Classes\PHPUnit\Framework\TestCaseWithSuffixRule`
This rule reports an error when a concrete class is a sub-class of `PHPUnit\Framework\TestCase` but does not have a `Test` suffix.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
testCaseWithSuffix:
enabled: false
```
### Closures
#### `Closures\NoNullableReturnTypeDeclarationRule`
This rule reports an error when a closure uses a nullable return type declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noNullableReturnTypeDeclaration:
enabled: false
```
#### `Closures\NoParameterPassedByReferenceRule`
This rule reports an error when a closure has a parameter that is [passed by reference](https://www.php.net/manual/en/language.references.pass.php).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterPassedByReference:
enabled: false
```
#### `Closures\NoParameterWithNullableTypeDeclarationRule`
This rule reports an error when a closure has a parameter with a nullable type declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithNullableTypeDeclaration:
enabled: false
```
#### `Closures\NoParameterWithNullDefaultValueRule`
This rule reports an error when a closure has a parameter with `null` as default value.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithNullDefaultValue:
enabled: false
```
### Expressions
#### `Expressions\NoAssignByReferenceRule`
This rule reports an error when [a variable is assigned by reference](https://www.php.net/manual/en/language.references.whatdo.php#language.references.whatdo.assign).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noAssignByReference:
enabled: false
```
#### `Expressions\NoCompactRule`
This rule reports an error when the function [`compact()`](https://www.php.net/compact) is used.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noCompact:
enabled: false
```
#### `Expressions\NoErrorSuppressionRule`
This rule reports an error when [`@`](https://www.php.net/manual/en/language.operators.errorcontrol.php) is used to suppress errors.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noErrorSuppression:
enabled: false
```
#### `Expressions\NoEvalRule`
This rule reports an error when the language construct [`eval()`](https://www.php.net/eval) is used.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noEval:
enabled: false
```
#### `Expressions\NoIssetRule`
This rule reports an error when the language construct [`isset()`](https://www.php.net/isset) is used.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noIsset:
enabled: false
```
### Files
#### `Files\DeclareStrictTypesRule`
This rule reports an error when a non-empty file does not contain a `declare(strict_types=1)` declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
declareStrictTypes:
enabled: false
```
#### `Files\NoPhpstanIgnoreRule`
This rule reports an error when a `@phpstan-ignore`, `@phpstan-ignore-line`, or `@phpstan-ignore-next-line` tag is used to [ignore errors reported by `phpstan/phpstan`](https://phpstan.org/user-guide/ignoring-errors#ignoring-in-code-using-phpdocs).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noPhpstanIgnore:
enabled: false
```
### Functions
#### `Functions\NoNullableReturnTypeDeclarationRule`
This rule reports an error when a function uses a nullable return type declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noNullableReturnTypeDeclaration:
enabled: false
```
#### `Functions\NoParameterPassedByReferenceRule`
This rule reports an error when a function has a parameter that is [passed by reference](https://www.php.net/manual/en/language.references.pass.php).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterPassedByReference:
enabled: false
```
#### `Functions\NoParameterWithNullableTypeDeclarationRule`
This rule reports an error when a function has a parameter with a nullable type declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithNullableTypeDeclaration:
enabled: false
```
#### `Functions\NoParameterWithNullDefaultValueRule`
This rule reports an error when a function has a parameter with `null` as default value.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithNullDefaultValue:
enabled: false
```
#### `Functions\NoReturnByReferenceRule`
This rule reports an error when a function [returns by reference](https://www.php.net/manual/en/language.references.return.php).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noReturnByReference:
enabled: false
```
### Methods
#### `Methods\FinalInAbstractClassRule`
This rule reports an error when a concrete `public` or `protected` method in an `abstract` class is not `final`.
:bulb: This rule ignores
- Doctrine embeddables
- Doctrine entities
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
finalInAbstractClass:
enabled: false
```
#### `Methods\InvokeParentHookMethodRule`
This rule reports an error when a hook method that overrides a hook method in a parent class does not invoke the overridden hook method in the expected order.
##### Defaults
By default, this rule requires the following hook methods to be invoked before doing something in the overriding method:
- [`Codeception\PHPUnit\TestCase::_setUp()`](https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L11-L13)
- [`Codeception\PHPUnit\TestCase::_setUpBeforeClass()`](https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L25-L27)
- [`Codeception\Test\Unit::_before()`](https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L63-L65)
- [`Codeception\Test\Unit::_setUp()`](https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L34-L58)
- [`PHPUnit\Framework\TestCase::assertPreConditions()`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2073-L2075)
- [`PHPUnit\Framework\TestCase::setUp()`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2063-L2065)
- [`PHPUnit\Framework\TestCase::setUpBeforeClass()`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2055-L2057)
By default, this rule requires the following hook methods to be invoked after doing something in the overriding method:
- [`Codeception\PHPUnit\TestCase::_tearDown()`](https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L18-L20)
- [`Codeception\PHPUnit\TestCase::_tearDownAfterClass()`](https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L32-L34)
- [`Codeception\Test\Unit::_after()`](https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L75-L77)
- [`Codeception\Test\Unit::_tearDown()`](https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L67-L70)
- [`PHPUnit\Framework\TestCase::assertPostConditions()`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2083-L2085)
- [`PHPUnit\Framework\TestCase::tearDown()`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2091-L2093)
- [`PHPUnit\Framework\TestCase::tearDownAfterClass()`](https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2098-L2100)
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
invokeParentHookMethod:
enabled: false
```
##### Configuring methods to invoke the parent method in the right order:
You can set the `hookMethods` parameter to a list of hook methods:
```neon
parameters:
ergebnis:
invokeParentHookMethod:
hookMethods:
- className: "Example\Test\Functional\AbstractCest"
methodName: "_before"
hasContent: "yes"
invocation: "first"
```
- `className`: name of the class that declares the hook method
- `methodName`: name of the hook method
- `hasContent`: one of `"yes"`, `"no"`, `"maybe"`
- `invocation`: one of `"any"` (needs to be invoked), `"first"` (needs to be invoked before all other statements in the overriding hook method, `"last"` (needs to be invoked after all other statements in the overriding hook method)
#### `Methods\NoConstructorParameterWithDefaultValueRule`
This rule reports an error when a constructor declared in
- an anonymous class
- a class
has a default value.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noConstructorParameterWithDefaultValue:
enabled: false
```
#### `Methods\NoParameterPassedByReferenceRule`
This rule reports an error when a method has a parameter that is [passed by reference](https://www.php.net/manual/en/language.references.pass.php).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterPassedByReference:
enabled: false
```
#### `Methods\NoNullableReturnTypeDeclarationRule`
This rule reports an error when a method declared in
- an anonymous class
- a class
- an interface
uses a nullable return type declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noNullableReturnTypeDeclaration:
enabled: false
```
#### `Methods\NoParameterWithContainerTypeDeclarationRule`
This rule reports an error when a method has a type declaration for a known dependency injection container or service locator.
##### Defaults
By default, this rule disallows the use of type declarations indicating an implementation of
- [`Psr\Container\ContainerInterface`](https://github.com/php-fig/container/blob/1.0.0/src/ContainerInterface.php)
is expected to be injected into a method.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithContainerTypeDeclaration:
enabled: false
```
##### Configuring container interfaces
You can set the `interfacesImplementedByContainers` parameter to a list of interface names of additional containers and service locators.
```neon
parameters:
ergebnis:
noParameterWithContainerTypeDeclaration:
interfacesImplementedByContainers:
- Fancy\DependencyInjection\ContainerInterface
- Other\ServiceLocatorInterface
```
##### Configuring methods allowed to use parameters with container type declarations
You can set the `methodsAllowedToUseContainerTypeDeclarations` parameter to a list of method names that are allowed to use parameters with container type declarations.
```neon
parameters:
ergebnis:
noParameterWithContainerTypeDeclaration:
methodsAllowedToUseContainerTypeDeclarations:
- loadExtension
```
#### `Methods\NoParameterWithNullableTypeDeclarationRule`
This rule reports an error when a method declared in
- an anonymous class
- a class
- an interface
has a parameter with a nullable type declaration.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithNullableTypeDeclaration:
enabled: false
```
#### `Methods\NoParameterWithNullDefaultValueRule`
This rule reports an error when a method declared in
- an anonymous class
- a class
- an interface
has a parameter with `null` as default value.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noParameterWithNullDefaultValue:
enabled: false
```
#### `Functions\NoReturnByReferenceRule`
This rule reports an error when a method [returns by reference](https://www.php.net/manual/en/language.references.return.php).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noReturnByReference:
enabled: false
```
#### `Methods\PrivateInFinalClassRule`
This rule reports an error when a method in a `final` class is `protected` but could be `private`.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
privateInFinalClass:
enabled: false
```
### Statements
#### `Statements\NoSwitchRule`
This rule reports an error when the statement [`switch()`](https://www.php.net/manual/control-structures.switch.php) is used.
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noSwitch:
enabled: false
```
## Disabling all rules
You can disable all rules using the `allRules` configuration parameter:
```neon
parameters:
ergebnis:
allRules: false
```
## Enabling rules one-by-one
If you have disabled all rules using the `allRules` configuration parameter, you can re-enable individual rules with their corresponding configuration parameters:
```neon
parameters:
ergebnis:
allRules: false
privateInFinalClass:
enabled: true
```
## Changelog
The maintainers of this project record notable changes to this project in a [changelog](CHANGELOG.md).
## Contributing
The maintainers of this project suggest following the [contribution guide](.github/CONTRIBUTING.md).
## Code of Conduct
The maintainers of this project ask contributors to follow the [code of conduct](https://github.com/ergebnis/.github/blob/main/CODE_OF_CONDUCT.md).
## General Support Policy
The maintainers of this project provide limited support.
You can support the maintenance of this project by [sponsoring @ergebnis](https://github.com/sponsors/ergebnis).
## PHP Version Support Policy
This project supports PHP versions with [active and security support](https://www.php.net/supported-versions.php).
The maintainers of this project add support for a PHP version following its initial release and drop support for a PHP version when it has reached the end of security support.
## Security Policy
This project has a [security policy](.github/SECURITY.md).
## License
This project uses the [MIT license](LICENSE.md).
## Credits
The method [`FinalRule::isWhitelistedClass()`](src/Classes/FinalRule.php) is inspired by the work on [`FinalClassFixer`](https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.15/src/Fixer/ClassNotation/FinalClassFixer.php) and [`FinalInternalClassFixer`](https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.15/src/Fixer/ClassNotation/FinalInternalClassFixer.php), contributed by [Dariusz Rumiński](https://github.com/keradus), [Filippo Tessarotto](https://github.com/Slamdunk), and [Spacepossum](https://github.com/SpacePossum) for [`friendsofphp/php-cs-fixer`](https://github.com/FriendsOfPHP/PHP-CS-Fixer) (originally licensed under MIT).
## Social
Follow [@localheinz](https://twitter.com/intent/follow?screen_name=localheinz) and [@ergebnis](https://twitter.com/intent/follow?screen_name=ergebnis) on Twitter.

View File

@ -0,0 +1,77 @@
{
"name": "ergebnis/phpstan-rules",
"description": "Provides rules for phpstan/phpstan.",
"license": "MIT",
"type": "phpstan-extension",
"keywords": [
"phpstan",
"phpstan-rules"
],
"authors": [
{
"name": "Andreas Möller",
"email": "am@localheinz.com",
"homepage": "https://localheinz.com"
}
],
"homepage": "https://github.com/ergebnis/phpstan-rules",
"support": {
"issues": "https://github.com/ergebnis/phpstan-rules/issues",
"source": "https://github.com/ergebnis/phpstan-rules",
"security": "https://github.com/ergebnis/phpstan-rules/blob/main/.github/SECURITY.md"
},
"require": {
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-mbstring": "*",
"phpstan/phpstan": "^2.1.35"
},
"require-dev": {
"codeception/codeception": "^4.0.0 || ^5.0.0",
"doctrine/orm": "^2.20.0 || ^3.3.0",
"ergebnis/composer-normalize": "^2.49.0",
"ergebnis/license": "^2.7.0",
"ergebnis/php-cs-fixer-config": "^6.59.0",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
"fakerphp/faker": "^1.24.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpunit/phpunit": "^9.6.21",
"psr/container": "^2.0.2",
"symfony/finder": "^5.4.45",
"symfony/process": "^5.4.47"
},
"autoload": {
"psr-4": {
"Ergebnis\\PHPStan\\Rules\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Ergebnis\\PHPStan\\Rules\\Test\\": "test/"
}
},
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true,
"infection/extension-installer": true,
"phpstan/extension-installer": true
},
"audit": {
"abandoned": "report"
},
"platform": {
"php": "7.4.33"
},
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"phpstan": {
"includes": [
"rules.neon"
]
}
}
}

View File

@ -0,0 +1,302 @@
conditionalTags:
Ergebnis\PHPStan\Rules\CallLikes\NoNamedArgumentRule:
phpstan.rules.rule: %ergebnis.noNamedArgument.enabled%
Ergebnis\PHPStan\Rules\Classes\FinalRule:
phpstan.rules.rule: %ergebnis.final.enabled%
Ergebnis\PHPStan\Rules\Classes\NoExtendsRule:
phpstan.rules.rule: %ergebnis.noExtends.enabled%
Ergebnis\PHPStan\Rules\Classes\PHPUnit\Framework\TestCaseWithSuffixRule:
phpstan.rules.rule: %ergebnis.testCaseWithSuffix.enabled%
Ergebnis\PHPStan\Rules\Closures\NoNullableReturnTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noNullableReturnTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Closures\NoParameterPassedByReferenceRule:
phpstan.rules.rule: %ergebnis.noParameterPassedByReference.enabled%
Ergebnis\PHPStan\Rules\Closures\NoParameterWithNullableTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noParameterWithNullableTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Expressions\NoAssignByReferenceRule:
phpstan.rules.rule: %ergebnis.noAssignByReference.enabled%
Ergebnis\PHPStan\Rules\Expressions\NoCompactRule:
phpstan.rules.rule: %ergebnis.noCompact.enabled%
Ergebnis\PHPStan\Rules\Expressions\NoErrorSuppressionRule:
phpstan.rules.rule: %ergebnis.noErrorSuppression.enabled%
Ergebnis\PHPStan\Rules\Expressions\NoEvalRule:
phpstan.rules.rule: %ergebnis.noEval.enabled%
Ergebnis\PHPStan\Rules\Expressions\NoIssetRule:
phpstan.rules.rule: %ergebnis.noIsset.enabled%
Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule:
phpstan.rules.rule: %ergebnis.declareStrictTypes.enabled%
Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule:
phpstan.rules.rule: %ergebnis.noPhpstanIgnore.enabled%
Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noNullableReturnTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule:
phpstan.rules.rule: %ergebnis.noParameterPassedByReference.enabled%
Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullableTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noParameterWithNullableTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullDefaultValueRule:
phpstan.rules.rule: %ergebnis.noParameterWithNullDefaultValue.enabled%
Ergebnis\PHPStan\Rules\Functions\NoReturnByReferenceRule:
phpstan.rules.rule: %ergebnis.noReturnByReference.enabled%
Ergebnis\PHPStan\Rules\Methods\FinalInAbstractClassRule:
phpstan.rules.rule: %ergebnis.finalInAbstractClass.enabled%
Ergebnis\PHPStan\Rules\Methods\InvokeParentHookMethodRule:
phpstan.rules.rule: %ergebnis.invokeParentHookMethod.enabled%
Ergebnis\PHPStan\Rules\Methods\NoConstructorParameterWithDefaultValueRule:
phpstan.rules.rule: %ergebnis.noConstructorParameterWithDefaultValue.enabled%
Ergebnis\PHPStan\Rules\Methods\NoNullableReturnTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noNullableReturnTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Methods\NoParameterPassedByReferenceRule:
phpstan.rules.rule: %ergebnis.noParameterPassedByReference.enabled%
Ergebnis\PHPStan\Rules\Methods\NoParameterWithContainerTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noParameterWithContainerTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullableTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noParameterWithNullableTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullDefaultValueRule:
phpstan.rules.rule: %ergebnis.noParameterWithNullDefaultValue.enabled%
Ergebnis\PHPStan\Rules\Methods\NoReturnByReferenceRule:
phpstan.rules.rule: %ergebnis.noReturnByReference.enabled%
Ergebnis\PHPStan\Rules\Methods\PrivateInFinalClassRule:
phpstan.rules.rule: %ergebnis.privateInFinalClass.enabled%
Ergebnis\PHPStan\Rules\Statements\NoSwitchRule:
phpstan.rules.rule: %ergebnis.noSwitch.enabled%
parameters:
ergebnis:
allRules: true
declareStrictTypes:
enabled: %ergebnis.allRules%
final:
allowAbstractClasses: true
classesNotRequiredToBeAbstractOrFinal: []
enabled: %ergebnis.allRules%
finalInAbstractClass:
enabled: %ergebnis.allRules%
invokeParentHookMethod:
enabled: %ergebnis.allRules%
hookMethods: []
noAssignByReference:
enabled: %ergebnis.allRules%
noCompact:
enabled: %ergebnis.allRules%
noConstructorParameterWithDefaultValue:
enabled: %ergebnis.allRules%
noErrorSuppression:
enabled: %ergebnis.allRules%
noEval:
enabled: %ergebnis.allRules%
noExtends:
classesAllowedToBeExtended: []
enabled: %ergebnis.allRules%
noIsset:
enabled: %ergebnis.allRules%
noNamedArgument:
enabled: %ergebnis.allRules%
noNullableReturnTypeDeclaration:
enabled: %ergebnis.allRules%
noParameterPassedByReference:
enabled: %ergebnis.allRules%
noParameterWithContainerTypeDeclaration:
enabled: %ergebnis.allRules%
interfacesImplementedByContainers:
- Psr\Container\ContainerInterface
methodsAllowedToUseContainerTypeDeclarations: []
noParameterWithNullableTypeDeclaration:
enabled: %ergebnis.allRules%
noParameterWithNullDefaultValue:
enabled: %ergebnis.allRules%
noPhpstanIgnore:
enabled: %ergebnis.allRules%
noReturnByReference:
enabled: %ergebnis.allRules%
noSwitch:
enabled: %ergebnis.allRules%
privateInFinalClass:
enabled: %ergebnis.allRules%
testCaseWithSuffix:
enabled: %ergebnis.allRules%
parametersSchema:
ergebnis: structure([
allRules: bool()
declareStrictTypes: structure([
enabled: bool(),
])
final: structure([
allowAbstractClasses: bool()
classesNotRequiredToBeAbstractOrFinal: listOf(string())
enabled: bool(),
])
finalInAbstractClass: structure([
enabled: bool(),
])
invokeParentHookMethod: structure([
enabled: bool(),
hookMethods: listOf(structure([
className: string(),
hasContent: anyOf("no", "yes"),
invocation: anyOf("any", "first", "last"),
methodName: string(),
]))
])
noAssignByReference: structure([
enabled: bool(),
])
noCompact: structure([
enabled: bool(),
])
noConstructorParameterWithDefaultValue: structure([
enabled: bool(),
])
noErrorSuppression: structure([
enabled: bool(),
])
noExtends: structure([
classesAllowedToBeExtended: listOf(string())
enabled: bool(),
])
noEval: structure([
enabled: bool(),
])
noIsset: structure([
enabled: bool(),
])
noNamedArgument: structure([
enabled: bool(),
])
noNullableReturnTypeDeclaration: structure([
enabled: bool(),
])
noParameterPassedByReference: structure([
enabled: bool(),
])
noParameterWithContainerTypeDeclaration: structure([
enabled: bool(),
interfacesImplementedByContainers: listOf(string())
methodsAllowedToUseContainerTypeDeclarations: listOf(string())
])
noParameterWithNullableTypeDeclaration: structure([
enabled: bool(),
])
noParameterWithNullDefaultValue: structure([
enabled: bool(),
])
noPhpstanIgnore: structure([
enabled: bool(),
])
noReturnByReference: structure([
enabled: bool(),
])
noSwitch: structure([
enabled: bool(),
])
privateInFinalClass: structure([
enabled: bool(),
])
testCaseWithSuffix: structure([
enabled: bool(),
])
])
services:
-
class: Ergebnis\PHPStan\Rules\Analyzer
-
class: Ergebnis\PHPStan\Rules\CallLikes\NoNamedArgumentRule
-
class: Ergebnis\PHPStan\Rules\Classes\FinalRule
arguments:
allowAbstractClasses: %ergebnis.final.allowAbstractClasses%
classesNotRequiredToBeAbstractOrFinal: %ergebnis.final.classesNotRequiredToBeAbstractOrFinal%
-
class: Ergebnis\PHPStan\Rules\Classes\NoExtendsRule
arguments:
classesAllowedToBeExtended: %ergebnis.noExtends.classesAllowedToBeExtended%
-
class: Ergebnis\PHPStan\Rules\Classes\PHPUnit\Framework\TestCaseWithSuffixRule
-
class: Ergebnis\PHPStan\Rules\Closures\NoNullableReturnTypeDeclarationRule
-
class: Ergebnis\PHPStan\Rules\Closures\NoParameterPassedByReferenceRule
-
class: Ergebnis\PHPStan\Rules\Closures\NoParameterWithNullableTypeDeclarationRule
-
class: Ergebnis\PHPStan\Rules\Expressions\NoAssignByReferenceRule
-
class: Ergebnis\PHPStan\Rules\Expressions\NoCompactRule
-
class: Ergebnis\PHPStan\Rules\Expressions\NoErrorSuppressionRule
-
class: Ergebnis\PHPStan\Rules\Expressions\NoEvalRule
-
class: Ergebnis\PHPStan\Rules\Expressions\NoIssetRule
-
class: Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule
-
class: Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule
-
class: Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule
-
class: Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule
-
class: Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullableTypeDeclarationRule
-
class: Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullDefaultValueRule
-
class: Ergebnis\PHPStan\Rules\Functions\NoReturnByReferenceRule
-
class: Ergebnis\PHPStan\Rules\Methods\FinalInAbstractClassRule
-
class: Ergebnis\PHPStan\Rules\Methods\InvokeParentHookMethodRule
arguments:
hookMethods: %ergebnis.invokeParentHookMethod.hookMethods%
-
class: Ergebnis\PHPStan\Rules\Methods\NoConstructorParameterWithDefaultValueRule
-
class: Ergebnis\PHPStan\Rules\Methods\NoNullableReturnTypeDeclarationRule
-
class: Ergebnis\PHPStan\Rules\Methods\NoParameterPassedByReferenceRule
-
class: Ergebnis\PHPStan\Rules\Methods\NoParameterWithContainerTypeDeclarationRule
arguments:
interfacesImplementedByContainers: %ergebnis.noParameterWithContainerTypeDeclaration.interfacesImplementedByContainers%
methodsAllowedToUseContainerTypeDeclarations: %ergebnis.noParameterWithContainerTypeDeclaration.methodsAllowedToUseContainerTypeDeclarations%
-
class: Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullableTypeDeclarationRule
-
class: Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullDefaultValueRule
-
class: Ergebnis\PHPStan\Rules\Methods\NoReturnByReferenceRule
-
class: Ergebnis\PHPStan\Rules\Methods\PrivateInFinalClassRule
-
class: Ergebnis\PHPStan\Rules\Statements\NoSwitchRule

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
use PhpParser\Node;
/**
* @internal
*/
final class Analyzer
{
public function hasNullDefaultValue(Node\Param $parameter): bool
{
if (!$parameter->default instanceof Node\Expr\ConstFetch) {
return false;
}
return 'null' === $parameter->default->name->toLowerString();
}
/**
* @param null|Node\ComplexType|Node\Identifier|Node\Name $typeDeclaration
*/
public function isNullableTypeDeclaration($typeDeclaration): bool
{
if ($typeDeclaration instanceof Node\NullableType) {
return true;
}
if ($typeDeclaration instanceof Node\UnionType) {
foreach ($typeDeclaration->types as $type) {
if (
$type instanceof Node\Identifier
&& 'null' === $type->toLowerString()
) {
return true;
}
if (
$type instanceof Node\Name\FullyQualified
&& $type->hasAttribute('originalName')
) {
$originalName = $type->getAttribute('originalName');
if (
$originalName instanceof Node\Name
&& 'null' === $originalName->toLowerString()
) {
return true;
}
}
}
}
return false;
}
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\CallLikes;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
use PHPStan\ShouldNotHappenException;
/**
* @implements Rules\Rule<Node\Expr\CallLike>
*/
final class NoNamedArgumentRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\CallLike::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->getArgs())) {
return [];
}
/** @var list<Node\Arg> $namedArguments */
$namedArguments = \array_values(\array_filter($node->getArgs(), static function (Node\Arg $argument): bool {
if (!$argument->name instanceof Node\Identifier) {
return false;
}
return true;
}));
if (0 === \count($namedArguments)) {
return [];
}
$callLike = self::describeCallLike(
$node,
$scope,
);
return \array_map(static function (Node\Arg $namedArgument) use ($callLike): Rules\RuleError {
/** @var Node\Identifier $argumentName */
$argumentName = $namedArgument->name;
$message = \sprintf(
'%s is invoked with named argument for parameter $%s.',
$callLike,
$argumentName->toString(),
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noNamedArgument()->toString())
->build();
}, $namedArguments);
}
private static function describeCallLike(
Node\Expr\CallLike $node,
Analyser\Scope $scope
): string {
if ($node instanceof Node\Expr\FuncCall) {
$functionName = $node->name;
if ($functionName instanceof Node\Expr\PropertyFetch) {
return \sprintf(
'Callable referenced by property $%s',
$functionName->name,
);
}
if ($functionName instanceof Node\Expr\Variable) {
return \sprintf(
'Callable referenced by $%s',
$functionName->name,
);
}
if ($functionName instanceof Node\Name) {
return \sprintf(
'Function %s()',
$functionName->name,
);
}
}
if ($node instanceof Node\Expr\MethodCall) {
$methodName = $node->name;
if ($methodName instanceof Node\Identifier) {
$objectType = $scope->getType($node->var);
$methodReflection = $scope->getMethodReflection(
$objectType,
$methodName->name,
);
if (null === $methodReflection) {
throw new ShouldNotHappenException();
}
$declaringClass = $methodReflection->getDeclaringClass();
if ($declaringClass->isAnonymous()) {
return \sprintf(
'Method %s() of anonymous class',
$methodName->toString(),
);
}
return \sprintf(
'Method %s::%s()',
$declaringClass->getName(),
$methodName->toString(),
);
}
return 'Method';
}
if ($node instanceof Node\Expr\StaticCall) {
$methodName = $node->name;
if ($methodName instanceof Node\Identifier) {
$className = $node->class;
if ($className instanceof Node\Name) {
return \sprintf(
'Method %s::%s()',
$className->toString(),
$methodName->toString(),
);
}
return \sprintf(
'Method %s()',
$methodName->toString(),
);
}
return 'Method';
}
if ($node instanceof Node\Expr\New_) {
$className = $node->class;
if ($className instanceof Node\Name) {
return \sprintf(
'Constructor of %s',
$className->toString(),
);
}
return 'Constructor';
}
return 'Callable';
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
/**
* @internal
*/
final class ClassName
{
private string $value;
/**
* @param class-string $value
*/
private function __construct(string $value)
{
$this->value = $value;
}
/**
* @param class-string $value
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* @return class-string
*/
public function toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Classes;
use Doctrine\ORM;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Comment;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Class_>
*/
final class FinalRule implements Rules\Rule
{
/**
* @var list<string>
*/
private static array $whitelistedAnnotations = [
'Entity',
'ORM\Entity',
'ORM\Mapping\Entity',
];
/**
* @var list<class-string>
*/
private static array $whitelistedAttributes = [
ORM\Mapping\Entity::class,
];
private bool $allowAbstractClasses;
/**
* @var list<string>
*/
private array $classesNotRequiredToBeAbstractOrFinal;
private string $errorMessageTemplate = 'Class %s is not final.';
/**
* @param list<class-string> $classesNotRequiredToBeAbstractOrFinal
*/
public function __construct(
bool $allowAbstractClasses,
array $classesNotRequiredToBeAbstractOrFinal
) {
$this->allowAbstractClasses = $allowAbstractClasses;
$this->classesNotRequiredToBeAbstractOrFinal = \array_map(static function (string $classNotRequiredToBeAbstractOrFinal): string {
return $classNotRequiredToBeAbstractOrFinal;
}, $classesNotRequiredToBeAbstractOrFinal);
if ($allowAbstractClasses) {
$this->errorMessageTemplate = 'Class %s is neither abstract nor final.';
}
}
public function getNodeType(): string
{
return Node\Stmt\Class_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (!isset($node->namespacedName)) {
return [];
}
if (\in_array($node->namespacedName->toString(), $this->classesNotRequiredToBeAbstractOrFinal, true)) {
return [];
}
if (
$this->allowAbstractClasses
&& $node->isAbstract()
) {
return [];
}
if ($node->isFinal()) {
return [];
}
if (self::hasWhitelistedAnnotation($node)) {
return [];
}
if (self::hasWhitelistedAttribute($node)) {
return [];
}
$message = \sprintf(
$this->errorMessageTemplate,
$node->namespacedName->toString(),
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::final()->toString())
->build(),
];
}
/**
* This method is inspired by the work on PhpCsFixer\Fixer\ClassNotation\FinalClassFixer and
* PhpCsFixer\Fixer\ClassNotation\FinalInternalClassFixer contributed by Dariusz Rumiński, Filippo Tessarotto, and
* Spacepossum for friendsofphp/php-cs-fixer.
*
* @see https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.15/src/Fixer/ClassNotation/FinalClassFixer.php
* @see https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/2.15/src/Fixer/ClassNotation/FinalInternalClassFixer.php
* @see https://github.com/keradus
* @see https://github.com/SpacePossum
* @see https://github.com/Slamdunk
*/
private static function hasWhitelistedAnnotation(Node\Stmt\Class_ $node): bool
{
$docComment = $node->getDocComment();
if (!$docComment instanceof Comment\Doc) {
return false;
}
$reformattedComment = $docComment->getReformattedText();
if (\is_int(\preg_match_all('/@(\S+)(?=\s|$)/', $reformattedComment, $matches))) {
foreach ($matches[1] as $annotation) {
foreach (self::$whitelistedAnnotations as $whitelistedAnnotation) {
if (0 === \mb_strpos($annotation, $whitelistedAnnotation)) {
return true;
}
}
}
}
return false;
}
private static function hasWhitelistedAttribute(Node\Stmt\Class_ $node): bool
{
foreach ($node->attrGroups as $attributeGroup) {
foreach ($attributeGroup->attrs as $attribute) {
if (\in_array($attribute->name->toString(), self::$whitelistedAttributes, true)) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Classes;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
use PHPUnit\Framework;
/**
* @implements Rules\Rule<Node\Stmt\Class_>
*/
final class NoExtendsRule implements Rules\Rule
{
/**
* @var list<class-string>
*/
private static array $defaultClassesAllowedToBeExtended = [
Framework\TestCase::class,
];
/**
* @var list<class-string>
*/
private array $classesAllowedToBeExtended;
/**
* @param list<class-string> $classesAllowedToBeExtended
*/
public function __construct(array $classesAllowedToBeExtended)
{
$this->classesAllowedToBeExtended = \array_values(\array_unique(\array_merge(
self::$defaultClassesAllowedToBeExtended,
\array_map(static function (string $classAllowedToBeExtended): string {
/** @var class-string $classAllowedToBeExtended */
return $classAllowedToBeExtended;
}, $classesAllowedToBeExtended),
)));
}
public function getNodeType(): string
{
return Node\Stmt\Class_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (!$node->extends instanceof Node\Name) {
return [];
}
$extendedClassName = $node->extends->toString();
if (\in_array($extendedClassName, $this->classesAllowedToBeExtended, true)) {
return [];
}
if (!isset($node->namespacedName)) {
$message = \sprintf(
'Anonymous class is not allowed to extend "%s".',
$extendedClassName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noExtends()->toString())
->build(),
];
}
$extendingClassName = $node->namespacedName->toString();
$message = \sprintf(
'Class "%s" is not allowed to extend "%s".',
$extendingClassName,
$extendedClassName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noExtends()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Classes\PHPUnit\Framework;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Class_>
*/
final class TestCaseWithSuffixRule implements Rules\Rule
{
/**
* @var list<class-string>
*/
private static array $phpunitTestCaseClassNames = [
'PHPUnit\Framework\TestCase',
];
private Reflection\ReflectionProvider $reflectionProvider;
public function __construct(Reflection\ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}
public function getNodeType(): string
{
return Node\Stmt\Class_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if ($node->isAbstract()) {
return [];
}
if (!$node->extends instanceof Node\Name) {
return [];
}
if (!isset($node->namespacedName)) {
return [];
}
$fullyQualifiedClassName = $node->namespacedName->toString();
$classReflection = $this->reflectionProvider->getClass($fullyQualifiedClassName);
$extendedPhpunitTestCaseClassName = '';
foreach (self::$phpunitTestCaseClassNames as $phpunitTestCaseClassName) {
if ($classReflection->isSubclassOfClass($this->reflectionProvider->getClass($phpunitTestCaseClassName))) {
$extendedPhpunitTestCaseClassName = $phpunitTestCaseClassName;
break;
}
}
if ('' === $extendedPhpunitTestCaseClassName) {
return [];
}
if (1 === \preg_match('/Test$/', $fullyQualifiedClassName)) {
return [];
}
$message = \sprintf(
'Class %s extends %s, is concrete, but does not have a Test suffix.',
$fullyQualifiedClassName,
$extendedPhpunitTestCaseClassName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::testCaseWithSuffix()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Closures;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\Closure>
*/
final class NoNullableReturnTypeDeclarationRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Expr\Closure::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (!$this->analyzer->isNullableTypeDeclaration($node->getReturnType())) {
return [];
}
return [
Rules\RuleErrorBuilder::message('Closure has a nullable return type declaration.')
->identifier(ErrorIdentifier::noNullableReturnTypeDeclaration()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Closures;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\Closure>
*/
final class NoParameterPassedByReferenceRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\Closure::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersPassedByReference = \array_values(\array_filter($node->params, static function (Node\Param $parameter): bool {
return $parameter->byRef;
}));
if (0 === \count($parametersPassedByReference)) {
return [];
}
return \array_map(static function (Node\Param $parameterPassedByReference): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterPassedByReference->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Closure has parameter $%s that is passed by reference.',
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterPassedByReference()->toString())
->build();
}, $parametersPassedByReference);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Closures;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\Closure>
*/
final class NoParameterWithNullDefaultValueRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Expr\Closure::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersWithNullDefaultValue = \array_values(\array_filter($node->params, function (Node\Param $parameter): bool {
return $this->analyzer->hasNullDefaultValue($parameter);
}));
if (0 === \count($parametersWithNullDefaultValue)) {
return [];
}
return \array_map(static function (Node\Param $parameterWithNullDefaultValue): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullDefaultValue->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Closure has parameter $%s with null as default value.',
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullDefaultValue()->toString())
->build();
}, $parametersWithNullDefaultValue);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Closures;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\Closure>
*/
final class NoParameterWithNullableTypeDeclarationRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Expr\Closure::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersWithNullableTypeDeclaration = \array_values(\array_filter($node->params, function (Node\Param $parameter): bool {
return $this->analyzer->isNullableTypeDeclaration($parameter->type);
}));
if (0 === \count($parametersWithNullableTypeDeclaration)) {
return [];
}
return \array_map(static function (Node\Param $parameterWithNullableTypeDeclaration): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullableTypeDeclaration->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Closure has parameter $%s with a nullable type declaration.',
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullableTypeDeclaration()->toString())
->build();
}, $parametersWithNullableTypeDeclaration);
}
}

View File

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
/**
* @internal
*/
final class ErrorIdentifier
{
private string $value;
private function __construct(string $value)
{
$this->value = $value;
}
public static function declareStrictTypes(): self
{
return new self('declareStrictTypes');
}
public static function final(): self
{
return new self('final');
}
public static function finalInAbstractClass(): self
{
return new self('finalInAbstractClass');
}
public static function invokeParentHookMethod(): self
{
return new self('invokeParentHookMethod');
}
public static function noCompact(): self
{
return new self('noCompact');
}
public static function noConstructorParameterWithDefaultValue(): self
{
return new self('noConstructorParameterWithDefaultValue');
}
public static function noAssignByReference(): self
{
return new self('noAssignByReference');
}
public static function noErrorSuppression(): self
{
return new self('noErrorSuppression');
}
public static function noEval(): self
{
return new self('noEval');
}
public static function noExtends(): self
{
return new self('noExtends');
}
public static function noIsset(): self
{
return new self('noIsset');
}
public static function noNamedArgument(): self
{
return new self('noNamedArgument');
}
public static function noParameterPassedByReference(): self
{
return new self('noParameterPassedByReference');
}
public static function noParameterWithContainerTypeDeclaration(): self
{
return new self('noParameterWithContainerTypeDeclaration');
}
public static function noParameterWithNullDefaultValue(): self
{
return new self('noParameterWithNullDefaultValue');
}
public static function noParameterWithNullableTypeDeclaration(): self
{
return new self('noParameterWithNullableTypeDeclaration');
}
public static function noNullableReturnTypeDeclaration(): self
{
return new self('noNullableReturnTypeDeclaration');
}
public static function noPhpstanIgnore(): self
{
return new self('noPhpstanIgnore');
}
public static function noReturnByReference(): self
{
return new self('noReturnByReference');
}
public static function noSwitch(): self
{
return new self('noSwitch');
}
public static function privateInFinalClass(): self
{
return new self('privateInFinalClass');
}
public static function testCaseWithSuffix(): self
{
return new self('testCaseWithSuffix');
}
public function toString(): string
{
return \sprintf(
'ergebnis.%s',
$this->value,
);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Expressions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\AssignRef>
*/
final class NoAssignByReferenceRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\AssignRef::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
return [
Rules\RuleErrorBuilder::message('Assign by reference should not be used.')
->identifier(ErrorIdentifier::noAssignByReference()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Expressions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\FuncCall>
*/
final class NoCompactRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (!$node->name instanceof Node\Name) {
return [];
}
if ('compact' !== \mb_strtolower($scope->resolveName($node->name))) {
return [];
}
return [
Rules\RuleErrorBuilder::message('Function compact() should not be used.')
->identifier(ErrorIdentifier::noCompact()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Expressions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\ErrorSuppress>
*/
final class NoErrorSuppressionRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\ErrorSuppress::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
return [
Rules\RuleErrorBuilder::message('Error suppression via "@" should not be used.')
->identifier(ErrorIdentifier::noErrorSuppression()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Expressions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\Eval_>
*/
final class NoEvalRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\Eval_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
return [
Rules\RuleErrorBuilder::message('Language construct eval() should not be used.')
->identifier(ErrorIdentifier::noEval()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Expressions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Expr\Isset_>
*/
final class NoIssetRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Expr\Isset_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
return [
Rules\RuleErrorBuilder::message('Language construct isset() should not be used.')
->identifier(ErrorIdentifier::noIsset()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Files;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Node\FileNode;
use PHPStan\Rules;
/**
* @implements Rules\Rule<FileNode>
*/
final class DeclareStrictTypesRule implements Rules\Rule
{
public function getNodeType(): string
{
return FileNode::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
$nodes = $node->getNodes();
if (0 === \count($nodes)) {
return [];
}
$firstNode = \array_shift($nodes);
if (
$firstNode instanceof Node\Stmt\InlineHTML
&& 2 === $firstNode->getEndLine()
&& 0 === \mb_strpos($firstNode->value, '#!')
) {
$firstNode = \array_shift($nodes);
}
if ($firstNode instanceof Node\Stmt\Declare_) {
foreach ($firstNode->declares as $declare) {
if (
'strict_types' === $declare->key->toLowerString()
&& $declare->value instanceof Node\Scalar\LNumber
&& 1 === $declare->value->value
) {
return [];
}
}
}
return [
Rules\RuleErrorBuilder::message('File is missing a "declare(strict_types=1)" declaration.')
->identifier(ErrorIdentifier::declareStrictTypes()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Files;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PHPStan\Analyser;
use PHPStan\Node\FileNode;
use PHPStan\Rules;
/**
* @implements Rules\Rule<FileNode>
*/
final class NoPhpstanIgnoreRule implements Rules\Rule
{
public function getNodeType(): string
{
return FileNode::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
$nodeFinder = new NodeFinder();
$foundNodes = $nodeFinder->find($node->getNodes(), static function (Node $node): bool {
return true;
});
$errors = [];
foreach ($foundNodes as $foundNode) {
foreach ($foundNode->getComments() as $comment) {
foreach (\explode("\n", $comment->getText()) as $index => $line) {
if (0 === \preg_match('/@phpstan-ignore(-line|-next-line)?(?=\s|$|,)/', $line, $matches)) {
continue;
}
$tag = $matches[0];
$message = \sprintf(
'Errors reported by phpstan/phpstan should not be ignored via "%s", fix the error or use the baseline instead.',
$tag,
);
$errors[] = Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noPhpstanIgnore()->toString())
->line($comment->getStartLine() + $index)
->nonIgnorable()
->build();
}
}
}
return $errors;
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Functions;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Function_>
*/
final class NoNullableReturnTypeDeclarationRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Stmt\Function_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (!isset($node->namespacedName)) {
return [];
}
if (!$this->analyzer->isNullableTypeDeclaration($node->getReturnType())) {
return [];
}
$message = \sprintf(
'Function %s() has a nullable return type declaration.',
$node->namespacedName->toString(),
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noNullableReturnTypeDeclaration()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Functions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Function_>
*/
final class NoParameterPassedByReferenceRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Stmt\Function_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersPassedByReference = \array_values(\array_filter($node->params, static function (Node\Param $parameter): bool {
return $parameter->byRef;
}));
if (0 === \count($parametersPassedByReference)) {
return [];
}
$functionName = $node->namespacedName;
return \array_map(static function (Node\Param $parameterPassedByReference) use ($functionName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterPassedByReference->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Function %s() has parameter $%s that is passed by reference.',
$functionName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullDefaultValue()->toString())
->build();
}, $parametersPassedByReference);
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Functions;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Function_>
*/
final class NoParameterWithNullDefaultValueRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Stmt\Function_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersWithNullDefaultValue = \array_values(\array_filter($node->params, function (Node\Param $parameter): bool {
return $this->analyzer->hasNullDefaultValue($parameter);
}));
if (0 === \count($parametersWithNullDefaultValue)) {
return [];
}
$functionName = $node->namespacedName;
return \array_map(static function (Node\Param $parameterWithNullDefaultValue) use ($functionName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullDefaultValue->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Function %s() has parameter $%s with null as default value.',
$functionName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullDefaultValue()->toString())
->build();
}, $parametersWithNullDefaultValue);
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Functions;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Function_>
*/
final class NoParameterWithNullableTypeDeclarationRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Stmt\Function_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersWithNullableTypeDeclaration = \array_values(\array_filter($node->params, function (Node\Param $parameter): bool {
return $this->analyzer->isNullableTypeDeclaration($parameter->type);
}));
if (0 === \count($parametersWithNullableTypeDeclaration)) {
return [];
}
$functionName = $node->namespacedName;
return \array_map(static function (Node\Param $parameterWithNullableTypeDeclaration) use ($functionName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullableTypeDeclaration->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Function %s() has parameter $%s with a nullable type declaration.',
$functionName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullableTypeDeclaration()->toString())
->build();
}, $parametersWithNullableTypeDeclaration);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Functions;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Function_>
*/
final class NoReturnByReferenceRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Stmt\Function_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (false === $node->byRef) {
return [];
}
$message = \sprintf(
'Function %s() returns by reference.',
$node->namespacedName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noReturnByReference()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
/**
* @internal
*/
final class HasContent
{
private string $value;
private function __construct(string $value)
{
$this->value = $value;
}
/**
* @throws \InvalidArgumentException
*/
public static function fromString(string $value): self
{
$values = [
'maybe',
'no',
'yes',
];
if (!\in_array($value, $values, true)) {
throw new \InvalidArgumentException(\sprintf(
'Value needs to be one of "%s", got "%s" instead.',
\implode('", "', $values),
$value,
));
}
return new self($value);
}
public static function maybe(): self
{
return new self('maybe');
}
public static function no(): self
{
return new self('no');
}
public static function yes(): self
{
return new self('yes');
}
public function toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
/**
* @internal
*/
final class HookMethod
{
private ClassName $className;
private MethodName $methodName;
private Invocation $invocation;
private HasContent $hasContent;
private function __construct(
ClassName $className,
MethodName $methodName,
Invocation $invocation,
HasContent $hasContent
) {
$this->className = $className;
$this->methodName = $methodName;
$this->invocation = $invocation;
$this->hasContent = $hasContent;
}
public static function create(
ClassName $className,
MethodName $methodName,
Invocation $invocation,
HasContent $hasContent
): self {
return new self(
$className,
$methodName,
$invocation,
$hasContent,
);
}
public function className(): ClassName
{
return $this->className;
}
public function methodName(): MethodName
{
return $this->methodName;
}
public function invocation(): Invocation
{
return $this->invocation;
}
public function hasContent(): HasContent
{
return $this->hasContent;
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
/**
* @internal
*/
final class Invocation
{
private string $value;
/**
* @param non-empty-string $value
*/
private function __construct(string $value)
{
$this->value = $value;
}
/**
* @throws \InvalidArgumentException
*/
public static function fromString(string $value): self
{
$values = [
'any',
'first',
'last',
'never',
];
if (!\in_array($value, $values, true)) {
throw new \InvalidArgumentException(\sprintf(
'Value needs to be one of "%s", got "%s" instead.',
\implode('", "', $values),
$value,
));
}
return new self($value);
}
public static function any(): self
{
return new self('any');
}
public static function first(): self
{
return new self('first');
}
public static function last(): self
{
return new self('last');
}
public static function never(): self
{
return new self('never');
}
/**
* @return non-empty-string
*/
public function toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules;
/**
* @internal
*/
final class MethodName
{
private string $value;
/**
* @param non-empty-string $value
*/
private function __construct(string $value)
{
$this->value = $value;
}
/**
* @param non-empty-string $value
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* @return non-empty-string
*/
public function toString(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Doctrine\ORM;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\PhpDoc;
use PHPStan\PhpDocParser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class FinalInAbstractClassRule implements Rules\Rule
{
private const DOCTRINE_ATTRIBUTE_NAMES = [
ORM\Mapping\Embeddable::class,
ORM\Mapping\Entity::class,
];
private const DOCTRINE_ANNOTATION_NAMES = [
'@ORM\\Mapping\\Embeddable',
'@ORM\\Embeddable',
'@Embeddable',
'@ORM\\Mapping\\Entity',
'@ORM\\Entity',
'@Entity',
];
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
/** @var Reflection\ClassReflection $containingClass */
$containingClass = $scope->getClassReflection();
if (self::isDoctrineEntity($containingClass)) {
return [];
}
if (!$containingClass->isAbstract()) {
return [];
}
if ($containingClass->isInterface()) {
return [];
}
if ($node->isAbstract()) {
return [];
}
if ($node->isFinal()) {
return [];
}
if ($node->isPrivate()) {
return [];
}
if ('__construct' === $node->name->name) {
return [];
}
$message = \sprintf(
'Method %s::%s() is not final, but since the containing class is abstract, it should be.',
$containingClass->getName(),
$node->name->toString(),
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::finalInAbstractClass()->toString())
->build(),
];
}
private static function isDoctrineEntity(Reflection\ClassReflection $containingClass): bool
{
$attributes = $containingClass->getNativeReflection()->getAttributes();
foreach ($attributes as $attribute) {
if (\in_array($attribute->getName(), self::DOCTRINE_ATTRIBUTE_NAMES, true)) {
return true;
}
}
$resolvedPhpDocBlock = $containingClass->getResolvedPhpDoc();
if ($resolvedPhpDocBlock instanceof PhpDoc\ResolvedPhpDocBlock) {
foreach ($resolvedPhpDocBlock->getPhpDocNodes() as $phpDocNode) {
foreach ($phpDocNode->children as $child) {
if (!$child instanceof PhpDocParser\Ast\PhpDoc\PhpDocTagNode) {
continue;
}
if (\in_array($child->name, self::DOCTRINE_ANNOTATION_NAMES, true)) {
return true;
}
}
}
}
return false;
}
}

View File

@ -0,0 +1,436 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Codeception\PHPUnit;
use Codeception\Test;
use Ergebnis\PHPStan\Rules\ClassName;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use Ergebnis\PHPStan\Rules\HasContent;
use Ergebnis\PHPStan\Rules\HookMethod;
use Ergebnis\PHPStan\Rules\Invocation;
use Ergebnis\PHPStan\Rules\MethodName;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
use PHPStan\ShouldNotHappenException;
use PHPUnit\Framework;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class InvokeParentHookMethodRule implements Rules\Rule
{
private Reflection\ReflectionProvider $reflectionProvider;
/**
* @var list<HookMethod>
*/
private array $hookMethods;
/**
* @param array<class-string, array<string, string>> $hookMethods
*/
public function __construct(
Reflection\ReflectionProvider $reflectionProvider,
array $hookMethods = []
) {
$this->reflectionProvider = $reflectionProvider;
$this->hookMethods = self::sort(
$reflectionProvider,
...self::filter(
$reflectionProvider,
...\array_merge(
self::defaultHookMethods(),
\array_map(static function (array $hookMethod): HookMethod {
return HookMethod::create(
ClassName::fromString($hookMethod['className']),
MethodName::fromString($hookMethod['methodName']),
Invocation::fromString($hookMethod['invocation']),
HasContent::fromString($hookMethod['hasContent']),
);
}, $hookMethods),
),
),
);
}
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
$classReflection = $scope->getClassReflection();
if (null === $classReflection) {
return [];
}
$parentClassReflection = $classReflection->getParentClass();
if (null === $parentClassReflection) {
return [];
}
$methodName = $node->name->toString();
$hookMethod = $this->findMatchingHookMethod(
$scope,
$parentClassReflection,
$classReflection,
$methodName,
);
if (!$hookMethod instanceof HookMethod) {
return [];
}
$statements = $node->getStmts();
if (!\is_array($statements)) {
throw new ShouldNotHappenException();
}
$parentHookMethodInvocation = self::findParentHookMethodInvocation(
\array_values($statements),
$hookMethod,
);
if ($parentHookMethodInvocation->equals(Invocation::never())) {
if ($hookMethod->hasContent()->equals(HasContent::no())) {
return [];
}
$message = \sprintf(
'Method %s::%s() does not invoke parent::%s().',
$classReflection->getName(),
$methodName,
$hookMethod->methodName()->toString(),
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::invokeParentHookMethod()->toString())
->build(),
];
}
if ($parentHookMethodInvocation->equals($hookMethod->invocation())) {
return [];
}
if ($hookMethod->invocation()->equals(Invocation::first())) {
$message = \sprintf(
'Method %s::%s() does not invoke parent::%s() before all other statements.',
$classReflection->getName(),
$methodName,
$hookMethod->methodName()->toString(),
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::invokeParentHookMethod()->toString())
->build(),
];
}
if ($hookMethod->invocation()->equals(Invocation::last())) {
$message = \sprintf(
'Method %s::%s() does not invoke parent::%s() after all other statements.',
$classReflection->getName(),
$methodName,
$hookMethod->methodName()->toString(),
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::invokeParentHookMethod()->toString())
->build(),
];
}
throw new ShouldNotHappenException();
}
private function findMatchingHookMethod(
Analyser\Scope $scope,
Reflection\ClassReflection $parentClassReflection,
Reflection\ClassReflection $classReflection,
string $methodName
): ?HookMethod {
foreach ($this->hookMethods as $hookMethod) {
if (!$classReflection->isSubclassOfClass($this->reflectionProvider->getClass($hookMethod->className()->toString()))) {
continue;
}
if (\mb_strtolower($hookMethod->methodName()->toString()) !== \mb_strtolower($methodName)) {
continue;
}
$parentMethodReflection = $parentClassReflection->getMethod(
$methodName,
$scope,
);
$declaringClassReflection = $parentMethodReflection->getDeclaringClass();
if (\mb_strtolower($hookMethod->className()->toString()) !== \mb_strtolower($declaringClassReflection->getName())) {
return HookMethod::create(
ClassName::fromString($declaringClassReflection->getName()),
MethodName::fromString($methodName),
$hookMethod->invocation(),
HasContent::maybe(),
);
}
return $hookMethod;
}
return null;
}
/**
* @param list<Node\Stmt> $statements
*/
private static function findParentHookMethodInvocation(
array $statements,
HookMethod $hookMethod
): Invocation {
$statementsWithOperations = \array_filter($statements, static function (Node $statement): bool {
if ($statement instanceof Node\Stmt\Nop) {
return false;
}
return true;
});
$statementCount = \count($statementsWithOperations);
foreach ($statementsWithOperations as $index => $statement) {
if (!$statement instanceof Node\Stmt\Expression) {
continue;
}
if (!$statement->expr instanceof Node\Expr\StaticCall) {
continue;
}
if (!$statement->expr->class instanceof Node\Name) {
continue;
}
$className = (string) $statement->expr->class;
if (\mb_strtolower($className) !== 'parent') {
continue;
}
if (!$statement->expr->name instanceof Node\Identifier) {
continue;
}
if (\mb_strtolower($statement->expr->name->toString()) === \mb_strtolower($hookMethod->methodName()->toString())) {
if (1 === $statementCount) {
return $hookMethod->invocation();
}
if (0 === $index) {
return Invocation::first();
}
if ($statementCount - 1 === $index) {
return Invocation::last();
}
return Invocation::any();
}
}
return Invocation::never();
}
/**
* @return list<HookMethod>
*/
private static function filter(
Reflection\ReflectionProvider $reflectionProvider,
HookMethod ...$hookMethods
): array {
return \array_values(\array_filter($hookMethods, static function (HookMethod $hookMethod) use ($reflectionProvider): bool {
return $reflectionProvider->hasClass($hookMethod->className()->toString());
}));
}
/**
* @return list<HookMethod>
*/
private static function sort(
Reflection\ReflectionProvider $reflectionProvider,
HookMethod ...$hookMethods
): array {
\usort($hookMethods, static function (HookMethod $a, HookMethod $b) use ($reflectionProvider): int {
if (\mb_strtolower($a->className()->toString()) === \mb_strtolower($b->className()->toString())) {
return 0;
}
if ($reflectionProvider->getClass($a->className()->toString())->isSubclassOfClass($reflectionProvider->getClass($b->className()->toString()))) {
return -1;
}
return 1;
});
return $hookMethods;
}
/**
* @return list<HookMethod>
*/
private static function defaultHookMethods(): array
{
return [
/**
* @see https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2083-L2085
*/
HookMethod::create(
ClassName::fromString(Framework\TestCase::class),
MethodName::fromString('assertPostConditions'),
Invocation::last(),
HasContent::no(),
),
/**
* @see https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2073-L2075
*/
HookMethod::create(
ClassName::fromString(Framework\TestCase::class),
MethodName::fromString('assertPreConditions'),
Invocation::first(),
HasContent::no(),
),
/**
* @see https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2063-L2065
*/
HookMethod::create(
ClassName::fromString(Framework\TestCase::class),
MethodName::fromString('setUp'),
Invocation::first(),
HasContent::no(),
),
/**
* @see https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2055-L2057
*/
HookMethod::create(
ClassName::fromString(Framework\TestCase::class),
MethodName::fromString('setUpBeforeClass'),
Invocation::first(),
HasContent::no(),
),
/**
* @see https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2091-L2093
*/
HookMethod::create(
ClassName::fromString(Framework\TestCase::class),
MethodName::fromString('tearDown'),
Invocation::last(),
HasContent::no(),
),
/**
* @see https://github.com/sebastianbergmann/phpunit/blob/6.0.0/src/Framework/TestCase.php#L2098-L2100
*/
HookMethod::create(
ClassName::fromString(Framework\TestCase::class),
MethodName::fromString('tearDownAfterClass'),
Invocation::last(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L11-L13
*/
HookMethod::create(
ClassName::fromString(PHPUnit\TestCase::class),
MethodName::fromString('_setUp'),
Invocation::first(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L25-L27
*/
HookMethod::create(
ClassName::fromString(PHPUnit\TestCase::class),
MethodName::fromString('_setUpBeforeClass'),
Invocation::first(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L18-L20
*/
HookMethod::create(
ClassName::fromString(PHPUnit\TestCase::class),
MethodName::fromString('_tearDown'),
Invocation::last(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/phpunit-wrapper/blob/9.0.0/src/TestCase.php#L32-L34
*/
HookMethod::create(
ClassName::fromString(PHPUnit\TestCase::class),
MethodName::fromString('_tearDownAfterClass'),
Invocation::last(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L75-L77
*/
HookMethod::create(
ClassName::fromString(Test\Unit::class),
MethodName::fromString('_after'),
Invocation::last(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L63-L65
*/
HookMethod::create(
ClassName::fromString(Test\Unit::class),
MethodName::fromString('_before'),
Invocation::first(),
HasContent::no(),
),
/**
* @see https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L34-L58
*/
HookMethod::create(
ClassName::fromString(Test\Unit::class),
MethodName::fromString('_setUp'),
Invocation::first(),
HasContent::yes(),
),
/**
* @see https://github.com/Codeception/Codeception/blob/4.2.2/src/Codeception/Test/Unit.php#L67-L70
*/
HookMethod::create(
ClassName::fromString(Test\Unit::class),
MethodName::fromString('_tearDown'),
Invocation::last(),
HasContent::yes(),
),
];
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoConstructorParameterWithDefaultValueRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if ('__construct' !== $node->name->toLowerString()) {
return [];
}
if (0 === \count($node->params)) {
return [];
}
$parametersWithDefaultValue = \array_values(\array_filter($node->params, static function (Node\Param $parameter): bool {
return self::hasDefaultValue($parameter);
}));
if (0 === \count($parametersWithDefaultValue)) {
return [];
}
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
return \array_map(static function (Node\Param $parameterWithDefaultValue): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithDefaultValue->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Constructor in anonymous class has parameter $%s with default value.',
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noConstructorParameterWithDefaultValue()->toString())
->build();
}, $parametersWithDefaultValue);
}
$className = $classReflection->getName();
return \array_map(static function (Node\Param $parameterWithDefaultValue) use ($className): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithDefaultValue->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Constructor in %s has parameter $%s with default value.',
$className,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noConstructorParameterWithDefaultValue()->toString())
->build();
}, $parametersWithDefaultValue);
}
private static function hasDefaultValue(Node\Param $parameter): bool
{
return null !== $parameter->default;
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoNullableReturnTypeDeclarationRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (!$this->analyzer->isNullableTypeDeclaration($node->getReturnType())) {
return [];
}
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
$message = \sprintf(
'Method %s() in anonymous class has a nullable return type declaration.',
$node->name->name,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noNullableReturnTypeDeclaration()->toString())
->build(),
];
}
$message = \sprintf(
'Method %s::%s() has a nullable return type declaration.',
$classReflection->getName(),
$node->name->name,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noNullableReturnTypeDeclaration()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoParameterPassedByReferenceRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersExplicitlyPassedByReference = \array_values(\array_filter($node->params, static function (Node\Param $parameter): bool {
return $parameter->byRef;
}));
if (0 === \count($parametersExplicitlyPassedByReference)) {
return [];
}
$methodName = $node->name->toString();
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
return \array_map(static function (Node\Param $parameterExplicitlyPassedByReference) use ($methodName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterExplicitlyPassedByReference->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Method %s() in anonymous class has parameter $%s that is passed by reference.',
$methodName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterPassedByReference()->toString())
->build();
}, $parametersExplicitlyPassedByReference);
}
$className = $classReflection->getName();
return \array_map(static function (Node\Param $parameterExplicitlyPassedByReference) use ($className, $methodName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterExplicitlyPassedByReference->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Method %s::%s() has parameter $%s that is passed by reference.',
$className,
$methodName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterPassedByReference()->toString())
->build();
}, $parametersExplicitlyPassedByReference);
}
}

View File

@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoParameterWithContainerTypeDeclarationRule implements Rules\Rule
{
private Reflection\ReflectionProvider $reflectionProvider;
/**
* @var list<string>
*/
private array $interfacesImplementedByContainers;
/**
* @var list<string>
*/
private array $methodsAllowedToUseContainerTypeDeclarations;
/**
* @param list<string> $interfacesImplementedByContainers
* @param list<string> $methodsAllowedToUseContainerTypeDeclarations
*/
public function __construct(
Reflection\ReflectionProvider $reflectionProvider,
array $interfacesImplementedByContainers,
array $methodsAllowedToUseContainerTypeDeclarations
) {
$this->reflectionProvider = $reflectionProvider;
$this->interfacesImplementedByContainers = \array_values(\array_filter(
\array_map(static function (string $interfaceImplementedByContainers): string {
return $interfaceImplementedByContainers;
}, $interfacesImplementedByContainers),
static function (string $interfaceImplementedByContainer): bool {
return \interface_exists($interfaceImplementedByContainer);
},
));
$this->methodsAllowedToUseContainerTypeDeclarations = $methodsAllowedToUseContainerTypeDeclarations;
}
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($this->interfacesImplementedByContainers)) {
return [];
}
if (0 === \count($node->params)) {
return [];
}
$methodName = $node->name->toString();
if (\in_array($methodName, $this->methodsAllowedToUseContainerTypeDeclarations, true)) {
return [];
}
/** @var Reflection\ClassReflection $containingClass */
$containingClass = $scope->getClassReflection();
return \array_values(\array_reduce(
$node->params,
function (array $errors, Node\Param $node) use ($scope, $containingClass, $methodName): array {
$type = $node->type;
if (!$type instanceof Node\Name) {
return $errors;
}
/** @var Node\Expr\Variable $variable */
$variable = $node->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$classUsedInTypeDeclaration = $this->reflectionProvider->getClass($scope->resolveName($type));
if ($classUsedInTypeDeclaration->isInterface()) {
foreach ($this->interfacesImplementedByContainers as $interfaceImplementedByContainer) {
if ($classUsedInTypeDeclaration->getName() === $interfaceImplementedByContainer) {
$errors[] = self::createError(
$containingClass,
$methodName,
$parameterName,
$classUsedInTypeDeclaration,
);
return $errors;
}
if ($classUsedInTypeDeclaration->getNativeReflection()->isSubclassOf($interfaceImplementedByContainer)) {
$errors[] = self::createError(
$containingClass,
$methodName,
$parameterName,
$classUsedInTypeDeclaration,
);
return $errors;
}
}
}
foreach ($this->interfacesImplementedByContainers as $interfaceImplementedByContainer) {
if ($classUsedInTypeDeclaration->getNativeReflection()->implementsInterface($interfaceImplementedByContainer)) {
$errors[] = self::createError(
$containingClass,
$methodName,
$parameterName,
$classUsedInTypeDeclaration,
);
return $errors;
}
}
return $errors;
},
[],
));
}
private static function createError(
Reflection\ClassReflection $classReflection,
string $methodName,
string $parameterName,
Reflection\ClassReflection $classUsedInTypeDeclaration
): Rules\RuleError {
if ($classReflection->isAnonymous()) {
$message = \sprintf(
'Method %s() in anonymous class has a parameter $%s with a type declaration of %s, but containers should not be injected.',
$methodName,
$parameterName,
$classUsedInTypeDeclaration->getName(),
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithContainerTypeDeclaration()->toString())
->build();
}
$message = \sprintf(
'Method %s::%s() has a parameter $%s with a type declaration of %s, but containers should not be injected.',
$classReflection->getName(),
$methodName,
$parameterName,
$classUsedInTypeDeclaration->getName(),
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithContainerTypeDeclaration()->toString())
->build();
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoParameterWithNullDefaultValueRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersWithNullDefaultValue = \array_values(\array_filter($node->params, function (Node\Param $parameter): bool {
return $this->analyzer->hasNullDefaultValue($parameter);
}));
if (0 === \count($parametersWithNullDefaultValue)) {
return [];
}
$methodName = $node->name->toString();
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
return \array_map(static function (Node\Param $parameterWithNullDefaultValue) use ($methodName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullDefaultValue->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Method %s() in anonymous class has parameter $%s with null as default value.',
$methodName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullDefaultValue()->toString())
->build();
}, $parametersWithNullDefaultValue);
}
$className = $classReflection->getName();
return \array_map(static function (Node\Param $parameterWithNullDefaultValue) use ($className, $methodName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullDefaultValue->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Method %s::%s() has parameter $%s with null as default value.',
$className,
$methodName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullDefaultValue()->toString())
->build();
}, $parametersWithNullDefaultValue);
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\Analyzer;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoParameterWithNullableTypeDeclarationRule implements Rules\Rule
{
private Analyzer $analyzer;
public function __construct(Analyzer $analyzer)
{
$this->analyzer = $analyzer;
}
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (0 === \count($node->params)) {
return [];
}
$parametersWithNullableTypeDeclaration = \array_values(\array_filter($node->params, function (Node\Param $parameter): bool {
return $this->analyzer->isNullableTypeDeclaration($parameter->type);
}));
if (0 === \count($parametersWithNullableTypeDeclaration)) {
return [];
}
$methodName = $node->name->toString();
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
return \array_map(static function (Node\Param $parameterWithNullableTypeDeclaration) use ($methodName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullableTypeDeclaration->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Method %s() in anonymous class has parameter $%s with a nullable type declaration.',
$methodName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullableTypeDeclaration()->toString())
->build();
}, $parametersWithNullableTypeDeclaration);
}
$className = $classReflection->getName();
return \array_map(static function (Node\Param $parameterWithNullableTypeDeclaration) use ($className, $methodName): Rules\RuleError {
/** @var Node\Expr\Variable $variable */
$variable = $parameterWithNullableTypeDeclaration->var;
/** @var string $parameterName */
$parameterName = $variable->name;
$message = \sprintf(
'Method %s::%s() has parameter $%s with a nullable type declaration.',
$className,
$methodName,
$parameterName,
);
return Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noParameterWithNullableTypeDeclaration()->toString())
->build();
}, $parametersWithNullableTypeDeclaration);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class NoReturnByReferenceRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if (false === $node->byRef) {
return [];
}
$methodName = $node->name->toString();
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
$message = \sprintf(
'Method %s() in anonymous class returns by reference.',
$methodName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noReturnByReference()->toString())
->build(),
];
}
$className = $classReflection->getName();
$message = \sprintf(
'Method %s::%s() returns by reference.',
$className,
$methodName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noReturnByReference()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Methods;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Comment;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;
use PHPStan\Type;
use PHPUnit\Framework;
/**
* @implements Rules\Rule<Node\Stmt\ClassMethod>
*/
final class PrivateInFinalClassRule implements Rules\Rule
{
/**
* @var list<string>
*/
private static array $whitelistedAnnotations = [
'@after',
'@before',
'@postCondition',
'@preCondition',
];
/**
* @var list<class-string>
*/
private static array $whitelistedAttributes = [
Framework\Attributes\After::class,
Framework\Attributes\Before::class,
Framework\Attributes\PostCondition::class,
Framework\Attributes\PreCondition::class,
];
private Type\FileTypeMapper $fileTypeMapper;
public function __construct(Type\FileTypeMapper $fileTypeMapper)
{
$this->fileTypeMapper = $fileTypeMapper;
}
public function getNodeType(): string
{
return Node\Stmt\ClassMethod::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
/** @var Reflection\ClassReflection $containingClass */
$containingClass = $scope->getClassReflection();
if (!$containingClass->isFinal()) {
return [];
}
if ($node->isPublic()) {
return [];
}
if ($node->isPrivate()) {
return [];
}
if ($this->hasWhitelistedAnnotation($node, $containingClass)) {
return [];
}
if (self::hasWhitelistedAttribute($node)) {
return [];
}
$methodName = $node->name->toString();
if (self::isDeclaredByParentClass($containingClass, $methodName)) {
return [];
}
if (self::isDeclaredByTrait($containingClass, $methodName)) {
return [];
}
/** @var Reflection\ClassReflection $classReflection */
$classReflection = $scope->getClassReflection();
if ($classReflection->isAnonymous()) {
$message = \sprintf(
'Method %s() in anonymous class is protected, but since the containing class is final, it can be private.',
$node->name->name,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::privateInFinalClass()->toString())
->build(),
];
}
$message = \sprintf(
'Method %s::%s() is protected, but since the containing class is final, it can be private.',
$containingClass->getName(),
$methodName,
);
return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::privateInFinalClass()->toString())
->build(),
];
}
private function hasWhitelistedAnnotation(
Node\Stmt\ClassMethod $node,
Reflection\ClassReflection $containingClass
): bool {
$docComment = $node->getDocComment();
if (!$docComment instanceof Comment\Doc) {
return false;
}
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
null,
$containingClass->getName(),
null,
null,
$docComment->getText(),
);
foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpDocNode) {
foreach ($phpDocNode->getTags() as $tag) {
if (\in_array($tag->name, self::$whitelistedAnnotations, true)) {
return true;
}
}
}
return false;
}
private static function hasWhitelistedAttribute(Node\Stmt\ClassMethod $node): bool
{
foreach ($node->attrGroups as $attributeGroup) {
foreach ($attributeGroup->attrs as $attribute) {
if (\in_array($attribute->name->toString(), self::$whitelistedAttributes, true)) {
return true;
}
}
}
return false;
}
private static function isDeclaredByParentClass(
Reflection\ClassReflection $containingClass,
string $methodName
): bool {
$parentClass = $containingClass->getNativeReflection()->getParentClass();
if (!$parentClass instanceof \ReflectionClass) {
return false;
}
if (!$parentClass->hasMethod($methodName)) {
return false;
}
return true;
}
private static function isDeclaredByTrait(
Reflection\ClassReflection $containingClass,
string $methodName
): bool {
foreach ($containingClass->getTraits() as $trait) {
if ($trait->hasMethod($methodName)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Statements;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Rules;
/**
* @implements Rules\Rule<Node\Stmt\Switch_>
*/
final class NoSwitchRule implements Rules\Rule
{
public function getNodeType(): string
{
return Node\Stmt\Switch_::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
return [
Rules\RuleErrorBuilder::message('Control structures using switch should not be used.')
->identifier(ErrorIdentifier::noSwitch()->toString())
->build(),
];
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Ondřej Mirtes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,93 @@
# PHPStan Extension Installer
[![Build](https://github.com/phpstan/extension-installer/workflows/Build/badge.svg)](https://github.com/phpstan/extension-installer/actions)
[![Latest Stable Version](https://poser.pugx.org/phpstan/extension-installer/v/stable)](https://packagist.org/packages/phpstan/extension-installer)
[![License](https://poser.pugx.org/phpstan/extension-installer/license)](https://packagist.org/packages/phpstan/extension-installer)
Composer plugin for automatic installation of [PHPStan](https://phpstan.org/) extensions.
# Motivation
```diff
diff --git a/phpstan.neon b/phpstan.neon
index db4e3df32e..2ca30fa20a 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,12 +1,3 @@
-includes:
- - vendor/phpstan/phpstan-doctrine/extension.neon
- - vendor/phpstan/phpstan-doctrine/rules.neon
- - vendor/phpstan/phpstan-nette/extension.neon
- - vendor/phpstan/phpstan-nette/rules.neon
- - vendor/phpstan/phpstan-phpunit/extension.neon
- - vendor/phpstan/phpstan-phpunit/rules.neon
- - vendor/phpstan/phpstan-strict-rules/rules.neon
-
parameters:
autoload_directories:
- %rootDir%/../../../build/SlevomatSniffs
diff --git a/composer.json b/composer.json
index 1b578dd624..f6ebf6e477 100644
--- a/composer.json
+++ b/composer.json
@@ -142,6 +142,7 @@
"jakub-onderka/php-parallel-lint": "1.0.0",
"justinrainbow/json-schema": "5.2.8",
"ondrejmirtes/mocktainer": "0.8",
+ "phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.11.7",
"phpstan/phpstan-doctrine": "^0.11.3",
"phpstan/phpstan-nette": "^0.11.1",
```
## Usage
```bash
composer require --dev phpstan/extension-installer
```
Starting from Composer 2.2.0 you'll get the following question:
```
phpstan/extension-installer contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins
Do you trust "phpstan/extension-installer" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]
```
Answer with `y` to allow the plugin.
## Instructions for extension developers
It's best (but optional) to set the extension's composer package [type](https://getcomposer.org/doc/04-schema.md#type) to `phpstan-extension` for this plugin to be able to recognize it and to be [discoverable on Packagist](https://packagist.org/explore/?type=phpstan-extension).
Add `phpstan` key in the extension `composer.json`'s `extra` section:
```json
{
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
}
}
```
## Ignoring a particular extension
You may want to disable auto-installation of a particular extension to handle installation manually. Ignore an extension by adding an `extra.phpstan/extension-installer.ignore` array in `composer.json` that specifies a list of packages to ignore:
```json
{
"extra": {
"phpstan/extension-installer": {
"ignore": [
"phpstan/phpstan-phpunit"
]
}
}
}
```
## Limitations
The extension installer depends on Composer script events, therefore you cannot use `--no-scripts` flag.

View File

@ -0,0 +1,33 @@
{
"name": "phpstan/extension-installer",
"type": "composer-plugin",
"description": "Composer plugin for automatic installation of PHPStan extensions",
"license": [
"MIT"
],
"keywords": ["dev", "static analysis"],
"require": {
"php": "^7.2 || ^8.0",
"composer-plugin-api": "^2.0",
"phpstan/phpstan": "^1.9.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2.0",
"phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"ocramius/package-versions": true
}
},
"extra": {
"class": "PHPStan\\ExtensionInstaller\\Plugin"
},
"autoload": {
"psr-4": {
"PHPStan\\ExtensionInstaller\\": "src/"
}
}
}

View File

@ -0,0 +1,67 @@
<?php declare(strict_types = 1);
namespace PHPStan\ExtensionInstaller;
/**
* This class is generated by phpstan/extension-installer.
* @internal
*/
final class GeneratedConfig
{
public const EXTENSIONS = array (
'ergebnis/phpstan-rules' =>
array (
'install_path' => '/usr/local/src/exporter/tools/.phpstan/vendor/ergebnis/phpstan-rules',
'relative_install_path' => '../../../ergebnis/phpstan-rules',
'extra' =>
array (
'includes' =>
array (
0 => 'rules.neon',
),
),
'version' => '2.13.1',
'phpstanVersionConstraint' => '>=2.1.35.0-dev, <3.0.0.0-dev',
),
'phpstan/phpstan-strict-rules' =>
array (
'install_path' => '/usr/local/src/exporter/tools/.phpstan/vendor/phpstan/phpstan-strict-rules',
'relative_install_path' => '../../phpstan-strict-rules',
'extra' =>
array (
'includes' =>
array (
0 => 'rules.neon',
),
),
'version' => '2.0.11',
'phpstanVersionConstraint' => '>=2.1.39.0-dev, <3.0.0.0-dev',
),
'tomasvotruba/type-coverage' =>
array (
'install_path' => '/usr/local/src/exporter/tools/.phpstan/vendor/tomasvotruba/type-coverage',
'relative_install_path' => '../../../tomasvotruba/type-coverage',
'extra' =>
array (
'includes' =>
array (
0 => 'config/extension.neon',
),
),
'version' => '2.1.0',
'phpstanVersionConstraint' => '>=2.0.0.0-dev, <3.0.0.0-dev',
),
);
public const NOT_INSTALLED = array (
);
/** @var string|null */
public const PHPSTAN_VERSION_CONSTRAINT = '>=2.1.39.0-dev, <3.0.0.0-dev';
private function __construct()
{
}
}

View File

@ -0,0 +1,228 @@
<?php declare(strict_types = 1);
namespace PHPStan\ExtensionInstaller;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Semver\Intervals;
use Composer\Util\Filesystem;
use function array_key_exists;
use function array_keys;
use function class_exists;
use function count;
use function dirname;
use function file_exists;
use function file_put_contents;
use function getcwd;
use function in_array;
use function is_file;
use function ksort;
use function md5;
use function md5_file;
use function sort;
use function sprintf;
use function strpos;
use function var_export;
use const DIRECTORY_SEPARATOR;
final class Plugin implements PluginInterface, EventSubscriberInterface
{
/** @var string */
private static $generatedFileTemplate = <<<'PHP'
<?php declare(strict_types = 1);
namespace PHPStan\ExtensionInstaller;
/**
* This class is generated by phpstan/extension-installer.
* @internal
*/
final class GeneratedConfig
{
public const EXTENSIONS = %s;
public const NOT_INSTALLED = %s;
/** @var string|null */
public const PHPSTAN_VERSION_CONSTRAINT = %s;
private function __construct()
{
}
}
PHP;
public function activate(Composer $composer, IOInterface $io): void
{
// noop
}
public function deactivate(Composer $composer, IOInterface $io): void
{
// noop
}
public function uninstall(Composer $composer, IOInterface $io): void
{
// noop
}
/**
* @return array<string, string>
*/
public static function getSubscribedEvents(): array
{
return [
ScriptEvents::POST_INSTALL_CMD => 'process',
ScriptEvents::POST_UPDATE_CMD => 'process',
];
}
public function process(Event $event): void
{
$io = $event->getIO();
if (!file_exists(__DIR__)) {
$io->write('<info>phpstan/extension-installer:</info> Package not found (probably scheduled for removal); extensions installation skipped.');
return;
}
$composer = $event->getComposer();
$installationManager = $composer->getInstallationManager();
$generatedConfigFilePath = __DIR__ . '/GeneratedConfig.php';
$oldGeneratedConfigFileHash = null;
if (is_file($generatedConfigFilePath)) {
$oldGeneratedConfigFileHash = md5_file($generatedConfigFilePath);
}
$notInstalledPackages = [];
$installedPackages = [];
$ignoredPackages = [];
$data = [];
$fs = new Filesystem();
$ignore = [];
$packageExtra = $composer->getPackage()->getExtra();
if (isset($packageExtra['phpstan/extension-installer']['ignore'])) {
$ignore = $packageExtra['phpstan/extension-installer']['ignore'];
}
$phpstanVersionConstraints = [];
foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) {
if (
$package->getType() !== 'phpstan-extension'
&& !isset($package->getExtra()['phpstan'])
) {
if (
strpos($package->getName(), 'phpstan') !== false
&& !in_array($package->getName(), [
'phpstan/phpstan',
'phpstan/phpstan-shim',
'phpstan/phpdoc-parser',
'phpstan/extension-installer',
], true)
) {
$notInstalledPackages[$package->getName()] = $package->getFullPrettyVersion();
}
continue;
}
if (in_array($package->getName(), $ignore, true)) {
$ignoredPackages[] = $package->getName();
continue;
}
$installPath = $installationManager->getInstallPath($package);
if ($installPath === null) {
continue;
}
$absoluteInstallPath = $fs->isAbsolutePath($installPath)
? $installPath
: getcwd() . DIRECTORY_SEPARATOR . $installPath;
$packageRequires = $package->getRequires();
$phpstanConstraint = null;
if (array_key_exists('phpstan/phpstan', $packageRequires)) {
$phpstanConstraint = $packageRequires['phpstan/phpstan']->getConstraint();
if ($phpstanConstraint->getLowerBound()->isZero()) {
continue;
}
if ($phpstanConstraint->getUpperBound()->isPositiveInfinity()) {
continue;
}
$phpstanVersionConstraints[] = $phpstanConstraint;
}
$data[$package->getName()] = [
'install_path' => $absoluteInstallPath,
'relative_install_path' => $fs->findShortestPath(dirname($generatedConfigFilePath), $absoluteInstallPath, true),
'extra' => $package->getExtra()['phpstan'] ?? null,
'version' => $package->getFullPrettyVersion(),
'phpstanVersionConstraint' => $phpstanConstraint !== null ? $this->constraintIntoString($phpstanConstraint) : null,
];
$installedPackages[$package->getName()] = true;
}
$phpstanVersionConstraint = null;
if (count($phpstanVersionConstraints) > 0 && class_exists(Intervals::class)) {
if (count($phpstanVersionConstraints) === 1) {
$multiConstraint = $phpstanVersionConstraints[0];
} else {
$multiConstraint = new MultiConstraint($phpstanVersionConstraints);
}
$phpstanVersionConstraint = $this->constraintIntoString(Intervals::compactConstraint($multiConstraint));
}
ksort($data);
ksort($installedPackages);
ksort($notInstalledPackages);
sort($ignoredPackages);
$generatedConfigFileContents = sprintf(self::$generatedFileTemplate, var_export($data, true), var_export($notInstalledPackages, true), var_export($phpstanVersionConstraint, true));
file_put_contents($generatedConfigFilePath, $generatedConfigFileContents);
$io->write('<info>phpstan/extension-installer:</info> Extensions installed');
if ($oldGeneratedConfigFileHash === md5($generatedConfigFileContents)) {
return;
}
foreach (array_keys($installedPackages) as $name) {
$io->write(sprintf('> <info>%s:</info> installed', $name));
}
foreach (array_keys($notInstalledPackages) as $name) {
$io->write(sprintf('> <comment>%s:</comment> not supported', $name));
}
foreach ($ignoredPackages as $name) {
$io->write(sprintf('> <comment>%s:</comment> ignored', $name));
}
}
private function constraintIntoString(ConstraintInterface $constraint): string
{
return sprintf(
'%s%s, %s%s',
$constraint->getLowerBound()->isInclusive() ? '>=' : '>',
$constraint->getLowerBound()->getVersion(),
$constraint->getUpperBound()->isInclusive() ? '<=' : '<',
$constraint->getUpperBound()->getVersion()
);
}
}

Some files were not shown because too many files have changed in this diff Show More