diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 00000000..d2b754ee --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,65 @@ +name: Psalm + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'bin/**' + - 'resources/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - '.editorconfig' + - 'psalm.xml' + + push: + paths-ignore: + - 'docs/**' + - 'bin/**' + - 'resources/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - '.editorconfig' + - 'psalm.xml' + +jobs: + psalm: + name: Psalm Validation (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + php: [8.2] + os: [ubuntu-latest] + steps: + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom + + - name: Check Out Code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: php-${{ matrix.php }}-${{ runner.os }}-composer- + + - name: Install Composer Dependencies + run: composer install --prefer-dist --no-interaction + + - name: Run Tests + run: vendor/bin/psalm diff --git a/.github/workflows/run-test-suite.yml b/.github/workflows/run-test-suite.yml new file mode 100644 index 00000000..3ee86004 --- /dev/null +++ b/.github/workflows/run-test-suite.yml @@ -0,0 +1,112 @@ +name: Unit + +on: + workflow_call: + inputs: + test-suite: + required: true + type: string + fail-fast: + required: false + type: boolean + default: true + test-timeout: + required: false + type: number + default: 15 + + pull_request: + paths-ignore: + - 'docs/**' + - 'bin/**' + - 'resources/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - '.editorconfig' + - 'psalm.xml' + + push: + paths-ignore: + - 'docs/**' + - 'bin/**' + - 'resources/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - '.editorconfig' + - 'psalm.xml' + +jobs: + test: + name: (PHP ${{ matrix.php }}, ${{ matrix.os }}, ${{ matrix.dependencies }} deps + runs-on: ${{ matrix.os }} + timeout-minutes: ${{ matrix.timeout-minutes }} + env: { GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' } + strategy: + fail-fast: ${{ inputs.fail-fast }} + matrix: + php: [ 8.1, 8.2 ] + os: [ ubuntu-latest, windows-latest ] + dependencies: [ lowest , highest ] + timeout-minutes: [ '${{ inputs.test-timeout }}' ] + exclude: + - os: windows-latest + php: 8.2 + include: + - os: ubuntu-latest + php: 8.3 + dependencies: highest + timeout-minutes: 40 + steps: + - name: Set Git To Use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + extensions: sockets, curl + + - name: Check Out Code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Composer Dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: php-${{ matrix.php }}-${{ matrix.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + php-${{ matrix.php }}-${{ matrix.os }}-composer- + + - name: Install lowest dependencies from composer.json + if: matrix.dependencies == 'lowest' + run: composer update --no-interaction --no-progress --prefer-lowest + + - name: Validate lowest dependencies + if: matrix.dependencies == 'lowest' + env: + COMPOSER_POOL_OPTIMIZER: 0 + run: vendor/bin/validate-prefer-lowest + + - name: Install highest dependencies from composer.json + if: matrix.dependencies == 'highest' + run: composer update --no-interaction --no-progress + + - name: Run tests + run: vendor/bin/phpunit --testsuite=${{ inputs.test-suite }} --testdox diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..3adb5889 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,65 @@ +name: Security + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'bin/**' + - 'resources/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - '.editorconfig' + - 'psalm.xml' + + push: + paths-ignore: + - 'docs/**' + - 'bin/**' + - 'resources/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - '.editorconfig' + - 'psalm.xml' + +jobs: + security: + name: Security Checks (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Note: This workflow requires only the LATEST version of PHP + php: [ 8.2 ] + os: [ ubuntu-latest ] + steps: + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, sockets, grpc, curl + + - name: Check Out Code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: php-${{ matrix.php }}-${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: php-${{ matrix.php }}-${{ runner.os }}-composer- + + - name: Install Composer Dependencies + run: composer install --prefer-dist --no-interaction + + - name: Verify + run: composer require --dev roave/security-advisories:dev-latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..d8e8773b --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,11 @@ +name: Testing + +on: [push, pull_request] + +jobs: + unit: + name: Unit Testing + uses: ./.github/workflows/run-test-suite.yml + with: + fail-fast: false + test-suite: Unit diff --git a/README.md b/README.md index e454efbe..48aba03a 100644 --- a/README.md +++ b/README.md @@ -12,26 +12,55 @@ [![Twitter](https://img.shields.io/badge/-Follow-black?style=flat-square&logo=X)](https://twitter.com/buggregator) [![Discord](https://img.shields.io/discord/1172942458598985738?style=flat-square&logo=discord&color=0000ff)](https://discord.gg/qF3HpXhMEP) - - -- [Intro](#intro) +
+ +**Buggregator Trap** is a minified version of the [Buggregator Server](https://github.com/buggregator/server) +in the form of a terminal application and a set of utilities to assist with debugging. +The package is designed to enhance the debugging experience in conjunction with the Buggregator Server. + - [Installation](#installation) +- [Overview](#overview) - [Usage](#usage) - [Contributing](#contributing) - [License](#license) +## Installation + +To install Buggregator Trap in your PHP application, add the package as a dev dependency +to your project using Composer: + +```bash +composer require --dev buggregator/trap +``` + +[![PHP](https://img.shields.io/packagist/php-v/buggregator/trap.svg?style=flat-square&logo=php)](https://packagist.org/packages/buggregator/trap) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/buggregator/trap.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/buggregator/trap) +[![License](https://img.shields.io/packagist/l/buggregator/trap.svg?style=flat-square)](LICENSE.md) +[![Total Downloads](https://img.shields.io/packagist/dt/buggregator/trap.svg?style=flat-square)](https://packagist.org/packages/buggregator/trap) + +And that's it. Trap is [ready to go](#usage). + +## Overview -## Intro +Buggregator Trap provides a toolkit for use in your code. Firstly, just having Buggregator Trap in your +package enhances the capabilities of Symfony Var-Dumper. -Buggregator Trap, the streamlined Command Line Interface (CLI) version of Buggregator, marks a new era in debugging PHP -applications. Boasting an array of powerful debugging "traps", including: +If you've already worked with google/protobuf, you probably know how unpleasant it can be. +Now let's compare the dumps of protobuf-message: on the left (with trap) and on the right (without trap). + +![trap-proto-diff](https://github.com/buggregator/trap/assets/4152481/30662429-809e-422a-83c6-61d7d2788b18) + +This simultaneously compact and informative output format of protobuf message will be just as compact +and informative in the Buggregator Server interface. Now, working with protobuf is enjoyable. + +--- + +Buggreagtor Trap includes a console application - a mini-server. +The application is entirely written in PHP and does not require Docker to be installed in the system. +It may seem like it's just the same as the `symfony/var-dumper` server, but it's not. +Buggregator Trap boasts a much wider range of handlers ("traps") for debug messages: - Symfony var-dumper, - Monolog, @@ -39,18 +68,11 @@ applications. Boasting an array of powerful debugging "traps", including: - SMTP, - HTTP dumps, - Ray, -- And any raw data - -This lightweight tool facilitates and streamlines the process of debugging for developers, regardless of their -environment. +- Any raw data -It enables you to easily install a client in your PHP application using a Composer package and run a local server -specifically designed for debugging. This isn't just a debugging tool, it's a supercharged version of -the `symfony/var-dumper` server, designed to offer more versatility and in-depth insights into your code. +You can effortlessly visualize and analyze console information about various elements of your codebase. -Now you can effortlessly visualize and analyze console information about various elements of your codebase. - -Here's a sneak peek into the console output you can expect with traps: +Here's a sneak peek into the console output you can expect with Trap: | symfony/var-dumper (proto) | Binary Data | |--------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| @@ -60,40 +82,39 @@ Here's a sneak peek into the console output you can expect with traps: |--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| | ![smtp](https://github.com/buggregator/trap/assets/4152481/b11c4a7f-072a-4e66-b11d-9bbd3177bfe2) | ![http-dump](https://github.com/buggregator/trap/assets/4152481/48201ce6-7756-4402-8954-76a27489b632) | -In addition to the local debugging features, Buggregator Trap provides an innovative functionality as a proxy client. It -can transmit data to a remote Buggregator server, thereby facilitating a centralized debugging process for your team or -organization. - -## Installation +--- -To install Buggregator Trap in your PHP application, add the package as a dependency to your project using Composer: +Additionally, you can manually set traps in the code. Use the `trap()` function, +which works almost the same as Symfony's `dump()`, but configures the dumper to send dumps to the local server, +unless other user settings are provided. -```bash -composer require --dev buggregator/trap -``` +--- +We care about the quality of our products' codebase and strive to provide the best user experience. +Buggregator Trap has passed the Proof of Concept stage and is now an important part of the Buggregator ecosystem. +We have big plans for the development of the entire ecosystem and we would be delighted if you join us on this journey. ## Usage -After successfully installing Buggregator Trap, you can initiate the debugging process by using the following command: +After successfully [installing](#installation) Buggregator Trap, you can initiate the debugging process by using the following command: ```bash vendor/bin/trap ``` -This command will activate the Trap server, setting it up to listen for all TCP requests. Upon receiving these requests, -the server will search for a corresponding listener that can process the incoming data and display a dump accordingly. +This command will start the Trap server, ready to receive any debug messages. Once a debug message is trapped, you will see a convenient report about it right here in the terminal. Then just call the `trap()` function in your code: ```php -trap(); // dump current stack trace -trap($var); // dump variable -trap($var, foo: $far, bar: $bar); // dump variables sequence +trap(); // dump the current stack trace +trap($var); // dump a variable +trap($var, foo: $far, bar: $bar); // dump a variables sequence ``` -The `trap()` function configures `$_SERVER['REMOTE_ADDR']` and `$_SERVER['REMOTE_PORT']` automatically, if they are not -set. Also, it can dump google/protobuf messages. +> **Note**: +> The `trap()` function configures `$_SERVER['REMOTE_ADDR']` and `$_SERVER['REMOTE_PORT']` automatically, +> if they are not set. ### Default port @@ -159,3 +180,22 @@ becoming a better developer. So, don't hesitate to jump in and start contributin ## License Buggregator Trap is open-sourced software licensed under the BSD-3 license. + + + + + diff --git a/composer.json b/composer.json index c69e97f8..1c999455 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,11 @@ "name": "buggregator/trap", "type": "library", "license": "BSD-3-Clause", + "description": "A simple and powerful tool for debugging PHP applications.", "homepage": "https://buggregator.dev/", + "keywords": [ + "debug", "cli", "console", "sentry", "smtp", "dump" + ], "authors": [ { "name": "Aleksei Gagarin (roxblnfk)", @@ -55,16 +59,17 @@ "nunomaduro/termwind": "^1.15", "nyholm/psr7": "^1.8", "php-http/message": "^1.15", - "psr/http-message": "^1 || ^2", - "symfony/console": "^5 || ^6 || ^7" + "psr/http-message": "^1.1 || ^2", + "symfony/console": "^5.4 || ^6 || ^7" }, "suggest": { "symfony/var-dumper": "To use the trap() function" }, "require-dev": { + "dereuromark/composer-prefer-lowest": "^0.1.10", "google/protobuf": "^3.23", "phpunit/phpunit": "^10.4", - "vimeo/psalm": "^5.11", - "symfony/var-dumper": "^6 || ^7" + "symfony/var-dumper": "^6 || ^7", + "vimeo/psalm": "^5.11" } } diff --git a/phpunit.xml b/phpunit.xml index d92f77f0..c2d8d4a1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,17 +1,19 @@ - - tests + + tests/Unit diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 00000000..7f3ec765 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,589 @@ + + + + + $data + + + + + $port + + + + + $output::OUTPUT_RAW + $output::OUTPUT_RAW + + + $logger + + + \is_resource($fp) + + + + $buf === null + + + + + $time + + + $response + + + $time + + + $response + + + + + $time + $time + + + $time + + + + + $payload + getAttribute('begin_at', null)]]> + getAttribute('begin_at', null)]]> + + + $payload + + + + + $offset - $currentPos + + + $headers + $itemHeader + + + + + + + $headers + $itemHeader + $type + + + + + TReturn + + + method]]> + + + {$this->method}(...$arguments)]]> + + + + + $values + $values + $values + $values + + + + + stream->getSize()]]> + + + int + + + + + request->getBody()->getSize() + \array_reduce( + $this->request->getUploadedFiles(), + static fn(int $carry, array $file): int => $carry + $file['size'], + 0 + )]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + $payload + + + int + + + + + + + + + request->getUploadedFiles()]]> + array + + + int + + + request->getBody()->getSize()]]> + + + + + \json_decode($payload, true, JSON_THROW_ON_ERROR) + + + + + SentryEnvelope::fromArray($data, $time), + $data['type'] === SentryStore::SENTRY_FRAME_TYPE => SentryStore::fromArray($data, $time), + default => throw new \InvalidArgumentException('Unknown Sentry frame type.'), + }]]> + + + $data + $data + + + + + + + $data + + + static + + + + + + $item + $item + + + $item + + + + + public function __construct( + + + + + $payload + + + $payload + + + + + $payload + + + + + + + + Files::normalizeSize($size) + + + + + $frame + + + getCookieParams()]]> + getQueryParams()]]> + + + + + + + + + + + $file + $file + $function + + + $output->writeln(]]> + + + + + + + $row + $stacktrace + + + + + + $file + $file + $function + + + + + renderMessageHeader + + + + + + $message + $meta + $tags + + + + + + + + + + + + + payload]]> + $type + + + payload['exceptions']]]> + + + $type + + + $type + + + + + getHeaders()]]> + getHeaders()]]> + + + getClientFilename()]]> + getClientMediaType()]]> + + + + + render + + + + + + + + + + + $context + $controller + $data + $fileLink + + + + + + + $meta + + + $context + + $data + + + $controller + $describer + $fileLink + + + + + $payload + [$data, $context] + + + describe + + + dumper->dump($controller, true)]]> + dumper->dump($data, true)]]> + + + + + $item + + + + \array_keys($data) + + + $item + + + + + $size + + + + + + + + $key + + + $value + + + + + Termwind::renderUsing($output) + new HtmlRenderer() + + + Termwind::renderUsing($output) + + + + + new SplQueue() + + + + + payloadSize]]> + + + \Closure::bind($callable(...), $this) + \Closure::bind($callable(...), $this) + + + + + socket]]> + socket]]> + + + + + new \SplQueue() + + + + + getClass()]]> + $value + $value + + + getName()]]]> + getName()]]]> + + + $value + getName()]]]> + + + getDescriptorByClassName + + + + + + + + + + $value + $values + + + headers[$header]]]> + + + headers[$header]]]> + headers[$new->headerNames[$normalized]]]]> + headers[$header]]]> + headers[$header]]]> + headers[$header]]]> + + + headers[$header]]]> + + + $header + $header + $header + + + string[] + + + + + FieldDataArray + $this->value, + ]]]> + + + + + FileDataArray + $this->fileName, + 'size' => $this->getSize(), + ]]]> + + + + + new File($headers, $name, $fileName), + false => new Field($headers, $name), + }]]> + + + $headers + $headers + $headers + + + static + + + + + $addrs + protocol['BCC'] ?? []]]> + + + protocol]]> + + + $attach + + + $message + + + $attach + $message + + + >]]> + + + $attachments + $messages + + + + + $boundary + $headersBlock + read($blockEnd - $pos + 2)]]> + + + $parts + $result + + + >]]> + array{0: non-empty-string, 1: non-empty-string, 2: non-empty-string} + + + getSize()]]> + + + $value + $value + + + + + $boundary + $headerBlock + + + + + AbstractCloner::$defaultCasters[EnumValue::class] + AbstractCloner::$defaultCasters[MapField::class] + AbstractCloner::$defaultCasters[Message::class] + AbstractCloner::$defaultCasters[RepeatedField::class] + + + AbstractCloner::$defaultCasters[EnumValue::class] + AbstractCloner::$defaultCasters[MapField::class] + AbstractCloner::$defaultCasters[Message::class] + AbstractCloner::$defaultCasters[RepeatedField::class] + + + $value + AbstractCloner::$defaultCasters[EnumValue::class] + AbstractCloner::$defaultCasters[MapField::class] + AbstractCloner::$defaultCasters[Message::class] + AbstractCloner::$defaultCasters[RepeatedField::class] + + + diff --git a/psalm.xml b/psalm.xml index 0153171d..120f6059 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,6 +6,7 @@ resolveFromConfigFile="true" findUnusedBaselineEntry="false" findUnusedCode="false" + errorBaseline="psalm-baseline.xml" > @@ -15,6 +16,7 @@ + diff --git a/src/Handler/Http/Handler/Websocket.php b/src/Handler/Http/Handler/Websocket.php index bcfbb141..9d6dbf9d 100644 --- a/src/Handler/Http/Handler/Websocket.php +++ b/src/Handler/Http/Handler/Websocket.php @@ -66,7 +66,7 @@ public function handle(StreamClient $streamClient, ServerRequestInterface $reque } /** - * Todo: extract to a separate Websocket service + * Todo: extract to a separated Websocket service * * @return iterable */ diff --git a/src/Handler/Pipeline.php b/src/Handler/Pipeline.php index 00e3e8b2..2e1c4d36 100644 --- a/src/Handler/Pipeline.php +++ b/src/Handler/Pipeline.php @@ -15,7 +15,6 @@ * * @psalm-type TLast = Closure(mixed ...): mixed * - * @psalm-immutable * @internal * @psalm-internal Buggregator\Trap */ @@ -30,7 +29,7 @@ final class Pipeline /** * @param iterable $middlewares * @param non-empty-string $method - * @param Closure(): TReturn $last + * @param Closure(mixed...): TReturn $last * @param class-string|string $returnType */ private function __construct( diff --git a/src/Proto/Frame/Http.php b/src/Proto/Frame/Http.php index fc2b9009..69cdf8f4 100644 --- a/src/Proto/Frame/Http.php +++ b/src/Proto/Frame/Http.php @@ -50,7 +50,7 @@ public static function fromString(string $payload, DateTimeImmutable $time): sta $request = new ServerRequest( $payload['method'] ?? 'GET', $payload['uri'] ?? '/', - (array)$payload['headers'] ?? [], + (array)($payload['headers'] ?? []), $payload['body'] ?? '', $payload['protocolVersion'] ?? '1.1', $payload['serverParams'] ?? [], diff --git a/src/Proto/Frame/Sentry/SentryStore.php b/src/Proto/Frame/Sentry/SentryStore.php index 67f5c23e..81034df3 100644 --- a/src/Proto/Frame/Sentry/SentryStore.php +++ b/src/Proto/Frame/Sentry/SentryStore.php @@ -39,7 +39,7 @@ final class SentryStore extends Frame\Sentry * filename: non-empty-string, * lineno: positive-int, * abs_path: non-empty-string, - * context_line: non-empty-string, + * context_line: non-empty-string * } * } * }> diff --git a/src/Sender/Console/Renderer/Monolog.php b/src/Sender/Console/Renderer/Monolog.php index 9cd364dc..071c6f17 100644 --- a/src/Sender/Console/Renderer/Monolog.php +++ b/src/Sender/Console/Renderer/Monolog.php @@ -28,6 +28,8 @@ public function isSupport(Frame $frame): bool public function render(OutputInterface $output, Frame $frame): void { + \assert($frame instanceof Frame\Monolog); + $payload = $frame->message; $levelColor = match (\strtolower($payload['level_name'])) { 'notice', 'info' => 'blue', @@ -41,7 +43,7 @@ public function render(OutputInterface $output, Frame $frame): void [ 'date' => $payload['datetime'], 'channel' => $payload['channel'] ?? '', - 'level' => $payload['level_name'] . '' ?? 'DEBUG', + 'level' => $payload['level_name'] ?? 'DEBUG', 'levelColor' => $levelColor, 'messages' => \explode("\n", $payload['message']), ] diff --git a/src/Sender/Console/Renderer/Sentry/Exceptions.php b/src/Sender/Console/Renderer/Sentry/Exceptions.php index 70dffc81..c6d0f333 100644 --- a/src/Sender/Console/Renderer/Sentry/Exceptions.php +++ b/src/Sender/Console/Renderer/Sentry/Exceptions.php @@ -10,8 +10,6 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @implements RendererInterface - * * @internal * @psalm-internal Buggregator\Trap\Sender\Console\Renderer */ diff --git a/src/Sender/Console/Renderer/Sentry/Header.php b/src/Sender/Console/Renderer/Sentry/Header.php index b207f16c..99818fb8 100644 --- a/src/Sender/Console/Renderer/Sentry/Header.php +++ b/src/Sender/Console/Renderer/Sentry/Header.php @@ -11,8 +11,6 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @implements RendererInterface - * * @internal */ final class Header diff --git a/src/Sender/Console/Renderer/SentryEnvelope.php b/src/Sender/Console/Renderer/SentryEnvelope.php index b34736d0..1491daf2 100644 --- a/src/Sender/Console/Renderer/SentryEnvelope.php +++ b/src/Sender/Console/Renderer/SentryEnvelope.php @@ -26,6 +26,8 @@ public function isSupport(Frame $frame): bool public function render(OutputInterface $output, Frame $frame): void { + \assert($frame instanceof Frame\Sentry\SentryEnvelope); + Common::renderHeader1($output, 'SENTRY', 'ENVELOPE'); Header::renderMessageHeader($output, $frame->headers + ['timestamp' => $frame->time->format('U.u')]); diff --git a/src/Sender/Console/Renderer/SentryStore.php b/src/Sender/Console/Renderer/SentryStore.php index 43f51143..fc9867d3 100644 --- a/src/Sender/Console/Renderer/SentryStore.php +++ b/src/Sender/Console/Renderer/SentryStore.php @@ -26,6 +26,8 @@ public function isSupport(Frame $frame): bool public function render(OutputInterface $output, Frame $frame): void { + \assert($frame instanceof Frame\Sentry\SentryStore); + Common::renderHeader1($output, 'SENTRY'); try { diff --git a/src/Sender/Console/Renderer/Smtp.php b/src/Sender/Console/Renderer/Smtp.php index b49b83c4..5b40302d 100644 --- a/src/Sender/Console/Renderer/Smtp.php +++ b/src/Sender/Console/Renderer/Smtp.php @@ -25,6 +25,8 @@ public function isSupport(Frame $frame): bool public function render(OutputInterface $output, Frame $frame): void { + \assert($frame instanceof Frame\Smtp); + Common::renderHeader1($output, 'SMTP'); Common::renderMetadata($output, [ 'Time' => $frame->time->format('Y-m-d H:i:s.u'), diff --git a/src/Sender/Console/Renderer/VarDumper.php b/src/Sender/Console/Renderer/VarDumper.php index db465489..93b86766 100644 --- a/src/Sender/Console/Renderer/VarDumper.php +++ b/src/Sender/Console/Renderer/VarDumper.php @@ -32,6 +32,8 @@ public function isSupport(Frame $frame): bool public function render(OutputInterface $output, Frame $frame): void { + \assert($frame instanceof Frame\VarDumper); + $payload = @\unserialize(\base64_decode($frame->dump), ['allowed_classes' => [Data::class, Stub::class]]); // Impossible to decode the message, give up. @@ -99,6 +101,7 @@ public function describe(OutputInterface $output, Data $data, array $context, in // Do nothing. } + /** @psalm-suppress InternalMethod, InternalClass */ Common::renderMetadata($output, $meta); $output->writeln(''); diff --git a/src/Sender/Console/RendererInterface.php b/src/Sender/Console/RendererInterface.php index c9937263..c75e8e2d 100644 --- a/src/Sender/Console/RendererInterface.php +++ b/src/Sender/Console/RendererInterface.php @@ -18,7 +18,7 @@ interface RendererInterface public function isSupport(Frame $frame): bool; /** - * @param TFrame $frame + * @psalm-assert-if-true TFrame $frame */ public function render(OutputInterface $output, Frame $frame): void; } diff --git a/src/Sender/Console/Support/Files.php b/src/Sender/Console/Support/Files.php index d3162bff..387e5376 100644 --- a/src/Sender/Console/Support/Files.php +++ b/src/Sender/Console/Support/Files.php @@ -55,6 +55,9 @@ public static function renderFile( $output->writeln(" └───┘ $type"); } + /** + * @param int<0, max>|null $size + */ public static function normalizeSize(?int $size): ?string { if ($size === null) { @@ -62,8 +65,11 @@ public static function normalizeSize(?int $size): ?string } $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; - $power = \floor(\log($size, 1024)); + $power = (int)\floor(\log($size, 1024)); $float = $power > 0 ? \round($size / (1024 ** $power), 2) : $size; - return $float . ' ' . $units[$power]; + + \assert($power >= 0 && $power <= 5); + + return \sprintf('%s %s', \number_format($float, 2), $units[$power]); } } diff --git a/src/Socket/Client.php b/src/Socket/Client.php index ed026fce..d90523d6 100644 --- a/src/Socket/Client.php +++ b/src/Socket/Client.php @@ -71,14 +71,17 @@ public function process(): void throw new \RuntimeException('Socket select failed.'); } + /** @psalm-suppress RedundantCondition */ if ($read !== []) { $this->readMessage(); } + /** @psalm-suppress RedundantCondition */ if ($write !== [] && $this->writeQueue !== []) { $this->writeQueue(); } + /** @psalm-suppress RedundantCondition */ if ($except !== [] || \socket_last_error($this->socket) !== 0) { throw new \RuntimeException('Socket exception.'); } @@ -150,14 +153,13 @@ private function readMessage(): void /** * @param positive-int $length - * - * @return non-empty-string */ private function readBytes(int $length, bool $canBeLess = false): string { while (($left = $length - \strlen($this->readBuffer)) > 0) { $data = ''; $read = @\socket_recv($this->socket, $data, $left, 0); + /** @psalm-suppress TypeDoesNotContainNull */ if ($read === false || $data === null) { if ($this->readBuffer !== '') { $result = $this->readBuffer; diff --git a/src/Socket/SocketStream.php b/src/Socket/SocketStream.php index 5d0d3c34..aca31bfd 100644 --- a/src/Socket/SocketStream.php +++ b/src/Socket/SocketStream.php @@ -16,6 +16,7 @@ * Use {@see Server::$clientInflector} to wrap {@see Client} into {@see self}. * * @internal + * @implements IteratorAggregate */ final class SocketStream implements IteratorAggregate, StreamClient { diff --git a/src/Support/ProtobufCaster.php b/src/Support/ProtobufCaster.php index 234e1558..a7d26023 100644 --- a/src/Support/ProtobufCaster.php +++ b/src/Support/ProtobufCaster.php @@ -173,19 +173,4 @@ class: $ed->getClass(), return $values; } - - private static function extractViaPublic(Message $message, PublicDescriptor $descriptor): mixed - { - for ($i = 0; $i < $descriptor->getFieldCount(); $i++) { - /** @var FieldDescriptor $d */ - $d = $descriptor->getField($i); - // $d-> - if ($d->getType() !== GPBType::MESSAGE) { - continue; - } - - // todo what can we do? - // $descriptor = $d->getMessageType(); - } - } } diff --git a/src/Support/StreamHelper.php b/src/Support/StreamHelper.php index 5d2b40d4..02e6889f 100644 --- a/src/Support/StreamHelper.php +++ b/src/Support/StreamHelper.php @@ -56,6 +56,8 @@ public static function strpos(StreamInterface $stream, string $substr): int|fals } $stream->seek($caret, \SEEK_SET); + + \assert($result >= 0); return $result; } diff --git a/src/Traffic/Message/Headers.php b/src/Traffic/Message/Headers.php index 4e96ab03..3be3a37f 100644 --- a/src/Traffic/Message/Headers.php +++ b/src/Traffic/Message/Headers.php @@ -30,10 +30,6 @@ public function hasHeader(string $header): bool */ public function getHeader(string $header): array { - if (!\is_string($header)) { - throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); - } - $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$header])) { return []; @@ -66,7 +62,7 @@ public function withHeader(string $header, $value): static public function withAddedHeader(string $header, string $value): static { - if (!\is_string($header) || '' === $header) { + if ('' === $header) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } @@ -78,10 +74,6 @@ public function withAddedHeader(string $header, string $value): static public function withoutHeader(string $header): static { - if (!\is_string($header)) { - throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); - } - $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$normalized])) { return $this; @@ -137,7 +129,7 @@ private function setHeaders(array $headers): void */ private function validateAndTrimHeader(string $header, $values): array { - if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) { + if (1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } diff --git a/src/Traffic/Message/Smtp.php b/src/Traffic/Message/Smtp.php index 996019cc..12d9c3e5 100644 --- a/src/Traffic/Message/Smtp.php +++ b/src/Traffic/Message/Smtp.php @@ -17,13 +17,6 @@ * @psalm-import-type FieldDataArray from Field * @psalm-import-type FileDataArray from File * - * @psalm-type SmtpDataArray = array{ - * protocol: array>, - * headers: array>, - * messages: array, - * attachments: array - * } - * * @internal */ final class Smtp implements JsonSerializable @@ -31,10 +24,10 @@ final class Smtp implements JsonSerializable use Headers; use StreamBody; - /** @var Field[] */ + /** @var list */ private array $messages = []; - /** @var File[] */ + /** @var list */ private array $attachments = []; /** @@ -57,9 +50,6 @@ public static function create(array $protocol, array $headers): self return new self($protocol, $headers); } - /** - * @param SmtpDataArray $data - */ public static function fromArray(array $data): self { $self = new self($data['protocol'], $data['headers']); @@ -73,9 +63,6 @@ public static function fromArray(array $data): self return $self; } - /** - * @return SmtpDataArray - */ public function jsonSerialize(): array { return [ diff --git a/src/Traffic/StreamClient.php b/src/Traffic/StreamClient.php index 717638af..b64a6dc7 100644 --- a/src/Traffic/StreamClient.php +++ b/src/Traffic/StreamClient.php @@ -11,7 +11,8 @@ /** * Simple abstraction over a client two-way stream. * @internal - * @psalm-internal Buggregator\Trap\Traffic + * @psalm-internal Buggregator\Trap + * @extends \IteratorAggregate */ interface StreamClient extends \IteratorAggregate { diff --git a/src/functions.php b/src/functions.php index 0cbfa4fb..764279f1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -57,6 +57,7 @@ function trap(mixed ...$values): void // Dump sequence of values foreach ($values as $key => $value) { + /** @psalm-suppress TooManyArguments */ VarDumper::dump($value, $key); } } diff --git a/tests/FiberTrait.php b/tests/Unit/FiberTrait.php similarity index 96% rename from tests/FiberTrait.php rename to tests/Unit/FiberTrait.php index fb80457c..c7bdf6c4 100644 --- a/tests/FiberTrait.php +++ b/tests/Unit/FiberTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Buggregator\Trap\Tests; +namespace Buggregator\Trap\Tests\Unit; trait FiberTrait { diff --git a/tests/Traffic/Dispatcher/HttpTest.php b/tests/Unit/Traffic/Dispatcher/HttpTest.php similarity index 93% rename from tests/Traffic/Dispatcher/HttpTest.php rename to tests/Unit/Traffic/Dispatcher/HttpTest.php index 37f7e03b..6e324c3b 100644 --- a/tests/Traffic/Dispatcher/HttpTest.php +++ b/tests/Unit/Traffic/Dispatcher/HttpTest.php @@ -1,6 +1,6 @@ parseStream($http); - $file = \file_get_contents(__DIR__ . '/../../Stub/sentry-body.bin'); + $file = \file_get_contents(__DIR__ . '/../../../Stub/sentry-body.bin'); self::assertSame($file, $request->getBody()->__toString()); } diff --git a/tests/Traffic/Parser/MultipartBodyParserTest.php b/tests/Unit/Traffic/Parser/MultipartBodyParserTest.php similarity index 94% rename from tests/Traffic/Parser/MultipartBodyParserTest.php rename to tests/Unit/Traffic/Parser/MultipartBodyParserTest.php index 0e804abc..3a16c0ae 100644 --- a/tests/Traffic/Parser/MultipartBodyParserTest.php +++ b/tests/Unit/Traffic/Parser/MultipartBodyParserTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Buggregator\Trap\Tests\Traffic\Parser; +namespace Buggregator\Trap\Tests\Unit\Traffic\Parser; -use Buggregator\Trap\Tests\FiberTrait; +use Buggregator\Trap\Tests\Unit\FiberTrait; use Buggregator\Trap\Traffic\Message\Multipart\Field; use Buggregator\Trap\Traffic\Message\Multipart\File; use Buggregator\Trap\Traffic\Message\Multipart\Part; @@ -50,8 +50,8 @@ public function testParse(): void public function testWithFileAttach(): void { - $file1 = \file_get_contents(__DIR__ . '/../../Stub/deburger.png'); - $file2 = \file_get_contents(__DIR__ . '/../../Stub/buggregator.png'); + $file1 = \file_get_contents(__DIR__ . '/../../../Stub/deburger.png'); + $file2 = \file_get_contents(__DIR__ . '/../../../Stub/buggregator.png'); $body = $this->makeStream(<<