Skip to content

Commit 9d1c19a

Browse files
authored
Merge pull request #11237 from vimeo/psalm-review
Add new psalm-review tool
2 parents 13a0520 + 7b6abbf commit 9d1c19a

File tree

6 files changed

+200
-1
lines changed

6 files changed

+200
-1
lines changed

.github/workflows/build-phar.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
cancel_others: true
2727
do_not_skip: '["release"]'
2828
# list files that may affect or are included into the built phar
29-
paths: '["bin/**", "assets/**", "build/**", "dictionaries/**", "src/**", "stubs/**", "psalm", "psalm-language-server", "psalm-plugin", "psalm-refactor", "psalter", "box.json.dist", "composer.json", "config.xsd", "keys.asc.gpg", "scoper.inc.php"]'
29+
paths: '["bin/**", "assets/**", "build/**", "dictionaries/**", "src/**", "stubs/**", "psalm", "psalm-language-server", "psalm-plugin", "psalm-refactor", "psalm-review", "psalter", "box.json.dist", "composer.json", "config.xsd", "keys.asc.gpg", "scoper.inc.php"]'
3030

3131
build-phar:
3232
permissions:

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"psalm-language-server",
107107
"psalm-plugin",
108108
"psalm-refactor",
109+
"psalm-review",
109110
"psalter"
110111
],
111112
"scripts": {

docs/running_psalm/command_line_usage.md

+14
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,17 @@ If you are running Psalm on a build server, you may want to configure the server
4545
is preserved between runs.
4646

4747
Running them together (e.g. `--threads=8 --diff`) will result in the fastest possible Psalm run.
48+
49+
## Reviewing issues in your IDE of choice
50+
51+
Psalm now offers a `psalm-review` tool which allows you to manually review issues one by one in your favorite IDE.
52+
53+
```bash
54+
./vendor/bin/psalm-review report.json code|phpstorm|code-server [ inv|rev|[~-]IssueType1 ] [ [~-]IssueType2 ] ...
55+
```
56+
57+
`psalm-review` parse the Psalm JSON report in report.json (generated using `vendor/bin/psalm --report=report.json`) and open the specified IDE at the line and column of the issue, one by one for all issues; press enter to go to the next issue.
58+
59+
The extra arguments may be used to filter only for issues of the specified types, or for all issues except the specified types (with the `~` or `-` inversion).
60+
61+
The `rev` or `inv` keywords may be used to start from the end of the report instead of at the beginning.

psalm-review

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
namespace Psalm;
5+
6+
use Psalm\Internal\Cli\Review;
7+
8+
require_once __DIR__ . '/src/Psalm/Internal/Cli/Review.php';
9+
Review::run($argv);

psalm.xml.dist

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<file name="psalm-language-server"/>
2727
<file name="psalm-plugin"/>
2828
<file name="psalm-refactor"/>
29+
<file name="psalm-review"/>
2930
<file name="psalter"/>
3031
<ignoreFiles>
3132
<file name="src/Psalm/Internal/PhpTraverser/CustomTraverser.php"/>

src/Psalm/Internal/Cli/Review.php

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psalm\Internal\Cli;
6+
7+
use AssertionError;
8+
use Psalm\Internal\CliUtils;
9+
use Psalm\Internal\ErrorHandler;
10+
use RuntimeException;
11+
12+
use function array_filter;
13+
use function array_reverse;
14+
use function array_shift;
15+
use function array_slice;
16+
use function array_values;
17+
use function assert;
18+
use function count;
19+
use function escapeshellarg;
20+
use function file_exists;
21+
use function file_get_contents;
22+
use function fputs;
23+
use function gc_collect_cycles;
24+
use function gc_disable;
25+
use function getenv;
26+
use function ini_set;
27+
use function json_decode;
28+
use function ltrim;
29+
use function passthru;
30+
use function printf;
31+
use function readline;
32+
use function str_repeat;
33+
use function strlen;
34+
use function strpos;
35+
use function substr;
36+
37+
use const JSON_THROW_ON_ERROR;
38+
use const PHP_EOL;
39+
use const PHP_OS_FAMILY;
40+
use const STDERR;
41+
42+
// phpcs:disable PSR1.Files.SideEffects
43+
44+
require_once __DIR__ . '/../ErrorHandler.php';
45+
require_once __DIR__ . '/../CliUtils.php';
46+
require_once __DIR__ . '/../Composer.php';
47+
require_once __DIR__ . '/../IncludeCollector.php';
48+
require_once __DIR__ . '/../../IssueBuffer.php';
49+
50+
/**
51+
* @internal
52+
*/
53+
final class Review
54+
{
55+
private static function r(string $cmd): void
56+
{
57+
passthru($cmd, $result);
58+
if ($result) {
59+
exit("Return code {$result}\n");
60+
}
61+
}
62+
63+
/** @param list<string> $argv */
64+
public static function run(array $argv): void
65+
{
66+
CliUtils::checkRuntimeRequirements();
67+
ini_set('memory_limit', '8192M');
68+
69+
gc_collect_cycles();
70+
gc_disable();
71+
72+
ErrorHandler::install($argv);
73+
74+
$args = array_slice($argv, 1);
75+
if (count($args) === 0) {
76+
self::help();
77+
}
78+
79+
$issues = array_shift($args);
80+
$mode = array_shift($args);
81+
82+
/** @psalm-suppress RiskyTruthyFalsyComparison */
83+
$mode = match ($mode) {
84+
'code-server' => static fn(string $file, int $line, int $column) => 'code-server -r ' .
85+
escapeshellarg($file) . ':' .
86+
escapeshellarg((string)$line) . ':' .
87+
escapeshellarg((string)$column),
88+
89+
'phpstorm' => static fn(string $file, int $line, int $column) => (PHP_OS_FAMILY === 'Darwin'
90+
? 'open -na \'/Applications/PhpStorm.app\' --args'
91+
: escapeshellarg(getenv('PHPSTORM') ?: 'phpstorm')
92+
). ' --line ' . escapeshellarg((string) $line) . " --column {$column} " . escapeshellarg($file),
93+
94+
'code' => static fn(string $file, int $line, int $column)
95+
=> 'code --goto ' . escapeshellarg($file) . ':' .
96+
escapeshellarg((string) $line) . ':' .
97+
escapeshellarg((string) $column),
98+
99+
null => throw new AssertionError("No IDE was specified as second parameter!"),
100+
default => throw new AssertionError("The only allowed IDEs are vscode, phpstorm, code-server, got $mode")
101+
};
102+
103+
if (!file_exists($issues)) {
104+
throw new RuntimeException("$issues does not exist!");
105+
}
106+
107+
$issues = file_get_contents($issues);
108+
if ($issues === false) {
109+
throw new AssertionError("Could not read issues");
110+
}
111+
112+
/** @var list<array{type: string, snippet: string, selected_text: string, line_from: int, column_from: int, file_name: string}> */
113+
$issues = json_decode($issues, true, flags: JSON_THROW_ON_ERROR);
114+
foreach ($args as $issue) {
115+
if ($issue[0] === '~' || $issue[0] === '-') {
116+
$issue = substr($issue, 1);
117+
$issues = array_filter($issues, static fn(array $i) => $i['type'] !== $issue);
118+
} elseif ($issue === 'inv' || $issue === 'rev') {
119+
$issues = array_reverse($issues);
120+
} else {
121+
$issues = array_filter($issues, static fn(array $i) => $i['type'] === $issue);
122+
}
123+
}
124+
125+
$allCount = count($issues);
126+
$issues = array_values($issues);
127+
foreach ($issues as $k => [
128+
'line_from' => $line,
129+
'column_from' => $column,
130+
'type' => $type,
131+
'message' => $message,
132+
'file_name' => $file,
133+
'snippet' => $snippet,
134+
'selected_text' => $selected,
135+
]) {
136+
self::r('clear');
137+
echo "{$type}: {$message}" . PHP_EOL . PHP_EOL;
138+
echo $snippet . PHP_EOL;
139+
140+
$pos = strpos($snippet, $selected);
141+
assert($pos !== false);
142+
$snippetTrimmed = ltrim($snippet);
143+
$lenTab = strlen($snippet) - strlen($snippetTrimmed);
144+
145+
echo substr($snippet, 0, $lenTab);
146+
echo str_repeat(' ', $pos - $lenTab);
147+
echo str_repeat('^', strlen($selected));
148+
echo PHP_EOL . PHP_EOL;
149+
150+
printf('%d%% (%d/%d)', (int)($k * 100 / $allCount), $k, $allCount);
151+
echo PHP_EOL;
152+
153+
self::r($mode($file, $line, $column));
154+
readline();
155+
}
156+
}
157+
158+
private static function help(): never
159+
{
160+
fputs(
161+
STDERR,
162+
PHP_EOL . "Usage: psalm-review report.json code|phpstorm|code-server " .
163+
"[ inv|rev|[~-]IssueType1 ] [ [~-]IssueType2 ] ... " . PHP_EOL . PHP_EOL .
164+
"Will parse the Psalm JSON report in report.json ".
165+
"and open the specified IDE at the line and column of the issue, ".
166+
"one by one for all issues.".PHP_EOL.
167+
"Press enter to go to the next issue.".PHP_EOL.PHP_EOL.
168+
"The extra arguments may be used to filter only for issues of the specified types, ".
169+
"or for all issues except the specified types (with the ~ or - inversion);".PHP_EOL.
170+
"rev|inv keywords may be used to start from the end of the report.".PHP_EOL.PHP_EOL,
171+
);
172+
exit(1);
173+
}
174+
}

0 commit comments

Comments
 (0)