Skip to content

Commit bb15d11

Browse files
A909Mcalebdwtaylorotwell
authored
Add stdin support with explicit opt-in flag (#403)
* feat: add support for formatting code from stdin (#390) * feat: add support for formatting code from stdin * Formatting --------- Co-authored-by: Taylor Otwell <[email protected]> * feat: require explicit --stdin flag for stdin input * feat: add stdin support with dash and stdin-filename option - Replace --stdin flag with Unix-standard dash (-) for stdin input - Add --stdin-filename option for editor integration and context - Support both 'pint -' and 'pint --stdin-filename' patterns - Add comprehensive tests for stdin formatting scenarios * feat(stdin): respect exclusion rules for stdin-filename option - Add ConfigurationFactory::isPathExcluded() to check file exclusion rules - Check stdin-filename against default and pint.json exclusions - Return original code unchanged when path is excluded - Add comprehensive unit and feature tests for exclusion behavior * Update DefaultCommand.php --------- Co-authored-by: Caleb White <[email protected]> Co-authored-by: Taylor Otwell <[email protected]>
1 parent c7b1b72 commit bb15d11

File tree

5 files changed

+394
-0
lines changed

5 files changed

+394
-0
lines changed

app/Commands/DefaultCommand.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace App\Commands;
44

5+
use App\Actions\FixCode;
6+
use App\Factories\ConfigurationFactory;
57
use LaravelZero\Framework\Commands\Command;
68
use Symfony\Component\Console\Input\InputArgument;
79
use Symfony\Component\Console\Input\InputOption;
10+
use Throwable;
811

912
class DefaultCommand extends Command
1013
{
@@ -46,6 +49,7 @@ protected function configure()
4649
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'The output format that should be used'),
4750
new InputOption('output-to-file', '', InputOption::VALUE_REQUIRED, 'Output the test results to a file at this path'),
4851
new InputOption('output-format', '', InputOption::VALUE_REQUIRED, 'The format that should be used when outputting the test results to a file'),
52+
new InputOption('stdin-filename', null, InputOption::VALUE_REQUIRED, 'File path context for stdin input'),
4953
new InputOption('cache-file', '', InputArgument::OPTIONAL, 'The path to the cache file'),
5054
new InputOption('parallel', 'p', InputOption::VALUE_NONE, 'Runs the linter in parallel (Experimental)'),
5155
new InputOption('max-processes', null, InputOption::VALUE_REQUIRED, 'The number of processes to spawn when using parallel execution'),
@@ -62,8 +66,68 @@ protected function configure()
6266
*/
6367
public function handle($fixCode, $elaborateSummary)
6468
{
69+
if ($this->hasStdinInput()) {
70+
return $this->fixStdinInput($fixCode);
71+
}
72+
6573
[$totalFiles, $changes] = $fixCode->execute();
6674

6775
return $elaborateSummary->execute($totalFiles, $changes);
6876
}
77+
78+
/**
79+
* Fix the code sent to Pint on stdin and output to stdout.
80+
*
81+
* The stdin-filename option provides file path context. If the path matches
82+
* exclusion rules, the original code is returned unchanged. Falls back to
83+
* 'stdin.php' if not provided.
84+
*/
85+
protected function fixStdinInput(FixCode $fixCode): int
86+
{
87+
$contextPath = $this->option('stdin-filename') ?: 'stdin.php';
88+
89+
if ($this->option('stdin-filename') && ConfigurationFactory::isPathExcluded($contextPath)) {
90+
fwrite(STDOUT, stream_get_contents(STDIN));
91+
92+
return self::SUCCESS;
93+
}
94+
95+
$tempFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.'pint_stdin_'.uniqid().'.php';
96+
97+
$this->input->setArgument('path', [$tempFile]);
98+
$this->input->setOption('format', 'json');
99+
100+
try {
101+
file_put_contents($tempFile, stream_get_contents(STDIN));
102+
$fixCode->execute();
103+
fwrite(STDOUT, file_get_contents($tempFile));
104+
105+
return self::SUCCESS;
106+
} catch (Throwable $e) {
107+
fwrite(STDERR, "pint: error processing {$contextPath}: {$e->getMessage()}\n");
108+
109+
return self::FAILURE;
110+
} finally {
111+
if (file_exists($tempFile)) {
112+
@unlink($tempFile);
113+
}
114+
}
115+
}
116+
117+
/**
118+
* Determine if there is input available on stdin.
119+
*
120+
* Stdin mode is triggered by either:
121+
* - Passing '-' as path (transformed to '__STDIN_PLACEHOLDER__' in pint:56-61)
122+
* - Providing the --stdin-filename option
123+
*/
124+
protected function hasStdinInput(): bool
125+
{
126+
$paths = $this->argument('path');
127+
128+
$hasStdinPlaceholder = ! empty($paths) && $paths[0] === '__STDIN_PLACEHOLDER__';
129+
$hasStdinFilename = ! empty($this->option('stdin-filename'));
130+
131+
return $hasStdinPlaceholder || $hasStdinFilename;
132+
}
69133
}

app/Factories/ConfigurationFactory.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,62 @@ public static function finder()
7676

7777
return $finder;
7878
}
79+
80+
/**
81+
* Check if a file path should be excluded based on finder rules.
82+
*/
83+
public static function isPathExcluded(string $filePath): bool
84+
{
85+
$localConfiguration = resolve(ConfigurationJsonRepository::class);
86+
$basePath = getcwd();
87+
88+
$relativePath = str_starts_with($filePath, $basePath)
89+
? substr($filePath, strlen($basePath) + 1)
90+
: $filePath;
91+
92+
$relativePath = str_replace('\\', '/', $relativePath);
93+
$fileName = basename($filePath);
94+
95+
foreach (static::$notName as $pattern) {
96+
if (fnmatch($pattern, $fileName)) {
97+
return true;
98+
}
99+
}
100+
101+
foreach (static::$exclude as $excludedFolder) {
102+
$excludedFolder = str_replace('\\', '/', $excludedFolder);
103+
if (str_starts_with($relativePath, $excludedFolder.'/') || $relativePath === $excludedFolder) {
104+
return true;
105+
}
106+
}
107+
108+
$finderConfig = $localConfiguration->finder();
109+
110+
if (isset($finderConfig['notName'])) {
111+
foreach ((array) $finderConfig['notName'] as $pattern) {
112+
if (fnmatch($pattern, $fileName)) {
113+
return true;
114+
}
115+
}
116+
}
117+
118+
if (isset($finderConfig['exclude'])) {
119+
foreach ((array) $finderConfig['exclude'] as $excludedFolder) {
120+
$excludedFolder = str_replace('\\', '/', $excludedFolder);
121+
if (str_starts_with($relativePath, $excludedFolder.'/') || $relativePath === $excludedFolder) {
122+
return true;
123+
}
124+
}
125+
}
126+
127+
if (isset($finderConfig['notPath'])) {
128+
foreach ((array) $finderConfig['notPath'] as $pattern) {
129+
if (fnmatch($pattern, $relativePath)) {
130+
return true;
131+
}
132+
}
133+
}
134+
135+
return false;
136+
}
79137
}

