diff --git a/.ddev/config.yaml b/.ddev/config.yaml new file mode 100644 index 0000000..4618f43 --- /dev/null +++ b/.ddev/config.yaml @@ -0,0 +1,15 @@ +name: phpstan-faker +type: php +docroot: "" +php_version: "8.1" +webserver_type: nginx-fpm +xdebug_enabled: false +additional_hostnames: [] +additional_fqdns: [] +database: + type: mariadb + version: "10.4" +use_dns_when_possible: true +composer_version: "2" +web_environment: [] +nodejs_version: "18" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b26a9a9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at https://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0a53b49 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.ddev export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/PULL_REQUEST_TEMPLATE.md export-ignore +/ISSUE_TEMPLATE.md export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/docs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2acd464 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +build +composer.lock +vendor +phpunit.xml +.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ed324b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to `swisnl/phpstan-faker` will be documented in this file. + +Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. + +## [Unreleased] + +- First working version of the extension diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0a5aada --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +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. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at `service@swis.nl`. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9537a5e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [GitHub](https://github.com/swisnl/phpstan-faker). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Fix the code style with ``$ composer format``. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + + +**Happy coding**! diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..0fc72af --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ + + +## Detailed description + +Provide a detailed description of the change or addition you are proposing. + +Make it clear if the issue is a bug, an enhancement or just a question. + +## Context + +Why is this change important to you? How would you use it? + +How can it benefit other users? + +## Possible implementation + +Not obligatory, but suggest an idea for implementing addition or change. + +## Your environment + +Include as many relevant details about the environment you experienced the bug in and how to reproduce it. + +* Version used (e.g. PHP 8.0): +* Operating system and version (e.g. Ubuntu 22.04, Windows 10): +* Link to your project: +* ... +* ... diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..afb7017 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 SWIS BV + +> 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. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..86246b3 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ + + +## Description + +Describe your changes in detail. + +## Motivation and context + +Why is this change required? What problem does it solve? + +If it fixes an open issue, please link to the issue here (if you write `fixes #num` +or `closes #num`, the issue will be automatically closed when the pull is accepted.) + +## How has this been tested? + +Please describe in detail how you tested your changes. + +Include details of your testing environment, and the tests you ran to +see how your change affects other areas of the code, etc. + +## Screenshots (if appropriate) + +## Types of changes + +What types of changes does your code introduce? Put an `x` in all the boxes that apply: +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + +Go over all the following points, and put an `x` in all the boxes that apply. + +Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). + +- [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. +- [ ] My pull request addresses exactly one patch/feature. +- [ ] I have created a branch for this patch/feature. +- [ ] Each individual commit in the pull request is meaningful. +- [ ] I have added tests to cover my changes. +- [ ] If my change requires a change to the documentation, I have updated it accordingly. + +If you're unsure about any of these, don't hesitate to ask. We're here to help! diff --git a/README.md b/README.md new file mode 100644 index 0000000..dafbcd5 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# PHPStan Faker Provider Extension + +[![Latest Version on Packagist][ico-version]][link-packagist] +[![Software License][ico-license]](LICENSE.md) +[![Buy us a tree][ico-treeware]][link-treeware] +[![Total Downloads][ico-downloads]][link-downloads] +[![Maintained by SWIS][ico-swis]][link-swis] + +This PHPStan Reflection Extension can automatically register the methods and properties created by custom Faker providers, so you don't need to instruct PHPStan to ignore the usage of those methods. + +## Installation + +Via Composer + +```bash +composer require --dev swisnl/phpstan-faker +``` + +If you also have [phpstan/extension-installer](https://github.com/phpstan/extension-installer) installed, then you don't need to follow the instructions for manual installation. Regardless of the installation method, you need to follow the instructions for configuration. + +
+ Manual installation + +If you don't want to use `phpstan/extension-installer`, include extension.neon in your project's PHPStan config: + +```neon +includes: + - vendor/swisnl/phpstan-faker/extension.neon +``` +
+ +## Configuration + +Add the custom faker provider classes to the extension configuration. + +```neon +parameters: + faker: + providerClasses: + - App\Faker\MyProvider +``` + +**This does not actually register the custom providers in Faker, this just tells PHPStan about the custom providers.** + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. + +## Security + +If you discover any security related issues, please email security@swis.nl instead of using the issue tracker. + +## Credits + +- [All Contributors][link-contributors] + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**][link-treeware] to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats. + +## SWIS :heart: Open Source + +[SWIS][link-swis] is a web agency from Leiden, the Netherlands. We love working with open source software. + +[ico-version]: https://img.shields.io/packagist/v/swisnl/phpstan-faker.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-treeware]: https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/swisnl/phpstan-faker.svg?style=flat-square +[ico-swis]: https://img.shields.io/badge/%F0%9F%9A%80-maintained%20by%20SWIS-%230737A9.svg?style=flat-square + +[link-packagist]: https://packagist.org/packages/swisnl/phpstan-faker +[link-downloads]: https://packagist.org/packages/swisnl/phpstan-faker +[link-treeware]: https://plant.treeware.earth/swisnl/phpstan-faker +[link-fork]: https://github.com/modprobe/phpstan-faker +[link-author]: https://github.com/modprobe +[link-contributors]: ../../contributors +[link-swis]: https://www.swis.nl diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ea18c0c --- /dev/null +++ b/composer.json @@ -0,0 +1,48 @@ +{ + "name": "swisnl/phpstan-faker", + "type": "phpstan-extension", + "description": "PHPStan Reflection extension for custom Faker providers", + "keywords": [ + "phpstan", + "reflection", + "faker", + "testing" + ], + "homepage": "https://github.com/swisnl/phpstan-faker", + "license": "MIT", + "authors": [ + { + "name": "Rolf van de Krol", + "email": "rvandekrol@swis.nl", + "role": "Developer" + } + ], + "require": { + "php": "^7.4|^8.0", + "ext-json": "*", + "phpstan/phpstan": "^1.0" + }, + "require-dev": { + "fakerphp/faker": "^1.21", + "laravel/pint": "^1.2" + }, + "autoload": { + "psr-4": { + "Swis\\PHPStan\\Reflection\\": "src" + } + }, + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "scripts": { + "analyse": "vendor/bin/phpstan analyse", + "format": "vendor/bin/pint" + }, + "config": { + "sort-packages": true + } +} diff --git a/extension.neon b/extension.neon new file mode 100644 index 0000000..4c099b2 --- /dev/null +++ b/extension.neon @@ -0,0 +1,17 @@ +parametersSchema: + faker: structure([ + providerClasses: listOf(string()) + ]) + +parameters: + faker: + providerClasses: [] + +services: + reflectionExtension.fakerProvider: + class: Swis\PHPStan\Reflection\FakerProviderReflectionExtension + tags: + - phpstan.broker.propertiesClassReflectionExtension + - phpstan.broker.methodsClassReflectionExtension + arguments: + providerClasses: '%faker.providerClasses%' diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..32d0f9e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +includes: + - ./extension.neon + +parameters: + paths: + - src + + level: 8 + + ignoreErrors: + + excludePaths: diff --git a/src/FakerProviderReflectionExtension.php b/src/FakerProviderReflectionExtension.php new file mode 100644 index 0000000..9c69620 --- /dev/null +++ b/src/FakerProviderReflectionExtension.php @@ -0,0 +1,145 @@ + + */ + protected array $methodCache = []; + + /** + * @var array + */ + protected array $propertyCache = []; + + /** + * @param class-string[] $providerClasses + */ + public function __construct(ReflectionProvider $reflectionProvider, array $providerClasses) + { + $this->reflectionProvider = $reflectionProvider; + $this->providerClasses = $providerClasses; + } + + public function hasMethod(ClassReflection $classReflection, string $methodName): bool + { + return $this->findMethodWithCache($classReflection, $methodName) !== null; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + $method = $this->findMethodWithCache($classReflection, $methodName); + + if ($method === null) { + // This should never happen, because hasMethod() should always be + // called first. + throw new \PHPStan\ShouldNotHappenException(); + } + + return $method; + } + + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findPropertyWithCache($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + $property = $this->findPropertyWithCache($classReflection, $propertyName); + + if ($property === null) { + // This should never happen, because hasProperty() should always be + // called first. + throw new \PHPStan\ShouldNotHappenException(); + } + + return $property; + } + + protected function findMethodWithCache(ClassReflection $classReflection, string $methodName): ?MethodReflection + { + if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->methodCache)) { + return $this->methodCache[$classReflection->getCacheKey().'-'.$methodName]; + } + + $methodReflection = $this->findMethod($classReflection, $methodName); + + if ($methodReflection !== null) { + $this->methodCache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection; + + return $methodReflection; + } + + return null; + } + + protected function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection + { + if ($classReflection->isGeneric()) { + return null; + } + + if ($classReflection->getName() === 'Faker\\Generator') { + foreach ($this->providerClasses as $providerClass) { + $providerReflection = $this->reflectionProvider->getClass($providerClass); + try { + $methodReflection = $providerReflection->getMethod($methodName, new OutOfClassScope()); + if ($methodReflection->isPublic()) { + return $methodReflection; + } + } catch (MissingMethodFromReflectionException $e) { + continue; + } + } + } + + return null; + } + + protected function findPropertyWithCache(ClassReflection $classReflection, string $propertyName): ?PropertyReflection + { + if (array_key_exists($classReflection->getCacheKey().'-'.$propertyName, $this->propertyCache)) { + return $this->propertyCache[$classReflection->getCacheKey().'-'.$propertyName]; + } + + $propertyReflection = $this->findProperty($classReflection, $propertyName); + + if ($propertyReflection !== null) { + $this->propertyCache[$classReflection->getCacheKey().'-'.$propertyName] = $propertyReflection; + + return $propertyReflection; + } + + return null; + } + + protected function findProperty(ClassReflection $classReflection, string $propertyName): ?PropertyReflection + { + $method = $this->findMethodWithCache($classReflection, $propertyName); + + if ($method !== null) { + return new MethodPropertyReflection($method); + } + + return null; + } +} diff --git a/src/MethodPropertyReflection.php b/src/MethodPropertyReflection.php new file mode 100644 index 0000000..1b05a6c --- /dev/null +++ b/src/MethodPropertyReflection.php @@ -0,0 +1,84 @@ +method = $method; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->method->getDeclaringClass(); + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return $this->method->getDocComment(); + } + + public function getReadableType(): Type + { + return $this->method->getVariants()[0]->getReturnType(); + } + + public function getWritableType(): Type + { + return $this->getReadableType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return false; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return false; + } + + public function isDeprecated(): TrinaryLogic + { + return $this->method->isDeprecated(); + } + + public function getDeprecatedDescription(): ?string + { + return $this->method->getDeprecatedDescription(); + } + + public function isInternal(): TrinaryLogic + { + return $this->method->isInternal(); + } +}