diff --git a/src/Handlers/SuppressHandler.php b/src/Handlers/SuppressHandler.php
index d87b1bfb..3fd4c60f 100644
--- a/src/Handlers/SuppressHandler.php
+++ b/src/Handlers/SuppressHandler.php
@@ -6,6 +6,7 @@
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;
@@ -13,11 +14,30 @@
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',
@@ -32,129 +52,107 @@ final class SuppressHandler implements AfterClassLikeVisitInterface
],
];
- 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)
+ {
+ // TODO: Implement afterCodebasePopulated() method.
+ }
}
diff --git a/tests/Application/laravel-test-baseline.xml b/tests/Application/laravel-test-baseline.xml
index 24a684d8..3c8634e8 100644
--- a/tests/Application/laravel-test-baseline.xml
+++ b/tests/Application/laravel-test-baseline.xml
@@ -23,13 +23,6 @@
-
-
-
-
-
-
-
@@ -55,17 +48,6 @@
-
-
-
-
-
-
-
-
-
-
-
@@ -96,16 +78,6 @@
-
-
-
-
-
-
-
-
-
-
@@ -130,9 +102,6 @@
-
-
-
@@ -160,10 +129,6 @@
-
-
-
-