pint

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,27 @@ $app = require_once __DIR__.'/bootstrap/app.php';
4040

4141
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
4242

43+
/*
44+
|--------------------------------------------------------------------------
45+
| Handle Stdin Mode
46+
|--------------------------------------------------------------------------
47+
|
48+
| When using '-' to indicate stdin input (following Unix convention like
49+
| Black, cat, etc.), Symfony Console's ArgvInput parser fails because it
50+
| treats '-' as a malformed option. We work around this by replacing '-'
51+
| with a placeholder before the input is parsed. The DefaultCommand then
52+
| detects this placeholder to enable stdin mode.
53+
|
54+
*/
55+
56+
if (isset($_SERVER['argv'])) {
57+
$stdinIndex = array_search('-', $_SERVER['argv'], true);
58+
59+
if ($stdinIndex !== false) {
60+
$_SERVER['argv'][$stdinIndex] = '__STDIN_PLACEHOLDER__';
61+
}
62+
}
63+
4364
$status = $kernel->handle(
4465
$input = new Symfony\Component\Console\Input\ArgvInput,
4566
new Symfony\Component\Console\Output\ConsoleOutput

tests/Feature/StdinTest.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Process;
4+
5+
it('formats code from stdin', function (string $input, ?string $expected) {
6+
$result = Process::input($input)
7+
->run('php pint - --stdin-filename=app/Test.php')
8+
->throw();
9+
10+
expect($result)
11+
->output()
12+
->toBe($expected ?? $input)
13+
->errorOutput()
14+
->toBe('');
15+
})->with([
16+
'basic array and conditional' => [
17+
<<<'PHP'
18+
<?php
19+
$array = array("a","b");
20+
if($condition==true){
21+
echo "test";
22+
}
23+
PHP
24+
,
25+
<<<'PHP'
26+
<?php
27+
28+
$array = ['a', 'b'];
29+
if ($condition == true) {
30+
echo 'test';
31+
}
32+
33+
PHP
34+
,
35+
],
36+
'class with method' => [
37+
<<<'PHP'
38+
<?php
39+
class Test{
40+
public function method(){
41+
return array("key"=>"value");
42+
}
43+
}
44+
PHP
45+
,
46+
<<<'PHP'
47+
<?php
48+
49+
class Test
50+
{
51+
public function method()
52+
{
53+
return ['key' => 'value'];
54+
}
55+
}
56+
57+
PHP
58+
,
59+
],
60+
'already formatted code' => [
61+
<<<'PHP'
62+
<?php
63+
64+
class AlreadyFormatted
65+
{
66+
public function method()
67+
{
68+
return ['key' => 'value'];
69+
}
70+
}
71+
72+
PHP
73+
,
74+
null,
75+
],
76+
]);
77+
78+
it('formats code from stdin without filename', function () {
79+
$input = <<<'PHP'
80+
<?php
81+
$array = array("a","b");
82+
PHP;
83+
84+
$expected = <<<'PHP'
85+
<?php
86+
87+
$array = ['a', 'b'];
88+
89+
PHP;
90+
91+
$result = Process::input($input)->run('php pint -')->throw();
92+
93+
expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
94+
});
95+
96+
it('uses stdin-filename for context', function () {
97+
$input = <<<'PHP'
98+
<?php
99+
$array = array("test");
100+
PHP;
101+
102+
$expected = <<<'PHP'
103+
<?php
104+
105+
$array = ['test'];
106+
107+
PHP;
108+
109+
$result = Process::input($input)
110+
->run('php pint - --stdin-filename=app/Models/User.php')
111+
->throw();
112+
113+
expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
114+
});
115+
116+
it('formats code from stdin using only stdin-filename option', function () {
117+
$input = <<<'PHP'
118+
<?php
119+
$array = array("foo","bar");
120+
PHP;
121+
122+
$expected = <<<'PHP'
123+
<?php
124+
125+
$array = ['foo', 'bar'];
126+
127+
PHP;
128+
129+
$result = Process::input($input)
130+
->run('php pint --stdin-filename=app/Models/Example.php')
131+
->throw();
132+
133+
expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
134+
});
135+
136+
it('skips formatting for excluded paths', function (string $filename) {
137+
$input = <<<'PHP'
138+
<?php
139+
$array = array("foo","bar");
140+
PHP;
141+
142+
$result = Process::input($input)
143+
->run("php pint --stdin-filename={$filename}")
144+
->throw();
145+
146+
expect($result)->output()->toBe($input)->errorOutput()->toBe('');
147+
})->with([
148+
'blade files' => ['resources/views/welcome.blade.php'],
149+
'storage folder' => ['storage/framework/views/compiled.php'],
150+
'node_modules' => ['node_modules/package/index.php'],
151+
]);
152+
153+
it('respects pint.json exclusion rules', function (string $filename, bool $shouldFormat) {
154+
$input = <<<'PHP'
155+
<?php
156+
$array = array("foo","bar");
157+
PHP;
158+
159+
$expected = $shouldFormat ? <<<'PHP'
160+
<?php
161+
162+
$array = ['foo', 'bar'];
163+
164+
PHP
165+
: $input;
166+
167+
$result = Process::input($input)
168+
->path(base_path('tests/Fixtures/finder'))
169+
->run('php '.base_path('pint')." --stdin-filename={$filename}")
170+
->throw();
171+
172+
expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
173+
})->with([
174+
'excluded folder' => ['my-dir/SomeFile.php', false],
175+
'excluded notName pattern' => ['src/test-my-file.php', false],
176+
'excluded notPath pattern' => ['path/to/excluded-file.php', false],
177+
'not excluded' => ['src/MyClass.php', true],
178+
]);

0 commit comments

Comments
 (0)