Skip to content

Commit 2ebf20b

Browse files
committed
Add stdin support with explicit opt-in flag
1 parent 0d02d80 commit 2ebf20b

File tree

5 files changed

+281
-7
lines changed

5 files changed

+281
-7
lines changed

app/Actions/ElaborateSummary.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,20 @@ public function execute($totalFiles, $changes)
4747

4848
if ($format = $this->input->getOption('format')) {
4949
$this->displayUsingFormatter($summary, $format);
50-
} else {
50+
} elseif (! $this->input->getOption('stdin')) {
5151
$this->summaryOutput->handle($summary, $totalFiles);
5252
}
5353

5454
if (($file = $this->input->getOption('output-to-file')) && (($outputFormat = $this->input->getOption('output-format')) || $format)) {
5555
$this->displayUsingFormatter($summary, $outputFormat ?: $format, $file);
5656
}
5757

58-
$failure = (($summary->isDryRun() || $this->input->getOption('repair')) && count($changes) > 0)
58+
return (($summary->isDryRun() || $this->input->getOption('repair')) && count($changes) > 0)
5959
|| count($this->errors->getInvalidErrors()) > 0
6060
|| count($this->errors->getExceptionErrors()) > 0
61-
|| count($this->errors->getLintErrors()) > 0;
62-
63-
return $failure ? Command::FAILURE : Command::SUCCESS;
61+
|| count($this->errors->getLintErrors()) > 0
62+
? Command::FAILURE
63+
: Command::SUCCESS;
6464
}
6565

6666
/**

app/Actions/FixCode.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Actions;
44

55
use App\Factories\ConfigurationResolverFactory;
6+
use App\Project;
67
use LaravelZero\Framework\Exceptions\ConsoleException;
78
use PhpCsFixer\Console\ConfigurationResolver;
89
use PhpCsFixer\Runner\Parallel\ParallelConfig;
@@ -45,7 +46,9 @@ public function execute()
4546
return [$exception->getCode(), []];
4647
}
4748

48-
if (is_null($this->input->getOption('format'))) {
49+
$isStdinMode = $this->input->getOption('stdin');
50+
51+
if (is_null($this->input->getOption('format')) && ! $isStdinMode) {
4952
$this->progress->subscribe();
5053
}
5154

@@ -67,7 +70,15 @@ public function execute()
6770
$this->getInput($resolver),
6871
));
6972

70-
return tap([$totalFiles, $changes], fn () => $this->progress->unsubscribe());
73+
if ($isStdinMode) {
74+
$this->handleStdinOutput();
75+
}
76+
77+
return tap([$totalFiles, $changes], function () use ($isStdinMode) {
78+
if (! $isStdinMode) {
79+
$this->progress->unsubscribe();
80+
}
81+
});
7182
}
7283

7384
/**
@@ -112,4 +123,22 @@ private function getInput(ConfigurationResolver $resolver): InputInterface
112123

113124
return $this->input;
114125
}
126+
127+
/**
128+
* Handle stdin output by writing the formatted content directly to stdout and cleaning up.
129+
*
130+
* @return void
131+
*/
132+
private function handleStdinOutput()
133+
{
134+
$tempFile = Project::getStdinTempFile();
135+
136+
if ($tempFile && file_exists($tempFile)) {
137+
if (($formattedContent = file_get_contents($tempFile)) !== false) {
138+
fwrite(STDOUT, $formattedContent);
139+
}
140+
141+
Project::cleanupStdinTempFile();
142+
}
143+
}
115144
}

app/Commands/DefaultCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ protected function configure()
4949
new InputOption('cache-file', '', InputArgument::OPTIONAL, 'The path to the cache file'),
5050
new InputOption('parallel', 'p', InputOption::VALUE_NONE, 'Runs the linter in parallel (Experimental)'),
5151
new InputOption('max-processes', null, InputOption::VALUE_REQUIRED, 'The number of processes to spawn when using parallel execution'),
52+
new InputOption('stdin', null, InputOption::VALUE_NONE, 'Read and format code from standard input'),
5253
],
5354
);
5455
}

