From 1020621869e9ccda37a20e1cc327c5181e8f498f Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 16 Sep 2026 00:04:38 +0200 Subject: [PATCH 1/2] fix(framework): read one-line docblock vars in `meta:view-component` The variable extractor only matched var tags on star-prefixed lines of a multiline docblock, so components declaring attributes in a one-line docblock (like the built-in `x-submit` does for `$label`) reported no variables. One-line docblocks directly followed by a plain assignment to the same variable, such as the `$formSession = get(...)` in `x-input`, type a local and stay excluded, so `x-input` does not suddenly list its services as attributes. Compound assignments such as `??=` keep the variable, since they are how components give an attribute a default. --- .../Commands/MetaViewComponentCommand.php | 17 ++++--- .../Commands/MetaViewComponentCommandTest.php | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/Tempest/Framework/Commands/MetaViewComponentCommand.php b/src/Tempest/Framework/Commands/MetaViewComponentCommand.php index ef9c5b0e14..7d8274f0de 100644 --- a/src/Tempest/Framework/Commands/MetaViewComponentCommand.php +++ b/src/Tempest/Framework/Commands/MetaViewComponentCommand.php @@ -6,7 +6,6 @@ use Tempest\Console\ConsoleCommand; use Tempest\Console\HasConsole; use Tempest\Support\Arr\ImmutableArray; -use Tempest\Support\Str\ImmutableString; use Tempest\View\Slot; use Tempest\View\ViewComponent; use Tempest\View\ViewConfig; @@ -81,11 +80,17 @@ private function resolveSlots(ViewComponent $viewComponent): ImmutableArray private function resolveVariables(ViewComponent $viewComponent): ImmutableArray { return str($viewComponent->contents) - ->matchAll('/^\s*\*\s*@var.*$/m') - ->map(fn (array $matches) => str($matches[0])) - ->map(fn (ImmutableString $line) => $line->replaceRegex('/^\s*\*\s*@var\s*/', '')) - ->map(fn (ImmutableString $line) => $line->trim()) - ->map(fn (ImmutableString $line) => $line->explode(limit: 3)) + ->matchAll( + pattern: '/(?:^\s*\*|\/\*\*)[ \t]*@var[ \t]+(?[^\r\n]*?)[ \t]*(?:\*\/(?:\s*(?\$\w+)\s*=(?!=))?|$)/m', + matches: ['declaration', 'assignee'], + ) + ->map(fn (array $matches) => [ + 'parts' => str($matches['declaration'])->explode(limit: 3), + 'assignee' => $matches['assignee'] ?? null, + ]) + // A one-line `@var` right before an assignment types a local variable, not an attribute. + ->filter(fn (array $match) => $match['assignee'] === null || $match['assignee'] !== ($match['parts'][1] ?? null)) + ->map(fn (array $match) => $match['parts']) ->mapWithKeys( fn (ImmutableArray $parts) => yield $parts[1] => [ 'type' => $parts[0], diff --git a/tests/Integration/Framework/Commands/MetaViewComponentCommandTest.php b/tests/Integration/Framework/Commands/MetaViewComponentCommandTest.php index 0b4621309b..b4b80f655d 100644 --- a/tests/Integration/Framework/Commands/MetaViewComponentCommandTest.php +++ b/tests/Integration/Framework/Commands/MetaViewComponentCommandTest.php @@ -61,4 +61,51 @@ public function show_meta_for_view_component(): void ], JSON); } + + #[Test] + public function show_variables_declared_in_one_line_docblock(): void + { + $this->console + ->call('meta:view-component x-with-variable') + ->assertSuccess() + ->assertSee(<<<'JSON' + "variables": [ + { + "type": "string", + "name": "$variable", + "attributeName": "variable", + "description": null + } + ] + JSON); + } + + #[Test] + public function show_description_of_one_line_docblock_variable(): void + { + $this->console + ->call('meta:view-component x-submit') + ->assertSuccess() + ->assertSee(<<<'JSON' + "variables": [ + { + "type": "null|string", + "name": "$label", + "attributeName": "label", + "description": "The submit button's label" + } + ] + JSON); + } + + #[Test] + public function ignore_one_line_docblock_typing_a_local_assignment(): void + { + $this->console + ->call('meta:view-component x-input') + ->assertSuccess() + ->assertSee('"name": "$default",') + ->assertNotSee('$formSession') + ->assertNotSee('$validator'); + } } From 1324e7256872fdac3de7022e1cbd3759507e5c5a Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 16 Sep 2026 09:42:43 +0200 Subject: [PATCH 2/2] fix(framework): tolerate CRLF when reading docblock var tags With /m, `$` matches before `\n` but not before `\r`, and neither the declaration class nor the trailing whitespace can consume a carriage return. On a CRLF checkout (Windows CI) no multiline var tag matched and every component reported an empty variable list. --- src/Tempest/Framework/Commands/MetaViewComponentCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tempest/Framework/Commands/MetaViewComponentCommand.php b/src/Tempest/Framework/Commands/MetaViewComponentCommand.php index 7d8274f0de..b43d889a62 100644 --- a/src/Tempest/Framework/Commands/MetaViewComponentCommand.php +++ b/src/Tempest/Framework/Commands/MetaViewComponentCommand.php @@ -81,7 +81,7 @@ private function resolveVariables(ViewComponent $viewComponent): ImmutableArray { return str($viewComponent->contents) ->matchAll( - pattern: '/(?:^\s*\*|\/\*\*)[ \t]*@var[ \t]+(?[^\r\n]*?)[ \t]*(?:\*\/(?:\s*(?\$\w+)\s*=(?!=))?|$)/m', + pattern: '/(?:^\s*\*|\/\*\*)[ \t]*@var[ \t]+(?[^\r\n]*?)[ \t]*(?:\*\/(?:\s*(?\$\w+)\s*=(?!=))?|\r?$)/m', matches: ['declaration', 'assignee'], ) ->map(fn (array $matches) => [