-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement addItemToReturnArray method #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d289350
1f165a0
cf924f2
b88c431
b3fc7be
aab7ff3
ca7af3f
58b0487
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| namespace RonasIT\Larabuilder\Exceptions; | ||
|
|
||
| use Exception; | ||
|
|
||
| class MultipleReturnStatementsException extends Exception | ||
| { | ||
| public function __construct(string $method) | ||
| { | ||
| parent::__construct("Method '{$method}' contains multiple return statements."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| namespace RonasIT\Larabuilder\Exceptions; | ||
|
|
||
| use Exception; | ||
|
|
||
| class UnexpectedReturnTypeException extends Exception | ||
| { | ||
| public function __construct(string $method, string $expectedType, ?string $actualType = null) | ||
| { | ||
| parent::__construct( | ||
| "Method '{$method}' return value has unexpected type. Expected '{$expectedType}'" | ||
| . (!empty($actualType) ? ", actual '{$actualType}'." : '.'), | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| <?php | ||
|
|
||
| namespace RonasIT\Larabuilder\Nodes; | ||
|
|
||
| use Illuminate\Support\Str; | ||
| use PhpParser\Error; | ||
| use PhpParser\Node\Expr; | ||
| use PhpParser\ParserFactory; | ||
| use RonasIT\Larabuilder\Exceptions\InvalidPHPCodeException; | ||
|
|
||
| /** | ||
| * Used to insert expression code with saving original formatting | ||
| */ | ||
| class PreformattedExpression extends Expr | ||
| { | ||
| public function __construct( | ||
| public string $value, | ||
| public array $attributes = [], | ||
| ) { | ||
| parent::__construct($this->attributes); | ||
|
|
||
| if ($this->isPlainStringValue($value)) { | ||
| $this->value = "'{$value}'"; | ||
| } else { | ||
| $this->value = Str::chopStart($this->value, '<?php'); | ||
| $this->value = trim($this->value); | ||
| $this->value = Str::chopEnd($this->value, ';'); | ||
|
|
||
| $this->validatePHPCode($this->value); | ||
| } | ||
| } | ||
|
|
||
| public function getSubNodeNames(): array | ||
| { | ||
| return ['value']; | ||
| } | ||
|
|
||
| public function getType(): string | ||
| { | ||
| return 'Expr_PreformattedExpression'; | ||
| } | ||
|
|
||
| protected function isPlainStringValue(string $value): bool | ||
| { | ||
| return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $value) === 1 | ||
| && !in_array(strtolower($value), ['null', 'true', 'false']); | ||
| } | ||
|
|
||
| protected function validatePHPCode(string $code): void | ||
| { | ||
| try { | ||
| new ParserFactory()->createForHostVersion()->parse("<?php\n{$code}?>"); | ||
| } catch (Error) { | ||
| throw new InvalidPHPCodeException($code); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| <?php | ||
|
|
||
| namespace RonasIT\Larabuilder\Visitors\MethodVisitors; | ||
|
|
||
| use Illuminate\Support\Arr; | ||
| use PhpParser\Node; | ||
| use PhpParser\Node\ArrayItem; | ||
| use PhpParser\Node\Expr\Array_; | ||
| use PhpParser\Node\FunctionLike; | ||
| use PhpParser\Node\Stmt\ClassMethod; | ||
| use PhpParser\Node\Stmt\Return_; | ||
| use RonasIT\Larabuilder\Contracts\UpdateNodeContract; | ||
| use RonasIT\Larabuilder\Enums\DefaultValue; | ||
| use RonasIT\Larabuilder\Exceptions\MultipleReturnStatementsException; | ||
| use RonasIT\Larabuilder\Exceptions\UnexpectedReturnTypeException; | ||
| use RonasIT\Larabuilder\Nodes\PreformattedExpression; | ||
| use RonasIT\Larabuilder\Printer; | ||
|
|
||
| class AddItemToReturnArray extends BaseMethodVisitor implements UpdateNodeContract | ||
| { | ||
| protected PreformattedExpression $valueExpr; | ||
| protected ?PreformattedExpression $keyExpr; | ||
|
|
||
| public function __construct( | ||
| protected string $methodName, | ||
| string $value, | ||
| string|DefaultValue $key = DefaultValue::None, | ||
| ) { | ||
| parent::__construct($methodName); | ||
|
|
||
| $this->valueExpr = new PreformattedExpression($value); | ||
| $this->keyExpr = ($key === DefaultValue::None) ? null : new PreformattedExpression($key); | ||
| } | ||
|
|
||
| public function shouldUpdateNode(Node $node): bool | ||
| { | ||
| $isTarget = $node instanceof ClassMethod && $this->methodName === $node->name->name; | ||
|
|
||
|
artengin marked this conversation as resolved.
|
||
| if ($isTarget) { | ||
| $this->hasTargetMethod = true; | ||
| } | ||
|
|
||
| return $isTarget; | ||
| } | ||
|
|
||
| public function updateNode(Node $node): void | ||
| { | ||
| $returnNodes = $this->findReturnsInScope($node->stmts ?? []); | ||
|
|
||
| if (count($returnNodes) > 1) { | ||
| throw new MultipleReturnStatementsException($this->methodName); | ||
| } | ||
|
|
||
| $returnNode = $returnNodes[0] ?? null; | ||
|
|
||
| if (!$returnNode?->expr instanceof Array_) { | ||
| throw new UnexpectedReturnTypeException($this->methodName, 'array', $node->returnType?->toString()); | ||
| } | ||
|
|
||
| if (empty($this->keyExpr)) { | ||
| $returnNode->expr->items[] = new ArrayItem($this->valueExpr); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $printer = new Printer(); | ||
|
|
||
| foreach ($returnNode->expr->items as $item) { | ||
| if ($item instanceof ArrayItem | ||
| && !empty($item->key) | ||
| && $printer->prettyPrintExpr($item->key) === $this->keyExpr->value | ||
| ) { | ||
| $item->value = $this->valueExpr; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the existing return array contains a keyed by-reference item, e.g. Useful? React with 👍 / 👎. |
||
|
|
||
| return; | ||
| } | ||
| } | ||
|
|
||
| $returnNode->expr->items[] = new ArrayItem($this->valueExpr, $this->keyExpr); | ||
| } | ||
|
|
||
| protected function findReturnsInScope(array $nodes): array | ||
| { | ||
| $returns = []; | ||
|
|
||
| foreach ($nodes as $node) { | ||
| if ($node instanceof Return_) { | ||
| $returns[] = $node; | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| if ($node instanceof FunctionLike) { | ||
| continue; | ||
| } | ||
|
|
||
| foreach ($node->getSubNodeNames() as $name) { | ||
| foreach (Arr::wrap($node->$name) as $child) { | ||
| if ($child instanceof Node) { | ||
| $returns = [...$returns, ...$this->findReturnsInScope([$child])]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return $returns; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
$valueor$keyis a syntactically valid PHP statement but not an expression (for examplereturn 1;orif ($x) {}), this validation succeeds because it parses the snippet as a whole PHP file. The same raw text is then printed inside an array item expression byAddItemToReturnArray, producing an invalid PHP file instead of throwingInvalidPHPCodeException; validate these snippets specifically as expressions before accepting them.Useful? React with 👍 / 👎.