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
64 changes: 64 additions & 0 deletions app/Commands/DefaultCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace App\Commands;

use App\Actions\FixCode;
use App\Factories\ConfigurationFactory;
use LaravelZero\Framework\Commands\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Throwable;

class DefaultCommand extends Command
{
Expand Down Expand Up @@ -46,6 +49,7 @@ protected function configure()
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'The output format that should be used'),
new InputOption('output-to-file', '', InputOption::VALUE_REQUIRED, 'Output the test results to a file at this path'),
new InputOption('output-format', '', InputOption::VALUE_REQUIRED, 'The format that should be used when outputting the test results to a file'),
new InputOption('stdin-filename', null, InputOption::VALUE_REQUIRED, 'File path context for stdin input'),
new InputOption('cache-file', '', InputArgument::OPTIONAL, 'The path to the cache file'),
new InputOption('parallel', 'p', InputOption::VALUE_NONE, 'Runs the linter in parallel (Experimental)'),
new InputOption('max-processes', null, InputOption::VALUE_REQUIRED, 'The number of processes to spawn when using parallel execution'),
Expand All @@ -62,8 +66,68 @@ protected function configure()
*/
public function handle($fixCode, $elaborateSummary)
{
if ($this->hasStdinInput()) {
return $this->fixStdinInput($fixCode);
}

[$totalFiles, $changes] = $fixCode->execute();

return $elaborateSummary->execute($totalFiles, $changes);
}

/**
* Fix the code sent to Pint on stdin and output to stdout.
*
* The stdin-filename option provides file path context. If the path matches
* exclusion rules, the original code is returned unchanged. Falls back to
* 'stdin.php' if not provided.
*/
protected function fixStdinInput(FixCode $fixCode): int
{
$contextPath = $this->option('stdin-filename') ?: 'stdin.php';

if ($this->option('stdin-filename') && ConfigurationFactory::isPathExcluded($contextPath)) {
fwrite(STDOUT, stream_get_contents(STDIN));

return self::SUCCESS;
}

$tempFile = sys_get_temp_dir().DIRECTORY_SEPARATOR.'pint_stdin_'.uniqid().'.php';

$this->input->setArgument('path', [$tempFile]);
$this->input->setOption('format', 'json');

try {
file_put_contents($tempFile, stream_get_contents(STDIN));
$fixCode->execute();
fwrite(STDOUT, file_get_contents($tempFile));

return self::SUCCESS;
} catch (Throwable $e) {
fwrite(STDERR, "pint: error processing {$contextPath}: {$e->getMessage()}\n");

return self::FAILURE;
} finally {
if (file_exists($tempFile)) {
@unlink($tempFile);
}
}
}

/**
* Determine if there is input available on stdin.
*
* Stdin mode is triggered by either:
* - Passing '-' as path (transformed to '__STDIN_PLACEHOLDER__' in pint:56-61)
* - Providing the --stdin-filename option
*/
protected function hasStdinInput(): bool
{
$paths = $this->argument('path');

$hasStdinPlaceholder = ! empty($paths) && $paths[0] === '__STDIN_PLACEHOLDER__';
$hasStdinFilename = ! empty($this->option('stdin-filename'));

return $hasStdinPlaceholder || $hasStdinFilename;
}
}
58 changes: 58 additions & 0 deletions app/Factories/ConfigurationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,62 @@ public static function finder()

return $finder;
}

/**
* Check if a file path should be excluded based on finder rules.
*/
public static function isPathExcluded(string $filePath): bool
{
$localConfiguration = resolve(ConfigurationJsonRepository::class);
$basePath = getcwd();

$relativePath = str_starts_with($filePath, $basePath)
? substr($filePath, strlen($basePath) + 1)
: $filePath;

$relativePath = str_replace('\\', '/', $relativePath);
$fileName = basename($filePath);

foreach (static::$notName as $pattern) {
if (fnmatch($pattern, $fileName)) {
return true;
}
}

foreach (static::$exclude as $excludedFolder) {
$excludedFolder = str_replace('\\', '/', $excludedFolder);
if (str_starts_with($relativePath, $excludedFolder.'/') || $relativePath === $excludedFolder) {
return true;
}
}

$finderConfig = $localConfiguration->finder();

if (isset($finderConfig['notName'])) {
foreach ((array) $finderConfig['notName'] as $pattern) {
if (fnmatch($pattern, $fileName)) {
return true;
}
}
}

if (isset($finderConfig['exclude'])) {
foreach ((array) $finderConfig['exclude'] as $excludedFolder) {
$excludedFolder = str_replace('\\', '/', $excludedFolder);
if (str_starts_with($relativePath, $excludedFolder.'/') || $relativePath === $excludedFolder) {
return true;
}
}
}

if (isset($finderConfig['notPath'])) {
foreach ((array) $finderConfig['notPath'] as $pattern) {
if (fnmatch($pattern, $relativePath)) {
return true;
}
}
}

return false;
}
}
21 changes: 21 additions & 0 deletions pint
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,27 @@ $app = require_once __DIR__.'/bootstrap/app.php';

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

