diff --git a/packages/dev/composer.json b/packages/dev/composer.json index 799068231..ca79e4fe8 100644 --- a/packages/dev/composer.json +++ b/packages/dev/composer.json @@ -19,15 +19,14 @@ }, "require": { "php": "^8.1|^8.2|^8.3", - "illuminate/console": "^10.34.0|^11.0.0", "illuminate/contracts": "^10.34.0|^11.0.0", "illuminate/support": "^10.34.0|^11.0.0", "larastan/larastan": "^2.8.1", "lastdragon-ru/lara-asp-core": "self.version", + "lastdragon-ru/lara-asp-documentator": "self.version", "nikic/php-parser": "^4.18|^5.0", "nette/neon": "^3.4", "phpstan/phpstan": "^1.10", - "symfony/console": "^6.3.0|^7.0.0", "symfony/var-dumper": "^6.3.0|^7.0.0" }, "require-dev": { @@ -52,6 +51,8 @@ "sort-packages": true, "optimize-autoloader": true }, + "minimum-stability": "dev", + "prefer-stable": true, "repositories": { "core": { "type": "path", diff --git a/packages/dev/src/App/Example.php b/packages/dev/src/App/Example.php index 8a442ff32..48beec065 100644 --- a/packages/dev/src/App/Example.php +++ b/packages/dev/src/App/Example.php @@ -2,12 +2,14 @@ namespace LastDragon_ru\LaraASP\Dev\App; -use Illuminate\Console\Command; use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Foundation\Application; -use LastDragon_ru\LaraASP\Core\Utils\Cast; +use LastDragon_ru\LaraASP\Core\Application\ApplicationResolver; use LastDragon_ru\LaraASP\Core\Utils\ConfigMerger; +use LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample\Contracts\Runner; +use LastDragon_ru\LaraASP\Documentator\Processor\FileSystem\File; use LogicException; +use Override; use PhpParser\ErrorHandler\Collecting; use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; @@ -17,36 +19,29 @@ use PhpParser\ParserFactory; use PhpParser\PrettyPrinter\Standard; use Stringable; -use Symfony\Component\Console\Attribute\AsCommand; use function array_map; use function array_slice; use function debug_backtrace; use function end; -use function file; use function implode; +use function preg_split; use function sprintf; +use function str_contains; use function trim; use const DEBUG_BACKTRACE_IGNORE_ARGS; -#[AsCommand( - name : Example::Name, - description: 'Executes example file within Application context.', -)] -final class Example extends Command { - private const Name = 'dev:example'; - +final class Example implements Runner { private static ?Application $app = null; + private static ?File $file = null; private static ?Dumper $dumper = null; - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingNativeTypeHint - * @var string - */ - protected $signature = self::Name.<<<'SIGNATURE' - {file : example file} - SIGNATURE; + public function __construct( + private readonly ApplicationResolver $appResolver, + ) { + // empty + } protected static function getDumper(): Dumper { if (!self::$dumper) { @@ -61,28 +56,39 @@ protected static function getDumper(): Dumper { return self::$dumper; } - public function __invoke(Dumper $dumper): void { - self::$dumper = $dumper; - self::$app = $this->laravel; - $file = Cast::toString($this->argument('file')); + #[Override] + public function __invoke(File $file): ?string { + // Runnable? + if ($file->getExtension() !== 'php' || !str_contains($file->getContent(), 'Example::')) { + return null; + } + + // Run + $result = null; + self::$app = $this->appResolver->getInstance(); + self::$file = $file; + self::$dumper = self::$app->make(Dumper::class); try { - // Run + // Execute (static function () use ($file): void { - include $file; + include $file->getPath(); })(); // Output - $dumps = $dumper->getDumps(); + $dumps = self::$dumper->getDumps(); $output = implode("\n\n", array_map(trim(...), $dumps)); if ($output) { - $this->output->writeln("{$output}"); + $result = "{$output}"; } } finally { self::$app = null; + self::$file = null; self::$dumper = null; } + + return $result; } public static function dump(mixed $value, string $expression = null): void { @@ -130,7 +136,8 @@ private static function getExpression(string $method): ?string { } // Extract first arg - $code = implode("\n", array_slice((array) file($context['file']), $context['line'] - 1)); + $lines = preg_split('/\R/u', self::$file?->getContent() ?? '') ?: []; + $code = implode("\n", array_slice($lines, $context['line'] - 1)); $parser = (new ParserFactory())->createForNewestSupportedVersion(); $stmts = (array) $parser->parse("findFirst($stmts, static function (Node $node) use ($method): bool { diff --git a/packages/dev/src/App/Provider.php b/packages/dev/src/App/Provider.php index a3e68d62a..f699645f1 100644 --- a/packages/dev/src/App/Provider.php +++ b/packages/dev/src/App/Provider.php @@ -3,11 +3,14 @@ namespace LastDragon_ru\LaraASP\Dev\App; use Illuminate\Support\ServiceProvider; +use LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample\Contracts\Runner; +use Override; class Provider extends ServiceProvider { - public function boot(): void { - $this->commands( - Example::class, - ); + #[Override] + public function register(): void { + parent::register(); + + $this->app->bind(Runner::class, Example::class); } } diff --git a/packages/documentator/UPGRADE.md b/packages/documentator/UPGRADE.md index ab034f82c..62e044d27 100644 --- a/packages/documentator/UPGRADE.md +++ b/packages/documentator/UPGRADE.md @@ -41,6 +41,8 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases) * `\LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\Instruction` * `\LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\Resolver`. +* [ ] Instruction `include:example` not check/run `.run` file anymore. The `\LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample\Contracts\Runner` should be used/provided instead. + # Upgrade from v5 [include:file]: ../../docs/Shared/Upgrade/FromV5.md diff --git a/packages/documentator/docs/Commands/preprocess.md b/packages/documentator/docs/Commands/preprocess.md index 4f54d2ce9..14874337d 100644 --- a/packages/documentator/docs/Commands/preprocess.md +++ b/packages/documentator/docs/Commands/preprocess.md @@ -65,10 +65,10 @@ after the Header will be used as a summary. * `` - File path. Includes contents of the `` file as an example wrapped into -` ```code block``` `. It also searches for `.run` file, execute -it if found, and include its result right after the code block. +` ```code block``` `. If {@see Runner} bound, it will be called to execute +the example. Its return value will be added right after the code block. -By default, output of `.run` will be included as ` ```plain text``` ` +By default, the `Runner` return value will be included as ` ```plain text``` ` block. You can wrap the output into `text` tags to insert it as is. diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Contracts/Runner.php b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Contracts/Runner.php new file mode 100644 index 000000000..14a95854c --- /dev/null +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Contracts/Runner.php @@ -0,0 +1,9 @@ +example->getRelativePath($context->root), + $context->file->getRelativePath($context->root), + ), + $previous, + ); + } + + public function getExample(): File { + return $this->example; + } +} diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Instruction.php b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Instruction.php index 4e727d576..c83a9f01c 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Instruction.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/Instruction.php @@ -2,31 +2,28 @@ namespace LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample; -use Exception; -use Illuminate\Process\Factory; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Context; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\Instruction as InstructionContract; -use LastDragon_ru\LaraASP\Documentator\Preprocessor\Exceptions\TargetExecFailed; +use LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample\Contracts\Runner; +use LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample\Exceptions\ExampleFailed; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Resolvers\FileResolver; use LastDragon_ru\LaraASP\Documentator\Processor\FileSystem\File; use Override; +use Throwable; -use function dirname; -use function pathinfo; use function preg_match; use function preg_match_all; use function preg_replace_callback; use function trim; -use const PATHINFO_FILENAME; use const PREG_UNMATCHED_AS_NULL; /** * Includes contents of the `` file as an example wrapped into - * ` ```code block``` `. It also searches for `.run` file, execute - * it if found, and include its result right after the code block. + * ` ```code block``` `. If {@see Runner} bound, it will be called to execute + * the example. Its return value will be added right after the code block. * - * By default, output of `.run` will be included as ` ```plain text``` ` + * By default, the `Runner` return value will be included as ` ```plain text``` ` * block. You can wrap the output into `text` tags to * insert it as is. * @@ -37,7 +34,7 @@ class Instruction implements InstructionContract { protected const MarkdownRegexp = '/^\<(?Pmarkdown)\>(?P.*?)\<\/(?P=tag)\>$/msu'; public function __construct( - protected readonly Factory $factory, + protected readonly ?Runner $runner = null, ) { // empty } @@ -68,17 +65,13 @@ public function __invoke(Context $context, mixed $target, mixed $parameters): st ``` CODE; - // Command? - $command = $this->getCommand($context, $target, $parameters); - - if ($command) { - // Call + // Runner? + if ($this->runner) { + // Run try { - $dir = dirname($target->getPath()); - $output = $this->factory->newPendingProcess()->path($dir)->run($command)->throw()->output(); - $output = trim($output); - } catch (Exception $exception) { - throw new TargetExecFailed($context, $exception); + $output = trim((string) ($this->runner)($target)); + } catch (Throwable $exception) { + throw new ExampleFailed($context, $target, $exception); } // Markdown? @@ -120,7 +113,7 @@ public function __invoke(Context $context, mixed $target, mixed $parameters): st CODE; - } else { + } elseif ($output) { $output = <<getExtension(); } - - protected function getCommand(Context $context, File $target, mixed $parameters): ?string { - $command = pathinfo($target->getName(), PATHINFO_FILENAME).'.run'; - $command = $context->root->getDirectory($target)?->getFile($command)?->getPath(); - - return $command; - } } diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~example.md b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest.md similarity index 100% rename from packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~example.md rename to packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest.md diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest.php b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest.php index d56133b16..0aeb21240 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest.php @@ -2,14 +2,16 @@ namespace LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample; -use Illuminate\Process\Factory; -use Illuminate\Process\PendingProcess; +use LastDragon_ru\LaraASP\Core\Utils\Path; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Context; +use LastDragon_ru\LaraASP\Documentator\Preprocessor\Instructions\IncludeExample\Contracts\Runner; use LastDragon_ru\LaraASP\Documentator\Processor\FileSystem\Directory; use LastDragon_ru\LaraASP\Documentator\Processor\FileSystem\File; use LastDragon_ru\LaraASP\Documentator\Testing\Package\ProcessorHelper; use LastDragon_ru\LaraASP\Documentator\Testing\Package\TestCase; +use Mockery\MockInterface; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use function dirname; use function implode; @@ -21,141 +23,39 @@ */ #[CoversClass(Instruction::class)] final class InstructionTest extends TestCase { - public function testInvokeNoRun(): void { - $path = self::getTestData()->path('~example.md'); - $root = new Directory(dirname($path), false); - $file = new File($path, false); - $params = null; - $context = new Context($root, $file, $file->getName(), $params); - $content = self::getTestData()->content('~example.md'); - $expected = trim($content); - $factory = $this->override(Factory::class, function (): Factory { - return $this->app()->make(Factory::class) - ->preventStrayProcesses() - ->fake(); + // + // ========================================================================= + #[DataProvider('dataProviderInvoke')] + public function testInvoke(string $expected, string $output): void { + $path = Path::normalize(self::getTestData()->path('.md')); + $root = new Directory(dirname($path), false); + $file = new File($path, false); + $params = null; + $context = new Context($root, $file, $file->getName(), $params); + + $this->override(Runner::class, static function (MockInterface $mock) use ($file, $output): void { + $mock + ->shouldReceive('__invoke') + ->with($file) + ->once() + ->andReturn($output); }); - $instance = $this->app()->make(Instruction::class); - $actual = ProcessorHelper::runInstruction($instance, $context, $file, $params); - self::assertEquals( - <<assertNothingRan(); - } - - public function testInvoke(): void { - $path = self::getTestData()->path('~runnable.md'); - $root = new Directory(dirname($path), false); - $file = new File($path, false); - $params = null; - $context = new Context($root, $file, $file->getName(), $params); - $content = self::getTestData()->content('~runnable.md'); - $command = self::getTestData()->path('~runnable.run'); - $expected = trim($content); - $output = 'command output'; - $factory = $this->override(Factory::class, function () use ($command, $output): Factory { - $factory = $this->app()->make(Factory::class); - $factory->preventStrayProcesses(); - $factory->fake([ - $command => $factory->result($output), - ]); - - return $factory; - }); $instance = $this->app()->make(Instruction::class); $actual = ProcessorHelper::runInstruction($instance, $context, $file, $params); - self::assertEquals( - <<assertRan(static function (PendingProcess $process) use ($path, $command): bool { - return $process->path === dirname($path) - && $process->command === $command; - }); + self::assertEquals($expected, $actual); } - public function testInvokeLongOutput(): void { - $path = self::getTestData()->path('~runnable.md'); - $root = new Directory(dirname($path), false); - $file = new File($path, false); - $params = null; - $context = new Context($root, $file, $file->getPath(), $params); - $content = self::getTestData()->content('~runnable.md'); - $command = self::getTestData()->path('~runnable.run'); - $expected = trim($content); - $output = implode("\n", range(0, Instruction::Limit + 1)); - $factory = $this->override(Factory::class, function () use ($command, $output): Factory { - $factory = $this->app()->make(Factory::class); - $factory->preventStrayProcesses(); - $factory->fake([ - $command => $factory->result($output), - ]); - - return $factory; - }); - $instance = $this->app()->make(Instruction::class); - $actual = ProcessorHelper::runInstruction($instance, $context, $file, $params); - - self::assertEquals( - <<Example output - - ```plain - {$output} - ``` - - - EXPECTED, - $actual, - ); - - $factory->assertRan(static function (PendingProcess $process) use ($path, $command): bool { - return $process->path === dirname($path) - && $process->command === $command; - }); - } + public function testInvokeNoRun(): void { + self::assertFalse($this->app()->bound(Runner::class)); - public function testInvokeMarkdown(): void { - $path = self::getTestData()->path('~runnable.md'); + $path = Path::normalize(self::getTestData()->path('.md')); $root = new Directory(dirname($path), false); $file = new File($path, false); $params = null; $context = new Context($root, $file, $file->getName(), $params); - $content = self::getTestData()->content('~runnable.md'); - $command = self::getTestData()->path('~runnable.run'); - $expected = trim($content); - $output = 'command output'; - $factory = $this->override(Factory::class, function () use ($command, $output): Factory { - $factory = $this->app()->make(Factory::class); - $factory->preventStrayProcesses(); - $factory->fake([ - $command => $factory->result("{$output}"), - ]); - - return $factory; - }); + $expected = trim($file->getContent()); $instance = $this->app()->make(Instruction::class); $actual = ProcessorHelper::runInstruction($instance, $context, $file, $params); @@ -164,58 +64,89 @@ public function testInvokeMarkdown(): void { ```md {$expected} ``` - - {$output} EXPECTED, $actual, ); - - $factory->assertRan(static function (PendingProcess $process) use ($path, $command): bool { - return $process->path === dirname($path) - && $process->command === $command; - }); } - - public function testInvokeMarkdownLongOutput(): void { - $path = self::getTestData()->path('~runnable.md'); - $root = new Directory(dirname($path), false); - $file = new File($path, false); - $params = null; - $context = new Context($root, $file, $file->getPath(), $params); - $content = self::getTestData()->content('~runnable.md'); - $command = self::getTestData()->path('~runnable.run'); - $expected = trim($content); - $output = implode("\n", range(0, Instruction::Limit + 1)); - $factory = $this->override(Factory::class, function () use ($command, $output): Factory { - $factory = $this->app()->make(Factory::class); - $factory->preventStrayProcesses(); - $factory->fake([ - $command => $factory->result("{$output}"), - ]); - - return $factory; - }); - $instance = $this->app()->make(Instruction::class); - $actual = ProcessorHelper::runInstruction($instance, $context, $file, $params); - - self::assertEquals( - <<Example output - - {$output} - - - EXPECTED, - $actual, - ); - - $factory->assertRan(static function (PendingProcess $process) use ($path, $command): bool { - return $process->path === dirname($path) - && $process->command === $command; - }); + // + + // + // ========================================================================= + /** + * @return array + */ + public static function dataProviderInvoke(): array { + $long = implode("\n", range(0, Instruction::Limit + 1)); + $content = <<<'FILE' + # File + + content of the file + FILE; + + return [ + 'empty output' => [ + << [ + << [ + <<Example output + + ```plain + {$long} + ``` + + + EXPECTED, + $long, + ], + 'markdown output' => [ + <<example', + ], + 'markdown long output' => [ + <<Example output + + {$long} + + + EXPECTED, + "{$long}", + ], + ]; } + // } diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~runnable.md b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~runnable.md deleted file mode 100644 index a6886c0f7..000000000 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~runnable.md +++ /dev/null @@ -1,3 +0,0 @@ -# File - -content of the file diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~runnable.run b/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~runnable.run deleted file mode 100644 index 8eca9947a..000000000 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeExample/InstructionTest~runnable.run +++ /dev/null @@ -1 +0,0 @@ -/command diff --git a/packages/documentator/src/Preprocessor/Exceptions/TargetExecFailed.php b/packages/documentator/src/Preprocessor/Instructions/IncludeExec/Exceptions/TargetExecFailed.php similarity index 75% rename from packages/documentator/src/Preprocessor/Exceptions/TargetExecFailed.php rename to packages/documentator/src/Preprocessor/Instructions/IncludeExec/Exceptions/TargetExecFailed.php index d05f2bafd..321cb22ee 100644 --- a/packages/documentator/src/Preprocessor/Exceptions/TargetExecFailed.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeExec/Exceptions/TargetExecFailed.php @@ -1,8 +1,9 @@