diff --git a/src/ArchPresets/Laravel.php b/src/ArchPresets/Laravel.php index 5b52fea5..971aea63 100644 --- a/src/ArchPresets/Laravel.php +++ b/src/ArchPresets/Laravel.php @@ -31,14 +31,15 @@ final class Laravel extends AbstractPreset ->not->toImplement(Throwable::class) ->ignoring('App\Exceptions'); - $this->expectations[] = expect('App\Http\Controllers') - ->classes() - ->toHaveSuffix('Controller'); - $this->expectations[] = expect('App') ->not->toHaveSuffix('Controller') ->ignoring('App\Http\Controllers'); + $this->expectations[] = expect('App\Http\Controllers') + ->classes() + ->toHaveSuffix('Controller') + ->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy']); + $this->expectations[] = expect('App\Http\Middleware') ->classes() ->toHaveMethod('handle'); diff --git a/src/ArchPresets/Relaxed.php b/src/ArchPresets/Relaxed.php index bea4d0e3..4513fa5b 100644 --- a/src/ArchPresets/Relaxed.php +++ b/src/ArchPresets/Relaxed.php @@ -20,6 +20,7 @@ final class Relaxed extends AbstractPreset $this->eachUserNamespace( fn (Expectation $namespace): ArchExpectation => $namespace->not->toUseStrictTypes(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeFinal(), + fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHavePrivateMethodsBesides([]), ); } } diff --git a/src/ArchPresets/Strict.php b/src/ArchPresets/Strict.php index 9e994147..32fb4cce 100644 --- a/src/ArchPresets/Strict.php +++ b/src/ArchPresets/Strict.php @@ -21,6 +21,7 @@ final class Strict extends AbstractPreset fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(), fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(), + fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethodsBesides([]) ); $this->expectations[] = expect([ diff --git a/src/Expectation.php b/src/Expectation.php index 5efeb8e9..ec1cc7df 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -588,6 +588,30 @@ final class Expectation ); } + /** + * Not supported. + */ + public function toHavePublicMethodsBesides(): never + { + throw InvalidExpectation::fromMethods(['toHavePublicMethodsBesides']); + } + + /** + * Not supported. + */ + public function toHaveProtectedMethodsBesides(): never + { + throw InvalidExpectation::fromMethods(['toHaveProtectedMethodsBesides']); + } + + /** + * Not supported. + */ + public function toHavePrivateMethodsBesides(): never + { + throw InvalidExpectation::fromMethods(['toHavePrivateMethodsBesides']); + } + /** * Asserts that the given expectation target is enum. */ diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index eba4ac5a..7444c176 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -224,6 +224,93 @@ final class OppositeExpectation ); } + /** + * Asserts that the given expectation target not to have the public methods besides the given methods. + */ + public function toHavePublicMethodsBesides(array|string $methods): ArchExpectation + { + $methods = is_array($methods) ? $methods : [$methods]; + + return Targeted::make( + $this->original, + function (ObjectDescription $object) use ($methods): bool { + $reflectionMethods = isset($object->reflectionClass) + ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PUBLIC) + : []; + + foreach ($reflectionMethods as $reflectionMethod) { + if (! in_array($reflectionMethod->name, $methods, true)) { + return false; + } + } + + return true; + }, + count($methods) === 0 + ? 'not to have public methods' + : sprintf("not to have public methods besides '%s'", implode("', '", $methods)), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'public function')), + ); + } + + /** + * Asserts that the given expectation target not to have the protected methods besides the given methods. + */ + public function toHaveProtectedMethodsBesides(array|string $methods): ArchExpectation + { + $methods = is_array($methods) ? $methods : [$methods]; + + return Targeted::make( + $this->original, + function (ObjectDescription $object) use ($methods): bool { + $reflectionMethods = isset($object->reflectionClass) + ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PROTECTED) + : []; + + foreach ($reflectionMethods as $reflectionMethod) { + if (! in_array($reflectionMethod->name, $methods, true)) { + return false; + } + } + + return true; + }, + count($methods) === 0 + ? 'not to have protected methods' + : sprintf("not to have protected methods besides '%s'", implode("', '", $methods)), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'protected function')), + ); + } + + /** + * Asserts that the given expectation target not to have the private methods besides the given methods. + */ + public function toHavePrivateMethodsBesides(array|string $methods): ArchExpectation + { + $methods = is_array($methods) ? $methods : [$methods]; + + return Targeted::make( + $this->original, + function (ObjectDescription $object) use ($methods): bool { + $reflectionMethods = isset($object->reflectionClass) + ? Reflection::getMethodsFromReflectionClass($object->reflectionClass, ReflectionMethod::IS_PRIVATE) + : []; + + foreach ($reflectionMethods as $reflectionMethod) { + if (! in_array($reflectionMethod->name, $methods, true)) { + return false; + } + } + + return true; + }, + count($methods) === 0 + ? 'not to have private methods' + : sprintf("not to have private methods besides '%s'", implode("', '", $methods)), + FileLineFinder::where(fn (string $line): bool => str_contains($line, 'private function')), + ); + } + /** * Asserts that the given expectation target is not enum. */ diff --git a/src/Logging/TeamCity/Subscriber/Subscriber.php b/src/Logging/TeamCity/Subscriber/Subscriber.php index 6b306695..d5eaa1f8 100644 --- a/src/Logging/TeamCity/Subscriber/Subscriber.php +++ b/src/Logging/TeamCity/Subscriber/Subscriber.php @@ -19,7 +19,7 @@ abstract class Subscriber // @pest-arch-ignore-line /** * Creates a new TeamCityLogger instance. */ - final protected function logger(): TeamCityLogger + final protected function logger(): TeamCityLogger // @pest-arch-ignore-line { return $this->logger; } diff --git a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php index 2e91ac4c..d2801ced 100644 --- a/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php +++ b/src/Plugins/Parallel/Paratest/CleanConsoleOutput.php @@ -11,7 +11,7 @@ final class CleanConsoleOutput extends ConsoleOutput /** * {@inheritdoc} */ - protected function doWrite(string $message, bool $newline): void + protected function doWrite(string $message, bool $newline): void // @pest-arch-ignore-line { if ($this->isOpeningHeadline($message)) { return; diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 5ffc26ae..d5526069 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -258,12 +258,12 @@ final class Reflection * @param ReflectionClass $reflectionClass * @return array */ - public static function getMethodsFromReflectionClass(ReflectionClass $reflectionClass): array + public static function getMethodsFromReflectionClass(ReflectionClass $reflectionClass, int $filter = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED | ReflectionMethod::IS_PRIVATE): array { $getMethods = fn (ReflectionClass $reflectionClass): array => array_filter( array_map( fn (ReflectionMethod $method): \ReflectionMethod => $method, - $reflectionClass->getMethods(), + $reflectionClass->getMethods($filter), ), fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $reflectionClass->getName(), ); diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 6c685f40..e3919000 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -1,6 +1,6 @@ PASS Tests\Arch - ✓ arch "base" preset + ✓ arch "php" preset ✓ arch "strict" preset ✓ arch "security" preset ✓ globals @@ -831,6 +831,10 @@ ✓ opposite missing prefix ✓ opposite has prefix + PASS Tests\Features\Expect\toHavePrivateMethodsBesides + ✓ pass + ✓ failures + PASS Tests\Features\Expect\toHaveProperties ✓ pass ✓ failures @@ -849,6 +853,14 @@ ✓ failures with message and Any matcher ✓ not failures + PASS Tests\Features\Expect\toHaveProtectedMethodsBesides + ✓ pass + ✓ failures + + PASS Tests\Features\Expect\toHavePublicMethodsBesides + ✓ pass + ✓ failures + PASS Tests\Features\Expect\toHaveSameSize ✓ failures with wrong type ✓ pass @@ -1547,4 +1559,4 @@ WARN Tests\Visual\Version - visual snapshot of help command output - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 24 skipped, 1078 passed (2632 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 24 skipped, 1084 passed (2644 assertions) \ No newline at end of file diff --git a/tests/Features/Expect/toHavePrivateMethodsBesides.php b/tests/Features/Expect/toHavePrivateMethodsBesides.php new file mode 100644 index 00000000..fc2c55ba --- /dev/null +++ b/tests/Features/Expect/toHavePrivateMethodsBesides.php @@ -0,0 +1,12 @@ +not->toHavePrivateMethodsBesides(['privateMethod']); +}); + +test('failures', function () { + expect(UserController::class)->not->toHavePrivateMethodsBesides([]); +})->throws(ArchExpectationFailedException::class); diff --git a/tests/Features/Expect/toHaveProtectedMethodsBesides.php b/tests/Features/Expect/toHaveProtectedMethodsBesides.php new file mode 100644 index 00000000..e232ec14 --- /dev/null +++ b/tests/Features/Expect/toHaveProtectedMethodsBesides.php @@ -0,0 +1,12 @@ +not->toHaveProtectedMethodsBesides(['protectedMethod']); +}); + +test('failures', function () { + expect(UserController::class)->not->toHaveProtectedMethodsBesides([]); +})->throws(ArchExpectationFailedException::class); diff --git a/tests/Features/Expect/toHavePublicMethodsBesides.php b/tests/Features/Expect/toHavePublicMethodsBesides.php new file mode 100644 index 00000000..889a266c --- /dev/null +++ b/tests/Features/Expect/toHavePublicMethodsBesides.php @@ -0,0 +1,12 @@ +not->toHavePublicMethodsBesides(['publicMethod']); +}); + +test('failures', function () { + expect(UserController::class)->not->toHavePublicMethodsBesides([]); +})->throws(ArchExpectationFailedException::class); diff --git a/tests/Fixtures/Arch/ToHavePublicMethodsBesides/UserController.php b/tests/Fixtures/Arch/ToHavePublicMethodsBesides/UserController.php new file mode 100644 index 00000000..a8cd2767 --- /dev/null +++ b/tests/Fixtures/Arch/ToHavePublicMethodsBesides/UserController.php @@ -0,0 +1,23 @@ +toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 19 skipped, 1064 passed (2602 assertions)') + ->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 19 skipped, 1070 passed (2612 assertions)') ->toContain('Parallel: 3 processes'); })->skipOnWindows();