/*
|--------------------------------------------------------------------------
| Handle Stdin Mode
|--------------------------------------------------------------------------
|
| When using '-' to indicate stdin input (following Unix convention like
| Black, cat, etc.), Symfony Console's ArgvInput parser fails because it
| treats '-' as a malformed option. We work around this by replacing '-'
| with a placeholder before the input is parsed. The DefaultCommand then
| detects this placeholder to enable stdin mode.
|
*/

if (isset($_SERVER['argv'])) {
$stdinIndex = array_search('-', $_SERVER['argv'], true);

if ($stdinIndex !== false) {
$_SERVER['argv'][$stdinIndex] = '__STDIN_PLACEHOLDER__';
}
}

$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
Expand Down
178 changes: 178 additions & 0 deletions tests/Feature/StdinTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php

use Illuminate\Support\Facades\Process;

it('formats code from stdin', function (string $input, ?string $expected) {
$result = Process::input($input)
->run('php pint - --stdin-filename=app/Test.php')
->throw();

expect($result)
->output()
->toBe($expected ?? $input)
->errorOutput()
->toBe('');
})->with([
'basic array and conditional' => [
<<<'PHP'
<?php
$array = array("a","b");
if($condition==true){
echo "test";
}
PHP
,
<<<'PHP'
<?php

$array = ['a', 'b'];
if ($condition == true) {
echo 'test';
}

PHP
,
],
'class with method' => [
<<<'PHP'
<?php
class Test{
public function method(){
return array("key"=>"value");
}
}
PHP
,
<<<'PHP'
<?php

class Test
{
public function method()
{
return ['key' => 'value'];
}
}

PHP
,
],
'already formatted code' => [
<<<'PHP'
<?php

class AlreadyFormatted
{
public function method()
{
return ['key' => 'value'];
}
}

PHP
,
null,
],
]);

it('formats code from stdin without filename', function () {
$input = <<<'PHP'
<?php
$array = array("a","b");
PHP;

$expected = <<<'PHP'
<?php

$array = ['a', 'b'];

PHP;

$result = Process::input($input)->run('php pint -')->throw();

expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
});

it('uses stdin-filename for context', function () {
$input = <<<'PHP'
<?php
$array = array("test");
PHP;

$expected = <<<'PHP'
<?php

$array = ['test'];

PHP;

$result = Process::input($input)
->run('php pint - --stdin-filename=app/Models/User.php')
->throw();

expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
});

it('formats code from stdin using only stdin-filename option', function () {
$input = <<<'PHP'
<?php
$array = array("foo","bar");
PHP;

$expected = <<<'PHP'
<?php

$array = ['foo', 'bar'];

PHP;

$result = Process::input($input)
->run('php pint --stdin-filename=app/Models/Example.php')
->throw();

expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
});

it('skips formatting for excluded paths', function (string $filename) {
$input = <<<'PHP'
<?php
$array = array("foo","bar");
PHP;

$result = Process::input($input)
->run("php pint --stdin-filename={$filename}")
->throw();

expect($result)->output()->toBe($input)->errorOutput()->toBe('');
})->with([
'blade files' => ['resources/views/welcome.blade.php'],
'storage folder' => ['storage/framework/views/compiled.php'],
'node_modules' => ['node_modules/package/index.php'],
]);

it('respects pint.json exclusion rules', function (string $filename, bool $shouldFormat) {
$input = <<<'PHP'
<?php
$array = array("foo","bar");
PHP;

$expected = $shouldFormat ? <<<'PHP'
<?php

$array = ['foo', 'bar'];

PHP
: $input;

$result = Process::input($input)
->path(base_path('tests/Fixtures/finder'))
->run('php '.base_path('pint')." --stdin-filename={$filename}")
->throw();

expect($result)->output()->toBe($expected)->errorOutput()->toBe('');
})->with([
'excluded folder' => ['my-dir/SomeFile.php', false],
'excluded notName pattern' => ['src/test-my-file.php', false],
'excluded notPath pattern' => ['path/to/excluded-file.php', false],
'not excluded' => ['src/MyClass.php', true],
]);
Loading