diff --git a/src/Handler/Http/Middleware/XHProfTrap.php b/src/Handler/Http/Middleware/XHProfTrap.php index f2b56cc4..662bc0dd 100644 --- a/src/Handler/Http/Middleware/XHProfTrap.php +++ b/src/Handler/Http/Middleware/XHProfTrap.php @@ -70,18 +70,17 @@ private function processStore(ServerRequestInterface $request): ResponseInterfac /** @var XHProfMessage $payload */ $payload = \json_decode((string) $request->getBody(), true, 96, \JSON_THROW_ON_ERROR); + \is_array($payload['profile'] ?? null) && \is_array($payload['tags'] ?? null) + or throw new \InvalidArgumentException('Invalid payload'); + $metadata = $payload; unset($metadata['profile'], $metadata['tags'], $metadata['date']); - isset($payload['profile'], $payload['tags']) - && \is_array($payload['profile']) - && \is_array($payload['tags']) - or throw new \InvalidArgumentException('Invalid payload'); - - /** @psalm-suppress MixedAssignment */ + /** @var mixed $time */ $time = $request->getAttribute('begin_at'); $time = $time instanceof \DateTimeImmutable ? $time : new \DateTimeImmutable(); + /** @psalm-suppress MixedArgumentTypeCoercion */ \Fiber::suspend( new Frame\Profiler( payload: Frame\Profiler\Payload::new( diff --git a/src/Module/Profiler/Struct/Profile.php b/src/Module/Profiler/Struct/Profile.php index 5123ae5c..b9052276 100644 --- a/src/Module/Profiler/Struct/Profile.php +++ b/src/Module/Profiler/Struct/Profile.php @@ -9,6 +9,7 @@ * app_name?: string, * hostname?: string, * filename?: string, + * filesize?: int<0, max>, * ... * } * @@ -30,6 +31,7 @@ final class Profile implements \JsonSerializable { public Peaks $peaks; + public Tree $calls; /** * @param Metadata $metadata @@ -40,9 +42,10 @@ public function __construct( public \DateTimeInterface $date = new \DateTimeImmutable(), public array $metadata = [], public array $tags = [], - public Tree $calls = new Tree(), + ?Tree $calls = null, ?Peaks $peaks = null, ) { + $this->calls = $calls ?? new Tree(); if ($peaks === null) { $this->peaks = new Peaks(); @@ -57,28 +60,32 @@ public function __construct( /** * @param ProfileData $data + * @psalm-suppress all */ public static function fromArray(array $data): self { $metadata = $data; unset($metadata['tags'], $metadata['peaks'], $metadata['total_edges'], $metadata['date']); - $self = new self( + return new self( date: new \DateTimeImmutable('@' . $data['date']), metadata: $metadata, tags: $data['tags'], // todo calls from edges peaks: Peaks::fromArray($data['peaks']), ); - - return $self; } /** * @return ProfileData + * @psalm-suppress all */ public function jsonSerialize(): array { + /** @var array $edges */ + $edges = \iterator_to_array($this->calls->getItemsSortedV1( + static fn(Branch $a, Branch $b): int => $b->item->cost->wt <=> $a->item->cost->wt, + )); return [ 'date' => $this->date->getTimestamp(), 'app_name' => $this->metadata['app_name'] ?? '', @@ -86,9 +93,7 @@ public function jsonSerialize(): array 'filename' => $this->metadata['filename'] ?? '', 'tags' => $this->tags, 'peaks' => $this->peaks, - 'edges' => \iterator_to_array($this->calls->getItemsSortedV1( - static fn(Branch $a, Branch $b): int => $b->item->cost->wt <=> $a->item->cost->wt, - )), + 'edges' => $edges, 'total_edges' => $this->calls->count(), ]; } diff --git a/src/Proto/Frame/Profiler.php b/src/Proto/Frame/Profiler.php index df15912d..c1039d48 100644 --- a/src/Proto/Frame/Profiler.php +++ b/src/Proto/Frame/Profiler.php @@ -4,13 +4,13 @@ namespace Buggregator\Trap\Proto\Frame; +use Buggregator\Trap\Module\Profiler\Struct\Profile; use Buggregator\Trap\Proto\Frame; use Buggregator\Trap\ProtoType; use Buggregator\Trap\Support\Json; /** - * @psalm-import-type Calls from \Buggregator\Trap\Proto\Frame\Profiler\Payload - * @psalm-import-type Metadata from \Buggregator\Trap\Proto\Frame\Profiler\Payload + * @psalm-import-type ProfileData from Profile * * @internal * @psalm-internal Buggregator @@ -26,7 +26,7 @@ public function __construct( public static function fromString(string $payload, \DateTimeImmutable $time): static { - /** @var array{type: non-empty-string}&Calls&Metadata $data */ + /** @var array{type: non-empty-string}|ProfileData $data */ $data = Json::decode($payload); return new self(Frame\Profiler\Payload::fromArray($data), $time); diff --git a/src/Proto/Frame/Profiler/Payload.php b/src/Proto/Frame/Profiler/Payload.php index 249a6833..51fd02c6 100644 --- a/src/Proto/Frame/Profiler/Payload.php +++ b/src/Proto/Frame/Profiler/Payload.php @@ -45,15 +45,21 @@ public static function new( } /** - * @param array{type: non-empty-string}&ProfileData $data + * @param array{type: non-empty-string}|ProfileData $data */ public static function fromArray(array $data, ?PayloadType $type = null): static { - /** @var \Closure(): Profile $provider */ + /** + * @var \Closure(): Profile $provider + * @psalm-suppress all + */ $provider = static fn(): Profile => Profile::fromArray($data); + /** @psalm-suppress all */ + $type ??= PayloadType::from($data['type']); + return new self( - $type ?? PayloadType::from($data['type']), + $type, $provider, ); } @@ -64,7 +70,7 @@ public function getProfile(): Profile } /** - * @return array{type: non-empty-string}&ProfileData + * @return array{type: non-empty-string}|ProfileData */ public function toArray(): array { @@ -72,7 +78,7 @@ public function toArray(): array } /** - * @return array{type: non-empty-string}&ProfileData + * @return array{type: non-empty-string}|ProfileData */ public function jsonSerialize(): array { diff --git a/src/Sender/Console/Renderer/Profiler.php b/src/Sender/Console/Renderer/Profiler.php index 2ba95d5f..7d7cdaf9 100644 --- a/src/Sender/Console/Renderer/Profiler.php +++ b/src/Sender/Console/Renderer/Profiler.php @@ -24,6 +24,9 @@ public function isSupport(Frame $frame): bool return $frame->type === ProtoType::Profiler; } + /** + * @psalm-suppress MixedAssignment + */ public function render(OutputInterface $output, Frame $frame): void { \assert($frame instanceof Frame\Profiler); @@ -37,11 +40,11 @@ public function render(OutputInterface $output, Frame $frame): void $data = []; isset($metadata['date']) && \is_numeric($metadata['date']) and $data['Time'] = new \DateTimeImmutable('@' . $metadata['date']); - isset($metadata['app_name']) and $data['App name'] = $metadata['app_name']; - isset($metadata['hostname']) and $data['Hostname'] = $metadata['hostname']; - isset($metadata['filename']) and $data['File name'] = $metadata['filename'] . ( - isset($metadata['filesize']) && \is_int($metadata['filesize']) - ? ' (' . Measure::memory($metadata['filesize']) . ')' + \is_string($m = $metadata['app_name'] ?? null) and $data['App name'] = $m; + \is_string($m = $metadata['hostname'] ?? null) and $data['Hostname'] = $m; + \is_string($m = $metadata['filename'] ?? null) and $data['File name'] = $m . ( + \is_int($m = $metadata['filesize'] ?? null) && $m >= 0 + ? ' (' . Measure::memory($m) . ')' : '' ); $data['Num edges'] = $profile->calls->count(); diff --git a/src/Sender/Console/Renderer/Smtp.php b/src/Sender/Console/Renderer/Smtp.php index 1b426431..e89c4ae8 100644 --- a/src/Sender/Console/Renderer/Smtp.php +++ b/src/Sender/Console/Renderer/Smtp.php @@ -89,7 +89,7 @@ public function render(OutputInterface $output, Frame $frame): void \array_map(static fn($attach) => [ 'CID' => $attach->getEmbeddingId(), 'Name' => $attach->getClientFilename(), - 'Size' => Measure::memory($attach->getSize()), + 'Size' => $attach->getSize() === null ? 'unknown size' : Measure::memory($attach->getSize()), 'MIME' => $attach->getClientMediaType(), ], $embeddings), 'compact', diff --git a/src/Sender/Console/Support/Files.php b/src/Sender/Console/Support/Files.php index 255538c8..c66ddb01 100644 --- a/src/Sender/Console/Support/Files.php +++ b/src/Sender/Console/Support/Files.php @@ -42,7 +42,7 @@ public static function renderFile( : \str_pad(\substr($fileName, $dotPos + 1), 3, ' ', \STR_PAD_BOTH); // File size - $sizeStr = Measure::memory($size) ?? 'unknown size'; + $sizeStr = $size === null ? 'unknown size' : Measure::memory($size); // Header with top border $output->writeln(" ┌───┐ $fileName"); diff --git a/src/Sender/Console/Support/Tables.php b/src/Sender/Console/Support/Tables.php index d15b3cd9..ae0ad2af 100644 --- a/src/Sender/Console/Support/Tables.php +++ b/src/Sender/Console/Support/Tables.php @@ -44,7 +44,7 @@ public static function renderKeyValueTable(OutputInterface $output, string $titl } /** - * @param array> $data + * @param array> $data * @param 'default'|'borderless'|'compact'|'symfony-style-guide'|'box'|'box-double' $style */ public static function renderMultiColumnTable( diff --git a/src/Sender/Frontend/Event.php b/src/Sender/Frontend/Event.php index bd2f68ea..87f9cc65 100644 --- a/src/Sender/Frontend/Event.php +++ b/src/Sender/Frontend/Event.php @@ -14,7 +14,7 @@ final class Event implements \JsonSerializable /** * @param non-empty-string $uuid * @param non-empty-string $type - * @param \ArrayAccess|null $assets + * @param \ArrayAccess&\Traversable|null $assets */ public function __construct( public readonly string $uuid, diff --git a/src/Sender/Frontend/Mapper/HttpRequest.php b/src/Sender/Frontend/Mapper/HttpRequest.php index 0fbf6742..8e25a678 100644 --- a/src/Sender/Frontend/Mapper/HttpRequest.php +++ b/src/Sender/Frontend/Mapper/HttpRequest.php @@ -19,7 +19,7 @@ public function map(HttpFrame $frame): Event $request = $frame->request; $uri = \ltrim($request->getUri()->getPath(), '/'); - /** @var \ArrayAccess $assets */ + /** @var \ArrayObject $assets */ $assets = new \ArrayObject(); return new Event( diff --git a/src/Sender/Frontend/Mapper/Smtp.php b/src/Sender/Frontend/Mapper/Smtp.php index 11dd9273..2b6ece70 100644 --- a/src/Sender/Frontend/Mapper/Smtp.php +++ b/src/Sender/Frontend/Mapper/Smtp.php @@ -51,11 +51,11 @@ private static function assetLink(string $uuid, Event\Asset $asset): string } /** - * @return \ArrayAccess + * @return \ArrayObject */ - private static function fetchAssets(SmtpMessage $message): \ArrayAccess + private static function fetchAssets(SmtpMessage $message): \ArrayObject { - /** @var \ArrayAccess $assets */ + /** @var \ArrayObject $assets */ $assets = new \ArrayObject(); foreach ($message->getAttachments() as $attachment) { @@ -83,9 +83,9 @@ private static function asset(File $attachment): Event\Asset /** * @param non-empty-string $uuid - * @param \ArrayAccess $assets + * @param \ArrayAccess&\Traversable $assets */ - private static function html(string $uuid, SmtpMessage $message, \ArrayAccess $assets): string + private static function html(string $uuid, SmtpMessage $message, \ArrayAccess&\Traversable $assets): string { $result = $message->getMessage(MessageFormat::Html)?->getValue() ?? ''; diff --git a/src/Sender/Frontend/Service.php b/src/Sender/Frontend/Service.php index ba41cccb..90a57f83 100644 --- a/src/Sender/Frontend/Service.php +++ b/src/Sender/Frontend/Service.php @@ -76,7 +76,7 @@ public function smtpAttachments(string $uuid): Attachments|Success } $attaches = []; - foreach ($event->assets as $attachment) { + foreach ($event->assets ?? [] as $attachment) { $attachment instanceof AttachedFile and $attaches[] = $attachment; } diff --git a/src/Support/Measure.php b/src/Support/Measure.php index 6077ada5..e7363870 100644 --- a/src/Support/Measure.php +++ b/src/Support/Measure.php @@ -11,15 +11,11 @@ final class Measure { /** - * @param int<0, max>|null $size - * @return non-empty-string|null + * @param int<0, max> $size + * @return non-empty-string */ - public static function memory(?int $size): ?string + public static function memory(int $size): string { - if ($size === null) { - return null; - } - $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; $power = (int) \floor(\log($size, 1024)); $float = $power > 0 ? \round($size / (1024 ** $power), 2) : $size; diff --git a/src/Support/Stream/Base64DecodeFilter.php b/src/Support/Stream/Base64DecodeFilter.php index 2a660533..3d8cd3ec 100644 --- a/src/Support/Stream/Base64DecodeFilter.php +++ b/src/Support/Stream/Base64DecodeFilter.php @@ -5,8 +5,11 @@ namespace Buggregator\Trap\Support\Stream; /** + * @psalm-suppress all + * * @internal * @psalm-internal Buggregator\Trap + * * @link https://www.php.net/manual/function.stream-filter-register.php */ final class Base64DecodeFilter extends \php_user_filter @@ -15,12 +18,18 @@ final class Base64DecodeFilter extends \php_user_filter private string $buffer = ''; + /** + * @param resource $in + * @param resource $out + * @param int<0, max> $consumed + */ public function filter($in, $out, &$consumed, bool $closing): int { // Buffer size $bs = \strlen($this->buffer); while ($bucket = \stream_bucket_make_writeable($in)) { + /** @var int<1, max> $len */ $len = $bs + $bucket->datalen; $d = $len % 4; @@ -43,7 +52,7 @@ public function filter($in, $out, &$consumed, bool $closing): int } // Decode part of the data - $bucket->data = \base64_decode($this->buffer . \substr($bucket->data, 0, -$d), true); + $bucket->data = \base64_decode($this->buffer . \substr($bucket->data, 0, -$d)); $consumed += $bucket->datalen; $this->buffer = \substr($bucket->data, -$d); diff --git a/src/Support/StreamHelper.php b/src/Support/StreamHelper.php index d7c99559..07f80626 100644 --- a/src/Support/StreamHelper.php +++ b/src/Support/StreamHelper.php @@ -131,21 +131,25 @@ public static function unzipBody(ServerRequestInterface $request): ServerRequest /** * @param array $readFilters filter name => filter options * @param array $writeFilters filter name => filter options + * + * @psalm-suppress UnusedFunctionCall */ public static function createFileStream(array $readFilters = [], array $writeFilters = []): StreamInterface { $stream = \fopen('php://temp/maxmemory:' . self::MAX_FILE_MEMORY_SIZE, 'w+b'); + /** @var mixed $options */ foreach ($readFilters as $filter => $options) { \is_string($filter) ? \stream_filter_append($stream, $filter, \STREAM_FILTER_READ, $options) - : \stream_filter_append($stream, $options, \STREAM_FILTER_READ); + : \stream_filter_append($stream, (string) $options, \STREAM_FILTER_READ); } + /** @var mixed $options */ foreach ($writeFilters as $filter => $options) { \is_string($filter) ? \stream_filter_append($stream, $filter, \STREAM_FILTER_WRITE, $options) - : \stream_filter_append($stream, $options, \STREAM_FILTER_WRITE); + : \stream_filter_append($stream, (string) $options, \STREAM_FILTER_WRITE); } return Stream::create($stream); diff --git a/src/Traffic/Message/Multipart/File.php b/src/Traffic/Message/Multipart/File.php index 64538495..4d866731 100644 --- a/src/Traffic/Message/Multipart/File.php +++ b/src/Traffic/Message/Multipart/File.php @@ -115,7 +115,7 @@ public function getClientFilename(): ?string public function getClientMediaType(): ?string { - return \explode(';', $this->getHeader('Content-Type')[0], 2)[0] ?? null; + return \explode(';', $this->getHeader('Content-Type')[0] ?? '', 2)[0] ?? null; } public function isEmbedded(): bool