diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e7c192..be7a600 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,3 +20,22 @@ jobs: cs-check: uses: zenstruck/.github/.github/workflows/php-cs-fixer.yml@main + + sca: + name: Static Code Analysis + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + coverage: none + + - name: Install Dependencies + uses: ramsey/composer-install@v1 + + - name: Run PHPStan + run: vendor/bin/phpstan --error-format=github diff --git a/composer.json b/composer.json index 75120d5..e0caf65 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "zenstruck/callback": "^1.1" }, "require-dev": { + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^9.5.0", "symfony/messenger": "^4.4|^5.0|^6.0", "symfony/phpunit-bridge": "^5.3", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..3b262e2 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,92 @@ +parameters: + ignoreErrors: + - + message: "#^Method Zenstruck\\\\Mailer\\\\Test\\\\SentEmails\\:\\:first\\(\\) should return T of Zenstruck\\\\Mailer\\\\Test\\\\TestEmail but returns Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\.$#" + count: 1 + path: src/SentEmails.php + + - + message: "#^Method Zenstruck\\\\Mailer\\\\Test\\\\SentEmails\\:\\:last\\(\\) should return T of Zenstruck\\\\Mailer\\\\Test\\\\TestEmail but returns Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\.$#" + count: 1 + path: src/SentEmails.php + + - + message: "#^Parameter \\#1 \\$filter of method Zenstruck\\\\Mailer\\\\Test\\\\SentEmails\\:\\:where\\(\\) expects callable\\(Symfony\\\\Component\\\\Mime\\\\Email\\|Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\)\\: bool, Closure\\(Symfony\\\\Component\\\\Mime\\\\Email\\)\\: bool given\\.$#" + count: 7 + path: src/SentEmails.php + + - + message: "#^Parameter \\#1 \\$filter of method Zenstruck\\\\Mailer\\\\Test\\\\SentEmails\\:\\:where\\(\\) expects callable\\(Symfony\\\\Component\\\\Mime\\\\Email\\|Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\)\\: bool, Closure\\(Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\)\\: bool given\\.$#" + count: 1 + path: src/SentEmails.php + + - + message: "#^Method Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\:\\:as\\(\\) should return T of Zenstruck\\\\Mailer\\\\Test\\\\TestEmail but returns \\$this\\(Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\)\\.$#" + count: 1 + path: src/TestEmail.php + + - + message: "#^Method Zenstruck\\\\Mailer\\\\Test\\\\TestEmail\\:\\:as\\(\\) should return T of Zenstruck\\\\Mailer\\\\Test\\\\TestEmail but returns object\\.$#" + count: 1 + path: src/TestEmail.php + + - + message: "#^Parameter \\#1 \\$events of static method Zenstruck\\\\Mailer\\\\Test\\\\SentEmails\\:\\:fromEvents\\(\\) expects Symfony\\\\Component\\\\Mailer\\\\Event\\\\MessageEvents, Symfony\\\\Component\\\\Mailer\\\\Event\\\\MessageEvents\\|null given\\.$#" + count: 1 + path: src/TestMailer.php + + - + message: "#^Cannot call method getBodyAsString\\(\\) on Symfony\\\\Component\\\\Mime\\\\Header\\\\HeaderInterface\\|null\\.$#" + count: 1 + path: tests/Fixture/CustomTestEmail.php + + - + message: "#^Call to method add\\(\\) on an unknown class Symfony\\\\Component\\\\Routing\\\\RouteCollectionBuilder\\.$#" + count: 2 + path: tests/Fixture/Kernel.php + + - + message: "#^Class Symfony\\\\Component\\\\Routing\\\\RouteCollectionBuilder not found\\.$#" + count: 1 + path: tests/Fixture/Kernel.php + + - + message: "#^Parameter \\$routes of method Zenstruck\\\\Mailer\\\\Test\\\\Tests\\\\Fixture\\\\Kernel\\:\\:configureRoutes\\(\\) has invalid type Symfony\\\\Component\\\\Routing\\\\RouteCollectionBuilder\\.$#" + count: 1 + path: tests/Fixture/Kernel.php + + - + message: "#^Access to an undefined static property Zenstruck\\\\Mailer\\\\Test\\\\Tests\\\\InteractsWithMailerTest\\:\\:\\$container\\.$#" + count: 2 + path: tests/InteractsWithMailerTest.php + + - + message: "#^Parameter \\#1 \\$class of method Zenstruck\\\\Mailer\\\\Test\\\\SentEmails\\:\\:all\\(\\) expects class\\-string\\, string given\\.$#" + count: 1 + path: tests/InteractsWithMailerTest.php + + - + message: "#^Access to an undefined static property Zenstruck\\\\Mailer\\\\Test\\\\Tests\\\\NonInteractsWithMailerTest\\:\\:\\$container\\.$#" + count: 1 + path: tests/NonInteractsWithMailerTest.php + + - + message: "#^Access to an undefined static property Zenstruck\\\\Mailer\\\\Test\\\\Tests\\\\NonKernelTestCaseTest\\:\\:\\$booted\\.$#" + count: 1 + path: tests/NonKernelTestCaseTest.php + + - + message: "#^Access to an undefined static property Zenstruck\\\\Mailer\\\\Test\\\\Tests\\\\NonKernelTestCaseTest\\:\\:\\$container\\.$#" + count: 1 + path: tests/NonKernelTestCaseTest.php + + - + message: "#^Call to an undefined static method Zenstruck\\\\Mailer\\\\Test\\\\Tests\\\\NonKernelTestCaseTest\\:\\:getContainer\\(\\)\\.$#" + count: 1 + path: tests/NonKernelTestCaseTest.php + + - + message: "#^Call to function method_exists\\(\\) with 'Zenstruck\\\\\\\\Mailer\\\\\\\\Test\\\\\\\\Tests\\\\\\\\NonKernelTestCaseTest' and 'getContainer' will always evaluate to false\\.$#" + count: 1 + path: tests/NonKernelTestCaseTest.php + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..62ab5d1 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src + - tests diff --git a/src/Bridge/Zenstruck/Browser/MailerComponent.php b/src/Bridge/Zenstruck/Browser/MailerComponent.php index cc8bd26..36af4d2 100644 --- a/src/Bridge/Zenstruck/Browser/MailerComponent.php +++ b/src/Bridge/Zenstruck/Browser/MailerComponent.php @@ -2,6 +2,7 @@ namespace Zenstruck\Mailer\Test\Bridge\Zenstruck\Browser; +use Symfony\Component\Mailer\DataCollector\MessageDataCollector; use Zenstruck\Browser\BrowserKitBrowser; use Zenstruck\Browser\Component; use Zenstruck\Mailer\Test\SentEmailMixin; @@ -26,6 +27,9 @@ public function sentEmails(): SentEmails throw new \RuntimeException('The profiler does not include the "mailer" collector. Is symfony/mailer installed?'); } - return SentEmails::fromEvents($browser->profile()->getCollector('mailer')->getEvents()); + /** @var MessageDataCollector $collector */ + $collector = $browser->profile()->getCollector('mailer'); + + return SentEmails::fromEvents($collector->getEvents()); } } diff --git a/src/SentEmails.php b/src/SentEmails.php index ab23f39..af6ee51 100644 --- a/src/SentEmails.php +++ b/src/SentEmails.php @@ -11,6 +11,8 @@ /** * @author Kevin Bond + * + * @implements \IteratorAggregate */ final class SentEmails implements \IteratorAggregate, \Countable { @@ -55,9 +57,9 @@ public static function fromEvents(MessageEvents $events): self /** * Get the first email in the collection - fail if none. * - * @template T + * @template T of TestEmail * - * @param class-string $class + * @param class-string $class * * @return T */ @@ -69,9 +71,9 @@ public function first(string $class = TestEmail::class): TestEmail /** * Get the last email in the collection - fail if none. * - * @template T + * @template T of TestEmail * - * @param class-string $class + * @param class-string $class * * @return T */ @@ -111,7 +113,7 @@ public function whereSubject(string $subject): self public function whereSubjectContains(string $needle): self { - return $this->where(fn(Email $email) => str_contains($email->getSubject(), $needle)); + return $this->where(fn(Email $email) => str_contains((string) $email->getSubject(), $needle)); } public function whereTag(string $tag): self @@ -152,7 +154,7 @@ public function dump(): self } /** - * @return never-return + * @return never */ public function dd(): void { @@ -161,9 +163,9 @@ public function dd(): void } /** - * @template T + * @template T of TestEmail * - * @param class-string $class + * @param class-string $class * * @return TestEmail[]|T[] */ @@ -194,6 +196,9 @@ public function assertNone(): self return $this->assertCount(0); } + /** + * @param array $context + */ public function ensureSome(string $message = 'No emails.', array $context = []): self { if (0 === \count($this->emails)) { diff --git a/src/TestEmail.php b/src/TestEmail.php index c78d2ac..9612b5d 100644 --- a/src/TestEmail.php +++ b/src/TestEmail.php @@ -6,6 +6,7 @@ use Symfony\Component\Mailer\Header\TagHeader; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\ParameterizedHeader; use Zenstruck\Assert; use Zenstruck\Callback; use Zenstruck\Callback\Parameter; @@ -24,15 +25,20 @@ final public function __construct(Email $email) $this->email = $email; } - final public function __call($name, $arguments) + /** + * @param mixed[] $arguments + * + * @return mixed + */ + final public function __call(string $name, array $arguments) { return $this->email->{$name}(...$arguments); } /** - * @template T + * @template T of self * - * @param class-string $class + * @param class-string $class * * @return T */ @@ -50,16 +56,21 @@ final public function as(string $class): self } /** - * @param callable(TestEmail|Email):mixed $callback + * @template T * - * @return mixed + * @param callable(TestEmail|Email):T $callback + * + * @return T */ final public function call(callable $callback) { return Callback::createFor($callback)->invoke(Parameter::union( Parameter::untyped($this), Parameter::typed(Email::class, $this->inner()), - Parameter::typed(self::class, Parameter::factory(fn(string $class) => $this->as($class))) + Parameter::typed(self::class, Parameter::factory(function(string $class) { + /** @var class-string $class */ + return $this->as($class); + })) )); } @@ -219,12 +230,19 @@ final public function assertTextContains(string $expected): self final public function assertHasFile(string $expectedFilename, string $expectedContentType, string $expectedContents): self { foreach ($this->email->getAttachments() as $attachment) { - if ($expectedFilename !== $attachment->getPreparedHeaders()->get('content-disposition')->getParameter('filename')) { + /** @var ParameterizedHeader $header */ + $header = $attachment->getPreparedHeaders()->get('content-disposition'); + + if ($expectedFilename !== $header->getParameter('filename')) { continue; } Assert::that($attachment->getBody())->is($expectedContents); - Assert::that($attachment->getPreparedHeaders()->get('content-type')->getBodyAsString()) + + /** @var ParameterizedHeader $header */ + $header = $attachment->getPreparedHeaders()->get('content-type'); + + Assert::that($header->getBodyAsString()) ->is($expectedContentType.'; name='.$expectedFilename) ; @@ -282,6 +300,8 @@ final public function dd(): void /** * @param Address[] $addresses + * + * @return static */ private function assertEmail(array $addresses, string $expectedEmail, ?string $expectedName, string $type): self { diff --git a/tests/EnvironmentProvider.php b/tests/EnvironmentProvider.php index 95459d2..45e8ec7 100644 --- a/tests/EnvironmentProvider.php +++ b/tests/EnvironmentProvider.php @@ -7,6 +7,9 @@ */ trait EnvironmentProvider { + /** + * @return array> + */ public static function environmentProvider(): iterable { yield ['test'];