mirror of
https://github.com/barryvdh/laravel-ide-helper.git
synced 2026-08-17 17:47:13 +00:00
fix: wrap bare intersection types in parentheses when adding nullable (#1756)
When a MorphTo relation's docblock uses a bare intersection type in the generic parameter (e.g. `MorphTo<BaseModel&CanBeAssigned, $this>`), the generated property type incorrectly becomes `BaseModel&CanBeAssigned|null` instead of the correct `(BaseModel&CanBeAssigned)|null`. This adds `wrapIntersectionType()` which ensures bare intersection types are wrapped in parentheses before `|null` is appended, producing valid DNF type syntax. Applied in both `setProperty()` and `applyNullability()`.
This commit is contained in:
@@ -534,7 +534,7 @@ class ModelsCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($isNullable) {
|
if ($isNullable) {
|
||||||
$type .= '|null';
|
$type = $this->wrapIntersectionType($type) . '|null';
|
||||||
} else {
|
} else {
|
||||||
$type = str_replace($nullString, '', $type);
|
$type = str_replace($nullString, '', $type);
|
||||||
}
|
}
|
||||||
@@ -542,6 +542,22 @@ class ModelsCommand extends Command
|
|||||||
return $type;
|
return $type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a bare intersection type in parentheses for correct DNF syntax.
|
||||||
|
*
|
||||||
|
* For example, `A&B` becomes `(A&B)` so that adding `|null` produces
|
||||||
|
* `(A&B)|null` instead of the ambiguous `A&B|null`.
|
||||||
|
* Types that are already parenthesized or contain union types are returned as-is.
|
||||||
|
*/
|
||||||
|
protected function wrapIntersectionType(string $type): string
|
||||||
|
{
|
||||||
|
if (str_contains($type, '&') && !str_contains($type, '|') && $type[0] !== '(') {
|
||||||
|
return '(' . $type . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the override type for the give type.
|
* Returns the override type for the give type.
|
||||||
*
|
*
|
||||||
@@ -995,7 +1011,7 @@ class ModelsCommand extends Command
|
|||||||
if ($type !== null) {
|
if ($type !== null) {
|
||||||
$newType = $this->getTypeOverride($type);
|
$newType = $this->getTypeOverride($type);
|
||||||
if ($nullable) {
|
if ($nullable) {
|
||||||
$newType .= '|null';
|
$newType = $this->wrapIntersectionType($newType) . '|null';
|
||||||
}
|
}
|
||||||
$this->properties[$name]['type'] = $newType;
|
$this->properties[$name]['type'] = $newType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\MorphToIntersection\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
abstract class BaseModel extends Model
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\MorphToIntersection\Models;
|
||||||
|
|
||||||
|
interface CanBeAssigned
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\MorphToIntersection\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
class MorphToIntersection extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'morphs';
|
||||||
|
|
||||||
|
/** @return MorphTo<(BaseModel&CanBeAssigned), $this> */
|
||||||
|
public function assigneeWithParens(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return MorphTo<BaseModel&CanBeAssigned, $this> */
|
||||||
|
public function assigneeWithoutParens(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return MorphTo<(BaseModel&CanBeAssigned), $this> */
|
||||||
|
public function nonNullableAssignee(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(type: 'relation_morph_to_type', id: 'relation_morph_to_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\MorphToIntersection;
|
||||||
|
|
||||||
|
use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
|
||||||
|
use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\AbstractModelsCommand;
|
||||||
|
|
||||||
|
class Test extends AbstractModelsCommand
|
||||||
|
{
|
||||||
|
public function test(): void
|
||||||
|
{
|
||||||
|
$command = $this->app->make(ModelsCommand::class);
|
||||||
|
|
||||||
|
$tester = $this->runCommand($command, [
|
||||||
|
'--write' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(0, $tester->getStatusCode());
|
||||||
|
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
|
||||||
|
$this->assertMatchesMockedSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\MorphToIntersection\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $relation_morph_to_type
|
||||||
|
* @property int $relation_morph_to_id
|
||||||
|
* @property string|null $nullable_relation_morph_to_type
|
||||||
|
* @property int|null $nullable_relation_morph_to_id
|
||||||
|
* @property-read (BaseModel&CanBeAssigned)|null $assigneeWithParens
|
||||||
|
* @property-read (BaseModel&CanBeAssigned)|null $assigneeWithoutParens
|
||||||
|
* @property-read (BaseModel&CanBeAssigned) $nonNullableAssignee
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection whereNullableRelationMorphToId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection whereNullableRelationMorphToType($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection whereRelationMorphToId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|MorphToIntersection whereRelationMorphToType($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class MorphToIntersection extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'morphs';
|
||||||
|
|
||||||
|
/** @return MorphTo<(BaseModel&CanBeAssigned), $this> */
|
||||||
|
public function assigneeWithParens(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return MorphTo<BaseModel&CanBeAssigned, $this> */
|
||||||
|
public function assigneeWithoutParens(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(type: 'nullable_relation_morph_to_type', id: 'nullable_relation_morph_to_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return MorphTo<(BaseModel&CanBeAssigned), $this> */
|
||||||
|
public function nonNullableAssignee(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(type: 'relation_morph_to_type', id: 'relation_morph_to_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user