Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 78 additions & 80 deletions src/Handlers/SuppressHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,38 @@

use Psalm\Plugin\EventHandler\AfterClassLikeVisitInterface;
use Psalm\Plugin\EventHandler\Event\AfterClassLikeVisitEvent;
use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Storage\PropertyStorage;

use function array_intersect;
use function in_array;
use function strtolower;
use function str_starts_with;

final class SuppressHandler implements AfterClassLikeVisitInterface
{
private const BY_CLASS = [
private const CLASS_LEVEL_BY_PARENT_CLASS = [
'PropertyNotSetInConstructor' => [
'Illuminate\Console\Command',
'Illuminate\Foundation\Http\FormRequest',
'Illuminate\Mail\Mailable',
'Illuminate\Notifications\Notification',
],
'UnusedClass' => [ // usually classes with auto-discovery
'Illuminate\Console\Command',
'Illuminate\Support\ServiceProvider',
],
];

private const CLASS_LEVEL_BY_USED_TRAITS = [
'PropertyNotSetInConstructor' => [
'Illuminate\Queue\InteractsWithQueue',
]
];

/** Less flexible way, used when we can't rely on parent classes */
private const CLASS_LEVEL_BY_FQCN = [
'UnusedClass' => [
'App\Console\Kernel',
'App\Exceptions\Handler',
Expand All @@ -32,129 +52,107 @@
],
];

private const BY_CLASS_METHOD = [
/** Not preferable way as applications may use custom namespaces and structure */
private const METHOD_LEVEL_BY_FQCN = [
'PossiblyUnusedMethod' => [
'App\Http\Middleware\RedirectIfAuthenticated' => ['handle'],
],
];

private const BY_NAMESPACE = [
'PropertyNotSetInConstructor' => [
'App\Jobs',
],
'PossiblyUnusedMethod' => [
'App\Events',
'App\Jobs',
],
];

private const BY_NAMESPACE_METHOD = [
'PossiblyUnusedMethod' => [
'App\Events' => ['broadcastOn'],
'App\Jobs' => ['handle'],
'App\Mail' => ['__construct', 'build'],
'App\Notifications' => ['__construct', 'via', 'toMail', 'toArray'],
]
];

private const BY_PARENT_CLASS = [
'PropertyNotSetInConstructor' => [
'Illuminate\Console\Command',
'Illuminate\Foundation\Http\FormRequest',
'Illuminate\Mail\Mailable',
'Illuminate\Notifications\Notification',
],
];

private const BY_PARENT_CLASS_PROPERTY = [
private const PROPERTY_LEVEL_BY_PARENT_CLASS = [
'NonInvariantDocblockPropertyType' => [
'Illuminate\Console\Command' => ['description'],
'Illuminate\View\Component' => ['componentName'],
],
];

private const BY_USED_TRAITS = [
'PropertyNotSetInConstructor' => [
'Illuminate\Queue\InteractsWithQueue',
]
'Illuminate\Foundation\Testing\TestCase' => ['callbackException', 'app'],
],
];

/** @inheritDoc */
#[\Override]
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void
{
$class = $event->getStorage();
$classStorage = $event->getStorage();

foreach (self::BY_CLASS as $issue => $class_names) {
if (in_array($class->name, $class_names, true)) {
self::suppress($issue, $class);
}
if (! $classStorage->user_defined) {
return;
}

foreach (self::BY_CLASS_METHOD as $issue => $method_by_class) {
foreach ($method_by_class[$class->name] ?? [] as $method_name) {
/** @psalm-suppress RedundantFunctionCall */
self::suppress($issue, $class->methods[strtolower($method_name)] ?? null);
}
if ($classStorage->is_interface) {
return;
}

foreach (self::BY_NAMESPACE as $issue => $namespaces) {
foreach ($namespaces as $namespace) {
if (!str_starts_with($class->name, "{$namespace}\\")) {
continue;
}

self::suppress($issue, $class);
break;
foreach (self::CLASS_LEVEL_BY_FQCN as $issue => $classNames) {
if (in_array($classStorage->name, $classNames, true)) {
self::suppress($issue, $classStorage);
}
}

foreach (self::BY_NAMESPACE_METHOD as $issue => $methods_by_namespaces) {
foreach ($methods_by_namespaces as $namespace => $method_names) {
if (!str_starts_with($class->name, "{$namespace}\\")) {
continue;
}

foreach ($method_names as $method_name) {
self::suppress($issue, $class->methods[strtolower($method_name)] ?? null);
foreach (self::METHOD_LEVEL_BY_FQCN as $issue => $method_by_class) {
foreach ($method_by_class[$classStorage->name] ?? [] as $method_name) {
/** @psalm-suppress RedundantFunctionCall */
$method_storage = $classStorage->methods[strtolower($method_name)] ?? null;
if ($method_storage instanceof MethodStorage) {
self::suppress($issue, $method_storage);
}
}
}

foreach (self::BY_PARENT_CLASS as $issue => $parent_classes) {
if (!array_intersect($class->parent_classes, $parent_classes)) {
continue;
foreach (self::CLASS_LEVEL_BY_PARENT_CLASS as $issue => $parent_classes) {
// Check if any of the parent classes match our targets
if ($classStorage->parent_classes !== [] && array_intersect($classStorage->parent_classes, $parent_classes)) {
self::suppress($issue, $classStorage);
} elseif (is_string($classStorage->parent_class) && in_array($classStorage->parent_class, $parent_classes, true)) {
// If parent_classes array is empty, but we have a direct parent_class, check that
self::suppress($issue, $classStorage);
}

self::suppress($issue, $class);
}

foreach (self::BY_PARENT_CLASS_PROPERTY as $issue => $properties_by_parent_class) {
foreach (self::PROPERTY_LEVEL_BY_PARENT_CLASS as $issue => $properties_by_parent_class) {
foreach ($properties_by_parent_class as $parent_class => $property_names) {
if (!in_array($parent_class, $class->parent_classes, true)) {
// Check both parent_classes array and direct parent_class property
$is_child_of_target_class = false;

// Check if it inherits from the specific parent class
if (in_array($parent_class, $classStorage->parent_classes, true)) {
$is_child_of_target_class = true;
} elseif (is_string($classStorage->parent_class) && ($classStorage->parent_class === $parent_class)) {
// If parent_classes array is empty, but we have a direct parent_class, check that
$is_child_of_target_class = true;
}

if (!$is_child_of_target_class) {
continue;
}

foreach ($property_names as $property_name) {
self::suppress($issue, $class->properties[$property_name] ?? null);
$property_storage = $classStorage->properties[$property_name] ?? null;
if ($property_storage instanceof PropertyStorage) {
self::suppress($issue, $property_storage);
}
}
}
}

foreach (self::BY_USED_TRAITS as $issue => $used_traits) {
if (!array_intersect($class->used_traits, $used_traits)) {
foreach (self::CLASS_LEVEL_BY_USED_TRAITS as $issue => $used_traits) {
// Skip if traits are empty or if no intersection found
if ($classStorage->used_traits === [] || !array_intersect($classStorage->used_traits, $used_traits)) {
continue;
}

self::suppress($issue, $class);
self::suppress($issue, $classStorage);
}
}

/**
* @param ClassLikeStorage|PropertyStorage|MethodStorage|null $storage
*/
private static function suppress(string $issue, $storage): void
private static function suppress(string $issue, ClassLikeStorage|PropertyStorage|MethodStorage $storage): void
{
if ($storage && !in_array($issue, $storage->suppressed_issues, true)) {
if (!in_array($issue, $storage->suppressed_issues, true)) {
$storage->suppressed_issues[] = $issue;
}
}

public static function afterCodebasePopulated(AfterCodebasePopulatedEvent $event)

Check failure on line 154 in src/Handlers/SuppressHandler.php

View workflow job for this annotation

GitHub Actions / Psalm

MissingReturnType

src/Handlers/SuppressHandler.php:154:28: MissingReturnType: Method Psalm\LaravelPlugin\Handlers\SuppressHandler::afterCodebasePopulated does not have a return type, expecting void (see https://psalm.dev/050)

Check failure on line 154 in src/Handlers/SuppressHandler.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyUnusedMethod

src/Handlers/SuppressHandler.php:154:28: PossiblyUnusedMethod: Cannot find any calls to method Psalm\LaravelPlugin\Handlers\SuppressHandler::afterCodebasePopulated (see https://psalm.dev/087)

Check failure on line 154 in src/Handlers/SuppressHandler.php

View workflow job for this annotation

GitHub Actions / Psalm

UnusedParam

src/Handlers/SuppressHandler.php:154:79: UnusedParam: Param #1 is never referenced in this method (see https://psalm.dev/135)

Check failure on line 154 in src/Handlers/SuppressHandler.php

View workflow job for this annotation

GitHub Actions / Psalm

MissingReturnType

src/Handlers/SuppressHandler.php:154:28: MissingReturnType: Method Psalm\LaravelPlugin\Handlers\SuppressHandler::afterCodebasePopulated does not have a return type, expecting void (see https://psalm.dev/050)

Check failure on line 154 in src/Handlers/SuppressHandler.php

View workflow job for this annotation

GitHub Actions / Psalm

PossiblyUnusedMethod

src/Handlers/SuppressHandler.php:154:28: PossiblyUnusedMethod: Cannot find any calls to method Psalm\LaravelPlugin\Handlers\SuppressHandler::afterCodebasePopulated (see https://psalm.dev/087)

Check failure on line 154 in src/Handlers/SuppressHandler.php

View workflow job for this annotation

GitHub Actions / Psalm

UnusedParam

src/Handlers/SuppressHandler.php:154:79: UnusedParam: Param #1 is never referenced in this method (see https://psalm.dev/135)
{
// TODO: Implement afterCodebasePopulated() method.
}
}
35 changes: 0 additions & 35 deletions tests/Application/laravel-test-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@
<NonInvariantDocblockPropertyType>
<code><![CDATA[$description]]></code>
</NonInvariantDocblockPropertyType>
<PropertyNotSetInConstructor>
<code><![CDATA[ExampleCommand]]></code>
<code><![CDATA[ExampleCommand]]></code>
<code><![CDATA[ExampleCommand]]></code>
<code><![CDATA[ExampleCommand]]></code>
<code><![CDATA[ExampleCommand]]></code>
</PropertyNotSetInConstructor>
<UnusedClass>
<code><![CDATA[ExampleCommand]]></code>
</UnusedClass>
Expand All @@ -55,17 +48,6 @@
</UnusedClass>
</file>
<file src="app/Http/Requests/ExampleRequest.php">
<PropertyNotSetInConstructor>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
<code><![CDATA[ExampleRequest]]></code>
</PropertyNotSetInConstructor>
<UnusedClass>
<code><![CDATA[ExampleRequest]]></code>
</UnusedClass>
Expand Down Expand Up @@ -96,16 +78,6 @@
<code><![CDATA[content]]></code>
<code><![CDATA[envelope]]></code>
</PossiblyUnusedMethod>
<PropertyNotSetInConstructor>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
<code><![CDATA[ExampleMail]]></code>
</PropertyNotSetInConstructor>
</file>
<file src="app/Models/Example.php">
<UnusedClass>
Expand All @@ -130,9 +102,6 @@
<code><![CDATA[toMail]]></code>
<code><![CDATA[via]]></code>
</PossiblyUnusedMethod>
<PropertyNotSetInConstructor>
<code><![CDATA[ExampleNotification]]></code>
</PropertyNotSetInConstructor>
</file>
<file src="app/Observers/ExampleObserver.php">
<UnusedClass>
Expand Down Expand Up @@ -160,10 +129,6 @@
</UnusedClass>
</file>
<file src="app/View/Components/ExampleComponent.php">
<PropertyNotSetInConstructor>
<code><![CDATA[ExampleComponent]]></code>
<code><![CDATA[ExampleComponent]]></code>
</PropertyNotSetInConstructor>
<UnusedClass>
<code><![CDATA[ExampleComponent]]></code>
</UnusedClass>
Expand Down
Loading