app/Project.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
class Project
88
{
9+
/**
10+
* The temporary file created from stdin input.
11+
*
12+
* @var string|null
13+
*/
14+
protected static $stdinTempFile = null;
15+
916
/**
1017
* Determine the project paths to apply the code style based on the options and arguments passed.
1118
*
@@ -14,6 +21,12 @@ class Project
1421
*/
1522
public static function paths($input)
1623
{
24+
if ($input->getOption('stdin')) {
25+
$paths = $input->getArgument('path');
26+
27+
return static::resolveStdinPath($paths[0] ?? null);
28+
}
29+
1730
if ($input->getOption('dirty')) {
1831
return static::resolveDirtyPaths();
1932
}
@@ -67,4 +80,90 @@ public static function resolveDiffPaths($branch)
6780

6881
return $files;
6982
}
83+
84+
/**
85+
* Resolves the stdin input by creating a temporary file.
86+
*
87+
* @param string|null $filepath
88+
* @return array<int, string>
89+
*/
90+
public static function resolveStdinPath($filepath = null)
91+
{
92+
if (! static::isStdinAvailable()) {
93+
abort(1, 'No input provided via stdin.');
94+
}
95+
96+
$content = static::readStdinContent();
97+
98+
if (empty(trim($content))) {
99+
abort(1, 'No content provided via stdin.');
100+
}
101+
102+
// If a filepath is provided, use it for better config resolution
103+
if ($filepath) {
104+
// Create temp file with same name in temp directory
105+
$tempDir = sys_get_temp_dir();
106+
$fileName = basename($filepath);
107+
static::$stdinTempFile = $tempDir.DIRECTORY_SEPARATOR.$fileName;
108+
109+
if (file_put_contents(static::$stdinTempFile, $content) === false) {
110+
abort(1, "Failed to create temporary file for stdin content: {$filepath}");
111+
}
112+
} else {
113+
// Create a temporary file with random name
114+
static::$stdinTempFile = tempnam(sys_get_temp_dir(), 'pint_stdin_').'.php';
115+
116+
if (file_put_contents(static::$stdinTempFile, $content) === false) {
117+
abort(1, 'Failed to create temporary file for stdin content.');
118+
}
119+
}
120+
121+
// Ensure cleanup happens even on unexpected exit
122+
register_shutdown_function([static::class, 'cleanupStdinTempFile']);
123+
124+
return [static::$stdinTempFile];
125+
}
126+
127+
/**
128+
* Check if stdin is available and has content.
129+
*
130+
* @return bool
131+
*/
132+
protected static function isStdinAvailable()
133+
{
134+
$stdin = fopen('php://stdin', 'r');
135+
$status = stream_get_meta_data($stdin);
136+
fclose($stdin);
137+
138+
return ! $status['eof'];
139+
}
140+
141+
/**
142+
* Read all content from stdin.
143+
*
144+
* @return string
145+
*/
146+
protected static function readStdinContent()
147+
{
148+
return stream_get_contents(STDIN);
149+
}
150+
151+
/**
152+
* Get the temporary file path created from stdin.
153+
*/
154+
public static function getStdinTempFile(): ?string
155+
{
156+
return static::$stdinTempFile;
157+
}
158+
159+
/**
160+
* Clean up the temporary file created from stdin.
161+
*/
162+
public static function cleanupStdinTempFile(): void
163+
{
164+
if (static::$stdinTempFile && file_exists(static::$stdinTempFile)) {
165+
unlink(static::$stdinTempFile);
166+
static::$stdinTempFile = null;
167+
}
168+
}
70169
}

