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:
Pieter Willekens
2026-02-10 15:19:29 +01:00
committed by GitHub
parent 5aca186d92
commit 3bd70eafd0
6 changed files with 141 additions and 2 deletions
@@ -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');
}
}