diff --git a/src/Plugin.php b/src/Plugin.php index 5d4dbb69..9127440f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -75,7 +75,10 @@ protected function getStubsForVersion(string $version): array { [$majorVersion] = explode('.', $version); - return glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp'); + return array_merge( + glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/*.stubphp'), + glob(dirname(__DIR__) . '/stubs/' . $majorVersion . '/**/*.stubphp'), + ); } private function registerStubs(RegistrationInterface $registration): void diff --git a/stubs/9/Foundation/helpers.stubphp b/stubs/9/Foundation/helpers.stubphp new file mode 100644 index 00000000..df993d4d --- /dev/null +++ b/stubs/9/Foundation/helpers.stubphp @@ -0,0 +1,12 @@ + $code * @param string $message * @param array $headers * @return void @@ -21,6 +23,39 @@ function abort_if($boolean, $code, $message = '', array $headers = []) {} /** + * Throw an HttpException with the given data unless the given condition is true. + * + * @param bool $boolean + * @param \Symfony\Component\HttpFoundation\Response|\Illuminate\Contracts\Support\Responsable|int<400, 511> $code + * @param string $message + * @param array $headers + * @return void + * @psalm-return ($boolean is true ? never-return : void ) + * @psalm-assert !falsy $boolean + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ +function abort_unless($boolean, $code, $message = '', array $headers = []) {} + +/** + * Generate the URL to a controller action. + * + * @param callable-array|class-string $name + * @param mixed $parameters + * @param bool $absolute + * @return string + */ +function action($name, $parameters = [], $absolute = true) {} + +// app: processed by Psalm handlers +// app_path: processed by Psalm handlers +// asset: nothing to stub +// auth: processed by Psalm handlers + +/** + * Get the available auth instance. + * * @param string|null $guard * @return ( * $guard is null @@ -30,6 +65,37 @@ function abort_if($boolean, $code, $message = '', array $headers = []) {} */ function auth($guard = null) {} +/** + * Create a new redirect response to the previous location. + * + * @param int<300, 308> $status + * @param array $headers + * @param false|string $fallback + * @return \Illuminate\Http\RedirectResponse + */ +function back($status = 302, $headers = [], $fallback = false) {} + +// base_path: processed by Psalm handlers + +/** + * Hash the given value against the bcrypt algorithm. + * + * @param string $value + * @param array{rounds?: string, ...} $options + * @return non-empty-string + */ +function bcrypt($value, $options = []) {} + +/** + * Begin broadcasting an event. + * + * @param object|null $event + * @return \Illuminate\Broadcasting\PendingBroadcast + */ +function broadcast($event = null) {} + +// cache: processed by Psalm handlers + /** * Get / set the specified configuration value. * @@ -41,6 +107,8 @@ function auth($guard = null) {} */ function config($key = null, $default = null) {} +// config_path: processed by Psalm handlers + /** * Create a new cookie instance. * @@ -57,32 +125,83 @@ function config($key = null, $default = null) {} */ function cookie($name = null, $value = null, $minutes = 0, $path = null, $domain = null, $secure = null, $httpOnly = true, $raw = false, $sameSite = null) {} +// csrf_field: nothing to stub +// csrf_token: nothing to stub +// database_path: processed by Psalm handlers +// decrypt: nothing to stub + /** * Dispatch a job to its appropriate handler. * - * @param mixed $job + * @param object|callable $job * @return ($job is \Closure ? \Illuminate\Foundation\Bus\PendingDispatch : \Illuminate\Foundation\Bus\PendingDispatch) */ function dispatch($job) {} +/** + * Dispatch a command to its appropriate handler in the current process. + * + * Queueable jobs will be dispatched to the "sync" queue. + * + * @param object|callable $job + * @param mixed $handler + * @return mixed + */ +function dispatch_sync($job, $handler = null) {} + +// encrypt: nothing to stub + +/** + * Dispatch an event and call the listeners. + * + * @param string|object $event + * @param mixed $payload + * @param bool $halt + * @return list|scalar|array|object|null + */ +function event(...$args) {} + +// fake: nothing to stub +// info: nothing to stub + /** * Log a debug message to the logs. * * @param string|null $message * @param array $context * @return \Illuminate\Log\LogManager|null - * @psalm-return (func_num_args() is 0 ? \Illuminate\Log\LogManager : void) + * @psalm-return ($message is null ? \Illuminate\Log\LogManager : null) */ function logger($message = null, array $context = []) {} +// lang_path: processed by Psalm handlers + /** * Get a log driver instance. * * @param string|null $driver - * @return ($driver is null ? \Illuminate\Log\LogManager : \Psr\Log\LoggerInterface) + * @return ($driver is null ? \Illuminate\Log\LogManager : \Psr\Log\LoggerInterface&\Illuminate\Log\Logger) */ function logs($driver = null) {} +// method_field: nothing to stub +// mix: nothing to stub +// now: nothing to stub +// old: nothing to stub +// policy: nothing to stub + +/** + * Handle a Precognition controller hook. + * + * @template TCallableReturn + * + * @param null|callable(callable(\Illuminate\Http\Response=, mixed=): void): TCallableReturn $callable + * @return TCallableReturn + */ +function precognitive($callable = null) {} + +// public_path: processed by Psalm handlers + /** * @param string|null $to * @param int<300, 308> $status @@ -92,6 +211,10 @@ function logs($driver = null) {} */ function redirect($to = null, $status = 302, $headers = [], $secure = null) {} +// report: nothing to stub +// report_if: nothing to stub +// report_unless: nothing to stub + /** * Get an instance of the current request or an input item from the request. * @@ -102,6 +225,8 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null) {} function request($key = null, $default = null) {} /** + * Catch a potential exception and return a default value. + * * @template TValue * @template TDefault * @template TDefaultCallableReturn @@ -113,6 +238,9 @@ function request($key = null, $default = null) {} */ function rescue(callable $callback, $rescue = null, $report = true) {} +// resolve: processed by Psalm handlers +// resource_path: processed by Psalm handlers + /** * Return a new response from the application. * @@ -124,6 +252,19 @@ function rescue(callable $callback, $rescue = null, $report = true) {} */ function response($content = '', $status = 200, array $headers = []) {} +/** + * Generate the URL to a named route. + * + * @param string $name + * @param scalar|array|null $parameters + * @param bool $absolute + * @return string + */ +function route($name, $parameters = [], $absolute = true) {} + +// secure_asset: nothing to stub +// secure_url: nothing to stub + /** * Get / set the specified session value. * @@ -135,6 +276,8 @@ function response($content = '', $status = 200, array $headers = []) {} */ function session($key = null, $default = null) {} +// storage_path: nothing to stub + /** * Create a new redirect response to a named route. * @@ -146,6 +289,10 @@ function session($key = null, $default = null) {} */ function to_route($route, $parameters = [], $status = 302, $headers = []) {} +// today: nothing to stub +// trans: processed by Psalm handlers +// trans_choice: processed by Psalm handlers + /** * Translate the given message. * @@ -158,6 +305,7 @@ function __($key = null, $replace = [], $locale = null) {} /** * Generate a url for the application. + * * @param string|null $path * @param mixed $parameters * @param bool|null $secure @@ -177,9 +325,11 @@ function url($path = null, $parameters = [], $secure = null) {} function validator(array $data = [], array $rules = [], array $messages = [], array $customAttributes = []) {} /** + * Get the evaluated view contents for the given view. + * * @param string|null $view - * @param \Illuminate\Contracts\Support\Arrayable|array $data + * @param \Illuminate\Contracts\Support\Arrayable|array $data * @param array $mergeData - * @return ($view is null ? \Illuminate\Contracts\View\Factory : \Illuminate\Contracts\View\View) + * @return ($view is null ? \Illuminate\Contracts\View\Factory : (\Illuminate\View\View|\Illuminate\Contracts\View\View)) */ function view($view = null, $data = [], $mergeData = []) {} diff --git a/stubs/Support/helpers.stubphp b/stubs/Support/helpers.stubphp index 443dd227..99c60585 100644 --- a/stubs/Support/helpers.stubphp +++ b/stubs/Support/helpers.stubphp @@ -1,5 +1,8 @@ ) ? true : bool) + * ) + */ +function blank($value) {} + +/** + * Get the class "basename" of the given object / class. + * + * @param class-string|object $class + * @return non-empty-string + */ +function class_basename($class) {} + +/** + * Returns all traits used by a class, its parent classes and trait of their traits. + * + * @param class-string|object $trait + * @return array + */ +function class_uses_recursive($class) {} + +// e: nothing to stub (taint analysis only) +// env: nothing to stub + +/** + * Determine if a value is "filled". + * + * @param mixed $value + * @psalm-return ($value is (bool|numeric|non-empty-array) + * ? true + * : ($value is (null|''|array) ? false : bool) + * ) + */ +function filled($value) {} + +/** + * Get an item from an object using "dot" notation. + * + * @template TObject + * + * @param TObject $object + * @param string|null $key + * @param mixed $default + * @return ($key is (null|'') ? TObject : mixed) + */ +function object_get($object, $key, $default = null) {} + /** * Provide access to optional objects. * @@ -31,14 +89,18 @@ class NullObject { * @template TCallback of null|callable(TValue): TResult * @psalm-param TValue $value * @psalm-return ( - TValue is null - ? NullObject - : ( TCallback is null ? TValue : TResult ) - ) + * TValue is null + * ? NullObject + * : (TCallback is null ? TValue : TResult) + * ) */ function optional($value = null, callable $callback = null) {} +// preg_replace_array: nothing to stub + /** + * Retry an operation a given number of times. + * * @template TValue * @param positive-int|list $times * @param callable(int): TValue $callback @@ -51,16 +113,30 @@ function optional($value = null, callable $callback = null) {} function retry($times, callable $callback, $sleep = 0, $when = null) {} /** + * Get a new stringable object from the given string. + * + * @param string|null $string + * @return (func_num_args() is 0 + * ? (stringable-object&\Illuminate\Support\Str) + * : \Illuminate\Support\Stringable + * ) + */ +function str($string = null) {} + +/** + * Call the given Closure with the given value then return the value. + * * @template TValue + * * @param TValue $value * @param null|callable(TValue): void $callback * @return TValue */ function tap($value, $callback = null) {} - /** - * Throw the given exception if the given condition is true. - * +/** + * Throw the given exception if the given condition is true. + * * @template TCondition * @template TException of \Throwable * @@ -88,16 +164,35 @@ function throw_if($condition, $exception = 'RuntimeException', ...$parameters) { */ function throw_unless($condition, $exception = 'RuntimeException', ...$parameters) {} -/** - * Returns all traits used by a class, its parent classes and trait of their traits. - * @param object|class-string $trait - * @return array - */ -function class_uses_recursive($class) {} - /** * Returns all traits used by a trait and its traits. + * * @param class-string $trait * @return array */ function trait_uses_recursive($trait) {} + +/** + * Transform the given value if it is present. + * + * @template TValue + * @template TCallbackReturn + * @template TDefault + * @template TDefaultReturn + * + * @param TValue $value + * @param callable(TValue): TCallbackReturn $callback + * @param TDefault|callable(TValue): TDefaultReturn $default + * @return mixed|null + * @psalm-return ($value is (bool|numeric|non-empty-array) + * ? TCallbackReturn + * : ($value is (''|array|null) + * ? (TDefault is callable ? TDefaultReturn : TDefault) + * : (TCallbackReturn|TDefaultReturn|TDefault) + * ) + * ) + */ +function transform($value, callable $callback, $default = null) {} + +// windows_os: nothing to stub +// with: nothing to stub diff --git a/tests/Acceptance/acceptance/CollectionHelpers.feature b/tests/Acceptance/acceptance/CollectionHelpers.feature index f3e33932..8a1e0cbc 100644 --- a/tests/Acceptance/acceptance/CollectionHelpers.feature +++ b/tests/Acceptance/acceptance/CollectionHelpers.feature @@ -18,6 +18,38 @@ Feature: Collection helpers 42]); // set value + } + """ + When I run Psalm + Then I see no errors + Scenario: cookie() support Given I have the following code """ @@ -150,6 +187,17 @@ Feature: Foundation helpers When I run Psalm Then I see no errors + Scenario: precognitive() support + Given I have the following code + """ + $payload = precognitive(function () { + return ['foo' => 'bar']; + }); + /** @psalm-check-type $payload = array{'foo': 'bar'} */ + """ + When I run Psalm + Then I see no errors + Scenario: redirect() support Given I have the following code """ @@ -310,7 +358,7 @@ Feature: Foundation helpers return view(); } - function view_with_one_arg(): \Illuminate\Contracts\View\View + function view_with_one_arg(): Illuminate\Contracts\View\View { return view('home'); } diff --git a/tests/Acceptance/acceptance/SupportHelpers.feature b/tests/Acceptance/acceptance/SupportHelpers.feature index e247794f..10ccc6c0 100644 --- a/tests/Acceptance/acceptance/SupportHelpers.feature +++ b/tests/Acceptance/acceptance/SupportHelpers.feature @@ -32,22 +32,179 @@ Feature: Support helpers When I run Psalm Then I see no errors - Scenario: cache() support + Scenario: blank() support Given I have the following code """ - function test_cache_call_without_args_should_return_CacheManager(): \Illuminate\Cache\CacheManager + /** @return false */ + function false_is_not_blank(): bool + { + return blank(false); + } + + /** @return false */ + function true_is_not_blank(): bool + { + return blank(true); + } + + /** @return false */ + function zero_int_is_not_blank(): bool + { + return blank(0); + } + + /** @return false */ + function zero_float_is_not_blank(): bool + { + return blank(0.0); + } + + /** @return false */ + function zero_numeric_string_is_not_blank(): bool + { + return blank('0'); + } + + /** @return false */ + function non_empty_array_is_not_blank(): bool + { + return blank(['a']); + } + + /** @return true */ + function null_is_blank(): bool + { + return blank(null); + } + + /** @return true */ + function empty_string_is_blank(): bool + { + return blank(''); + } + + /** @return true */ + function empty_array_is_blank(): bool + { + return blank([]); + } + + function string_that_trimmed_to_empty_string_is_bool(): bool + { + return blank(' '); + } + """ + When I run Psalm + Then I see no errors + + Scenario: class_basename() support + Given I have the following code + """ + function class_basename_allows_passing_fqcn(): string { - return cache(); + return class_basename(\App\Models\User::class); } - function test_cache_call_with_string_as_arg_should_return_string(): mixed + function class_basename_allows_passing_object(): string { - return cache('key'); // get value + return class_basename(new \stdClass()); } + """ + When I run Psalm + Then I see no errors - function test_cache_call_with_array_as_arg_should_return_bool(): bool + Scenario: class_uses_recursive() support + Given I have the following code + """ + function class_uses_recursive_allows_passing_fqcn(): array { - return cache(['key' => 42]); // set value + return class_uses_recursive(\App\Models\User::class); + } + + function class_uses_recursive_allows_passing_object(): array + { + return class_uses_recursive(new \stdClass()); + } + """ + When I run Psalm + Then I see no errors + + Scenario: filled() support + Given I have the following code + """ + /** @return true */ + function false_is_filled(): bool + { + return filled(false); + } + + /** @return true */ + function true_is_filled(): bool + { + return filled(true); + } + + /** @return true */ + function zero_int_is_filled(): bool + { + return filled(0); + } + + /** @return true */ + function zero_float_is_filled(): bool + { + return filled(0.0); + } + + /** @return true */ + function zero_numeric_string_is_filled(): bool + { + return filled('0'); + } + + /** @return true */ + function non_empty_array_is_filled(): bool + { + return filled(['a']); + } + + /** @return false */ + function null_is_not_filled(): bool + { + return filled(null); + } + + /** @return false */ + function empty_string_is_not_filled(): bool + { + return filled(''); + } + + /** @return false */ + function empty_array_is_not_filled(): bool + { + return filled([]); + } + + function string_that_trimmed_to_empty_string_is_bool(): bool + { + return filled(' '); + } + """ + When I run Psalm + Then I see no errors + + Scenario: object_get() support + Given I have the following code + """ + function object_get_returns_first_arg_when_second_is_null(\stdClass $object): \stdClass + { + return object_get($object, null); + } + + function object_get_returns_first_arg_when_second_is_empty_string(\stdClass $object): \stdClass + { + return object_get($object, ''); } """ When I run Psalm @@ -69,6 +226,23 @@ Feature: Support helpers When I run Psalm Then I see no errors + Scenario: str() support + Given I have the following code + """ + /** @return stringable-object */ + function str_without_args_returns_anonymous_class_instance(): object + { + return str(); + } + + function str_with_arh_returns_Stringable_instance(): \Illuminate\Support\Stringable + { + return str('some string'); + } + """ + When I run Psalm + Then I see no errors + Scenario: tap() support Given I have the following code """ @@ -184,3 +358,21 @@ Feature: Support helpers """ When I run Psalm Then I see no errors + + Scenario: transform() support + Given I have the following code + """ + function it_uses_callback_return_type_if_value_is_not_blank(): float { + return transform(42, fn ($value) => $value * 1.1, fn () => null); + } + + function it_uses_default_return_type_if_value_is_blank_and_default_is_callable(): int { + return transform([], fn () => 'any', fn () => 42); + } + + function it_uses_default_return_type_if_value_is_blank_and_default_is_not_callable(): int { + return transform(null, fn () => 'any', 42); + } + """ + When I run Psalm + Then I see no errors