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();
+ }
+}