Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ composer require --dev shipmonk/phpstan-ignore-inliner
vendor/bin/phpstan --error-format=json | vendor/bin/inline-phpstan-ignores
```

## Cli options
- `--comment`: Adds a comment to all inlined ignores, resulting in `// @phpstan-ignore empty.notAllowed (the comment)`

## Contributing
- Check your code by `composer check`
- Autofix coding-style by `composer fix:cs`
Expand Down
7 changes: 5 additions & 2 deletions bin/inline-phpstan-ignores
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ $usage = "\n\nUsage:\n$ vendor/bin/phpstan --error-format=json | vendor/bin/inli
try {
$io = new Io();
$input = $io->readInput();
$comment = $io->readCliComment($argv);
$errorsData = json_decode($input, associative: true, flags: JSON_THROW_ON_ERROR);
$errors = $errorsData['files'] ?? throw new FailureException('No \'files\' key found on input JSON.');

$inliner = new InlineIgnoreInliner($io);
$inliner->inlineErrors($errors);
$inliner->inlineErrors($errors, $comment);

$errorsCount = count($errors);
echo "Done, $errorsCount errors processed.\n";
exit(0);

} catch (JsonException | FailureException $e) {
echo $e->getMessage() . $usage;
echo 'ERROR: ' . $e->getMessage() . $usage;
exit(1);
}
10 changes: 7 additions & 3 deletions src/InlineIgnoreInliner.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public function __construct(private Io $io)
* @throws FailureException
*/
public function inlineErrors(
array $errors
array $errors,
?string $comment
): void
{
foreach ($errors as $filePath => $fileErrors) {
Expand All @@ -40,10 +41,13 @@ public function inlineErrors(

$lineContent = rtrim($lines[$line - 1]);
$lineEnding = substr($lines[$line - 1], strlen($lineContent));
$resolvedComment = $comment === null
? ''
: " ($comment)";

$append = str_contains($lineContent, '// @phpstan-ignore ')
? ', ' . $identifier
: ' // @phpstan-ignore ' . $identifier;
? ', ' . $identifier . $resolvedComment
: ' // @phpstan-ignore ' . $identifier . $resolvedComment;

$lines[$line - 1] = $lineContent . $append . $lineEnding;
$this->io->writeFile($trueFilePath, implode('', $lines));
Expand Down
36 changes: 36 additions & 0 deletions src/Io.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,50 @@

namespace ShipMonk\PHPStan\Errors;

use function array_slice;
use function file;
use function file_put_contents;
use function getopt;
use function in_array;
use function is_array;
use function is_string;
use function str_starts_with;
use function stream_get_contents;
use const STDIN;

class Io
{

/**
* @param array<string> $argv
* @throws FailureException
*/
public function readCliComment(array $argv): ?string
{
foreach (array_slice($argv, 1) as $arg) {
if (str_starts_with($arg, '--') && !str_starts_with($arg, '--comment')) {
throw new FailureException('Unexpected option: ' . $arg);
}
}

$options = getopt('', ['comment:']);
$comment = $options['comment'] ?? null;

if (is_string($comment)) {
return $comment;
}

if (is_array($comment)) {
throw new FailureException('Only one comment can be provided.');
}

if (in_array('--comment', $argv, true)) {
throw new FailureException('Missing comment value for --comment option.');
}

return null;
}

/**
* @throws FailureException
*/
Expand Down
18 changes: 12 additions & 6 deletions tests/InlineIgnoreInlinerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ class InlineIgnoreInlinerTest extends TestCase
{

#[DataProvider('lineEndingProvider')]
public function testInlineErrors(string $lineEnding): void
public function testInlineErrors(string $lineEnding, ?string $comment): void
{
$tmpFilePath = sys_get_temp_dir() . '/' . uniqid('ignore', true) . '.php';
$tmpExpectedPath = sys_get_temp_dir() . '/' . uniqid('ignore-expected', true) . '.php';

$expectedFileName = $comment !== null
? 'test.fixed.comment.php'
: 'test.fixed.php';

$testContent = $this->getTestFileContent('test.php', $lineEnding);
$expectedContent = $this->getTestFileContent('test.fixed.php', $lineEnding);
$expectedContent = $this->getTestFileContent($expectedFileName, $lineEnding);

self::assertNotFalse(file_put_contents($tmpFilePath, $testContent));
self::assertNotFalse(file_put_contents($tmpExpectedPath, $expectedContent));
Expand All @@ -43,7 +47,7 @@ public function testInlineErrors(string $lineEnding): void
$testData = json_decode($testJson, associative: true)['files']; // @phpstan-ignore argument.type

$inliner = new InlineIgnoreInliner($ioMock);
$inliner->inlineErrors($testData);
$inliner->inlineErrors($testData, $comment);

self::assertFileEquals($tmpExpectedPath, $tmpFilePath);
}
Expand All @@ -57,13 +61,15 @@ private function getTestFileContent(string $filename, string $lineEnding): strin
}

/**
* @return array<string, array{string}>
* @return array<string, array{string, ?string}>
*/
public static function lineEndingProvider(): array
{
return [
'Unix line endings' => ["\n"],
'Windows line endings' => ["\r\n"],
'Unix line endings' => ["\n", null],
'Unix line endings + comment' => ["\n", 'some comment'],
'Windows line endings' => ["\r\n", null],
'Windows line endings + comment' => ["\r\n", 'some comment'],
];
}

Expand Down
98 changes: 98 additions & 0 deletions tests/IoTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\Errors;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use function fclose;
use function fwrite;
use function is_array;
use function is_resource;
use function proc_close;
use function proc_open;
use function stream_get_contents;

class IoTest extends TestCase
{

/**
* @param list<string> $args
*/
#[DataProvider('optionsProvider')]
public function testValidCliOptions(int $exitCode, string $input, array $args, string $expectedOutput): void
{
$result = $this->runCliCommand($args, $input);
self::assertSame($exitCode, $result['exitCode']);
self::assertStringContainsString($expectedOutput, $result['stdout']);
}

/**
* @return array<string, array{int, string, array<string>, string}>
*/
public static function optionsProvider(): array
{
$validInput = '{"files": {}}';

return [
'no input' => [1, '', [], 'ERROR: Nothing found on input'],
'invalid json' => [1, 'invalid json', [], 'ERROR: Syntax error'],

'no options' => [0, $validInput, [], 'Done, 0 errors processed'],
'with comment' => [0, $validInput, ['--comment=test comment'], 'Done, 0 errors processed'],
'with single word comment' => [0, $validInput, ['--comment=test'], 'Done, 0 errors processed'],

'unexpected option' => [1, $validInput, ['--invalid'], 'ERROR: Unexpected option: --invalid'],
'comment without value' => [1, $validInput, ['--comment'], 'ERROR: Missing comment value for --comment option'],
'multiple comments' => [1, $validInput, ['--comment=first', '--comment=second'], 'ERROR: Only one comment can be provided'],
];
}

/**
* @param list<string> $args
* @return array{exitCode: int, stdout: string, stderr: string}
*/
private function runCliCommand(array $args, string $input): array
{
$binaryPath = __DIR__ . '/../bin/inline-phpstan-ignores';
$command = ['php', $binaryPath, ...$args];

$process = proc_open(
$command,
[
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
],
$pipes,
);

if (!is_resource($process)) {
self::fail('Failed to start process');
}

if (!is_array($pipes) || !isset($pipes[0], $pipes[1], $pipes[2])) {
self::fail('Failed to create pipes');
}

/** @var array{0: resource, 1: resource, 2: resource} $pipes */
fwrite($pipes[0], $input);
fclose($pipes[0]);

$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);

$exitCode = proc_close($process);

self::assertNotFalse($stdout, 'Failed to read stdout');
self::assertNotFalse($stderr, 'Failed to read stderr');

return [
'exitCode' => $exitCode,
'stdout' => $stdout,
'stderr' => $stderr,
];
}

}
11 changes: 11 additions & 0 deletions tests/data/test.fixed.comment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types = 1);

class Dummy
{

public function test($a, $b): void // @phpstan-ignore missingType.parameter, missingType.parameter (some comment)
{
return null; // @phpstan-ignore return.void (some comment)
}

}