diff --git a/src/Expectation.php b/src/Expectation.php index 927998bb..7943f6d0 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,6 +7,7 @@ namespace Pest; use BadMethodCallException; use Closure; use Pest\Arch\Contracts\ArchExpectation; +use Pest\Arch\Expectations\Targeted; use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeUsedIn; use Pest\Arch\Expectations\ToBeUsedInNothing; @@ -14,6 +15,7 @@ use Pest\Arch\Expectations\ToOnlyBeUsedIn; use Pest\Arch\Expectations\ToOnlyUse; use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseNothing; +use Pest\Arch\Support\FileLineFinder; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; use Pest\Concerns\Retrievable; @@ -376,10 +378,13 @@ final class Expectation */ public function toUseStrictTypes(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'to use strict types', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, ' $object->reflectionClass->isFinal(), 'to be final', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -400,10 +408,13 @@ final class Expectation */ public function toBeReadonly(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isReadOnly(), 'to be readonly', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -412,10 +423,13 @@ final class Expectation */ public function toBeTrait(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isTrait(), 'to be trait', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -424,10 +438,13 @@ final class Expectation */ public function toBeAbstract(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isAbstract(), 'to be abstract', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -436,10 +453,13 @@ final class Expectation */ public function toBeEnum(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isEnum(), 'to be enum', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -448,10 +468,13 @@ final class Expectation */ public function toBeInterface(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isInterface(), 'to be interface', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -462,10 +485,13 @@ final class Expectation */ public function toExtend(string $class): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->isSubclassOf($class), sprintf("to extend '%s'", $class), + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -474,10 +500,76 @@ final class Expectation */ public function toExtendNothing(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() === false, "to extend nothing", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to not implement any interfaces. + */ + public function toImplementNothing(): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() === [], + "to implement nothing", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to only implement the given interfaces. + */ + public function toOnlyImplement(array|string $interfaces): ArchExpectation + { + $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; + + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => count($interfaces) === count($object->reflectionClass->getInterfaceNames()) + && array_diff($interfaces, $object->reflectionClass->getInterfaceNames()) === [], + "to only implement '".implode("', '", (array) $interfaces)."'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to have the given suffix. + */ + public function toHaveSuffix(string $suffix): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => str_ends_with($object->reflectionClass->getName(), $suffix), + "to have suffix '{$suffix}'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to have the given suffix. + */ + public function toHavePrefix(string $suffix): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => str_starts_with($object->reflectionClass->getName(), $suffix), + "to have prefix '{$suffix}'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -488,7 +580,7 @@ final class Expectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; - return ToBe::make( + return Targeted::make( $this, function (ObjectDescription $object) use ($interfaces) : bool { foreach ($interfaces as $interface) { @@ -500,6 +592,9 @@ final class Expectation return true; }, "to implement '".implode("', '", (array) $interfaces)."'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php index bf2f49fe..310ad3ba 100644 --- a/src/Expectations/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -7,6 +7,7 @@ namespace Pest\Expectations; use Pest\Arch\Contracts\ArchExpectation; use Pest\Arch\Exceptions\ArchExpectationFailedException; use Pest\Arch\Expectations\NotToUseStrictTypes; +use Pest\Arch\Expectations\Targeted; use Pest\Arch\Expectations\ToBe; use Pest\Arch\Expectations\ToBeFinal; use Pest\Arch\Expectations\ToBeUsedIn; @@ -15,6 +16,7 @@ use Pest\Arch\Expectations\ToUse; use Pest\Arch\Expectations\ToUseStrictTypes; use Pest\Arch\GroupArchExpectation; use Pest\Arch\SingleArchExpectation; +use Pest\Arch\Support\FileLineFinder; use Pest\Exceptions\InvalidExpectation; use Pest\Expectation; use Pest\Support\Arr; @@ -83,10 +85,13 @@ final class OppositeExpectation */ public function toUseStrictTypes(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), 'declare(strict_types=1);'), 'not to use strict types', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isFinal(), 'not to be final', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -107,10 +115,13 @@ final class OppositeExpectation */ public function toBeReadonly(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isReadOnly(), 'not to be readonly', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -119,10 +130,13 @@ final class OppositeExpectation */ public function toBeTrait(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isTrait(), 'not to be trait', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -131,10 +145,13 @@ final class OppositeExpectation */ public function toBeAbstract(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isAbstract(), 'not to be abstract', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -143,10 +160,13 @@ final class OppositeExpectation */ public function toBeEnum(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isEnum(), 'not to be enum', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -155,10 +175,13 @@ final class OppositeExpectation */ public function toBeInterface(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isInterface(), 'not to be interface', + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -169,10 +192,13 @@ final class OppositeExpectation */ public function toExtend(string $class): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => ! $object->reflectionClass->isSubclassOf($class), sprintf("not to extend '%s'", $class), + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -181,10 +207,13 @@ final class OppositeExpectation */ public function toExtendNothing(): ArchExpectation { - return ToBe::make( + return Targeted::make( $this->original, fn (ObjectDescription $object): bool => $object->reflectionClass->getParentClass() !== false, "to extend a class", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } @@ -195,7 +224,7 @@ final class OppositeExpectation { $interfaces = is_array($interfaces) ? $interfaces : [$interfaces]; - return ToBe::make( + return Targeted::make( $this->original, function (ObjectDescription $object) use ($interfaces) : bool { foreach ($interfaces as $interface) { @@ -207,9 +236,45 @@ final class OppositeExpectation return true; }, "not to implement '".implode("', '", (array) $interfaces)."'", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), ); } + /** + * Asserts that the given expectation target to not implement any interfaces. + */ + public function toImplementNothing(): ArchExpectation + { + return Targeted::make( + $this, + fn (ObjectDescription $object): bool => $object->reflectionClass->getInterfaceNames() !== [], + "to implement an interface", + FileLineFinder::where(function (string $line): bool { + return str_contains($line, 'class'); + }), + ); + } + + /** + * Asserts that the given expectation target to not only implement the given interfaces. + */ + public function toOnlyImplement(array|string $interfaces): ArchExpectation + { + throw InvalidExpectation::fromMethods(['not', 'toOnlyImplement']); + } + + public function toHavePrefix(string $suffix): never + { + throw InvalidExpectation::fromMethods(['not', 'toHavePrefix']); + } + + public function toHaveSuffix(string $suffix): never + { + throw InvalidExpectation::fromMethods(['not', 'toHaveSuffix']); + } + /** * @param array|string $targets */ @@ -218,6 +283,7 @@ final class OppositeExpectation throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']); } + public function toUseNothing(): never { throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);