tests/Feature/StdinTest.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
use App\Project;
4+
5+
beforeEach(function () {
6+
Project::cleanupStdinTempFile();
7+
});
8+
9+
afterEach(function () {
10+
Project::cleanupStdinTempFile();
11+
});
12+
13+
it('has stdin option', function () {
14+
$command = resolve(App\Commands\DefaultCommand::class);
15+
$definition = $command->getDefinition();
16+
17+
expect($definition->hasOption('stdin'))->toBeTrue()
18+
->and($definition->getOption('stdin')->getDescription())
19+
->toBe('Read and format code from standard input');
20+
});
21+
22+
it('outputs clean formatted code without summary', function () {
23+
$testCode = '<?php
24+
$variable="test";echo $variable;';
25+
26+
$tempStdin = tempnam(sys_get_temp_dir(), 'test_stdin_');
27+
file_put_contents($tempStdin, $testCode);
28+
29+
$command = sprintf('cat %s | ./pint --stdin --preset=psr12', escapeshellarg($tempStdin));
30+
$output = shell_exec($command);
31+
32+
unlink($tempStdin);
33+
34+
expect($output)->toContain('<?php')
35+
->and($output)->toContain('$variable = "test";')
36+
->and($output)->toContain('echo $variable;')
37+
->and($output)->not->toContain('FIXED')
38+
->and($output)->not->toContain('Laravel')
39+
->and($output)->not->toContain('──────');
40+
});
41+
42+
it('outputs formatted code with silent flag', function () {
43+
$testCode = '<?php
44+
$variable="test";';
45+
46+
$tempStdin = tempnam(sys_get_temp_dir(), 'test_stdin_');
47+
file_put_contents($tempStdin, $testCode);
48+
49+
$command = sprintf('cat %s | ./pint --stdin --silent --preset=psr12', escapeshellarg($tempStdin));
50+
$output = shell_exec($command);
51+
52+
unlink($tempStdin);
53+
54+
expect($output)->toContain('<?php')
55+
->and($output)->toContain('$variable = "test";')
56+
->and($output)->not->toContain('FIXED')
57+
->and($output)->not->toContain('Laravel');
58+
});
59+
60+
it('returns exit code 1 in test mode with style issues', function () {
61+
$testCode = '<?php
62+
$variable="test";';
63+
64+
$tempStdin = tempnam(sys_get_temp_dir(), 'test_stdin_');
65+
file_put_contents($tempStdin, $testCode);
66+
67+
$command = sprintf('cat %s | ./pint --stdin --test --preset=psr12 2>&1', escapeshellarg($tempStdin));
68+
$output = shell_exec($command);
69+
$exitCode = shell_exec(sprintf('cat %s | ./pint --stdin --test --preset=psr12 >/dev/null 2>&1; echo $?', escapeshellarg($tempStdin)));
70+
71+
unlink($tempStdin);
72+
73+
expect(trim($exitCode))->toBe('1')
74+
->and($output)->toContain('<?php')
75+
->and($output)->toContain('$variable="test";');
76+
});
77+
78+
it('formats code with complex statements', function () {
79+
$testCode = '<?php
80+
$variable="test";if($variable=="test"){echo"Hello World";}';
81+
82+
$tempStdin = tempnam(sys_get_temp_dir(), 'test_stdin_');
83+
file_put_contents($tempStdin, $testCode);
84+
85+
$command = sprintf('cat %s | ./pint --stdin --preset=psr12', escapeshellarg($tempStdin));
86+
$output = shell_exec($command);
87+
88+
unlink($tempStdin);
89+
90+
expect($output)->toContain('<?php')
91+
->and($output)->toContain('$variable = "test";')
92+
->and($output)->toContain('if ($variable == "test") {')
93+
->and($output)->toContain('echo"Hello World";')
94+
->and($output)->not->toContain('FIXED')
95+
->and($output)->not->toContain('Laravel');
96+
});
97+
98+
it('accepts path argument for config resolution', function () {
99+
$testCode = '<?php
100+
$variable="test";';
101+
102+
$tempStdin = tempnam(sys_get_temp_dir(), 'test_stdin_');
103+
$testFilePath = 'app/Models/User.php';
104+
file_put_contents($tempStdin, $testCode);
105+
106+
$command = sprintf(
107+
'cat %s | ./pint %s --stdin --preset=psr12',
108+
escapeshellarg($tempStdin),
109+
escapeshellarg($testFilePath)
110+
);
111+
$output = shell_exec($command);
112+
113+
unlink($tempStdin);
114+
115+
expect($output)->toContain('<?php')
116+
->and($output)->toContain('$variable = "test";')
117+
->and($output)->not->toContain('FIXED')
118+
->and($output)->not->toContain('Laravel');
119+
});
120+
121+
it('exits successfully with empty stdin', function () {
122+
$command = 'echo "" | ./pint --stdin --preset=psr12 2>&1';
123+
$output = shell_exec($command);
124+
$exitCode = shell_exec('echo "" | ./pint --stdin --preset=psr12 >/dev/null 2>&1; echo $?');
125+
126+
expect(trim($exitCode))->toBe('0')
127+
->and($output)->toBeEmpty();
128+
});
129+
130+
it('outputs both formatted code and json with format option', function () {
131+
$testCode = '<?php
132+
$variable="test";';
133+
134+
$tempStdin = tempnam(sys_get_temp_dir(), 'test_stdin_');
135+
file_put_contents($tempStdin, $testCode);
136+
137+
$command = sprintf('cat %s | ./pint --stdin --preset=psr12 --format=json 2>&1', escapeshellarg($tempStdin));
138+
$output = shell_exec($command);
139+
140+
unlink($tempStdin);
141+
142+
expect($output)->toContain('<?php')
143+
->and($output)->toContain('$variable = "test";')
144+
->and($output)->toContain('"files":[');
145+
});

0 commit comments

Comments
 (0)