From b23059f5d233c9030afd6b7d75cc71751cb70127 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 18 Aug 2023 12:17:56 +0200 Subject: [PATCH 1/9] workflows: Add phpstan --- .github/workflows/php.yml | 4 ++++ phpstan.neon | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 phpstan.neon diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 77d798b..771878c 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -40,6 +40,10 @@ jobs: if: success() || matrix.allow_failure run: phpcs -wps --colors + - name: PHPStan + uses: php-actions/phpstan@v3 + if: success() || matrix.allow_failure + test: name: Unit tests with PHP ${{ matrix.php }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..a0c1029 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,31 @@ +parameters: + level: max + + paths: + - src + + scanDirectories: + - vendor + + ignoreErrors: + - '#Unsafe usage of new static\(\)#' + + - '#Method ipl\\Scheduler\\.*::getNextDue\(\) .* but returns DateTimeInterface\|null#' + + - '#Call to an undefined method DateTimeInterface::#' + + - '#Parameter \#1 \$timezone of class DateTimeZone constructor expects string, string\|null given#' + + - '#Call to an undefined method React\\Promise\\PromiseInterface::#' + + - '#Parameter \#1 \$timer of static method React\\EventLoop\\Loop::cancelTimer\(\) expects#' + + - '#Method ipl\\Scheduler\\.*::jsonSerialize\(\) return type has no value type specified in iterable type#' + + - '#Method ipl\\Scheduler\\.* should return \$this.* but returns static#' + + - '#Parameter \#2 \$before of class Recurr\\Transformer\\Constraint\\BetweenConstraint constructor#' + + - '#Parameter \#1 \$rrule of class Recurr\\Rule constructor expects string\|null, array.*\|string given#' + + - '#Parameter \#1 \$callback of function call_user_func_array expects callable\(\): mixed, array{Recurr\\Rule, string} given#' From cb59cbd2f56cae4375c24f5334cc25000e7cec97 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 18 Aug 2023 11:30:45 +0200 Subject: [PATCH 2/9] `RRule:` Add argument value type hint --- src/RRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RRule.php b/src/RRule.php index d71e754..386f1f3 100644 --- a/src/RRule.php +++ b/src/RRule.php @@ -296,7 +296,7 @@ public function jsonSerialize(): array * Redirect all public method calls to the underlying rrule object * * @param string $methodName - * @param array $args + * @param array $args * * @return mixed * From 794c1e66642778205dbe6b85c730e251f9a74a68 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 18 Aug 2023 11:50:58 +0200 Subject: [PATCH 3/9] RRule: Add generator value type hint --- src/RRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RRule.php b/src/RRule.php index 386f1f3..82f1dd8 100644 --- a/src/RRule.php +++ b/src/RRule.php @@ -240,7 +240,7 @@ public function getFrequency(): string * @param int $limit Limit the recurrences to be generated to the given value * @param bool $include Whether to include the passed time in the result set * - * @return Generator + * @return Generator */ public function getNextRecurrences( DateTimeInterface $dateTime, From b054ca9e38c82aacbf014c258c029a547c43fe31 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 18 Aug 2023 11:52:29 +0200 Subject: [PATCH 4/9] RRule: Raise an error if start can't be deserialized & add var type hint --- src/RRule.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/RRule.php b/src/RRule.php index 82f1dd8..448a33c 100644 --- a/src/RRule.php +++ b/src/RRule.php @@ -15,6 +15,7 @@ use Recurr\Transformer\ArrayTransformerConfig; use Recurr\Transformer\Constraint\AfterConstraint; use Recurr\Transformer\Constraint\BetweenConstraint; +use stdClass; use function ipl\Stdlib\get_php_type; @@ -124,11 +125,17 @@ public static function fromFrequency(string $frequency): self public static function fromJson(string $json): Frequency { + /** @var stdClass $data */ $data = json_decode($json); $self = new static($data->rrule); $self->frequency = $data->frequency; if (isset($data->start)) { - $self->startAt(DateTime::createFromFormat(static::SERIALIZED_DATETIME_FORMAT, $data->start)); + $start = DateTime::createFromFormat(static::SERIALIZED_DATETIME_FORMAT, $data->start); + if (! $start) { + throw new InvalidArgumentException(sprintf('Cannot deserialize start time: %s', $data->start)); + } + + $self->startAt($start); } return $self; From f1c097172435bbd6d3aa54a16c2e3399781c6f4e Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 18 Aug 2023 11:57:14 +0200 Subject: [PATCH 5/9] Add variable & value type hints --- src/Common/Promises.php | 4 +--- src/Common/Timers.php | 2 +- src/Cron.php | 4 ++-- src/RRule.php | 2 +- src/Scheduler.php | 5 +++-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Common/Promises.php b/src/Common/Promises.php index 503fdf2..b896627 100644 --- a/src/Common/Promises.php +++ b/src/Common/Promises.php @@ -10,7 +10,7 @@ trait Promises { - /** @var SplObjectStorage */ + /** @var SplObjectStorage> */ protected $promises; /** @@ -100,9 +100,7 @@ protected function detachPromises(UuidInterface $uuid): array return []; } - /** @var ArrayObject $promises */ $promises = $this->promises[$uuid]; - $this->promises->detach($uuid); return $promises->getArrayCopy(); diff --git a/src/Common/Timers.php b/src/Common/Timers.php index ee923ae..2d0641f 100644 --- a/src/Common/Timers.php +++ b/src/Common/Timers.php @@ -8,7 +8,7 @@ trait Timers { - /** @var SplObjectStorage */ + /** @var SplObjectStorage */ protected $timers; /** diff --git a/src/Cron.php b/src/Cron.php index 3f391a7..fbf9aac 100644 --- a/src/Cron.php +++ b/src/Cron.php @@ -20,10 +20,10 @@ class Cron implements Frequency /** @var CronExpression */ protected $cron; - /** @var DateTimeInterface Start time of this frequency */ + /** @var ?DateTimeInterface Start time of this frequency */ protected $start; - /** @var DateTimeInterface End time of this frequency */ + /** @var ?DateTimeInterface End time of this frequency */ protected $end; /** @var string String representation of the cron expression */ diff --git a/src/RRule.php b/src/RRule.php index 448a33c..bfad0e5 100644 --- a/src/RRule.php +++ b/src/RRule.php @@ -63,7 +63,7 @@ class RRule implements Frequency /** * Construct a new rrule instance * - * @param string|array $rule + * @param string|array $rule * * @throws InvalidRRule */ diff --git a/src/Scheduler.php b/src/Scheduler.php index 8146383..fd91832 100644 --- a/src/Scheduler.php +++ b/src/Scheduler.php @@ -124,7 +124,7 @@ class Scheduler */ public const ON_TASK_EXPIRED = 'task-expired'; - /** @var SplObjectStorage The scheduled tasks of this scheduler */ + /** @var SplObjectStorage The scheduled tasks of this scheduler */ protected $tasks; public function __construct() @@ -285,9 +285,10 @@ protected function cancelTask(Task $task): void { Loop::cancelTimer($this->detachTimer($task->getUuid())); + /** @var ExtendedPromiseInterface[] $promises */ $promises = $this->detachPromises($task->getUuid()); if (! empty($promises)) { - /** @var ExtendedPromiseInterface[] $promises */ + /** @var Promise\CancellablePromiseInterface $promise */ foreach ($promises as $promise) { $promise->cancel(); } From 9b2eb7f2e88805c2f94c4b3c88473f94c739033c Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Thu, 24 Aug 2023 12:08:08 +0200 Subject: [PATCH 6/9] Raise an exception when the json decoded type doesn't match the expected one --- src/Cron.php | 11 +++++++++++ src/OneOff.php | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/Cron.php b/src/Cron.php index fbf9aac..639957b 100644 --- a/src/Cron.php +++ b/src/Cron.php @@ -9,6 +9,8 @@ use InvalidArgumentException; use ipl\Scheduler\Contract\Frequency; +use function ipl\Stdlib\get_php_type; + class Cron implements Frequency { public const PART_MINUTE = 0; @@ -163,6 +165,15 @@ public static function isValid(string $expression): bool public static function fromJson(string $json): Frequency { $data = json_decode($json, true); + if (! is_array($data)) { + throw new InvalidArgumentException( + sprintf( + '%s expects json decoded value to be an array, got %s instead', + __METHOD__, + get_php_type($data) + ) + ); + } $self = new static($data['expression']); if (isset($data['start'])) { diff --git a/src/OneOff.php b/src/OneOff.php index 15a1a64..ebe945d 100644 --- a/src/OneOff.php +++ b/src/OneOff.php @@ -5,8 +5,11 @@ use DateTime; use DateTimeInterface; use DateTimeZone; +use InvalidArgumentException; use ipl\Scheduler\Contract\Frequency; +use function ipl\Stdlib\get_php_type; + class OneOff implements Frequency { /** @var DateTimeInterface Start time of this frequency */ @@ -46,6 +49,15 @@ public function getEnd(): ?DateTimeInterface public static function fromJson(string $json): Frequency { $data = json_decode($json, true); + if (! is_string($data)) { + throw new InvalidArgumentException( + sprintf( + '%s expects json decoded value to be string, got %s instead', + __METHOD__, + get_php_type($data) + ) + ); + } return new static(new DateTime($data)); } From 3eb53a298f9e6928f48f83331253f5f8d4932a66 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 18 Aug 2023 12:16:57 +0200 Subject: [PATCH 7/9] `Scheduler:isValidEvent():` Add argument & return type hint --- src/Scheduler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Scheduler.php b/src/Scheduler.php index fd91832..25ad3a1 100644 --- a/src/Scheduler.php +++ b/src/Scheduler.php @@ -262,7 +262,7 @@ public function schedule(Task $task, Frequency $frequency): self return $this; } - public function isValidEvent($event) + public function isValidEvent(string $event): bool { $events = array_flip([ static::ON_TASK_CANCEL, From 2b430f5e88ecb92b92be68b67195f2e2a04a996c Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Wed, 23 Aug 2023 14:52:26 +0200 Subject: [PATCH 8/9] Add common ignore errors pattern to phpstan config --- phpstan.neon | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index a0c1029..10b732a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,7 +8,11 @@ parameters: - vendor ignoreErrors: - - '#Unsafe usage of new static\(\)#' + - + messages: + - '#Unsafe usage of new static\(\)#' + - '#. but return statement is missing#' + reportUnmatched: false - '#Method ipl\\Scheduler\\.*::getNextDue\(\) .* but returns DateTimeInterface\|null#' From 2e32fb7dae74ddb016d94e739a91067884a8e4d1 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Thu, 24 Aug 2023 09:43:51 +0200 Subject: [PATCH 9/9] Add phpstan `baseline` config --- phpstan-baseline.neon | 36 ++++++++++++++++++++++++++++++++++++ phpstan.neon | 17 +++++++---------- 2 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..14e1edc --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,36 @@ +parameters: + ignoreErrors: + - + message: "#^Method ipl\\\\Scheduler\\\\Cron\\:\\:getNextDue\\(\\) should return DateTimeInterface but returns DateTimeInterface\\|null\\.$#" + count: 2 + path: src/Cron.php + + - + message: "#^Method ipl\\\\Scheduler\\\\Cron\\:\\:jsonSerialize\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Cron.php + + - + message: "#^Method ipl\\\\Scheduler\\\\RRule\\:\\:getNextDue\\(\\) should return DateTimeInterface but returns DateTimeInterface\\|null\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Method ipl\\\\Scheduler\\\\RRule\\:\\:jsonSerialize\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Parameter \\#1 \\$timezone of class DateTimeZone constructor expects string, string\\|null given\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Parameter \\#2 \\$before of class Recurr\\\\Transformer\\\\Constraint\\\\BetweenConstraint constructor expects DateTimeInterface, DateTimeInterface\\|null given\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Parameter \\#1 \\$timer of static method React\\\\EventLoop\\\\Loop\\:\\:cancelTimer\\(\\) expects React\\\\EventLoop\\\\TimerInterface, React\\\\EventLoop\\\\TimerInterface\\|null given\\.$#" + count: 1 + path: src/Scheduler.php diff --git a/phpstan.neon b/phpstan.neon index 10b732a..5891e2f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,13 @@ +includes: + - phpstan-baseline.neon + parameters: level: max + checkFunctionNameCase: true + checkInternalClassCaseSensitivity: true + treatPhpDocTypesAsCertain: false + paths: - src @@ -14,22 +21,12 @@ parameters: - '#. but return statement is missing#' reportUnmatched: false - - '#Method ipl\\Scheduler\\.*::getNextDue\(\) .* but returns DateTimeInterface\|null#' - - '#Call to an undefined method DateTimeInterface::#' - - '#Parameter \#1 \$timezone of class DateTimeZone constructor expects string, string\|null given#' - - '#Call to an undefined method React\\Promise\\PromiseInterface::#' - - '#Parameter \#1 \$timer of static method React\\EventLoop\\Loop::cancelTimer\(\) expects#' - - - '#Method ipl\\Scheduler\\.*::jsonSerialize\(\) return type has no value type specified in iterable type#' - - '#Method ipl\\Scheduler\\.* should return \$this.* but returns static#' - - '#Parameter \#2 \$before of class Recurr\\Transformer\\Constraint\\BetweenConstraint constructor#' - - '#Parameter \#1 \$rrule of class Recurr\\Rule constructor expects string\|null, array.*\|string given#' - '#Parameter \#1 \$callback of function call_user_func_array expects callable\(\): mixed, array{Recurr\\Rule, string} given#'