├── .github └── workflows │ └── full-checks.yml ├── .gitignore ├── .php-cs-fixer.php ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── build └── PHPStan │ └── Rules │ └── CheckRuleIsInExtension.php ├── composer.json ├── dependency-tests ├── .gitignore └── check-phpstan-dependencies.sh ├── docker-compose.yaml ├── docker └── composer.sh ├── e2e ├── PHPStanResultsChecker.php ├── data │ ├── BaseTraitClass.php │ ├── FriendProblems.php │ ├── InjectableBad.php │ ├── InjectableGood.php │ ├── MustUse.php │ ├── MyTrait.php │ ├── Person.php │ ├── PersonBuilder.php │ ├── PersonRepository.php │ ├── Repository.php │ ├── Shape.php │ ├── Square.php │ ├── TraitClass.php │ └── TraitClassProblems.php ├── phpstan-e2e.neon └── test-runner ├── extension.neon ├── phpstan.neon ├── phpunit.xml ├── src ├── AttributeValueReaders │ ├── AttributeFinder.php │ └── AttributeValueReader.php ├── Config │ └── TestConfig.php ├── Helpers │ ├── Assert.php │ ├── AttributeValueReader.php │ ├── Cache.php │ ├── SubNamespaceChecker.php │ └── TestClassChecker.php └── Rules │ ├── AbstractFriendRule.php │ ├── AbstractNamespaceVisibilityRule.php │ ├── AbstractPackageRule.php │ ├── AbstractTestTagRule.php │ ├── FriendMethodCallRule.php │ ├── FriendNewCallRule.php │ ├── FriendStaticCallRule.php │ ├── InjectableVersionRule.php │ ├── MustUseResultRule.php │ ├── NamespaceVisibilityMethodCallRule.php │ ├── NamespaceVisibilityNewCallRule.php │ ├── NamespaceVisibilitySetting.php │ ├── NamespaceVisibilityStaticCallRule.php │ ├── OverrideRule.php │ ├── PackageMethodCallRule.php │ ├── PackageNewCallRule.php │ ├── PackageStaticCallRule.php │ ├── RestrictTraitToRule.php │ ├── TestTagMethodCallRule.php │ ├── TestTagNewCallRule.php │ └── TestTagStaticCallRule.php └── tests ├── Rules ├── AbstractFriendRuleTest.php ├── AbstractInjectableVersionRuleTest.php ├── AbstractNamespaceVisibilityRuleTest.php ├── AbstractPackageRuleTest.php ├── FriendOnClassTest.php ├── FriendOnConstructorTest.php ├── FriendOnInterfaceClassTest.php ├── FriendOnInterfaceMethodTest.php ├── FriendOnNewTest.php ├── FriendRuleMethodCallTest.php ├── FriendRuleStaticCallTest.php ├── FriendWithTestClassNameTest.php ├── FriendWithTestNamespaceTest.php ├── InjectableVersionCheckOnMethodTest.php ├── InjectableVersionTest.php ├── InjectableVersionWithTestNamespaceTest.php ├── MustUseResultTest.php ├── NamespaceVisibilityOnClassTest.php ├── NamespaceVisibilityOnConstructorTest.php ├── NamespaceVisibilityOnMethodTest.php ├── NamespaceVisibilityOnNewTest.php ├── NamespaceVisibilityOnStaticTest.php ├── OverrideErrorFormatter.php ├── OverrideTest.php ├── PackageOnClassTest.php ├── PackageOnConstructorTest.php ├── PackageOnMethodTest.php ├── PackageOnNewTest.php ├── PackageOnStaticTest.php ├── PackageWithTestClassNameTest.php ├── PackageWithTestNamespaceTest.php ├── RestrictTraitToTest.php ├── TestTagClassOnConstructorIngoredOnTestClassTest.php ├── TestTagClassOnConstructorTest.php ├── TestTagClassOnMethodIgnoredOnTestClassTest.php ├── TestTagClassOnMethodTest.php ├── TestTagClassOnStaticIgnoredOnTestClassTest.php ├── TestTagClassOnStaticTest.php ├── TestTagOnConstructorIgnoredInTestNamespaceTest.php ├── TestTagOnConstructorIgnoredOnTestClassTest.php ├── TestTagOnConstructorTest.php ├── TestTagOnMethodIgnoredInTestNamespaceTest.php ├── TestTagOnMethodIgnoredOnTestClassTest.php ├── TestTagOnMethodTest.php ├── TestTagOnStaticIgnoreIOnTestNamespaceTest.php ├── TestTagOnStaticIgnoredOnTestClassTest.php ├── TestTagOnStaticTest.php └── data │ ├── friend │ ├── friendOnClass.php │ ├── friendOnConstructor.php │ ├── friendOnDifferentNamespace.php │ ├── friendOnInterfaceClass.php │ ├── friendOnInterfaceMethod.php │ ├── friendOnMethod.php │ ├── friendOnNew.php │ ├── friendOnStaticMethod.php │ ├── friendRulesIgnoredForTestClass.php │ ├── friendRulesIgnoredForTestNamespace.php │ └── multipleFriends.php │ ├── injectableVersion │ ├── InjectableVersionCheckOnMethod.php │ ├── InjectableVersionOnClass.php │ ├── InjectableVersionOnExtendsThenImplements.php │ ├── InjectableVersionOnInterface.php │ ├── InjectableVersionRulesIgnoredForTestNamespace.php │ ├── IterableInjectableVersion.php │ ├── MultipleLevelsOfInheritanceInjectableVersionOnInterface.php │ ├── MultipleLevelsOfInheritanceNoInjectableVersionOnClass.php │ └── MultipleLevelsOfInheritanceNoInjectableVersionOnInterface.php │ ├── mustUseResult │ ├── mustUseResultOnMethod.php │ ├── mustUseResultOnStaticMethod.php │ └── mustUseResultWithParent.php │ ├── namespaceVisibility │ ├── namespaceVisibilityOnClass.php │ ├── namespaceVisibilityOnClassExcludeSubNamespaces.php │ ├── namespaceVisibilityOnClassForDifferentNamespaces.php │ ├── namespaceVisibilityOnConstructor.php │ ├── namespaceVisibilityOnConstructorExcludeSubNamespaces.php │ ├── namespaceVisibilityOnConstructorExcludeSubNamespacesPositional.php │ ├── namespaceVisibilityOnConstructorForDifferentNamespaces.php │ ├── namespaceVisibilityOnMethod.php │ ├── namespaceVisibilityOnMethodExcludeSubNamespace.php │ ├── namespaceVisibilityOnMethodForDifferentNamespace.php │ ├── namespaceVisibilityOnNew.php │ ├── namespaceVisibilityOnNewExcludeSubNamespaces.php │ ├── namespaceVisibilityOnNewForDifferentNamespaces.php │ ├── namespaceVisibilityOnStaticMethod.php │ ├── namespaceVisibilityOnStaticMethodExcludeSubNamespaces.php │ ├── namespaceVisibilityOnStaticMethodForDifferentNamespaces.php │ ├── namespaceVisibilityRulesIgnoredForTestClass.php │ └── namespaceVisibilityRulesIgnoredForTestNamespace.php │ ├── override │ ├── overrideOnClass.php │ ├── overrideOnInterface.php │ └── overrideRfcExamples.php │ ├── package │ ├── packageOnClass.php │ ├── packageOnConstructor.php │ ├── packageOnMethod.php │ ├── packageOnNew.php │ ├── packageOnStaticMethod.php │ ├── packageRulesIgnoredForTestClass.php │ └── packageRulesIgnoredForTestNamespace.php │ ├── restrictTraitTo │ └── restrictTraitTo.php │ └── testTag │ ├── testTagClassOnConstructor.php │ ├── testTagClassOnConstructorIgnoredInTestClass.php │ ├── testTagClassOnMethod.php │ ├── testTagClassOnMethodIgnoredInTestClass.php │ ├── testTagClassOnStaticMethod.php │ ├── testTagClassOnStaticMethodIgnoredInTestClass.php │ ├── testTagOnConstructor.php │ ├── testTagOnConstructorIgnoredInTestClass.php │ ├── testTagOnConstructorIgnoredInTestNamespace.php │ ├── testTagOnMethod.php │ ├── testTagOnMethodIgnoredInTestClass.php │ ├── testTagOnMethodIgnoredInTestNamespace.php │ ├── testTagOnStaticMethod.php │ ├── testTagOnStaticMethodIgnoredInTestClass.php │ └── testTagOnStaticMethodIgnoredInTestNamespace.php └── Unit ├── AttributeValueReaderTest.php ├── AttributeValueReaders ├── AttributeFinderTest.php └── AttributeValueReaderTest.php ├── CacheTest.php ├── SubNamespaceCheckerTest.php ├── TestClassCheckerTest.php ├── TestConfigTest.php └── data ├── Bar.php ├── Class0Friends.php ├── Class1Friend.php ├── Class2Friends.php ├── Foo.php ├── MethodAttributes.php └── NamespaceVisibilityDemo.php /.github/workflows/full-checks.yml: -------------------------------------------------------------------------------- 1 | name: "Full checks" 2 | 3 | on: 4 | schedule: 5 | - cron: '0 10 * * *' 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | full-checks: 13 | name: "Full CI checks for all supported PHP versions" 14 | 15 | runs-on: ${{ matrix.operating-system }} 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | dependencies: 21 | - "lowest" 22 | - "highest" 23 | php-version: 24 | - "8.0" 25 | - "8.1" 26 | - "8.2" 27 | - "8.3" 28 | - "8.4" 29 | operating-system: 30 | - "ubuntu-22.04" 31 | 32 | steps: 33 | - name: "Checkout" 34 | uses: "actions/checkout@v2" 35 | 36 | - name: "Install PHP" 37 | uses: "shivammathur/setup-php@v2" 38 | with: 39 | coverage: Xdebug 40 | php-version: "${{ matrix.php-version }}" 41 | tools: composer 42 | 43 | - name: Get composer cache directory 44 | id: composercache 45 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 46 | 47 | - name: Cache dependencies 48 | uses: actions/cache@v2 49 | with: 50 | path: ${{ steps.composercache.outputs.dir }} 51 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}" 52 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}" 53 | 54 | - name: "Install lowest dependencies" 55 | if: ${{ matrix.dependencies == 'lowest' }} 56 | run: "composer update --prefer-lowest --no-interaction --no-progress" 57 | 58 | - name: "Install highest dependencies" 59 | if: ${{ matrix.dependencies == 'highest' }} 60 | run: "composer update --no-interaction --no-progress" 61 | 62 | - name: "Full CI" 63 | run: "composer ci" 64 | 65 | - name: "Check dependencies" 66 | run: | 67 | cd dependency-tests 68 | ./check-phpstan-dependencies.sh 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | vendor/ 4 | Vagrantfile 5 | .phpunit.cache 6 | .php-cs-fixer.cache 7 | 8 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | exclude('tests/Rules/data') 5 | ->in(__DIR__); 6 | 7 | $config = new PhpCsFixer\Config(); 8 | return $config 9 | ->setRiskyAllowed(true) 10 | ->setRules( 11 | [ 12 | '@PSR1' => true, 13 | '@PSR2' => true, 14 | '@PSR12' => true, 15 | '@Symfony' => true, 16 | '@Symfony:risky' => true, 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'no_useless_else' => true, 19 | 'no_useless_return' => true, 20 | 'ordered_imports' => true, 21 | 'phpdoc_order' => true, 22 | 'strict_comparison' => true, 23 | 'phpdoc_align' => false, 24 | 'phpdoc_to_comment' => false, 25 | 'native_function_invocation' => false, 26 | 'nullable_type_declaration_for_default_null_value' => true, 27 | ] 28 | ) 29 | ->setFinder($finder) 30 | ; 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION 2 | FROM php:${PHP_VERSION}cli 3 | 4 | # Increase memory limit 5 | RUN echo 'memory_limit = -1' >> /usr/local/etc/php/conf.d/docker-php-memlimit.ini 6 | 7 | # install Composer 8 | COPY ./docker/composer.sh /root/ 9 | 10 | RUN < 40 | Manual installation 41 | 42 | If you don't want to use `phpstan/extension-installer`, include rules.neon in your project's PHPStan config: 43 | 44 | ``` 45 | includes: 46 | - vendor/dave-liddament/phpstan-php-language-extensions/extension.neon 47 | ``` 48 | 49 | 50 | ### Configuring 51 | 52 | Some attributes, e.g. `#[package]`, might make testing difficult. It is possible to disable the checks for test code in one of two ways: 53 | 54 | #### Exclude checks on class names ending with Test: 55 | 56 | To exclude any checks from classes that where the name ends with `Test` add the following to the parameters section of your `phpstan.neon` file: 57 | 58 | ```yaml 59 | parameters: 60 | phpLanguageExtensions: 61 | mode: className 62 | ``` 63 | 64 | 65 | #### Exclude checks based on test namespace 66 | 67 | To exclude any checks from classes that are in the test namespace (e.g. `Acme\Test`) add the following to the parameters section of your `phpstan.neon` file: 68 | 69 | ```yaml 70 | parameters: 71 | phpLanguageExtensions: 72 | mode: namespace 73 | testNamespace: 'Acme\Test' 74 | ``` 75 | 76 | ## Contributing 77 | 78 | See [Contributing](CONTRIBUTING.md). 79 | 80 | ## Demo project 81 | 82 | See [PHP language extensions PHPStan demo](https://github.com/DaveLiddament/php-language-extensions-phpstan-demo) project. 83 | -------------------------------------------------------------------------------- /build/PHPStan/Rules/CheckRuleIsInExtension.php: -------------------------------------------------------------------------------- 1 | */ 14 | final class CheckRuleIsInExtension implements Rule 15 | { 16 | /** @var list */ 17 | private array $classes; 18 | 19 | public function __construct() 20 | { 21 | $file = Neon::decodeFile(__DIR__.'/../../../extension.neon'); 22 | 23 | if (!is_array($file)) { 24 | throw new \Exception('Expecting neon file to be parseable'); 25 | } 26 | 27 | $services = $file['services'] ?? []; 28 | Assert::assertArray($services); 29 | 30 | $classes = []; 31 | foreach ($services as $service) { 32 | Assert::assertArray($service); 33 | $class = $service['class'] ?? null; 34 | if (null === $class) { 35 | continue; 36 | } 37 | 38 | Assert::assertString($class); 39 | $classes[] = $class; 40 | } 41 | 42 | $this->classes = $classes; 43 | } 44 | 45 | public function getNodeType(): string 46 | { 47 | return InClassNode::class; 48 | } 49 | 50 | public function processNode(Node $node, Scope $scope): array 51 | { 52 | $classReflection = $scope->getClassReflection(); 53 | if (null === $classReflection) { 54 | return []; 55 | } 56 | 57 | if (!$classReflection->isSubclassOf(Rule::class)) { 58 | return []; 59 | } 60 | 61 | if ($classReflection->isAbstract()) { 62 | return []; 63 | } 64 | 65 | $className = $classReflection->getName(); 66 | 67 | if (str_starts_with(haystack: $className, needle: 'DaveLiddament\PhpstanPhpLanguageExtensions\Build')) { 68 | return []; 69 | } 70 | 71 | if (in_array($className, $this->classes)) { 72 | return []; 73 | } 74 | 75 | return [ 76 | RuleErrorBuilder::message("Rule [$className] not in extension.neon.")->identifier('phpstanExtensionLibrary.misconfigured')->build(), 77 | ]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dave-liddament/phpstan-php-language-extensions", 3 | "description": "PHPStan rules to implement the language extensions provided by the php-language-extensions", 4 | "keywords": ["static analysis", "phpstan", "namespace visibility attribute", "friend attribute"], 5 | "type": "phpstan-extension", 6 | "require": { 7 | "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 8 | "phpstan/phpstan": "^1.12.15 || ^2.0", 9 | "dave-liddament/php-language-extensions": "^0.8.0 || ^0.9.0" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^9.6.12", 13 | "friendsofphp/php-cs-fixer": "^3.26.1", 14 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 15 | "dave-liddament/phpstan-rule-test-helper": "^0.5.0", 16 | "nette/neon": "^3.4" 17 | }, 18 | "license": "MIT", 19 | "autoload": { 20 | "psr-4": { 21 | "DaveLiddament\\PhpstanPhpLanguageExtensions\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "DaveLiddament\\PhpstanPhpLanguageExtensions\\Tests\\": "tests/", 27 | "DaveLiddament\\PhpstanPhpLanguageExtensions\\Build\\": "build/" 28 | }, 29 | "classmap": [ 30 | "tests/Rules/data" 31 | ] 32 | }, 33 | "authors": [ 34 | { 35 | "name": "Dave Liddament", 36 | "email": "dave@lampbristol.com" 37 | } 38 | ], 39 | "scripts": { 40 | "composer-validate": "@composer validate --no-check-all --strict", 41 | "cs-fix": "php-cs-fixer fix", 42 | "cs": [ 43 | "@putenv PHP_CS_FIXER_IGNORE_ENV=1", 44 | "php-cs-fixer fix --dry-run -v" 45 | ], 46 | "analyse": "phpstan analyse", 47 | "lint": "parallel-lint src tests", 48 | "test": "phpunit", 49 | "e2e": "phpstan analyse --configuration=e2e/phpstan-e2e.neon --error-format=json | php e2e/test-runner", 50 | "ci": [ 51 | "@composer-validate", 52 | "@lint", 53 | "@cs", 54 | "@test", 55 | "@analyse", 56 | "@e2e" 57 | ] 58 | }, 59 | "extra": { 60 | "phpstan": { 61 | "includes": [ 62 | "extension.neon" 63 | ] 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /dependency-tests/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !check-phpstan-dependencies.sh -------------------------------------------------------------------------------- /dependency-tests/check-phpstan-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Remove any existing composer files from previous run of the script 6 | rm -f composer.* || true 7 | 8 | # Create composer.json 9 | cat <<- "EOF" > composer.json 10 | { 11 | "name": "demo/test_dependencies", 12 | "repositories" : [ 13 | { 14 | "type" : "path", 15 | "url" : "../" 16 | } 17 | ] 18 | } 19 | EOF 20 | 21 | # Check PHPStan v1 is OK 22 | composer require --dev phpstan/phpstan:^1.0 23 | composer require --dev dave-liddament/phpstan-php-language-extensions @dev 24 | composer update --prefer-lowest --no-interaction 25 | 26 | # Check PHPStan v2 is OK 27 | composer require --dev phpstan/phpstan:^2.0 28 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | latest: 3 | build: 4 | args: 5 | PHP_VERSION: "" 6 | tty: true 7 | volumes: 8 | - .:/app/ 9 | php80: 10 | build: 11 | args: 12 | PHP_VERSION: "8.0-" 13 | extends: 14 | service: latest 15 | php81: 16 | build: 17 | args: 18 | PHP_VERSION: "8.1-" 19 | extends: 20 | service: latest 21 | php82: 22 | build: 23 | args: 24 | PHP_VERSION: "8.2-" 25 | extends: 26 | service: latest 27 | php83: 28 | build: 29 | args: 30 | PHP_VERSION: "8.3-" 31 | extends: 32 | service: latest 33 | php84: 34 | build: 35 | args: 36 | PHP_VERSION: "8.4-rc-" 37 | XDEBUG_ENABLED: 0 38 | extends: 39 | service: latest 40 | -------------------------------------------------------------------------------- /docker/composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # copied from https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md 4 | 5 | EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')" 6 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 7 | ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" 8 | 9 | if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ] 10 | then 11 | >&2 echo 'ERROR: Invalid installer checksum' 12 | rm composer-setup.php 13 | exit 1 14 | fi 15 | 16 | php composer-setup.php --quiet 17 | RESULT=$? 18 | rm composer-setup.php 19 | exit $RESULT -------------------------------------------------------------------------------- /e2e/data/BaseTraitClass.php: -------------------------------------------------------------------------------- 1 | aMethod(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /e2e/data/InjectableBad.php: -------------------------------------------------------------------------------- 1 | getResult(); 18 | $this->getResult(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /e2e/data/MyTrait.php: -------------------------------------------------------------------------------- 1 | :: 10 | * 11 | * Where: 12 | * 13 | * `file` is relative to the `build/e2e/data` directory. 14 | * `line` is the line number in the file. 15 | * `identifier` is the rule's identifier (NOTE: the prefix `phpExtensionLibrary.` is automatically added) 16 | */ 17 | $expectedErrors = [ 18 | 'FriendProblems:9:friend', 19 | 'FriendProblems:10:friend', 20 | 'FriendProblems:11:friend', 21 | 'TraitClassProblems:7:restrictTraitTo', 22 | 'MustUse:18:mustUseResult', 23 | 'InjectableBad:7:injectableVersion', 24 | ]; 25 | 26 | /** 27 | * Main script. 28 | */ 29 | require_once __DIR__.'/PHPStanResultsChecker.php'; 30 | $phpStanResultsChecker = new PHPStanResultsChecker(); 31 | $stdIn = file_get_contents('php://stdin'); 32 | 33 | if (false === $stdIn) { 34 | echo "No input\n"; 35 | exit(2); 36 | } 37 | 38 | try { 39 | $phpStanResultsChecker->checkResults($stdIn, $expectedErrors); 40 | echo "E2E tests OK\n"; 41 | exit(0); 42 | } catch (Exception $e) { 43 | echo $e->getMessage().\PHP_EOL; 44 | exit(1); 45 | } 46 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | phpLanguageExtensions: 4 | mode: 'none' 5 | testNamespace: null 6 | 7 | services: 8 | - 9 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Config\TestConfig 10 | arguments: 11 | mode: %phpLanguageExtensions.mode% 12 | testNamespace: %phpLanguageExtensions.testNamespace% 13 | 14 | - 15 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\FriendMethodCallRule 16 | tags: 17 | - phpstan.rules.rule 18 | 19 | - 20 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\FriendNewCallRule 21 | tags: 22 | - phpstan.rules.rule 23 | 24 | - 25 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\FriendStaticCallRule 26 | tags: 27 | - phpstan.rules.rule 28 | 29 | - 30 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\PackageMethodCallRule 31 | tags: 32 | - phpstan.rules.rule 33 | 34 | - 35 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\PackageNewCallRule 36 | tags: 37 | - phpstan.rules.rule 38 | 39 | - 40 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\PackageStaticCallRule 41 | tags: 42 | - phpstan.rules.rule 43 | 44 | - 45 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\TestTagMethodCallRule 46 | tags: 47 | - phpstan.rules.rule 48 | 49 | - 50 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\TestTagNewCallRule 51 | tags: 52 | - phpstan.rules.rule 53 | 54 | - 55 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\TestTagStaticCallRule 56 | tags: 57 | - phpstan.rules.rule 58 | 59 | - 60 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\InjectableVersionRule 61 | tags: 62 | - phpstan.rules.rule 63 | 64 | - 65 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\NamespaceVisibilityMethodCallRule 66 | tags: 67 | - phpstan.rules.rule 68 | 69 | - 70 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\NamespaceVisibilityNewCallRule 71 | tags: 72 | - phpstan.rules.rule 73 | 74 | - 75 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\NamespaceVisibilityStaticCallRule 76 | tags: 77 | - phpstan.rules.rule 78 | 79 | - 80 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\OverrideRule 81 | tags: 82 | - phpstan.rules.rule 83 | 84 | - 85 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\MustUseResultRule 86 | tags: 87 | - phpstan.rules.rule 88 | 89 | - 90 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\RestrictTraitToRule 91 | tags: 92 | - phpstan.rules.rule 93 | 94 | 95 | 96 | parametersSchema: 97 | phpLanguageExtensions: structure([ 98 | testNamespace: schema(string(), nullable()) 99 | mode: schema(anyOf('none', 'className', 'namespace')) 100 | ]) 101 | 102 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | - tests 6 | - build 7 | - e2e 8 | excludePaths: 9 | - tests/Rules/data 10 | - e2e/data 11 | 12 | services: 13 | 14 | - 15 | class: DaveLiddament\PhpstanPhpLanguageExtensions\Build\PHPStan\Rules\CheckRuleIsInExtension 16 | tags: 17 | - phpstan.rules.rule -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 23 | 24 | src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AttributeValueReaders/AttributeFinder.php: -------------------------------------------------------------------------------- 1 | |\ReflectionEnum $class 13 | * @param class-string $attributeName 14 | * 15 | * @return \ReflectionAttribute|null 16 | */ 17 | public static function getAttributeOnClass( 18 | \ReflectionClass|\ReflectionEnum $class, 19 | string $attributeName, 20 | ): ?\ReflectionAttribute { 21 | $attributes = $class->getAttributes($attributeName); 22 | if (1 !== count($attributes)) { 23 | return null; 24 | } 25 | 26 | return $attributes[0]; 27 | } 28 | 29 | /** 30 | * @template T of object 31 | * 32 | * @param class-string $attributeName 33 | * @param \ReflectionClass|\ReflectionEnum $class 34 | * 35 | * @return \ReflectionAttribute|null 36 | */ 37 | public static function getAttributeOnMethod( 38 | \ReflectionClass|\ReflectionEnum $class, 39 | string $methodName, 40 | string $attributeName, 41 | ): ?\ReflectionAttribute { 42 | if (!$class->hasMethod($methodName)) { 43 | return null; 44 | } 45 | 46 | $method = $class->getMethod($methodName); 47 | 48 | $attributes = $method->getAttributes($attributeName); 49 | if (1 !== count($attributes)) { 50 | return null; 51 | } 52 | 53 | return $attributes[0]; 54 | } 55 | 56 | /** 57 | * @param class-string $attributeName 58 | * @param \ReflectionClass|\ReflectionEnum $class 59 | */ 60 | public static function hasAttributeOnClass( 61 | \ReflectionClass|\ReflectionEnum $class, 62 | string $attributeName, 63 | ): bool { 64 | $attributes = $class->getAttributes($attributeName); 65 | 66 | return 1 === count($attributes); 67 | } 68 | 69 | /** 70 | * @param class-string $attributeName 71 | * @param \ReflectionClass|\ReflectionEnum $class 72 | */ 73 | public static function hasAttributeOnMethod( 74 | \ReflectionClass|\ReflectionEnum $class, 75 | string $methodName, 76 | string $attributeName, 77 | ): bool { 78 | if (!$class->hasMethod($methodName)) { 79 | return false; 80 | } 81 | 82 | $method = $class->getMethod($methodName); 83 | 84 | $attributes = $method->getAttributes($attributeName); 85 | 86 | return 1 === count($attributes); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/AttributeValueReaders/AttributeValueReader.php: -------------------------------------------------------------------------------- 1 | $attribute 11 | * 12 | * @return list 13 | */ 14 | public static function getStrings(\ReflectionAttribute $attribute): array 15 | { 16 | $values = []; 17 | foreach ($attribute->getArguments() as $value) { 18 | if (is_string($value)) { 19 | $values[] = $value; 20 | } 21 | } 22 | 23 | return $values; 24 | } 25 | 26 | /** 27 | * @param \ReflectionAttribute $attribute 28 | */ 29 | public static function getString(\ReflectionAttribute $attribute, int $position, string $parameterName): ?string 30 | { 31 | $arguments = $attribute->getArguments(); 32 | 33 | if (array_key_exists($position, $arguments)) { 34 | $value = $arguments[$position]; 35 | } elseif (array_key_exists($parameterName, $arguments)) { 36 | $value = $arguments[$parameterName]; 37 | } else { 38 | return null; 39 | } 40 | 41 | if (!is_string($value)) { 42 | return null; 43 | } 44 | 45 | return $value; 46 | } 47 | 48 | /** 49 | * @param \ReflectionAttribute $attribute 50 | */ 51 | public static function getBool(\ReflectionAttribute $attribute, int $position, string $parameterName): ?bool 52 | { 53 | $arguments = $attribute->getArguments(); 54 | 55 | if (array_key_exists($position, $arguments)) { 56 | $value = $arguments[$position]; 57 | } elseif (array_key_exists($parameterName, $arguments)) { 58 | $value = $arguments[$parameterName]; 59 | } else { 60 | return null; 61 | } 62 | 63 | if (!is_bool($value)) { 64 | return null; 65 | } 66 | 67 | return $value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Config/TestConfig.php: -------------------------------------------------------------------------------- 1 | mode = $mode; 43 | $this->testNamespace = $testNamespace; 44 | } 45 | 46 | public function getMode(): string 47 | { 48 | return $this->mode; 49 | } 50 | 51 | public function getTestNamespace(): string 52 | { 53 | if (null === $this->testNamespace) { 54 | throw new \LogicException('Attempting to get testNamespace when it is not set.'); 55 | } 56 | 57 | return $this->testNamespace; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Helpers/Assert.php: -------------------------------------------------------------------------------- 1 | $value */ 8 | public static function assertArray(mixed $value): void 9 | { 10 | if (!is_array($value)) { 11 | throw new \InvalidArgumentException('Expecting value to be an array'); 12 | } 13 | } 14 | 15 | /** @phpstan-assert string $value */ 16 | public static function assertString(mixed $value): void 17 | { 18 | if (!is_string($value)) { 19 | throw new \InvalidArgumentException('Expecting value to be a string'); 20 | } 21 | } 22 | 23 | /** @phpstan-assert int $value */ 24 | public static function assertInt(mixed $value): void 25 | { 26 | if (!is_int($value)) { 27 | throw new \InvalidArgumentException('Expecting value to be an int'); 28 | } 29 | } 30 | 31 | /** 32 | * @phpstan-assert array $values 33 | */ 34 | public static function arrayOfStrings(mixed $values): void 35 | { 36 | self::assertArray($values); 37 | foreach ($values as $key => $value) { 38 | self::assertInt($key); 39 | self::assertString($value); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Helpers/AttributeValueReader.php: -------------------------------------------------------------------------------- 1 | $reflectionClass 11 | * @param class-string $attributeClassName 12 | * 13 | * @return array 14 | */ 15 | public static function getAttributeValuesForMethod( 16 | \ReflectionClass $reflectionClass, 17 | string $methodName, 18 | string $attributeClassName, 19 | ): array { 20 | if (!$reflectionClass->hasMethod($methodName)) { 21 | return []; 22 | } 23 | $reflectionMethod = $reflectionClass->getMethod($methodName); 24 | 25 | foreach ($reflectionMethod->getAttributes($attributeClassName) as $attribute) { 26 | $arguments = $attribute->getArguments(); 27 | Assert::arrayOfStrings($arguments); 28 | 29 | return $arguments; 30 | } 31 | 32 | return []; 33 | } 34 | 35 | /** 36 | * @param \ReflectionClass $reflectionClass 37 | * @param class-string $attributeClassName 38 | * 39 | * @return array 40 | */ 41 | public static function getAttributeValuesForClass( 42 | \ReflectionClass $reflectionClass, 43 | string $attributeClassName, 44 | ): array { 45 | foreach ($reflectionClass->getAttributes($attributeClassName) as $attribute) { 46 | $arguments = $attribute->getArguments(); 47 | Assert::arrayOfStrings($arguments); 48 | 49 | return $arguments; 50 | } 51 | 52 | return []; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Helpers/Cache.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $cache = []; 14 | 15 | public function hasEntry(string $key): bool 16 | { 17 | return array_key_exists($key, $this->cache); 18 | } 19 | 20 | /** @return TValue */ 21 | public function getEntry(string $key) 22 | { 23 | if (!array_key_exists($key, $this->cache)) { 24 | throw new \LogicException('Call hasEntry first'); 25 | } 26 | 27 | return $this->cache[$key]; 28 | } 29 | 30 | /** @param TValue $entry */ 31 | public function addEntry(string $key, $entry): void 32 | { 33 | $this->cache[$key] = $entry; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Helpers/SubNamespaceChecker.php: -------------------------------------------------------------------------------- 1 | testConfig->getMode()) { 19 | case TestConfig::NONE: 20 | return false; 21 | 22 | case TestConfig::NAMESPACE: 23 | return $this->checkNamespace($namespace); 24 | 25 | case TestConfig::CLASS_NAME: 26 | return $this->checkName($className); 27 | 28 | default: 29 | throw new \LogicException('Unknown test config type.'); 30 | } 31 | } 32 | 33 | private function checkNamespace(?string $namespace): bool 34 | { 35 | if (null === $namespace) { 36 | return false; 37 | } 38 | 39 | return str_starts_with( 40 | haystack: $namespace, 41 | needle: $this->testConfig->getTestNamespace(), 42 | ); 43 | } 44 | 45 | private function checkName(?string $className): bool 46 | { 47 | if (null === $className) { 48 | return false; 49 | } 50 | 51 | return str_ends_with( 52 | haystack: $className, 53 | needle: 'Test' 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Rules/AbstractFriendRule.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | abstract class AbstractFriendRule implements Rule 24 | { 25 | /** 26 | * @var Cache> 27 | */ 28 | private Cache $cache; 29 | private TestClassChecker $testClassChecker; 30 | 31 | final public function __construct( 32 | private ReflectionProvider $reflectionProvider, 33 | private TestConfig $testConfig, 34 | ) { 35 | $this->cache = new Cache(); 36 | $this->testClassChecker = new TestClassChecker($this->testConfig); 37 | } 38 | 39 | protected function getErrorOrNull( 40 | Scope $scope, 41 | string $class, 42 | string $methodName, 43 | ): ?IdentifierRuleError { 44 | $callingClass = $scope->getClassReflection()?->getName(); 45 | $classReflection = $this->reflectionProvider->getClass($class); 46 | $className = $classReflection->getName(); 47 | $fullMethodName = "{$className}::{$methodName}"; 48 | 49 | if ($this->cache->hasEntry($fullMethodName)) { 50 | $allowedCallingClassesFromMethod = $this->cache->getEntry($fullMethodName); 51 | } else { 52 | $allowedCallingClassesFromMethod = AttributeValueReader::getAttributeValuesForMethod( 53 | $classReflection->getNativeReflection(), 54 | $methodName, 55 | Friend::class 56 | ); 57 | $this->cache->addEntry($fullMethodName, $allowedCallingClassesFromMethod); 58 | } 59 | 60 | if ($this->cache->hasEntry($className)) { 61 | $allowedCallingClassesFromClass = $this->cache->getEntry($className); 62 | } else { 63 | $allowedCallingClassesFromClass = AttributeValueReader::getAttributeValuesForClass( 64 | $classReflection->getNativeReflection(), 65 | Friend::class 66 | ); 67 | $this->cache->addEntry($className, $allowedCallingClassesFromClass); 68 | } 69 | 70 | $allowedCallingClasses = array_merge($allowedCallingClassesFromClass, $allowedCallingClassesFromMethod); 71 | 72 | if ([] === $allowedCallingClasses) { 73 | return null; 74 | } 75 | 76 | if ($callingClass === $className) { 77 | return null; 78 | } 79 | 80 | if (in_array($callingClass, $allowedCallingClasses, true)) { 81 | return null; 82 | } 83 | 84 | if ($this->testClassChecker->isTestClass($scope->getNamespace(), $scope->getClassReflection()?->getName())) { 85 | return null; 86 | } 87 | 88 | $message = sprintf( 89 | '%s cannot be called from %s, it can only be called from its friend(s): %s', 90 | $fullMethodName, 91 | $callingClass ?? '', 92 | implode('|', $allowedCallingClasses) 93 | ); 94 | 95 | return RuleErrorBuilder::message($message)->identifier('phpExtensionLibrary.friend')->build(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Rules/AbstractPackageRule.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | abstract class AbstractPackageRule implements Rule 23 | { 24 | /** 25 | * @var Cache 26 | */ 27 | private Cache $cache; 28 | private TestClassChecker $testClassChecker; 29 | 30 | final public function __construct( 31 | private ReflectionProvider $reflectionProvider, 32 | TestConfig $testConfig, 33 | ) { 34 | $this->cache = new Cache(); 35 | $this->testClassChecker = new TestClassChecker($testConfig); 36 | } 37 | 38 | protected function getErrorOrNull( 39 | Scope $scope, 40 | string $class, 41 | string $methodName, 42 | ): ?IdentifierRuleError { 43 | $classReflection = $this->reflectionProvider->getClass($class); 44 | $className = $classReflection->getName(); 45 | $nativeReflection = $classReflection->getNativeReflection(); 46 | 47 | $fullMethodName = "{$className}::{$methodName}"; 48 | 49 | if ($this->cache->hasEntry($fullMethodName)) { 50 | $isMethodPackageLevel = $this->cache->getEntry($fullMethodName); 51 | } else { 52 | if ($nativeReflection->hasMethod($methodName)) { 53 | $methodReflection = $nativeReflection->getMethod($methodName); 54 | $isMethodPackageLevel = count($methodReflection->getAttributes(Package::class)) > 0; 55 | } else { 56 | $isMethodPackageLevel = false; 57 | } 58 | $this->cache->addEntry($fullMethodName, $isMethodPackageLevel); 59 | } 60 | 61 | if ($this->cache->hasEntry($className)) { 62 | $isClassPackageLevel = $this->cache->getEntry($className); 63 | } else { 64 | $isClassPackageLevel = count($nativeReflection->getAttributes(Package::class)) > 0; 65 | $this->cache->addEntry($className, $isClassPackageLevel); 66 | } 67 | 68 | $isPackageLevel = $isClassPackageLevel || $isMethodPackageLevel; 69 | 70 | if (!$isPackageLevel) { 71 | return null; 72 | } 73 | 74 | // Check namespaces match 75 | if ($scope->getNamespace() === $nativeReflection->getNamespaceName()) { 76 | return null; 77 | } 78 | 79 | if ($this->testClassChecker->isTestClass($scope->getNamespace(), $scope->getClassReflection()?->getName())) { 80 | return null; 81 | } 82 | 83 | $message = sprintf( 84 | '%s has package visibility and cannot be called from namespace %s', 85 | $fullMethodName, 86 | $scope->getNamespace() ?? '' 87 | ); 88 | 89 | return RuleErrorBuilder::message($message)->identifier('phpExtensionLibrary.package')->build(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Rules/AbstractTestTagRule.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | abstract class AbstractTestTagRule implements Rule 23 | { 24 | /** 25 | * @var Cache 26 | */ 27 | private Cache $cache; 28 | private TestClassChecker $testClassChecker; 29 | 30 | final public function __construct( 31 | private ReflectionProvider $reflectionProvider, 32 | TestConfig $testConfig, 33 | ) { 34 | $this->cache = new Cache(); 35 | $this->testClassChecker = new TestClassChecker($testConfig); 36 | } 37 | 38 | protected function getErrorOrNull( 39 | Scope $scope, 40 | string $class, 41 | string $methodName, 42 | ): ?IdentifierRuleError { 43 | $callingClass = $scope->getClassReflection()?->getName(); 44 | 45 | $classReflection = $this->reflectionProvider->getClass($class); 46 | $className = $classReflection->getName(); 47 | $nativeReflection = $classReflection->getNativeReflection(); 48 | 49 | $fullMethodName = "{$className}::{$methodName}"; 50 | 51 | if ($this->cache->hasEntry($className)) { 52 | $isTestTagOnClass = $this->cache->getEntry($className); 53 | } else { 54 | $isTestTagOnClass = count($nativeReflection->getAttributes(TestTag::class)) > 0; 55 | $this->cache->addEntry($className, $isTestTagOnClass); 56 | } 57 | 58 | if ($this->cache->hasEntry($fullMethodName)) { 59 | $isTestTagOnMethod = $this->cache->getEntry($fullMethodName); 60 | } else { 61 | if ($nativeReflection->hasMethod($methodName)) { 62 | $methodReflection = $nativeReflection->getMethod($methodName); 63 | $isTestTagOnMethod = count($methodReflection->getAttributes(TestTag::class)) > 0; 64 | } else { 65 | $isTestTagOnMethod = false; 66 | } 67 | $this->cache->addEntry($fullMethodName, $isTestTagOnMethod); 68 | } 69 | 70 | $hasTestTag = $isTestTagOnClass || $isTestTagOnMethod; 71 | 72 | if (!$hasTestTag) { 73 | return null; 74 | } 75 | 76 | if ($isTestTagOnClass && ($className === $callingClass)) { 77 | return null; 78 | } 79 | 80 | if ($this->testClassChecker->isTestClass($scope->getNamespace(), $scope->getClassReflection()?->getName())) { 81 | return null; 82 | } 83 | 84 | $message = sprintf( 85 | '%s is a test tag and can only be called from test code', 86 | $fullMethodName, 87 | ); 88 | 89 | return RuleErrorBuilder::message($message)->identifier('phpExtensionLibrary.testTag')->build(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Rules/FriendMethodCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendMethodCallRule extends AbstractFriendRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return MethodCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | $type = $scope->getType($node->var); 27 | 28 | foreach ($type->getReferencedClasses() as $class) { 29 | $error = $this->getErrorOrNull($scope, $class, $methodName); 30 | if (null !== $error) { 31 | return [$error]; 32 | } 33 | } 34 | 35 | return []; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rules/FriendNewCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendNewCallRule extends AbstractFriendRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return New_::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->class instanceof Node\Name) { 22 | return []; 23 | } 24 | 25 | $className = $scope->resolveName($node->class); 26 | $error = $this->getErrorOrNull($scope, $className, '__construct'); 27 | 28 | return (null === $error) ? [] : [$error]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Rules/FriendStaticCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendStaticCallRule extends AbstractFriendRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return StaticCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | $methodName = $node->name->name; 25 | 26 | if (!$node->class instanceof Node\Name) { 27 | return []; 28 | } 29 | $className = $scope->resolveName($node->class); 30 | 31 | $error = $this->getErrorOrNull($scope, $className, $methodName); 32 | 33 | return (null === $error) ? [] : [$error]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Rules/MustUseResultRule.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class MustUseResultRule implements Rule 18 | { 19 | /** @var Cache */ 20 | private Cache $cache; 21 | 22 | public function __construct( 23 | private ReflectionProvider $reflectionProvider, 24 | ) { 25 | $this->cache = new Cache(); 26 | } 27 | 28 | public function getNodeType(): string 29 | { 30 | return Node\Stmt\Expression::class; 31 | } 32 | 33 | /** @param Node\Stmt\Expression $node */ 34 | public function processNode(Node $node, Scope $scope): array 35 | { 36 | $expr = $node->expr; 37 | 38 | if ($expr instanceof Node\Expr\MethodCall) { 39 | $classReflections = $scope->getType($expr->var)->getObjectClassReflections(); 40 | } elseif ($expr instanceof Node\Expr\StaticCall) { 41 | $class = $expr->class; 42 | if (!$class instanceof Node\Name) { 43 | return []; 44 | } 45 | 46 | $className = $scope->resolveName($class); 47 | 48 | $classReflections = [ 49 | $this->reflectionProvider->getClass($className), 50 | ]; 51 | } else { 52 | return []; 53 | } 54 | 55 | $methodNameNode = $expr->name; 56 | if (!$methodNameNode instanceof Node\Identifier) { 57 | return []; 58 | } 59 | 60 | $methodName = $methodNameNode->toLowerString(); 61 | 62 | foreach ($classReflections as $classReflection) { 63 | $className = $classReflection->getName(); 64 | $fullMethodName = "{$className}::{$methodName}"; 65 | 66 | if ($this->cache->hasEntry($fullMethodName)) { 67 | $mustUseResult = $this->cache->getEntry($fullMethodName); 68 | } else { 69 | $mustUseResult = AttributeFinder::hasAttributeOnMethod( 70 | $classReflection->getNativeReflection(), 71 | $methodName, 72 | MustUseResult::class, 73 | ); 74 | $this->cache->addEntry($fullMethodName, $mustUseResult); 75 | } 76 | 77 | if ($mustUseResult) { 78 | return [ 79 | RuleErrorBuilder::message('Result returned by method must be used') 80 | ->identifier('phpExtensionLibrary.mustUseResult') 81 | ->build(), 82 | ]; 83 | } 84 | } 85 | 86 | return []; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Rules/NamespaceVisibilityMethodCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityMethodCallRule extends AbstractNamespaceVisibilityRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return MethodCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | $type = $scope->getType($node->var); 27 | 28 | foreach ($type->getReferencedClasses() as $class) { 29 | $error = $this->getErrorOrNull($scope, $class, $methodName); 30 | if (null !== $error) { 31 | return [$error]; 32 | } 33 | } 34 | 35 | return []; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rules/NamespaceVisibilityNewCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityNewCallRule extends AbstractNamespaceVisibilityRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return New_::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->class instanceof Node\Name) { 22 | return []; 23 | } 24 | 25 | $className = $scope->resolveName($node->class); 26 | $error = $this->getErrorOrNull($scope, $className, '__construct'); 27 | 28 | return (null === $error) ? [] : [$error]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Rules/NamespaceVisibilitySetting.php: -------------------------------------------------------------------------------- 1 | hasNamespace; 32 | } 33 | 34 | public function getNamespace(): ?string 35 | { 36 | return $this->namespace; 37 | } 38 | 39 | public function isExcludeSubNamespaces(): bool 40 | { 41 | if (null === $this->excludeSubNamespace) { 42 | throw new \LogicException('Only call isExcludeSubNamespace if hasNamespaceAttribute returns true'); 43 | } 44 | 45 | return $this->excludeSubNamespace; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Rules/NamespaceVisibilityStaticCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityStaticCallRule extends AbstractNamespaceVisibilityRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return StaticCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | 27 | if (!$node->class instanceof Node\Name) { 28 | return []; 29 | } 30 | $className = $scope->resolveName($node->class); 31 | 32 | $error = $this->getErrorOrNull($scope, $className, $methodName); 33 | 34 | return (null === $error) ? [] : [$error]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Rules/OverrideRule.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class OverrideRule implements Rule 20 | { 21 | public function getNodeType(): string 22 | { 23 | return InClassNode::class; 24 | } 25 | 26 | /** @param InClassNode $node */ 27 | public function processNode(Node $node, Scope $scope): array 28 | { 29 | $methods = $node->getOriginalNode()->getMethods(); 30 | 31 | $classReflection = $node->getClassReflection(); 32 | 33 | $errors = []; 34 | foreach ($methods as $method) { 35 | $methodName = $method->name->toLowerString(); 36 | 37 | if (!AttributeFinder::hasAttributeOnMethod( 38 | $classReflection->getNativeReflection(), 39 | $methodName, 40 | Override::class, 41 | )) { 42 | continue; 43 | } 44 | 45 | if ($this->isMethodInAncestor($classReflection, $methodName, $scope)) { 46 | continue; 47 | } 48 | 49 | $message = "Method {$methodName} has the Override attribute, but no matching parent method exists"; 50 | $errors[] = RuleErrorBuilder::message($message)->identifier('phpExtensionLibrary.override')->line($method->getLine())->build(); 51 | } 52 | 53 | return $errors; 54 | } 55 | 56 | private function isMethodInAncestor(ClassReflection $classReflection, string $methodName, Scope $scope): bool 57 | { 58 | foreach ($classReflection->getAncestors() as $ancestor) { 59 | if ($ancestor === $classReflection) { 60 | continue; 61 | } 62 | 63 | if ($ancestor->hasMethod($methodName)) { 64 | $method = $ancestor->getMethod($methodName, $scope); 65 | 66 | if ($method->isPrivate()) { 67 | continue; 68 | } 69 | 70 | return true; 71 | } 72 | } 73 | 74 | return false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Rules/PackageMethodCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageMethodCallRule extends AbstractPackageRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return MethodCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | $type = $scope->getType($node->var); 27 | 28 | foreach ($type->getReferencedClasses() as $class) { 29 | $error = $this->getErrorOrNull($scope, $class, $methodName); 30 | if (null !== $error) { 31 | return [$error]; 32 | } 33 | } 34 | 35 | return []; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rules/PackageNewCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageNewCallRule extends AbstractPackageRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return New_::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->class instanceof Node\Name) { 22 | return []; 23 | } 24 | 25 | $className = $scope->resolveName($node->class); 26 | $error = $this->getErrorOrNull($scope, $className, '__construct'); 27 | 28 | return (null === $error) ? [] : [$error]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Rules/PackageStaticCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageStaticCallRule extends AbstractPackageRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return StaticCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | 27 | if (!$node->class instanceof Node\Name) { 28 | return []; 29 | } 30 | $className = $scope->resolveName($node->class); 31 | 32 | $error = $this->getErrorOrNull($scope, $className, $methodName); 33 | 34 | return (null === $error) ? [] : [$error]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Rules/RestrictTraitToRule.php: -------------------------------------------------------------------------------- 1 | */ 17 | final class RestrictTraitToRule implements Rule 18 | { 19 | /** 20 | * @var Cache 21 | */ 22 | private Cache $cache; 23 | 24 | public function __construct( 25 | private ReflectionProvider $reflectionProvider, 26 | ) { 27 | $this->cache = new Cache(); 28 | } 29 | 30 | public function getNodeType(): string 31 | { 32 | return Node\Stmt\TraitUse::class; 33 | } 34 | 35 | public function processNode(Node $node, Scope $scope): array 36 | { 37 | $containingClassName = $scope->getClassReflection()?->getName(); 38 | 39 | if (null === $containingClassName) { 40 | return []; 41 | } 42 | 43 | $containingClassObjectType = new ObjectType($containingClassName); 44 | 45 | foreach ($node->traits as $trait) { 46 | $classReflection = $this->reflectionProvider->getClass($trait->toCodeString())->getNativeReflection(); 47 | 48 | if ($this->cache->hasEntry($classReflection)) { 49 | $restrictTraitToClassName = $this->cache->getEntry($classReflection); 50 | } else { 51 | $restrictTraitTo = AttributeFinder::getAttributeOnClass($classReflection, RestrictTraitTo::class); 52 | 53 | if (null === $restrictTraitTo) { 54 | $restrictTraitToClassName = null; 55 | } else { 56 | $restrictTraitToClassName = AttributeValueReader::getString($restrictTraitTo, 0, 'className'); 57 | } 58 | 59 | $this->cache->addEntry($classReflection, $restrictTraitToClassName); 60 | } 61 | 62 | if (null === $restrictTraitToClassName) { 63 | continue; 64 | } 65 | 66 | $restrictTraitToObjectType = new ObjectType($restrictTraitToClassName); 67 | 68 | if (!$restrictTraitToObjectType->isSuperTypeOf($containingClassObjectType)->yes()) { 69 | return [ 70 | RuleErrorBuilder::message("Trait can only be used on class or child of: $restrictTraitToClassName") 71 | ->identifier('phpExtensionLibrary.restrictTraitTo') 72 | ->build(), 73 | ]; 74 | } 75 | } 76 | 77 | return []; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Rules/TestTagMethodCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class TestTagMethodCallRule extends AbstractTestTagRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return MethodCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | $type = $scope->getType($node->var); 27 | 28 | foreach ($type->getReferencedClasses() as $class) { 29 | $error = $this->getErrorOrNull($scope, $class, $methodName); 30 | if (null !== $error) { 31 | return [$error]; 32 | } 33 | } 34 | 35 | return []; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rules/TestTagNewCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class TestTagNewCallRule extends AbstractTestTagRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return New_::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->class instanceof Node\Name) { 22 | return []; 23 | } 24 | 25 | $className = $scope->resolveName($node->class); 26 | $error = $this->getErrorOrNull($scope, $className, '__construct'); 27 | 28 | return (null === $error) ? [] : [$error]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Rules/TestTagStaticCallRule.php: -------------------------------------------------------------------------------- 1 | */ 12 | class TestTagStaticCallRule extends AbstractTestTagRule 13 | { 14 | public function getNodeType(): string 15 | { 16 | return StaticCall::class; 17 | } 18 | 19 | public function processNode(Node $node, Scope $scope): array 20 | { 21 | if (!$node->name instanceof Node\Identifier) { 22 | return []; 23 | } 24 | 25 | $methodName = $node->name->name; 26 | 27 | if (!$node->class instanceof Node\Name) { 28 | return []; 29 | } 30 | $className = $scope->resolveName($node->class); 31 | 32 | $error = $this->getErrorOrNull($scope, $className, $methodName); 33 | 34 | return (null === $error) ? [] : [$error]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Rules/AbstractFriendRuleTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class AbstractFriendRuleTest extends RuleTestCase 16 | { 17 | /** @param array $errors */ 18 | final protected function assertErrorsReported(string $file, array $errors): void 19 | { 20 | $expectedIssues = []; 21 | foreach ($errors as list($lineNumber, $targetMethodName, $callingClass, $friends)) { 22 | $expectedIssues[] = [ 23 | "$targetMethodName cannot be called from {$callingClass}, it can only be called from its friend(s): {$friends}", 24 | $lineNumber, 25 | ]; 26 | } 27 | 28 | $this->analyse([$file], $expectedIssues); 29 | } 30 | 31 | final protected function assertNoErrorsReported(string $file): void 32 | { 33 | $this->analyse([$file], []); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/AbstractInjectableVersionRuleTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class AbstractInjectableVersionRuleTest extends RuleTestCase 16 | { 17 | /** @param array $errors */ 18 | final protected function assertErrorsReported(string $file, array $errors): void 19 | { 20 | $expectedIssues = []; 21 | foreach ($errors as list($lineNumber, $argument, $injectedVersion, $injectableVersion)) { 22 | $expectedIssues[] = [ 23 | "Argument {$argument} has {$injectedVersion} injected, instead use $injectableVersion", 24 | $lineNumber, 25 | ]; 26 | } 27 | 28 | $this->analyse([$file], $expectedIssues); 29 | } 30 | 31 | final protected function assertNoErrorsReported(string $file): void 32 | { 33 | $this->analyse([$file], []); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/AbstractNamespaceVisibilityRuleTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class AbstractNamespaceVisibilityRuleTest extends RuleTestCase 16 | { 17 | /** @param array $errors */ 18 | final protected function assertErrorsReported(string $file, array $errors): void 19 | { 20 | $expectedIssues = []; 21 | foreach ($errors as list($lineNumber, $targetMethodName, $namespace, $subNamespacesAllowed)) { 22 | $issue = "$targetMethodName has Namespace Visibility, it can only be called from namespace $namespace"; 23 | if ($subNamespacesAllowed) { 24 | $issue .= " and sub-namespaces of $namespace"; 25 | } 26 | $expectedIssues[] = [ 27 | $issue, 28 | $lineNumber, 29 | ]; 30 | } 31 | 32 | $this->analyse([$file], $expectedIssues); 33 | } 34 | 35 | final protected function assertNoErrorsReported(string $file): void 36 | { 37 | $this->analyse([$file], []); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Rules/AbstractPackageRuleTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class AbstractPackageRuleTest extends RuleTestCase 16 | { 17 | /** @param array $errors */ 18 | final protected function assertErrorsReported(string $file, array $errors): void 19 | { 20 | $expectedIssues = []; 21 | foreach ($errors as list($lineNumber, $targetMethodName, $namespace)) { 22 | $expectedIssues[] = [ 23 | "$targetMethodName has package visibility and cannot be called from namespace {$namespace}", 24 | $lineNumber, 25 | ]; 26 | } 27 | 28 | $this->analyse([$file], $expectedIssues); 29 | } 30 | 31 | final protected function assertNoErrorsReported(string $file): void 32 | { 33 | $this->analyse([$file], []); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/FriendOnClassTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendOnClassTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnClass.php', 26 | [ 27 | [ 28 | 27, 29 | 'FriendOnClass\Person::updateName', 30 | 'FriendOnClass\Updater', 31 | 'FriendOnClass\FriendUpdater', 32 | ], 33 | [ 34 | 32, 35 | 'FriendOnClass\Person::updateName', 36 | '', 37 | 'FriendOnClass\FriendUpdater', 38 | ], 39 | ], 40 | ); 41 | } 42 | 43 | public function testMultipleFriends(): void 44 | { 45 | $this->assertNoErrorsReported(__DIR__.'/data/friend/multipleFriends.php'); 46 | } 47 | 48 | public function testDifferentNamespace(): void 49 | { 50 | $this->assertNoErrorsReported(__DIR__.'/data/friend/friendOnDifferentNamespace.php'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Rules/FriendOnConstructorTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendOnConstructorTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendNewCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testStaticCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnConstructor.php', 26 | [ 27 | [ 28 | 29, 29 | 'FriendOnConstructor\Person::__construct', 30 | 'FriendOnConstructor\Exam', 31 | 'FriendOnConstructor\PersonBuilder', 32 | ], 33 | [ 34 | 33, 35 | 'FriendOnConstructor\Person::__construct', 36 | '', 37 | 'FriendOnConstructor\PersonBuilder', 38 | ], 39 | ] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Rules/FriendOnInterfaceClassTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendOnInterfaceClassTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnInterfaceClass.php', 26 | [ 27 | [ 28 | 38, 29 | 'FriendOnInterfaceClass\MessageSender::sendMessage', 30 | 'FriendOnInterfaceClass\Foo', 31 | 'FriendOnInterfaceClass\MessageSendingService', 32 | ], 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Rules/FriendOnInterfaceMethodTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendOnInterfaceMethodTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnInterfaceMethod.php', 26 | [ 27 | [ 28 | 38, 29 | 'FriendOnInterfaceMethod\MessageSender::sendMessage', 30 | 'FriendOnInterfaceMethod\Foo', 31 | 'FriendOnInterfaceMethod\MessageSendingService', 32 | ], 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Rules/FriendOnNewTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendOnNewTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendNewCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testStaticCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnNew.php', 26 | [ 27 | [ 28 | 20, 29 | 'FriendOnNew\Person::__construct', 30 | 'FriendOnNew\Exam', 31 | 'FriendOnNew\PersonBuilder', 32 | ], 33 | [ 34 | 24, 35 | 'FriendOnNew\Person::__construct', 36 | '', 37 | 'FriendOnNew\PersonBuilder', 38 | ], 39 | ] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Rules/FriendRuleMethodCallTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendRuleMethodCallTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnMethod.php', 26 | [ 27 | [ 28 | 26, 29 | 'FriendOnMethod\Person::updateName', 30 | 'FriendOnMethod\Updater', 31 | 'FriendOnMethod\FriendUpdater', 32 | ], 33 | [ 34 | 31, 35 | 'FriendOnMethod\Person::updateName', 36 | '', 37 | 'FriendOnMethod\FriendUpdater', 38 | ], 39 | ], 40 | ); 41 | } 42 | 43 | public function testMultipleFriends(): void 44 | { 45 | $this->assertNoErrorsReported(__DIR__.'/data/friend/multipleFriends.php'); 46 | } 47 | 48 | public function testDifferentNamespace(): void 49 | { 50 | $this->assertNoErrorsReported(__DIR__.'/data/friend/friendOnDifferentNamespace.php'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Rules/FriendRuleStaticCallTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendRuleStaticCallTest extends AbstractFriendRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendStaticCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testStaticCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/friend/friendOnStaticMethod.php', 26 | [ 27 | [ 28 | 24, 29 | 'FriendOnStaticMethod\Person::updateName', 30 | 'FriendOnStaticMethod\Updater', 31 | 'FriendOnStaticMethod\FriendUpdater', 32 | ], 33 | [ 34 | 28, 35 | 'FriendOnStaticMethod\Person::updateName', 36 | '', 37 | 'FriendOnStaticMethod\FriendUpdater', 38 | ], 39 | ] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Rules/FriendWithTestClassNameTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendWithTestClassNameTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig( 19 | mode: TestConfig::CLASS_NAME, 20 | ), 21 | ); 22 | } 23 | 24 | public function testCallsFromTestClass(): void 25 | { 26 | $this->assertNoErrorsReported(__DIR__.'/data/friend/friendRulesIgnoredForTestClass.php'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Rules/FriendWithTestNamespaceTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class FriendWithTestNamespaceTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new FriendMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig( 19 | mode: TestConfig::NAMESPACE, 20 | testNamespace: 'FriendRulesIgnoredForTestNamespace\Test' 21 | ), 22 | ); 23 | } 24 | 25 | public function testCallsFromTestClass(): void 26 | { 27 | $this->assertNoErrorsReported(__DIR__.'/data/friend/friendRulesIgnoredForTestNamespace.php'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Rules/InjectableVersionCheckOnMethodTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class InjectableVersionCheckOnMethodTest extends AbstractInjectableVersionRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new InjectableVersionRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE) 19 | ); 20 | } 21 | 22 | public function testCheckOnMethod(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/injectableVersion/InjectableVersionCheckOnMethod.php', 26 | [ 27 | [ 28 | 34, 29 | 1, 30 | \InjectableVersionCheckOnMethod\DoctrineRepository::class, 31 | \InjectableVersionCheckOnMethod\Repository::class, 32 | ], 33 | ], 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Rules/InjectableVersionWithTestNamespaceTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class InjectableVersionWithTestNamespaceTest extends AbstractInjectableVersionRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new InjectableVersionRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig( 19 | TestConfig::NAMESPACE, 20 | 'InjectableVersionRulesIgnoredForTestNamespace\Test', 21 | ), 22 | ); 23 | } 24 | 25 | public function testOnClass(): void 26 | { 27 | $this->assertNoErrorsReported( 28 | __DIR__.'/data/injectableVersion/InjectableVersionRulesIgnoredForTestNamespace.php', 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Rules/MustUseResultTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | final class MustUseResultTest extends AbstractRuleTestCase 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new MustUseResultRule($this->createReflectionProvider()); 17 | } 18 | 19 | public function testMustUseResultRuleOnMethod(): void 20 | { 21 | $this->assertIssuesReported( 22 | __DIR__.'/data/mustUseResult/mustUseResultOnMethod.php', 23 | ); 24 | } 25 | 26 | public function testMustUseResultRuleOnStaticMethod(): void 27 | { 28 | $this->assertIssuesReported( 29 | __DIR__.'/data/mustUseResult/mustUseResultOnStaticMethod.php', 30 | ); 31 | } 32 | 33 | public function testMustUseWithParent(): void 34 | { 35 | $this->assertIssuesReported(__DIR__.'/data/mustUseResult/mustUseResultWithParent.php'); 36 | } 37 | 38 | protected function getErrorFormatter(): string 39 | { 40 | return 'Result returned by method must be used'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Rules/NamespaceVisibilityOnClassTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityOnClassTest extends AbstractNamespaceVisibilityRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new NamespaceVisibilityMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnClass.php', 26 | [ 27 | [ 28 | 61, 29 | 'NamespaceVisibilityOnClass\Person::updateName', 30 | 'NamespaceVisibilityOnClass', 31 | true, 32 | ], 33 | ], 34 | ); 35 | } 36 | 37 | public function testMethodCallSubNamespacesExcluded(): void 38 | { 39 | $this->assertErrorsReported( 40 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnClassExcludeSubNamespaces.php', 41 | [ 42 | [ 43 | 30, 44 | 'NamespaceVisibilityOnClassExcludeSubNamespaces\Person::updateName', 45 | 'NamespaceVisibilityOnClassExcludeSubNamespaces', 46 | false, 47 | ], 48 | ], 49 | ); 50 | } 51 | 52 | public function testMethodCallDifferentNamespace(): void 53 | { 54 | $this->assertErrorsReported( 55 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnClassForDifferentNamespaces.php', 56 | [ 57 | [ 58 | 44, 59 | 'NamespaceVisibilityOnClassForDifferentNamespaces\Person::updateName', 60 | 'NamespaceVisibilityOnClassDifferentNamespace', 61 | true, 62 | ], 63 | ], 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Rules/NamespaceVisibilityOnConstructorTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityOnConstructorTest extends AbstractNamespaceVisibilityRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new NamespaceVisibilityNewCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testConstructorCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnConstructor.php', 26 | [ 27 | [ 28 | 55, 29 | 'NamespaceVisibilityOnConstructor\Person::__construct', 30 | 'NamespaceVisibilityOnConstructor', 31 | true, 32 | ], 33 | ], 34 | ); 35 | } 36 | 37 | public function testExcludeSubNamespacePositionalArgumentUsed(): void 38 | { 39 | $this->assertErrorsReported( 40 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnConstructorExcludeSubNamespacesPositional.php', 41 | [ 42 | [ 43 | 40, 44 | 'NamespaceVisibilityOnConstructorExcludeSubNamespacesPositional\Person::__construct', 45 | 'NamespaceVisibilityOnConstructorExcludeSubNamespacesPositional', 46 | false, 47 | ], 48 | ], 49 | ); 50 | } 51 | 52 | public function testExcludeSubNamespaceNamedArgumentUsed(): void 53 | { 54 | $this->assertErrorsReported( 55 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnConstructorExcludeSubNamespaces.php', 56 | [ 57 | [ 58 | 40, 59 | 'NamespaceVisibilityOnConstructorExcludeSubNamespaces\Person::__construct', 60 | 'NamespaceVisibilityOnConstructorExcludeSubNamespaces', 61 | false, 62 | ], 63 | ], 64 | ); 65 | } 66 | 67 | public function testDifferentNamespace(): void 68 | { 69 | $this->assertNoErrorsReported( 70 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnConstructorForDifferentNamespaces.php', 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Rules/NamespaceVisibilityOnMethodTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityOnMethodTest extends AbstractNamespaceVisibilityRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new NamespaceVisibilityMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnMethod.php', 26 | [ 27 | [ 28 | 58, 29 | 'NamespaceVisibilityOnMethod\Person::updateName', 30 | 'NamespaceVisibilityOnMethod', 31 | true, 32 | ], 33 | ], 34 | ); 35 | } 36 | 37 | public function testMethodCallSubNamespacesExcluded(): void 38 | { 39 | $this->assertErrorsReported( 40 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnMethodExcludeSubNamespace.php', 41 | [ 42 | [ 43 | 32, 44 | 'NamespaceVisibilityOnMethodExcludeSubNamespace\Person::updateName', 45 | 'NamespaceVisibilityOnMethodExcludeSubNamespace', 46 | false, 47 | ], 48 | ], 49 | ); 50 | } 51 | 52 | public function testMethodCallDifferentNamespace(): void 53 | { 54 | $this->assertErrorsReported( 55 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnMethodForDifferentNamespace.php', 56 | [ 57 | [ 58 | 57, 59 | 'NamespaceVisibilityOnMethodForDifferentNamespace\Person::updateName', 60 | 'NamespaceVisibilityOnMethodDifferentNamespace', 61 | true, 62 | ], 63 | ], 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Rules/NamespaceVisibilityOnNewTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityOnNewTest extends AbstractNamespaceVisibilityRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new NamespaceVisibilityNewCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnNew.php', 26 | [ 27 | [ 28 | 53, 29 | 'NamespaceVisibilityOnNew\Person::__construct', 30 | 'NamespaceVisibilityOnNew', 31 | true, 32 | ], 33 | ], 34 | ); 35 | } 36 | 37 | public function testMethodCallSubNamespacesExcluded(): void 38 | { 39 | $this->assertErrorsReported( 40 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnNewExcludeSubNamespaces.php', 41 | [ 42 | [ 43 | 21, 44 | 'NamespaceVisibilityOnNewExcludeSubNamespaces\Person::__construct', 45 | 'NamespaceVisibilityOnNewExcludeSubNamespaces', 46 | false, 47 | ], 48 | ], 49 | ); 50 | } 51 | 52 | public function testMethodCallDifferentNamespace(): void 53 | { 54 | $this->assertErrorsReported( 55 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnNewForDifferentNamespaces.php', 56 | [ 57 | [ 58 | 52, 59 | 'NamespaceVisibilityOnNewForDifferentNamespaces\Person::__construct', 60 | 'NamespaceVisibilityOnNewDifferentNamespace', 61 | true, 62 | ], 63 | ], 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Rules/NamespaceVisibilityOnStaticTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class NamespaceVisibilityOnStaticTest extends AbstractNamespaceVisibilityRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new NamespaceVisibilityStaticCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnStaticMethod.php', 26 | [ 27 | [ 28 | 52, 29 | 'NamespaceVisibilityOnStaticMethod\Person::updateName', 30 | 'NamespaceVisibilityOnStaticMethod', 31 | true, 32 | ], 33 | ], 34 | ); 35 | } 36 | 37 | public function testExcludeSubNamespace(): void 38 | { 39 | $this->assertErrorsReported( 40 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnStaticMethodExcludeSubNamespaces.php', 41 | [ 42 | [ 43 | 26, 44 | 'NamespaceVisibilityOnStaticMethodExcludeSubNamespaces\Person::updateName', 45 | 'NamespaceVisibilityOnStaticMethodExcludeSubNamespaces', 46 | false, 47 | ], 48 | ], 49 | ); 50 | } 51 | 52 | public function testForDifferentNamespace(): void 53 | { 54 | $this->assertNoErrorsReported( 55 | __DIR__.'/data/namespaceVisibility/namespaceVisibilityOnStaticMethodForDifferentNamespaces.php', 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Rules/OverrideErrorFormatter.php: -------------------------------------------------------------------------------- 1 | */ 13 | final class OverrideTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new OverrideRule(); 18 | } 19 | 20 | public function testOverrideRuleOnClass(): void 21 | { 22 | $this->assertIssuesReported( 23 | __DIR__.'/data/override/overrideOnClass.php', 24 | ); 25 | } 26 | 27 | public function testOverrideRuleOnInterface(): void 28 | { 29 | $this->assertIssuesReported( 30 | __DIR__.'/data/override/overrideOnInterface.php', 31 | ); 32 | } 33 | 34 | public function testOverrideRuleRfcExamples(): void 35 | { 36 | $this->assertIssuesReported( 37 | __DIR__.'/data/override/overrideRfcExamples.php', 38 | ); 39 | } 40 | 41 | public function getErrorFormatter(): ErrorMessageFormatter 42 | { 43 | return new OverrideErrorFormatter(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Rules/PackageOnClassTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageOnClassTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/package/packageOnClass.php', 26 | [ 27 | [ 28 | 46, 29 | 'PackageOnClass\Person::updateName', 30 | 'PackageOnClass2', 31 | ], 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/PackageOnConstructorTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageOnConstructorTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageNewCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/package/packageOnConstructor.php', 26 | [ 27 | [ 28 | 44, 29 | 'PackageOnConstructor\Person::__construct', 30 | 'PackageOnConstructor2', 31 | ], 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/PackageOnMethodTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageOnMethodTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/package/packageOnMethod.php', 26 | [ 27 | [ 28 | 43, 29 | 'PackageOnMethod\Person::updateName', 30 | 'PackageOnMethod2', 31 | ], 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/PackageOnNewTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageOnNewTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageNewCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/package/packageOnNew.php', 26 | [ 27 | [ 28 | 40, 29 | 'PackageOnNew\Person::__construct', 30 | 'PackageOnNew2', 31 | ], 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/PackageOnStaticTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageOnStaticTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageStaticCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig(TestConfig::NONE), 19 | ); 20 | } 21 | 22 | public function testMethodCall(): void 23 | { 24 | $this->assertErrorsReported( 25 | __DIR__.'/data/package/packageOnStaticMethod.php', 26 | [ 27 | [ 28 | 45, 29 | 'PackageOnStaticMethod\Person::updateName', 30 | 'PackageOnStaticMethod2', 31 | ], 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Rules/PackageWithTestClassNameTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageWithTestClassNameTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig( 19 | mode: TestConfig::CLASS_NAME, 20 | ), 21 | ); 22 | } 23 | 24 | public function testCallsFromTestClass(): void 25 | { 26 | $this->assertNoErrorsReported(__DIR__.'/data/package/packageRulesIgnoredForTestClass.php'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Rules/PackageWithTestNamespaceTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | class PackageWithTestNamespaceTest extends AbstractPackageRuleTest 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new PackageMethodCallRule( 17 | $this->createReflectionProvider(), 18 | new TestConfig( 19 | mode: TestConfig::NAMESPACE, 20 | testNamespace: 'PackageRulesIgnoredForTestNamespace\Test' 21 | ), 22 | ); 23 | } 24 | 25 | public function testCallsFromTestClass(): void 26 | { 27 | $this->assertNoErrorsReported(__DIR__.'/data/package/packageRulesIgnoredForTestNamespace.php'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Rules/RestrictTraitToTest.php: -------------------------------------------------------------------------------- 1 | */ 12 | final class RestrictTraitToTest extends AbstractRuleTestCase 13 | { 14 | protected function getRule(): Rule 15 | { 16 | return new RestrictTraitToRule($this->createReflectionProvider()); 17 | } 18 | 19 | public function testRestrictTraitTo(): void 20 | { 21 | $this->assertIssuesReported( 22 | __DIR__.'/data/restrictTraitTo/restrictTraitTo.php', 23 | ); 24 | } 25 | 26 | protected function getErrorFormatter(): string 27 | { 28 | return 'Trait can only be used on class or child of: {0}'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Rules/TestTagClassOnConstructorIngoredOnTestClassTest.php: -------------------------------------------------------------------------------- 1 | */ 14 | class TestTagClassOnConstructorIngoredOnTestClassTest extends AbstractRuleTestCase 15 | { 16 | protected function getRule(): Rule 17 | { 18 | return new TestTagNewCallRule( 19 | $this->createReflectionProvider(), 20 | new TestConfig(TestConfig::CLASS_NAME), 21 | ); 22 | } 23 | 24 | public function testMethodCall(): void 25 | { 26 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagClassOnConstructorIgnoredInTestClass.php'); 27 | } 28 | 29 | protected function getErrorFormatter(): ErrorMessageFormatter|string 30 | { 31 | return 'TestTagClassOnConstructorIgnoredOnTestClass\Person::__construct is a test tag and can only be called from test code'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/TestTagClassOnConstructorTest.php: -------------------------------------------------------------------------------- 1 | */ 14 | class TestTagClassOnConstructorTest extends AbstractRuleTestCase 15 | { 16 | protected function getRule(): Rule 17 | { 18 | return new TestTagNewCallRule( 19 | $this->createReflectionProvider(), 20 | new TestConfig(TestConfig::CLASS_NAME), 21 | ); 22 | } 23 | 24 | public function testMethodCall(): void 25 | { 26 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagClassOnConstructor.php'); 27 | } 28 | 29 | protected function getErrorFormatter(): ErrorMessageFormatter|string 30 | { 31 | return 'TestTagClassOnConstructor\Person::__construct is a test tag and can only be called from test code'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/TestTagClassOnMethodIgnoredOnTestClassTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagClassOnMethodIgnoredOnTestClassTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagMethodCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported( 26 | __DIR__.'/data/testTag/testTagClassOnMethodIgnoredInTestClass.php', 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Rules/TestTagClassOnMethodTest.php: -------------------------------------------------------------------------------- 1 | */ 14 | class TestTagClassOnMethodTest extends AbstractRuleTestCase 15 | { 16 | protected function getRule(): Rule 17 | { 18 | return new TestTagMethodCallRule( 19 | $this->createReflectionProvider(), 20 | new TestConfig(TestConfig::CLASS_NAME), 21 | ); 22 | } 23 | 24 | public function testMethodCall(): void 25 | { 26 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagClassOnMethod.php'); 27 | } 28 | 29 | protected function getErrorFormatter(): ErrorMessageFormatter|string 30 | { 31 | return 'TestTagClassOnMethod\Person::updateName is a test tag and can only be called from test code'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/TestTagClassOnStaticIgnoredOnTestClassTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagClassOnStaticIgnoredOnTestClassTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagStaticCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagClassOnStaticMethodIgnoredInTestClass.php'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Rules/TestTagClassOnStaticTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | final class TestTagClassOnStaticTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagStaticCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagClassOnStaticMethod.php'); 26 | } 27 | 28 | protected function getErrorFormatter(): string 29 | { 30 | return 'TestTagClassOnStaticMethod\Person::updateName is a test tag and can only be called from test code'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnConstructorIgnoredInTestNamespaceTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagOnConstructorIgnoredInTestNamespaceTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagNewCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig( 20 | TestConfig::NAMESPACE, 21 | testNamespace: 'TestTagOnConstructorIgnoredInTestNamespace\Test', 22 | ), 23 | ); 24 | } 25 | 26 | public function testMethodCall(): void 27 | { 28 | $this->assertIssuesReported( 29 | __DIR__.'/data/testTag/testTagOnConstructorIgnoredInTestNamespace.php', 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnConstructorIgnoredOnTestClassTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagOnConstructorIgnoredOnTestClassTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagNewCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported( 26 | __DIR__.'/data/testTag/testTagOnConstructorIgnoredInTestClass.php', 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnConstructorTest.php: -------------------------------------------------------------------------------- 1 | */ 14 | class TestTagOnConstructorTest extends AbstractRuleTestCase 15 | { 16 | protected function getRule(): Rule 17 | { 18 | return new TestTagNewCallRule( 19 | $this->createReflectionProvider(), 20 | new TestConfig(TestConfig::CLASS_NAME), 21 | ); 22 | } 23 | 24 | public function testMethodCall(): void 25 | { 26 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagOnConstructor.php'); 27 | } 28 | 29 | protected function getErrorFormatter(): ErrorMessageFormatter|string 30 | { 31 | return 'TestTagOnConstructor\Person::__construct is a test tag and can only be called from test code'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnMethodIgnoredInTestNamespaceTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagOnMethodIgnoredInTestNamespaceTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagMethodCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig( 20 | TestConfig::NAMESPACE, 21 | testNamespace: 'TestTagOnMethodIgnoredInTestNamespace\Test', 22 | ), 23 | ); 24 | } 25 | 26 | public function testMethodCall(): void 27 | { 28 | $this->assertIssuesReported( 29 | __DIR__.'/data/testTag/testTagOnMethodIgnoredInTestNamespace.php', 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnMethodIgnoredOnTestClassTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagOnMethodIgnoredOnTestClassTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagMethodCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported( 26 | __DIR__.'/data/testTag/testTagOnMethodIgnoredInTestClass.php', 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnMethodTest.php: -------------------------------------------------------------------------------- 1 | */ 14 | class TestTagOnMethodTest extends AbstractRuleTestCase 15 | { 16 | protected function getRule(): Rule 17 | { 18 | return new TestTagMethodCallRule( 19 | $this->createReflectionProvider(), 20 | new TestConfig(TestConfig::CLASS_NAME), 21 | ); 22 | } 23 | 24 | public function testMethodCall(): void 25 | { 26 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagOnMethod.php'); 27 | } 28 | 29 | protected function getErrorFormatter(): ErrorMessageFormatter|string 30 | { 31 | return 'TestTagOnMethod\Person::updateName is a test tag and can only be called from test code'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnStaticIgnoreIOnTestNamespaceTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagOnStaticIgnoreIOnTestNamespaceTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagStaticCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig( 20 | TestConfig::NAMESPACE, 21 | testNamespace: 'TestTagOnStaticMethodIgnoredInTestNamepace\Test', 22 | ), 23 | ); 24 | } 25 | 26 | public function testMethodCall(): void 27 | { 28 | $this->assertIssuesReported( 29 | __DIR__.'/data/testTag/testTagOnStaticMethodIgnoredInTestNamespace.php', 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnStaticIgnoredOnTestClassTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | class TestTagOnStaticIgnoredOnTestClassTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagStaticCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagOnStaticMethodIgnoredInTestClass.php'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Rules/TestTagOnStaticTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | final class TestTagOnStaticTest extends AbstractRuleTestCase 14 | { 15 | protected function getRule(): Rule 16 | { 17 | return new TestTagStaticCallRule( 18 | $this->createReflectionProvider(), 19 | new TestConfig(TestConfig::CLASS_NAME), 20 | ); 21 | } 22 | 23 | public function testMethodCall(): void 24 | { 25 | $this->assertIssuesReported(__DIR__.'/data/testTag/testTagOnStaticMethod.php'); 26 | } 27 | 28 | protected function getErrorFormatter(): string 29 | { 30 | return 'TestTagOnStaticMethod\Person::updateName is a test tag and can only be called from test code'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendOnClass.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 20 | } 21 | } 22 | 23 | class Updater 24 | { 25 | public function updater(Person $person): void 26 | { 27 | $person->updateName(); // ERROR 28 | } 29 | } 30 | 31 | $person = new Person(); 32 | $person->updateName(); // ERROR 33 | 34 | class FriendUpdater 35 | { 36 | public function update(): void 37 | { 38 | $person = new Person(); 39 | $person->updateName(); // OK 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendOnConstructor.php: -------------------------------------------------------------------------------- 1 | update(); // OK 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendOnInterfaceClass.php: -------------------------------------------------------------------------------- 1 | messageSender->sendMessage(); // OK 29 | } 30 | } 31 | 32 | class Foo 33 | { 34 | public function __construct(public MessageSender $messageSender) {} 35 | 36 | public function sendMessage(): void 37 | { 38 | $this->messageSender->sendMessage(); // ERROR Foo is not a friend of MessageSender 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendOnInterfaceMethod.php: -------------------------------------------------------------------------------- 1 | messageSender->sendMessage(); // OK 29 | } 30 | } 31 | 32 | class Foo 33 | { 34 | public function __construct(public MessageSender $messageSender) {} 35 | 36 | public function sendMessage(): void 37 | { 38 | $this->messageSender->sendMessage(); // ERROR Foo is not a friend of MessageSender 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendOnMethod.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 19 | } 20 | } 21 | 22 | class Updater 23 | { 24 | public function updater(Person $person): void 25 | { 26 | $person->updateName(); // ERROR 27 | } 28 | } 29 | 30 | $person = new Person(); 31 | $person->updateName(); // ERROR 32 | 33 | class FriendUpdater 34 | { 35 | public function update(): void 36 | { 37 | $person = new Person(); 38 | $person->updateName(); // OK 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendOnNew.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK calling from a class with a name ending Test 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/friendRulesIgnoredForTestNamespace.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK calling from a test namespace 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /tests/Rules/data/friend/multipleFriends.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 22 | } 23 | } 24 | 25 | class AnotherFriendUpdater 26 | { 27 | public function update(Person $person): void 28 | { 29 | $person->updateName(); // OK 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Rules/data/injectableVersion/InjectableVersionCheckOnMethod.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 27 | } 28 | } 29 | 30 | class InjectingWrongVersion 31 | { 32 | public Repository $repository; 33 | 34 | #[CheckInjectableVersion] 35 | public function setRepository(DoctrineRepository $repository): void // ERROR 36 | { 37 | $this->repository = $repository; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Rules/data/injectableVersion/InjectableVersionOnClass.php: -------------------------------------------------------------------------------- 1 | dontNeedToUseResult(); // OK 27 | 28 | $class->mustUseResult(); // ERROR 29 | 30 | echo $class->mustUseResult(); // OK; 31 | 32 | $value = 1 + $class->mustUseResult(); // OK 33 | } -------------------------------------------------------------------------------- /tests/Rules/data/mustUseResult/mustUseResultOnStaticMethod.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK: Calls to same class allowed 20 | } 21 | } 22 | 23 | class Updater 24 | { 25 | public function updater(Person $person): void 26 | { 27 | $person->updateName(); // OK: Calls within same namespace allowed 28 | } 29 | } 30 | 31 | $person = new Person(); 32 | $person->updateName(); // OK: Calls within same namespace allowed 33 | 34 | } 35 | 36 | 37 | namespace NamespaceVisibilityOnClass\SubNamesapce { 38 | 39 | use NamespaceVisibilityOnClass\Person; 40 | 41 | class AnotherClass 42 | { 43 | public function update(): void 44 | { 45 | $person = new Person(); 46 | $person->updateName(); // OK: Calls within the same subnamespace allowed. 47 | } 48 | } 49 | } 50 | 51 | 52 | namespace NamespaceVisibilityOnClass2 { 53 | 54 | use NamespaceVisibilityOnClass\Person; 55 | 56 | class AnotherUpdater 57 | { 58 | public function update(): void 59 | { 60 | $person = new Person(); 61 | $person->updateName(); // ERROR: Call to Person::update method which has namespace visibility. 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityOnClassExcludeSubNamespaces.php: -------------------------------------------------------------------------------- 1 | updateName(); // ERROR: Call to Person::updateName in a sub namespace, where sub namespace is not allowed. 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityOnClassForDifferentNamespaces.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK: Call to Person::updateName is in the allowed namespace. 31 | } 32 | } 33 | } 34 | 35 | namespace NamespaceVisibilityOnClassAnotherNamespace { 36 | 37 | use NamespaceVisibilityOnClassForDifferentNamespaces\Person; 38 | 39 | class AnotherClass 40 | { 41 | public function update(): void 42 | { 43 | $person = new Person(); 44 | $person->updateName(); // ERROR 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityOnConstructor.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 19 | } 20 | } 21 | 22 | class Updater 23 | { 24 | public function updater(Person $person): void 25 | { 26 | $person->updateName(); // OK 27 | } 28 | } 29 | 30 | $person = new Person(); 31 | $person->updateName(); // OK 32 | } 33 | 34 | 35 | 36 | namespace NamespaceVisibilityOnMethod\SubNamespace { 37 | 38 | use NamespaceVisibilityOnMethod\Person; 39 | class AnotherPersonUpdater 40 | { 41 | public function update(Person $person): void 42 | { 43 | $person->updateName(); // OK - Subnamespace of NamespaceVisibilityOnMethod, which is allowed 44 | } 45 | } 46 | } 47 | 48 | 49 | namespace NamespaceVisibilityOnMethod2 { 50 | 51 | use NamespaceVisibilityOnMethod\Person; 52 | 53 | class AnotherUpdater 54 | { 55 | public function update(): void 56 | { 57 | $person = new Person(); 58 | $person->updateName(); // ERROR: Call to Person::updateName which has namespaceVisibility visibility 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityOnMethodExcludeSubNamespace.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 19 | } 20 | } 21 | } 22 | 23 | 24 | 25 | namespace NamespaceVisibilityOnMethodExcluceSubNamespace\SubNamespace { 26 | 27 | use NamespaceVisibilityOnMethodExcludeSubNamespace\Person; 28 | class AnotherPersonUpdater 29 | { 30 | public function update(Person $person): void 31 | { 32 | $person->updateName(); // ERROR - Subnamespace of NamespaceVisibilityOnMethod, which is not allowed 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityOnMethodForDifferentNamespace.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 29 | } 30 | } 31 | } 32 | 33 | 34 | namespace NamespaceVisibilityOnMethodDifferentNamespace\SubNamespace { 35 | 36 | use NamespaceVisibilityOnMethodForDifferentNamespace\Person; 37 | class AnotherPersonUpdater 38 | { 39 | public function update(Person $person): void 40 | { 41 | $person->updateName(); // OK 42 | } 43 | } 44 | } 45 | 46 | 47 | 48 | 49 | 50 | namespace NamespaceVisibilityOnMethodDifferentNamespace2 { 51 | 52 | use NamespaceVisibilityOnMethodForDifferentNamespace\Person; 53 | class AnotherPersonUpdater 54 | { 55 | public function update(Person $person): void 56 | { 57 | $person->updateName(); // ERROR 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityOnNew.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK: calling from a class with a name ending Test 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/Rules/data/namespaceVisibility/namespaceVisibilityRulesIgnoredForTestNamespace.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK: calling from a test namespace 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/data/override/overrideOnClass.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 20 | } 21 | } 22 | 23 | class Updater 24 | { 25 | public function updater(Person $person): void 26 | { 27 | $person->updateName(); // OK 28 | } 29 | } 30 | 31 | $person = new Person(); 32 | $person->updateName(); // OK 33 | 34 | } 35 | 36 | 37 | namespace PackageOnClass2 { 38 | 39 | use PackageOnClass\Person; 40 | 41 | class AnotherUpdater 42 | { 43 | public function update(): void 44 | { 45 | $person = new Person(); 46 | $person->updateName(); // ERROR 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Rules/data/package/packageOnConstructor.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK 19 | } 20 | } 21 | 22 | class Updater 23 | { 24 | public function updater(Person $person): void 25 | { 26 | $person->updateName(); // OK 27 | } 28 | } 29 | 30 | $person = new Person(); 31 | $person->updateName(); // OK 32 | } 33 | 34 | namespace PackageOnMethod2 { 35 | 36 | use PackageOnMethod\Person; 37 | 38 | class AnotherUpdater 39 | { 40 | public function update(): void 41 | { 42 | $person = new Person(); 43 | $person->updateName(); // ERROR 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Rules/data/package/packageOnNew.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK calling from a class with a name ending Test 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/Rules/data/package/packageRulesIgnoredForTestNamespace.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK calling from a test namespace 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rules/data/restrictTraitTo/restrictTraitTo.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK - whole class is marked with TestTag, so OK to call methods within it. 19 | } 20 | } 21 | 22 | class Updater 23 | { 24 | public function updater(Person $person): void 25 | { 26 | $person->updateName(); // ERROR 27 | } 28 | } 29 | 30 | $person = new Person(); 31 | $person->updateName(); // ERROR 32 | 33 | -------------------------------------------------------------------------------- /tests/Rules/data/testTag/testTagClassOnMethodIgnoredInTestClass.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK - Called from Test class 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/Rules/data/testTag/testTagClassOnStaticMethod.php: -------------------------------------------------------------------------------- 1 | updateName(); // ERROR 19 | } 20 | } 21 | 22 | class Updater 23 | { 24 | public function updater(Person $person): void 25 | { 26 | $person->updateName(); // ERROR 27 | } 28 | } 29 | 30 | $person = new Person(); 31 | $person->updateName(); // ERROR 32 | 33 | -------------------------------------------------------------------------------- /tests/Rules/data/testTag/testTagOnMethodIgnoredInTestClass.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK - Called from Test class 23 | } 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/Rules/data/testTag/testTagOnMethodIgnoredInTestNamespace.php: -------------------------------------------------------------------------------- 1 | updateName(); // OK - Called from Test namespace 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /tests/Rules/data/testTag/testTagOnStaticMethod.php: -------------------------------------------------------------------------------- 1 | }> */ 20 | public function methodDataProvider(): array 21 | { 22 | return [ 23 | ['noAttribute', []], 24 | ['__construct', []], 25 | ['friendWithOneValue', [Foo::class]], 26 | ['friendWithTwoValues', [Foo::class, Bar::class]], 27 | ['friendWithOneValueInArray', [Bar::class]], 28 | ]; 29 | } 30 | 31 | /** 32 | * @dataProvider methodDataProvider 33 | * 34 | * @param array $expectedValues 35 | */ 36 | public function testGettingMethodAttributeValues(string $methodName, array $expectedValues): void 37 | { 38 | $actualValues = AttributeValueReader::getAttributeValuesForMethod( 39 | new \ReflectionClass(MethodAttributes::class), 40 | $methodName, 41 | Friend::class, 42 | ); 43 | 44 | $this->assertEquals($expectedValues, $actualValues); 45 | } 46 | 47 | /** @return array}> */ 48 | public function classDataProvider(): array 49 | { 50 | return [ 51 | [Class0Friends::class, []], 52 | [Class1Friend::class, [Foo::class]], 53 | [Class2Friends::class, [Foo::class, Bar::class]], 54 | ]; 55 | } 56 | 57 | /** 58 | * @dataProvider classDataProvider 59 | * 60 | * @param class-string $class 61 | * @param array $expectedValues 62 | */ 63 | public function testGettingClassAttributeValues(string $class, array $expectedValues): void 64 | { 65 | $actualValues = AttributeValueReader::getAttributeValuesForClass( 66 | new \ReflectionClass($class), 67 | Friend::class, 68 | ); 69 | 70 | $this->assertEquals($expectedValues, $actualValues); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Unit/AttributeValueReaders/AttributeFinderTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($attribute); 25 | $this->assertSame($attribute->getName(), Friend::class); 26 | $this->assertSame([Foo::class], $attribute->getArguments()); 27 | } 28 | 29 | public function testFindAttributeNotFoundOnClass(): void 30 | { 31 | $attribute = AttributeFinder::getAttributeOnClass( 32 | new \ReflectionClass(Class0Friends::class), 33 | Friend::class, 34 | ); 35 | 36 | $this->assertNull($attribute); 37 | } 38 | 39 | public function testFindAttributeOnMethod(): void 40 | { 41 | $attribute = AttributeFinder::getAttributeOnMethod( 42 | new \ReflectionClass(MethodAttributes::class), 43 | 'friendWithOneValue', 44 | Friend::class, 45 | ); 46 | 47 | $this->assertNotNull($attribute); 48 | $this->assertSame($attribute->getName(), Friend::class); 49 | } 50 | 51 | public function testFindAttributeNotOnMethod(): void 52 | { 53 | $attribute = AttributeFinder::getAttributeOnMethod( 54 | new \ReflectionClass(MethodAttributes::class), 55 | 'noAttribute', 56 | Friend::class, 57 | ); 58 | 59 | $this->assertNull($attribute); 60 | } 61 | 62 | public function testHasAttributeOnClass(): void 63 | { 64 | $this->assertTrue(AttributeFinder::hasAttributeOnClass( 65 | new \ReflectionClass(Class1Friend::class), 66 | Friend::class, 67 | )); 68 | } 69 | 70 | public function testHasAttributeNotFoundOnClass(): void 71 | { 72 | $this->assertFalse(AttributeFinder::hasAttributeOnClass( 73 | new \ReflectionClass(Class0Friends::class), 74 | Friend::class, 75 | )); 76 | } 77 | 78 | public function testHasAttributeOnMethod(): void 79 | { 80 | $this->assertTrue(AttributeFinder::hasAttributeOnMethod( 81 | new \ReflectionClass(MethodAttributes::class), 82 | 'friendWithOneValue', 83 | Friend::class, 84 | )); 85 | } 86 | 87 | public function testHasAttributeNotOnMethod(): void 88 | { 89 | $this->assertFalse(AttributeFinder::hasAttributeOnMethod( 90 | new \ReflectionClass(MethodAttributes::class), 91 | 'noAttribute', 92 | Friend::class, 93 | )); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Unit/CacheTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | private Cache $cache; 19 | 20 | protected function setup(): void 21 | { 22 | $this->cache = new Cache(); 23 | } 24 | 25 | public function testEmptyCache(): void 26 | { 27 | $this->assertFalse($this->cache->hasEntry(self::ENTRY_1)); 28 | } 29 | 30 | public function testAddValueToCache(): void 31 | { 32 | $this->cache->addEntry(self::ENTRY_1, self::VALUE_1); 33 | $this->assertTrue($this->cache->hasEntry(self::ENTRY_1)); 34 | $this->assertSame(self::VALUE_1, $this->cache->getEntry(self::ENTRY_1)); 35 | } 36 | 37 | public function testAccessMissingEntry(): void 38 | { 39 | $this->expectException(\LogicException::class); 40 | $this->cache->getEntry(self::ENTRY_1); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/SubNamespaceCheckerTest.php: -------------------------------------------------------------------------------- 1 | */ 13 | public function dataProvider(): array 14 | { 15 | return [ 16 | 'same namespace' => [ 17 | true, 18 | 'Foo\Bar', 19 | 'Foo\Bar', 20 | ], 21 | 'different namespace' => [ 22 | false, 23 | 'Foo\Bar', 24 | 'Foo\Baz', 25 | ], 26 | 'sub namespace' => [ 27 | true, 28 | 'Foo\Bar\Baz', 29 | 'Foo\Bar', 30 | ], 31 | 'not sub namespace 1' => [ 32 | false, 33 | 'Foo\Bart', 34 | 'Foo\Bar', 35 | ], 36 | 'both null' => [ 37 | false, 38 | null, 39 | null, 40 | ], 41 | 'namespace is null' => [ 42 | false, 43 | null, 44 | 'Foo\Bar', 45 | ], 46 | 'namespace to check against is null' => [ 47 | false, 48 | 'Foo\Bart', 49 | null, 50 | ], 51 | ]; 52 | } 53 | 54 | /** @dataProvider dataProvider */ 55 | public function testSubNamespaceChecker(bool $expected, ?string $namespace, ?string $namespaceToCheckAgainst): void 56 | { 57 | $actual = SubNamespaceChecker::isSubNamespace($namespace, $namespaceToCheckAgainst); 58 | $this->assertSame($expected, $actual); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Unit/TestClassCheckerTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($testClassChecker->isTestClass(self::TEST_NAMESPACE, self::TEST_CLASS_NAME)); 25 | } 26 | 27 | public function testModeClassNameOnTestClass(): void 28 | { 29 | $testConfig = new TestConfig(TestConfig::CLASS_NAME); 30 | $testClassChecker = new TestClassChecker($testConfig); 31 | $this->assertTrue($testClassChecker->isTestClass(self::TEST_NAMESPACE, self::TEST_CLASS_NAME)); 32 | } 33 | 34 | public function testModeClassNameOnNonTestClass(): void 35 | { 36 | $testConfig = new TestConfig(TestConfig::CLASS_NAME); 37 | $testClassChecker = new TestClassChecker($testConfig); 38 | $this->assertFalse($testClassChecker->isTestClass(self::TEST_NAMESPACE, self::NON_TEST_CLASS_NAME)); 39 | } 40 | 41 | public function testModeNamespaceInTestNamespace(): void 42 | { 43 | $testConfig = new TestConfig(TestConfig::NAMESPACE, self::TEST_NAMESPACE); 44 | $testClassChecker = new TestClassChecker($testConfig); 45 | $this->assertTrue($testClassChecker->isTestClass(self::TEST_NAMESPACE, self::NON_TEST_CLASS_NAME)); 46 | } 47 | 48 | public function testModeNamespaceInTestSubNamespace(): void 49 | { 50 | $testConfig = new TestConfig(TestConfig::NAMESPACE, self::TEST_NAMESPACE); 51 | $testClassChecker = new TestClassChecker($testConfig); 52 | $this->assertTrue($testClassChecker->isTestClass(self::TEST_SUB_NAMESPACE, self::NON_TEST_CLASS_NAME)); 53 | } 54 | 55 | public function testModeNamespaceNotInTestNamespace(): void 56 | { 57 | $testConfig = new TestConfig(TestConfig::NAMESPACE, self::TEST_NAMESPACE); 58 | $testClassChecker = new TestClassChecker($testConfig); 59 | $this->assertFalse($testClassChecker->isTestClass(self::NON_TEST_NAMESPACE, self::NON_TEST_CLASS_NAME)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/TestConfigTest.php: -------------------------------------------------------------------------------- 1 | assertSame(TestConfig::NONE, $testConfig->getMode()); 18 | } 19 | 20 | public function testModeClassName(): void 21 | { 22 | $testConfig = new TestConfig(TestConfig::CLASS_NAME); 23 | $this->assertSame(TestConfig::CLASS_NAME, $testConfig->getMode()); 24 | } 25 | 26 | public function testNamespace(): void 27 | { 28 | $testConfig = new TestConfig(TestConfig::NAMESPACE, self::NAMESPACE); 29 | $this->assertSame(TestConfig::NAMESPACE, $testConfig->getMode()); 30 | $this->assertSame(self::NAMESPACE, $testConfig->getTestNamespace()); 31 | } 32 | 33 | public function testNamespaceMustBeSuppliedInNamespaceMode(): void 34 | { 35 | $this->expectException(\InvalidArgumentException::class); 36 | new TestConfig(TestConfig::NAMESPACE); 37 | } 38 | 39 | public function testErrorIfNamespaceSuppliedWhenNotNamespaceMode(): void 40 | { 41 | $this->expectException(\InvalidArgumentException::class); 42 | new TestConfig(TestConfig::NONE, self::NAMESPACE); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/data/Bar.php: -------------------------------------------------------------------------------- 1 |