diff --git a/src/Handlers/SuppressHandler.php b/src/Handlers/SuppressHandler.php index 3a41e8fe..f15895ef 100644 --- a/src/Handlers/SuppressHandler.php +++ b/src/Handlers/SuppressHandler.php @@ -9,6 +9,8 @@ use Psalm\Storage\PropertyStorage; use function array_intersect; use function in_array; +use function strpos; +use function strtolower; class SuppressHandler implements AfterClassLikeVisitInterface { @@ -39,6 +41,31 @@ class SuppressHandler implements AfterClassLikeVisitInterface ], ]; + /** + * @var array> + */ + private const BY_NAMESPACE = [ + 'PropertyNotSetInConstructor' => [ + 'App\Jobs', + ], + 'PossiblyUnusedMethod' => [ + 'App\Events', + 'App\Jobs', + ], + ]; + + /** + * @var array>> + */ + private const BY_NAMESPACE_METHOD = [ + 'PossiblyUnusedMethod' => [ + 'App\Events' => ['broadcastOn'], + 'App\Jobs' => ['handle'], + 'App\Mail' => ['__construct', 'build'], + 'App\Notifications' => ['__construct', 'via', 'toMail', 'toArray'], + ] + ]; + /** * @var array> */ @@ -46,6 +73,7 @@ class SuppressHandler implements AfterClassLikeVisitInterface 'PropertyNotSetInConstructor' => [ 'Illuminate\Console\Command', 'Illuminate\Foundation\Http\FormRequest', + 'Illuminate\Mail\Mailable', 'Illuminate\Notifications\Notification', ], ]; @@ -59,6 +87,15 @@ class SuppressHandler implements AfterClassLikeVisitInterface ], ]; + /** + * @var array>> + */ + private const BY_USED_TRAITS = [ + 'PropertyNotSetInConstructor' => [ + 'Illuminate\Queue\InteractsWithQueue', + ] + ]; + public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) { $class = $event->getStorage(); @@ -71,7 +108,31 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) foreach (self::BY_CLASS_METHOD as $issue => $method_by_class) { foreach ($method_by_class[$class->name] ?? [] as $method_name) { - self::suppress($issue, $class->methods[$method_name] ?? null); + /** @psalm-suppress RedundantCast */ + self::suppress($issue, $class->methods[strtolower($method_name)] ?? null); + } + } + + foreach (self::BY_NAMESPACE as $issue => $namespaces) { + foreach ($namespaces as $namespace) { + if (0 !== strpos($class->name, "$namespace\\")) { + continue; + } + + self::suppress($issue, $class); + break; + } + } + + foreach (self::BY_NAMESPACE_METHOD as $issue => $methods_by_namespaces) { + foreach ($methods_by_namespaces as $namespace => $method_names) { + if (0 !== strpos($class->name, "$namespace\\")) { + continue; + } + + foreach ($method_names as $method_name) { + self::suppress($issue, $class->methods[strtolower($method_name)] ?? null); + } } } @@ -83,8 +144,8 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) self::suppress($issue, $class); } - foreach (self::BY_PARENT_CLASS_PROPERTY as $issue => $methods_by_parent_class) { - foreach ($methods_by_parent_class as $parent_class => $property_names) { + foreach (self::BY_PARENT_CLASS_PROPERTY as $issue => $properties_by_parent_class) { + foreach ($properties_by_parent_class as $parent_class => $property_names) { if (!in_array($parent_class, $class->parent_classes)) { continue; } @@ -94,6 +155,14 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) } } } + + foreach (self::BY_USED_TRAITS as $issue => $used_traits) { + if (!array_intersect($class->used_traits, $used_traits)) { + continue; + } + + self::suppress($issue, $class); + } } /** diff --git a/tests/laravel-test-baseline.xml b/tests/laravel-test-baseline.xml index 144244cb..e0fa6b9f 100644 --- a/tests/laravel-test-baseline.xml +++ b/tests/laravel-test-baseline.xml @@ -1,5 +1,28 @@ + + + array|bool + + + ExampleChannel + + + + + ExampleCast + + + + + ExampleCommand + + + + + ExampleException + + $dontFlash @@ -9,16 +32,56 @@ $e + + + ExampleController + + + + + ExampleMiddleware + + $headers + + + ExampleRequest + + + + + ExampleResource + + + + + ExampleListener + + $fillable + + + ExampleObserver + + + + + ExamplePolicy + + + + + ExampleProvider + + optional($request->user())->id ?: $request->ip() @@ -30,4 +93,12 @@ optional($request->user())->id ?: $request->ip() + + + bool + + + ExampleRule + + diff --git a/tests/laravel-test.sh b/tests/laravel-test.sh index 8792cae7..7aa6efea 100755 --- a/tests/laravel-test.sh +++ b/tests/laravel-test.sh @@ -9,6 +9,29 @@ echo "Installing Laravel" composer create-project --quiet --prefer-dist "laravel/laravel" ../laravel cd ../laravel/ +echo "Preparing Laravel" +./artisan make:cast ExampleCast +./artisan make:channel ExampleChannel +./artisan make:command ExampleCommand +./artisan make:controller ExampleController +./artisan make:event ExampleEvent +./artisan make:exception ExampleException +./artisan make:factory ExampleFactory +./artisan make:job ExampleJob +./artisan make:listener ExampleListener +./artisan make:mail ExampleMail +./artisan make:middleware ExampleMiddleware +./artisan make:model Example +./artisan make:notification ExampleNotification +./artisan make:observer ExampleObserver +./artisan make:policy ExamplePolicy +./artisan make:provider ExampleProvider +./artisan make:request ExampleRequest +./artisan make:resource ExampleResource +./artisan make:rule ExampleRule +./artisan make:seeder ExampleSeeder +./artisan make:test ExampleTest + echo "Adding package from source" sed -e 's|"type": "project",|&"repositories": [ { "type": "path", "url": "../psalm-plugin-laravel" } ],|' -i composer.json COMPOSER_MEMORY_LIMIT=-1 composer require --dev "psalm/plugin-laravel:*"