Skip to content

Commit 7434735

Browse files
committed
Fix importing by namespace when relative to current namespace
1 parent 38fe579 commit 7434735

File tree

6 files changed

+344
-16
lines changed

6 files changed

+344
-16
lines changed

src/CodeGenerator.php

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -188,23 +188,36 @@ public function importEnum(UnitEnum $enum) : string
188188
/**
189189
* Imports a class, namespace, or function and returns the alias to use in the generated code
190190
*/
191-
public function import(Importable | string $fqcnOrEnum) : string
191+
public function import(Importable | string $name) : string
192192
{
193-
if ($fqcnOrEnum instanceof FunctionName) {
194-
$alias = $this->findAvailableAlias($fqcnOrEnum, $fqcnOrEnum->shortName);
195-
$this->imports[$alias] = $fqcnOrEnum;
193+
if ($name instanceof FunctionName) {
194+
$alias = $this->findAvailableAlias($name, $name->shortName);
195+
$this->imports[$alias] = $name;
196196

197197
return $alias;
198198
}
199199

200-
if ($fqcnOrEnum instanceof NamespaceName) {
201-
$alias = $this->findAvailableAlias($fqcnOrEnum, $fqcnOrEnum->lastPart);
202-
$this->imports[$alias] = $fqcnOrEnum;
200+
if ($name instanceof NamespaceName) {
201+
// Check if we're importing the same namespace as current namespace
202+
if ($this->namespace?->equals($name) === true) {
203+
// No import needed, just return empty string or handle as needed
204+
return '';
205+
}
206+
207+
$alias = $this->findAvailableAlias($name, $name->lastPart);
208+
$this->imports[$alias] = $name;
203209

204210
return $alias;
205211
}
206212

207-
$fqcn = FullyQualified::maybeFromString($fqcnOrEnum);
213+
$fqcn = FullyQualified::maybeFromString($name);
214+
215+
// Check if the class is in the same namespace as the current namespace
216+
if ($this->namespace !== null && $fqcn->namespace !== null && $this->namespace->equals($fqcn->namespace)) {
217+
// No import needed, just return the class name
218+
return (string) $fqcn->className;
219+
}
220+
208221
$alias = $this->findAvailableAlias($fqcn, $fqcn->className->name);
209222
$this->imports[$alias] = $fqcn;
210223

@@ -218,21 +231,26 @@ public function importByParent(Importable | string $name) : string
218231
{
219232
$fqcn = FullyQualified::maybeFromString($name);
220233

221-
// If there's no namespace, just return the class name
234+
// If the class has no namespace, just return the class name
222235
if ($fqcn->namespace === null) {
223236
return (string) $fqcn->className;
224237
}
225238

226-
// Check if the full target namespace is the same as the current namespace
227-
if ($this->namespace?->equals($fqcn->namespace) === true) {
239+
// If the class is in the same namespace as the current file, no import needed
240+
if ($fqcn->isInNamespace($this->namespace)) {
228241
return (string) $fqcn->className;
229242
}
230243

231-
// Import the namespace and return the alias with class name
232-
return (string) new FullyQualified(
233-
$this->import($fqcn->namespace),
234-
$fqcn->className,
235-
);
244+
// If it's a direct child of the current namespace, return relative path without import
245+
if ($this->namespace !== null && $fqcn->namespace->isDirectChildOf($this->namespace)) {
246+
return $fqcn->getRelativePathFrom($this->namespace);
247+
}
248+
249+
// Import the parent namespace and return the short form
250+
$alias = $this->findAvailableAlias($fqcn->namespace, $fqcn->namespace->lastPart);
251+
$this->imports[$alias] = $fqcn->namespace;
252+
253+
return $alias . '\\' . $fqcn->className;
236254
}
237255

238256
/**

src/FullyQualified.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,45 @@ public static function maybeFromString(null | Importable | self | string $input)
5353
return new self((string) $input);
5454
}
5555

56+
/**
57+
* Check if this class is in the given namespace
58+
*/
59+
public function isInNamespace(?NamespaceName $namespace) : bool
60+
{
61+
if ($namespace === null || $this->namespace === null) {
62+
return $namespace === null && $this->namespace === null;
63+
}
64+
65+
return $this->namespace->equals($namespace);
66+
}
67+
68+
/**
69+
* Get the relative path from a parent namespace with the class name
70+
*/
71+
public function getRelativePathFrom(?NamespaceName $parent) : string
72+
{
73+
// If no parent namespace given, return full path
74+
if ($parent === null) {
75+
return (string) $this;
76+
}
77+
78+
// If this class has no namespace, just return the class name
79+
if ($this->namespace === null) {
80+
return (string) $this->className;
81+
}
82+
83+
if ($this->namespace->equals($parent)) {
84+
return (string) $this->className;
85+
}
86+
87+
if ($this->namespace->isSubNamespaceOf($parent)) {
88+
return $this->namespace->getRelativePathFrom($parent) . '\\' . $this->className;
89+
}
90+
91+
// Not in a sub-namespace, return full path
92+
return (string) $this;
93+
}
94+
5695
#[Override]
5796
public function __toString() : string
5897
{

src/NamespaceName.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,40 @@ public function with(string $part, string ...$parts) : self
7171
return new self($this->namespace, $part, ...$parts);
7272
}
7373

74+
/**
75+
* Check if this namespace is a sub-namespace of another
76+
*/
77+
public function isSubNamespaceOf(self $parent) : bool
78+
{
79+
return str_starts_with($this->namespace, $parent->namespace . '\\');
80+
}
81+
82+
/**
83+
* Check if this namespace is a direct child of another
84+
*/
85+
public function isDirectChildOf(self $parent) : bool
86+
{
87+
if ( ! $this->isSubNamespaceOf($parent)) {
88+
return false;
89+
}
90+
91+
$relativePath = $this->getRelativePathFrom($parent);
92+
93+
return ! str_contains($relativePath, '\\');
94+
}
95+
96+
/**
97+
* Get the relative path from a parent namespace
98+
*/
99+
public function getRelativePathFrom(self $parent) : string
100+
{
101+
if ( ! $this->isSubNamespaceOf($parent)) {
102+
return $this->namespace;
103+
}
104+
105+
return substr($this->namespace, strlen($parent->namespace) + 1);
106+
}
107+
74108
#[Override]
75109
public function equals(object $other) : bool
76110
{

tests/CodeGeneratorTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,71 @@ public function testImportByParentWithThreePartNamespace() : void
347347
);
348348
}
349349

350+
public function testImportWithSameNamespaceReturnsClassName() : void
351+
{
352+
$this->generator = new CodeGenerator('App\\Models');
353+
354+
$alias = $this->generator->import('App\\Models\\User');
355+
356+
self::assertSame('User', $alias);
357+
358+
$this->assertDumpFile(
359+
<<<'PHP'
360+
<?php
361+
362+
declare(strict_types=1);
363+
364+
namespace App\Models;
365+
366+
PHP,
367+
[],
368+
);
369+
}
370+
371+
public function testImportByParentWithSubNamespace() : void
372+
{
373+
$this->generator = new CodeGenerator('App\\Models');
374+
375+
$alias = $this->generator->importByParent('App\\Models\\User\\Profile');
376+
377+
self::assertSame('User\\Profile', $alias);
378+
379+
$this->assertDumpFile(
380+
<<<'PHP'
381+
<?php
382+
383+
declare(strict_types=1);
384+
385+
namespace App\Models;
386+
387+
PHP,
388+
[],
389+
);
390+
}
391+
392+
public function testImportByParentWithDeepSubNamespace() : void
393+
{
394+
$this->generator = new CodeGenerator('App');
395+
396+
$alias = $this->generator->importByParent('App\\Models\\User\\Profile');
397+
398+
self::assertSame('User\\Profile', $alias);
399+
400+
$this->assertDumpFile(
401+
<<<'PHP'
402+
<?php
403+
404+
declare(strict_types=1);
405+
406+
namespace App;
407+
408+
use App\Models\User;
409+
410+
PHP,
411+
[],
412+
);
413+
}
414+
350415
public function testDumpAttribute() : void
351416
{
352417
self::assertSame(

tests/FullyQualifiedTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,92 @@ public function testLeadingBackslash() : void
7979
self::assertSame('App\\Models', $fqcn->namespace?->namespace);
8080
self::assertSame('App\\Models\\User', (string) $fqcn);
8181
}
82+
83+
public function testIsInNamespace() : void
84+
{
85+
$fqcn1 = new FullyQualified('App\\Models\\User');
86+
$fqcn2 = new FullyQualified('User');
87+
88+
$appModels = new NamespaceName('App\\Models');
89+
$app = new NamespaceName('App');
90+
$core = new NamespaceName('Core');
91+
92+
self::assertTrue($fqcn1->isInNamespace($appModels));
93+
self::assertFalse($fqcn1->isInNamespace($app));
94+
self::assertFalse($fqcn1->isInNamespace($core));
95+
self::assertFalse($fqcn1->isInNamespace(null));
96+
97+
self::assertFalse($fqcn2->isInNamespace($appModels));
98+
self::assertFalse($fqcn2->isInNamespace($app));
99+
self::assertTrue($fqcn2->isInNamespace(null)); // Class without namespace
100+
}
101+
102+
public function testGetRelativePathFrom() : void
103+
{
104+
$fqcn = new FullyQualified('App\\Models\\User\\Profile');
105+
106+
$app = new NamespaceName('App');
107+
$appModels = new NamespaceName('App\\Models');
108+
$appModelsUser = new NamespaceName('App\\Models\\User');
109+
$core = new NamespaceName('Core');
110+
111+
// Direct children and sub-namespaces
112+
self::assertSame('Models\\User\\Profile', $fqcn->getRelativePathFrom($app));
113+
self::assertSame('User\\Profile', $fqcn->getRelativePathFrom($appModels));
114+
self::assertSame('Profile', $fqcn->getRelativePathFrom($appModelsUser));
115+
116+
// Not in a sub-namespace - returns full path
117+
self::assertSame('App\\Models\\User\\Profile', $fqcn->getRelativePathFrom($core));
118+
119+
// Null namespace
120+
self::assertSame('App\\Models\\User\\Profile', $fqcn->getRelativePathFrom(null));
121+
}
122+
123+
public function testGetRelativePathFromForClassWithoutNamespace() : void
124+
{
125+
$fqcn = new FullyQualified('SimpleClass');
126+
127+
$app = new NamespaceName('App');
128+
129+
self::assertSame('SimpleClass', $fqcn->getRelativePathFrom($app));
130+
self::assertSame('SimpleClass', $fqcn->getRelativePathFrom(null));
131+
}
132+
133+
public function testMaybeFromString() : void
134+
{
135+
// Test with string input
136+
$result1 = FullyQualified::maybeFromString('App\\Models\\User');
137+
// @phpstan-ignore-next-line
138+
self::assertNotNull($result1);
139+
self::assertSame('App\\Models\\User', (string) $result1);
140+
141+
// Test with null input
142+
$result2 = FullyQualified::maybeFromString(null);
143+
// @phpstan-ignore-next-line
144+
self::assertNull($result2);
145+
146+
// Test with existing FullyQualified instance
147+
$existing = new FullyQualified('App\\Services\\UserService');
148+
$result3 = FullyQualified::maybeFromString($existing);
149+
self::assertSame($existing, $result3);
150+
151+
// Test with NamespaceName instance
152+
$namespaceName = new NamespaceName('App\\Models');
153+
$result4 = FullyQualified::maybeFromString($namespaceName);
154+
// @phpstan-ignore-next-line
155+
self::assertNotNull($result4);
156+
self::assertSame('App\\Models', (string) $result4);
157+
}
158+
159+
public function testEquals() : void
160+
{
161+
$fqcn1 = new FullyQualified('App\\Models\\User');
162+
$fqcn2 = new FullyQualified('App', 'Models', 'User');
163+
$fqcn3 = new FullyQualified('App\\Models\\Post');
164+
$fqcn4 = new FullyQualified('User');
165+
166+
self::assertTrue($fqcn1->equals($fqcn2));
167+
self::assertFalse($fqcn1->equals($fqcn3));
168+
self::assertFalse($fqcn1->equals($fqcn4));
169+
}
82170
}

0 commit comments

Comments
 (0)