diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5d66bc4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,27 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+trim_trailing_whitespace = true
+
+[*.{php,phpt}]
+indent_style = tab
+indent_size = 4
+
+[*.xml]
+indent_style = tab
+indent_size = 4
+
+[*.neon]
+indent_style = tab
+indent_size = 4
+
+[*.{yaml,yml}]
+indent_style = space
+indent_size = 2
+
+[composer.json]
+indent_style = tab
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..4838772
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+*.php text eol=lf
+
+.github export-ignore
+tests export-ignore
+tmp export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+Makefile export-ignore
+phpcs.xml export-ignore
+phpstan.neon export-ignore
+phpunit.xml export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..7877c31
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+- package-ecosystem: github-actions
+ directory: '/'
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+
+- package-ecosystem: composer
+ directory: '/'
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..3525498
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,162 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Build"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "1.1.x"
+
+jobs:
+ lint:
+ name: "Lint"
+ runs-on: "ubuntu-latest"
+
+ strategy:
+ matrix:
+ php-version:
+ - "7.2"
+ - "7.3"
+ - "7.4"
+ - "8.0"
+ - "8.1"
+ - "8.2"
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v3
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+
+ - name: "Validate Composer"
+ run: "composer validate"
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress"
+
+ - name: "Downgrade PHPUnit"
+ if: matrix.php-version == '7.2' || matrix.php-version == '7.3'
+ run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies"
+
+ - name: "Lint"
+ run: "make lint"
+
+ coding-standards:
+ name: "Coding Standard"
+
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v3
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "8.0"
+
+ - name: "Validate Composer"
+ run: "composer validate"
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress"
+
+ - name: "Lint"
+ run: "make lint"
+
+ - name: "Coding Standard"
+ run: "make cs"
+
+ tests:
+ name: "Tests"
+ runs-on: "ubuntu-latest"
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version:
+ - "7.2"
+ - "7.3"
+ - "7.4"
+ - "8.0"
+ - "8.1"
+ - "8.2"
+ dependencies:
+ - "lowest"
+ - "highest"
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v3
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+
+ - name: "Install lowest dependencies"
+ if: ${{ matrix.dependencies == 'lowest' }}
+ run: "composer update --prefer-lowest --no-interaction --no-progress"
+
+ - name: "Install highest dependencies"
+ if: ${{ matrix.dependencies == 'highest' }}
+ run: "composer update --no-interaction --no-progress"
+
+ - name: "Downgrade PHPUnit"
+ if: matrix.php-version == '7.2' || matrix.php-version == '7.3'
+ run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies"
+
+ - name: "Tests"
+ run: "make tests"
+
+ static-analysis:
+ name: "PHPStan"
+ runs-on: "ubuntu-latest"
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version:
+ - "7.2"
+ - "7.3"
+ - "7.4"
+ - "8.0"
+ - "8.1"
+ - "8.2"
+ dependencies:
+ - "lowest"
+ - "highest"
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v3
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+ extensions: mbstring
+ tools: composer:v2
+
+ - name: "Install lowest dependencies"
+ if: ${{ matrix.dependencies == 'lowest' }}
+ run: "composer update --prefer-lowest --no-interaction --no-progress"
+
+ - name: "Install highest dependencies"
+ if: ${{ matrix.dependencies == 'highest' }}
+ run: "composer update --no-interaction --no-progress"
+
+ - name: "Downgrade PHPUnit"
+ if: matrix.php-version == '7.2' || matrix.php-version == '7.3'
+ run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies"
+
+ - name: "PHPStan"
+ run: "make phpstan"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2db2131
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/tests/tmp
+/vendor
+/composer.lock
+.phpunit.result.cache
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..37a419f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# Rules for detecting usage of pseudo-private functions, classes, and methods.
+
+[](https://github.com/swissspidy/phpstan-no-private/actions)
+[](https://packagist.org/packages/swissspidy/phpstan-no-private)
+[](https://packagist.org/packages/swissspidy/phpstan-no-private)
+
+This extension emits deprecation warnings on code which uses properties/functions/methods/classes which are annotated as `@access private`.
+
+## Installation
+
+To use this extension, require it in [Composer](https://getcomposer.org/):
+
+```
+composer require --dev swissspidy/phpstan-no-private
+```
+
+If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set!
+
+
+ Manual installation
+
+If you don't want to use `phpstan/extension-installer`, include rules.neon in your project's PHPStan config:
+
+```
+includes:
+ - vendor/swissspidy/phpstan-no-private/rules.neon
+```
+
+
+## Credits
+
+This project is a fork of the excellent [phpstan/phpstan-deprecation-rules](https://github.com/phpstan/phpstan-deprecation-rules),
+which provides rules that detect usage of deprecated classes, methods, properties, constants and traits.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..84dccad
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,55 @@
+{
+ "name": "swissspidy/phpstan-no-private",
+ "type": "phpstan-extension",
+ "description": "PHPStan rules for detecting usage of pseudo-private functions, classes, and methods.",
+ "license": [
+ "MIT"
+ ],
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.10.3"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpstan/phpstan-php-parser": "^1.1",
+ "phpunit/phpunit": "^9.5",
+ "slevomat/coding-standard": "^8.8.0",
+ "squizlabs/php_codesniffer": "^3.5.3"
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ },
+ "platform": {
+ "php": "7.4.6"
+ },
+ "sort-packages": true
+ },
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Swissspidy\\PHPStan\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "classmap": [
+ "tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "scripts": {
+ "lint": "vendor/bin/phpcs",
+ "lint:fix": "vendor/bin/phpcbf",
+ "phpstan": "vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests",
+ "test": "vendor/bin/phpunit"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..4d95842
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+ src
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/*/data
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..e3c1480
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,9 @@
+includes:
+ - rules.neon
+ - vendor/phpstan/phpstan/conf/bleedingEdge.neon
+ - vendor/phpstan/phpstan-php-parser/extension.neon
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+
+parameters:
+ excludePaths:
+ - tests/*/data/*
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..edb71a8
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ ./src
+
+
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
diff --git a/rules.neon b/rules.neon
new file mode 100644
index 0000000..e02da1d
--- /dev/null
+++ b/rules.neon
@@ -0,0 +1,20 @@
+parameters:
+ deprecationRulesInstalled: true
+
+rules:
+ - Swissspidy\PHPStan\Rules\NoPrivate\AccessPrivatePropertyRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\AccessPrivateStaticPropertyRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\CallToPrivateFunctionRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\CallToPrivateMethodRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\CallToPrivateStaticMethodRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\FetchingClassConstOfPrivateClassRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\FetchingPrivateConstRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\ImplementationOfPrivateInterfaceRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\InheritanceOfPrivateClassRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\InheritanceOfPrivateInterfaceRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\InstantiationOfPrivateClassRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\TypeHintPrivateInClassMethodSignatureRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\TypeHintPrivateInClosureSignatureRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\TypeHintPrivateInFunctionSignatureRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\UsageOfPrivateCastRule
+ - Swissspidy\PHPStan\Rules\NoPrivate\UsageOfPrivateTraitRule
diff --git a/src/Rules/NoPrivate/CallToPrivateFunctionRule.php b/src/Rules/NoPrivate/CallToPrivateFunctionRule.php
new file mode 100644
index 0000000..b3b6b69
--- /dev/null
+++ b/src/Rules/NoPrivate/CallToPrivateFunctionRule.php
@@ -0,0 +1,74 @@
+
+ */
+class CallToPrivateFunctionRule implements Rule
+{
+
+ /** @var ReflectionProvider */
+ private $reflectionProvider;
+
+ /** @var FileTypeMapper */
+ private $fileTypeMapper;
+
+ public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper)
+ {
+ $this->reflectionProvider = $reflectionProvider;
+ $this->fileTypeMapper = $fileTypeMapper;
+ }
+
+ public function getNodeType(): string
+ {
+ return FuncCall::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ if (!($node->name instanceof Name)) {
+ return [];
+ }
+
+ try {
+ $function = $this->reflectionProvider->getFunction($node->name, $scope);
+ } catch (FunctionNotFoundException $e) {
+ // Other rules will notify if the function is not found
+ return [];
+ }
+
+ $docComment = $function->getDocComment();
+
+ if (!$docComment) {
+ return [];
+ }
+
+ $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
+ $scope->getFile(),
+ $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
+ $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
+ $function->getName(),
+ $docComment
+ );
+ if (!$resolvedPhpDoc || (!$function->isInternal() && !PrivateAnnotationHelper::isPrivate($resolvedPhpDoc))) {
+ return [];
+ }
+
+ return [sprintf(
+ 'Call to private function %s().',
+ $function->getName()
+ )];
+ }
+
+}
diff --git a/src/Rules/NoPrivate/CallToPrivateMethodRule.php b/src/Rules/NoPrivate/CallToPrivateMethodRule.php
new file mode 100644
index 0000000..ed8faef
--- /dev/null
+++ b/src/Rules/NoPrivate/CallToPrivateMethodRule.php
@@ -0,0 +1,86 @@
+
+ */
+class CallToPrivateMethodRule implements Rule
+{
+
+ /** @var ReflectionProvider */
+ private $reflectionProvider;
+
+ /** @var FileTypeMapper */
+ private $fileTypeMapper;
+
+ public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper)
+ {
+ $this->reflectionProvider = $reflectionProvider;
+ $this->fileTypeMapper = $fileTypeMapper;
+ }
+
+ public function getNodeType(): string
+ {
+ return MethodCall::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ if (!$node->name instanceof Identifier) {
+ return [];
+ }
+
+ $methodName = $node->name->name;
+ $methodCalledOnType = $scope->getType($node->var);
+ $referencedClasses = $methodCalledOnType->getObjectClassNames();
+
+ foreach ($referencedClasses as $referencedClass) {
+ try {
+ $classReflection = $this->reflectionProvider->getClass($referencedClass);
+ $methodReflection = $classReflection->getMethod($methodName, $scope);
+
+ $docComment = $methodReflection->getDocComment();
+
+ if (!$docComment) {
+ continue;
+ }
+
+ $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
+ $scope->getFile(),
+ $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
+ $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
+ $methodReflection->getName(),
+ $docComment
+ );
+ if (!$resolvedPhpDoc || (!$methodReflection->isInternal() && !PrivateAnnotationHelper::isPrivate($resolvedPhpDoc))) {
+ continue;
+ }
+
+ return [sprintf(
+ 'Call to private method %s() of class %s.',
+ $methodReflection->getName(),
+ $methodReflection->getDeclaringClass()->getName()
+ )];
+ } catch (ClassNotFoundException $e) {
+ // Other rules will notify if the class is not found
+ } catch (MissingMethodFromReflectionException $e) {
+ // Other rules will notify if the the method is not found
+ }
+ }
+
+ return [];
+ }
+
+}
diff --git a/src/Rules/NoPrivate/CallToPrivateStaticMethodRule.php b/src/Rules/NoPrivate/CallToPrivateStaticMethodRule.php
new file mode 100644
index 0000000..3e466af
--- /dev/null
+++ b/src/Rules/NoPrivate/CallToPrivateStaticMethodRule.php
@@ -0,0 +1,124 @@
+
+ */
+class CallToPrivateStaticMethodRule implements Rule
+{
+
+ /** @var ReflectionProvider */
+ private $reflectionProvider;
+
+ /** @var RuleLevelHelper */
+ private $ruleLevelHelper;
+
+ /** @var FileTypeMapper */
+ private $fileTypeMapper;
+
+ public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper, FileTypeMapper $fileTypeMapper)
+ {
+ $this->reflectionProvider = $reflectionProvider;
+ $this->ruleLevelHelper = $ruleLevelHelper;
+ $this->fileTypeMapper = $fileTypeMapper;
+ }
+
+ public function getNodeType(): string
+ {
+ return StaticCall::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ if (!$node->name instanceof Identifier) {
+ return [];
+ }
+
+ $methodName = $node->name->name;
+ $referencedClasses = [];
+
+ if ($node->class instanceof Name) {
+ $referencedClasses[] = $scope->resolveName($node->class);
+ } else {
+ $classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
+ $scope,
+ $node->class,
+ '', // We don't care about the error message
+ static function (Type $type) use ($methodName): bool {
+ return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes();
+ }
+ );
+
+ if ($classTypeResult->getType() instanceof ErrorType) {
+ return [];
+ }
+
+ $referencedClasses = $classTypeResult->getReferencedClasses();
+ }
+
+ $errors = [];
+
+ foreach ($referencedClasses as $referencedClass) {
+ try {
+ $class = $this->reflectionProvider->getClass($referencedClass);
+ $methodReflection = $class->getMethod($methodName, $scope);
+ } catch (ClassNotFoundException $e) {
+ continue;
+ } catch (MissingMethodFromReflectionException $e) {
+ continue;
+ }
+
+ $resolvedPhpDoc = $class->getResolvedPhpDoc();
+ if (($class->isInternal() || ( $resolvedPhpDoc && PrivateAnnotationHelper::isPrivate($resolvedPhpDoc)))) {
+ $errors[] = sprintf(
+ 'Call to method %s() of private class %s.',
+ $methodReflection->getName(),
+ $methodReflection->getDeclaringClass()->getName()
+ );
+ continue;
+ }
+
+ $docComment = $methodReflection->getDocComment();
+
+ if (!$docComment) {
+ continue;
+ }
+
+ $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
+ $scope->getFile(),
+ $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
+ $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
+ $methodReflection->getName(),
+ $docComment
+ );
+ if (!$resolvedPhpDoc || (!$methodReflection->isInternal() && !PrivateAnnotationHelper::isPrivate($resolvedPhpDoc))) {
+ continue;
+ }
+
+ $errors[] = sprintf(
+ 'Call to private method %s() of class %s.',
+ $methodReflection->getName(),
+ $methodReflection->getDeclaringClass()->getName()
+ );
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/Rules/NoPrivate/InheritanceOfPrivateClassRule.php b/src/Rules/NoPrivate/InheritanceOfPrivateClassRule.php
new file mode 100644
index 0000000..d4931dc
--- /dev/null
+++ b/src/Rules/NoPrivate/InheritanceOfPrivateClassRule.php
@@ -0,0 +1,78 @@
+
+ */
+class InheritanceOfPrivateClassRule implements Rule
+{
+
+ /** @var ReflectionProvider */
+ private $reflectionProvider;
+
+ public function __construct(ReflectionProvider $reflectionProvider)
+ {
+ $this->reflectionProvider = $reflectionProvider;
+ }
+
+ public function getNodeType(): string
+ {
+ return Class_::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ if ($node->extends === null) {
+ return [];
+ }
+
+ $errors = [];
+
+ $className = isset($node->namespacedName)
+ ? (string) $node->namespacedName
+ : (string) $node->name;
+
+ try {
+ $class = $this->reflectionProvider->getClass($className);
+ } catch (ClassNotFoundException $e) {
+ return [];
+ }
+
+ $parentClassName = (string) $node->extends;
+
+ try {
+ $parentClass = $this->reflectionProvider->getClass($parentClassName);
+ $resolvedPhpDoc = $parentClass->getResolvedPhpDoc();
+ if (!$resolvedPhpDoc || (!$parentClass->isInternal() && !PrivateAnnotationHelper::isPrivate($resolvedPhpDoc))) {
+ return $errors;
+ }
+
+ if (!$class->isAnonymous()) {
+ $errors[] = sprintf(
+ 'Class %s extends private class %s.',
+ $className,
+ $parentClassName
+ );
+ } else {
+ $errors[] = sprintf(
+ 'Anonymous class extends private class %s.',
+ $parentClassName
+ );
+ }
+ } catch (ClassNotFoundException $e) {
+ // Other rules will notify if the interface is not found
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/Rules/NoPrivate/InstantiationOfPrivateClassRule.php b/src/Rules/NoPrivate/InstantiationOfPrivateClassRule.php
new file mode 100644
index 0000000..39f8b47
--- /dev/null
+++ b/src/Rules/NoPrivate/InstantiationOfPrivateClassRule.php
@@ -0,0 +1,92 @@
+
+ */
+class InstantiationOfPrivateClassRule implements Rule
+{
+
+ /** @var ReflectionProvider */
+ private $reflectionProvider;
+
+ /** @var RuleLevelHelper */
+ private $ruleLevelHelper;
+
+ public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper)
+ {
+ $this->reflectionProvider = $reflectionProvider;
+ $this->ruleLevelHelper = $ruleLevelHelper;
+ }
+
+ public function getNodeType(): string
+ {
+ return New_::class;
+ }
+
+ public function processNode(Node $node, Scope $scope): array
+ {
+ $referencedClasses = [];
+
+ if ($node->class instanceof Name) {
+ $referencedClasses[] = $scope->resolveName($node->class);
+ } elseif ($node->class instanceof Class_) {
+ if (!isset($node->class->namespacedName)) {
+ return [];
+ }
+
+ $referencedClasses[] = $scope->resolveName($node->class->namespacedName);
+ } else {
+ $classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
+ $scope,
+ $node->class,
+ '', // We don't care about the error message
+ static function (): bool {
+ return true;
+ }
+ );
+
+ if ($classTypeResult->getType() instanceof ErrorType) {
+ return [];
+ }
+
+ $referencedClasses = $classTypeResult->getReferencedClasses();
+ }
+
+ $errors = [];
+
+ foreach ($referencedClasses as $referencedClass) {
+ try {
+ $class = $this->reflectionProvider->getClass($referencedClass);
+ } catch (ClassNotFoundException $e) {
+ continue;
+ }
+
+ $resolvedPhpDoc = $class->getResolvedPhpDoc();
+ if (!$resolvedPhpDoc || (!$class->isInternal() && !PrivateAnnotationHelper::isPrivate($resolvedPhpDoc))) {
+ continue;
+ }
+
+ $errors[] = sprintf(
+ 'Instantiation of private class %s.',
+ $referencedClass
+ );
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/Rules/NoPrivate/PrivateAnnotationHelper.php b/src/Rules/NoPrivate/PrivateAnnotationHelper.php
new file mode 100644
index 0000000..48be3aa
--- /dev/null
+++ b/src/Rules/NoPrivate/PrivateAnnotationHelper.php
@@ -0,0 +1,30 @@
+getPhpDocNodes() as $phpDocNode) {
+ foreach(array_column($phpDocNode->getTagsByName('@access'), 'value') as $accessTagValue) {
+ $scope = (string) $accessTagValue;
+ if ( 'private' === $scope) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/tests/Rules/NoPrivate/CallToPrivateFunctionRuleTest.php b/tests/Rules/NoPrivate/CallToPrivateFunctionRuleTest.php
new file mode 100644
index 0000000..6457f10
--- /dev/null
+++ b/tests/Rules/NoPrivate/CallToPrivateFunctionRuleTest.php
@@ -0,0 +1,38 @@
+
+ */
+class CallToPrivateFunctionRuleTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ return new CallToPrivateFunctionRule($this->createReflectionProvider(), self::getContainer()->getByType(FileTypeMapper::class));
+ }
+
+ public function testPrivateFunctionCall(): void
+ {
+ require_once __DIR__ . '/data/call-to-private-function-definition.php';
+ $this->analyse(
+ [__DIR__ . '/data/call-to-private-function.php'],
+ [
+ [
+ 'Call to private function CheckPrivateFunctionCall\private_function().',
+ 8,
+ ],
+ [
+ 'Call to private function CheckPrivateFunctionCall\private_function().',
+ 9,
+ ],
+ ]
+ );
+ }
+
+}
diff --git a/tests/Rules/NoPrivate/CallToPrivateMethodRuleTest.php b/tests/Rules/NoPrivate/CallToPrivateMethodRuleTest.php
new file mode 100644
index 0000000..b7954c2
--- /dev/null
+++ b/tests/Rules/NoPrivate/CallToPrivateMethodRuleTest.php
@@ -0,0 +1,34 @@
+
+ */
+class CallToPrivateMethodRuleTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ return new CallToPrivateMethodRule($this->createReflectionProvider(), self::getContainer()->getByType(FileTypeMapper::class));
+ }
+
+ public function testPrivateMethodCall(): void
+ {
+ require_once __DIR__ . '/data/call-to-private-method-definition.php';
+ $this->analyse(
+ [__DIR__ . '/data/call-to-private-method.php'],
+ [
+ [
+ 'Call to private method privateFoo() of class CheckPrivateMethodCall\Foo.',
+ 7,
+ ],
+ ]
+ );
+ }
+
+}
diff --git a/tests/Rules/NoPrivate/CallToPrivateStaticMethodRuleTest.php b/tests/Rules/NoPrivate/CallToPrivateStaticMethodRuleTest.php
new file mode 100644
index 0000000..ad89921
--- /dev/null
+++ b/tests/Rules/NoPrivate/CallToPrivateStaticMethodRuleTest.php
@@ -0,0 +1,67 @@
+
+ */
+class CallToPrivateStaticMethodRuleTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ return new CallToPrivateStaticMethodRule($this->createReflectionProvider(), self::getContainer()->getByType(RuleLevelHelper::class), self::getContainer()->getByType(FileTypeMapper::class));
+ }
+
+ public function testPrivateStaticMethodCall(): void
+ {
+ require_once __DIR__ . '/data/call-to-private-static-method-definition.php';
+ $this->analyse(
+ [__DIR__ . '/data/call-to-private-static-method.php'],
+ [
+ [
+ 'Call to private method privateFoo() of class CheckPrivateStaticMethodCall\Foo.',
+ 6,
+ ],
+ [
+ 'Call to method foo() of private class CheckPrivateStaticMethodCall\Foo.',
+ 10,
+ ],
+ [
+ 'Call to method privateFoo() of private class CheckPrivateStaticMethodCall\Foo.',
+ 11,
+ ],
+ [
+ 'Call to private method privateFoo() of class CheckPrivateStaticMethodCall\Foo.',
+ 19,
+ ],
+ [
+ 'Call to private method privateFoo() of class CheckPrivateStaticMethodCall\Foo.',
+ 28,
+ ],
+ [
+ 'Call to private method privateFoo() of class CheckPrivateStaticMethodCall\Foo.',
+ 44,
+ ],
+ [
+ 'Call to private method privateOtherFoo() of class CheckPrivateStaticMethodCall\Child.',
+ 45,
+ ],
+ [
+ 'Call to private method privateFoo() of class CheckPrivateStaticMethodCall\Foo.',
+ 46,
+ ],
+ [
+ 'Call to private method privateOtherFoo() of class CheckPrivateStaticMethodCall\Child.',
+ 47,
+ ],
+ ]
+ );
+ }
+
+}
diff --git a/tests/Rules/NoPrivate/InheritanceOfPrivateClassRuleTest.php b/tests/Rules/NoPrivate/InheritanceOfPrivateClassRuleTest.php
new file mode 100644
index 0000000..28ee130
--- /dev/null
+++ b/tests/Rules/NoPrivate/InheritanceOfPrivateClassRuleTest.php
@@ -0,0 +1,47 @@
+
+ */
+class InheritanceOfPrivateClassRuleTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ return new InheritanceOfPrivateClassRule($this->createReflectionProvider());
+ }
+
+ public function testInheritanceOfPrivateClassInClasses(): void
+ {
+ require_once __DIR__ . '/data/inheritance-of-private-class-definition.php';
+ $this->analyse(
+ [__DIR__ . '/data/inheritance-of-private-class-in-classes.php'],
+ [
+ [
+ 'Class InheritanceOfPrivateClass\Bar2 extends private class InheritanceOfPrivateClass\PrivateFoo.',
+ 10,
+ ],
+ ]
+ );
+ }
+
+ public function testInheritanceOfPrivateClassInAnonymousClasses(): void
+ {
+ require_once __DIR__ . '/data/inheritance-of-private-class-definition.php';
+ $this->analyse(
+ [__DIR__ . '/data/inheritance-of-private-class-in-anonymous-classes.php'],
+ [
+ [
+ 'Anonymous class extends private class InheritanceOfPrivateClass\PrivateFoo.',
+ 9,
+ ],
+ ]
+ );
+ }
+
+}
diff --git a/tests/Rules/NoPrivate/InstantiationOfPrivateClassRuleTest.php b/tests/Rules/NoPrivate/InstantiationOfPrivateClassRuleTest.php
new file mode 100644
index 0000000..e2e1ba3
--- /dev/null
+++ b/tests/Rules/NoPrivate/InstantiationOfPrivateClassRuleTest.php
@@ -0,0 +1,34 @@
+
+ */
+class InstantiationOfPrivateClassRuleTest extends RuleTestCase
+{
+
+ protected function getRule(): Rule
+ {
+ return new InstantiationOfPrivateClassRule($this->createReflectionProvider(), self::getContainer()->getByType(RuleLevelHelper::class));
+ }
+
+ public function testInstantiationOfPrivateClass(): void
+ {
+ require_once __DIR__ . '/data/instantiation-of-private-class-definition.php';
+ $this->analyse(
+ [__DIR__ . '/data/instantiation-of-private-class.php'],
+ [
+ [
+ 'Instantiation of private class InstantiationOfPrivateClass\PrivateFoo.',
+ 6,
+ ],
+ ]
+ );
+ }
+
+}
diff --git a/tests/Rules/NoPrivate/data/call-to-private-function-definition.php b/tests/Rules/NoPrivate/data/call-to-private-function-definition.php
new file mode 100644
index 0000000..b217f24
--- /dev/null
+++ b/tests/Rules/NoPrivate/data/call-to-private-function-definition.php
@@ -0,0 +1,17 @@
+foo();
+$foo->privateFoo();
diff --git a/tests/Rules/NoPrivate/data/call-to-private-static-method-definition.php b/tests/Rules/NoPrivate/data/call-to-private-static-method-definition.php
new file mode 100644
index 0000000..e3d1e54
--- /dev/null
+++ b/tests/Rules/NoPrivate/data/call-to-private-static-method-definition.php
@@ -0,0 +1,38 @@
+