diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 177295dd..57cafe05 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -104,6 +104,15 @@ trait NameResolutionTrait if (isset($this->zendTypeMap[strtolower($typeName)]) || in_array(strtolower($typeName), ['self', 'static', 'parent'], true)) { return $type; } + // NameResolver has already applied the declaring file's namespace + // imports. Keep that canonical identity when a trait signature is + // copied into its consuming class. In particular, after + // `use X\X; use X\Y;`, resolving Y produces X\Y; feeding that string + // through the import table again would incorrectly produce X\X\Y. + $resolvedName = $type->getAttribute('resolvedName'); + if ($resolvedName instanceof Node\Name\FullyQualified) { + return new Node\Name\FullyQualified($resolvedName->toString(), $type->getAttributes()); + } $resolved = $typeName; $firstSegment = explode('\\', $typeName, 2)[0]; $hasImportedPrefix = $this->getClassImportAlias($firstSegment) !== null; diff --git a/tests/compiler/namespace/use-import-root-semantics.phpt b/tests/compiler/namespace/use-import-root-semantics.phpt new file mode 100644 index 00000000..30c23e73 --- /dev/null +++ b/tests/compiler/namespace/use-import-root-semantics.phpt @@ -0,0 +1,40 @@ +--TEST-- +Namespace imports are rooted while qualified name uses still expand their first alias segment +--FILE-- + +--EXPECT-- +Carbon\Carbon +Carbon\CarbonInterface +Carbon\Carbon +Carbon\CarbonInterface +Carbon\Carbon\CarbonInterface diff --git a/tests/compiler/trait/trait-import-same-as-namespace.phpt b/tests/compiler/trait/trait-import-same-as-namespace.phpt new file mode 100644 index 00000000..31f88881 --- /dev/null +++ b/tests/compiler/trait/trait-import-same-as-namespace.phpt @@ -0,0 +1,88 @@ +--TEST-- +Trait types retain rooted imports when an imported class has the same short name as its namespace +--FILE-- +value = $value; + return $value; + } + + public function acceptNullable(?CarbonInterface $value): ?CarbonInterface { + return $value; + } + + public function check(mixed $value): bool { + return $value instanceof CarbonInterface; + } + + public function make(): CarbonInterface { + return Carbon::parse('value'); + } + + public function importedNames(): array { + return [ + Carbon::class, + CarbonInterface::class, + Carbon\CarbonInterface::class, + ]; + } + } + + class Example { + use Checker; + } +} + +namespace { + function main(): void { + eval(<<<'PHP' +namespace Carbon; +interface CarbonInterface {} +class Carbon implements CarbonInterface { + public static function parse(string $value): CarbonInterface { + return new self(); + } +} +PHP); + + $example = new App\Example(); + $value = new Carbon\Carbon(); + + var_dump($example->accept($value) === $value); + var_dump($example->acceptNullable($value) === $value); + var_dump($example->acceptNullable(null)); + var_dump($example->check($value)); + var_dump($example->make() instanceof Carbon\CarbonInterface); + + $method = new ReflectionMethod(App\Example::class, 'accept'); + echo $method->getParameters()[0]->getType()->getName(), "\n"; + echo $method->getReturnType()->getName(), "\n"; + + $property = new ReflectionProperty(App\Example::class, 'value'); + echo $property->getType()->getName(), "\n"; + + foreach ($example->importedNames() as $name) { + echo $name, "\n"; + } + } +} +?> +--EXPECT-- +bool(true) +bool(true) +NULL +bool(true) +bool(true) +Carbon\CarbonInterface +Carbon\CarbonInterface +Carbon\CarbonInterface +Carbon\Carbon +Carbon\CarbonInterface +Carbon\Carbon\CarbonInterface