diff --git a/src/DoctrineFacade.php b/src/DoctrineFacade.php new file mode 100644 index 0000000..21eb314 --- /dev/null +++ b/src/DoctrineFacade.php @@ -0,0 +1,79 @@ +em = $em; + } + + /** + * @return non-empty-array + * @throws InvalidArgumentException when encountering unreadable config + */ + private static function loadPaths(SimpleXMLElement $container): array + { + if (!isset($container->path) || !$container->path instanceof SimpleXMLElement) { + throw new InvalidArgumentException('expecting at least one element'); + } + + $paths = []; + + /** @var SimpleXMLElement $path */ + foreach ($container->path as $path) { + $paths[] = (string) $path; + } + + if (empty($paths)) { + throw new InvalidArgumentException('expecting at least one element'); + } + + return $paths; + } + + /** + * @throws InvalidArgumentException when encountering unreadable config + */ + public static function load(SimpleXMLElement $config): self + { + if (!isset($config->doctrine) || !$config->doctrine instanceof SimpleXMLElement) { + throw new InvalidArgumentException('expecting subelement'); + } + + $doctrine = $config->doctrine; + + if (isset($doctrine->annotations) && $doctrine->annotations instanceof SimpleXMLElement) { + $paths = self::loadPaths($doctrine->annotations); + $doctrineConfig = Setup::createAnnotationMetadataConfiguration($paths, true); + } elseif (isset($doctrine->yaml) && $doctrine->yaml instanceof SimpleXMLElement) { + $paths = self::loadPaths($doctrine->yaml); + $doctrineConfig = Setup::createYAMLMetadataConfiguration($paths, true); + } elseif (isset($doctrine->xml) && $doctrine->xml instanceof SimpleXMLElement) { + $paths = self::loadPaths($doctrine->xml); + $doctrineConfig = Setup::createXMLMetadataConfiguration($paths, true); + } else { + throw new InvalidArgumentException('expecting one of , , subelements'); + } + + $em = EntityManager::create(['driverClass' => DummyDriver::class], $doctrineConfig); + $em->getMetadataFactory()->setReflectionService(new StaticReflectionService()); + return new self($em); + } + + /** @param class-string $class */ + public function getClassMetadata(string $class): ClassMetadata + { + return $this->em->getClassMetadata($class); + } +} diff --git a/src/DummyDriver.php b/src/DummyDriver.php new file mode 100644 index 0000000..0a3ca74 --- /dev/null +++ b/src/DummyDriver.php @@ -0,0 +1,34 @@ +value->inferredType; + $methodId = ($calledFqClasslikeName ?? $fqClasslikeName) . '::' . ($calledMethodNameLc ?? $methodNameLc); + /** @var Atomic $type */ + foreach ($potentialEntityType->getTypes() as $type) { + if ($type instanceof Atomic\TLiteralClassString) { + /** @var class-string */ + $className = $type->value; + try { + Plugin::doctrine()->getClassMetadata($className); + } catch (CommonMappingException | MappingException $e) { + IssueBuffer::accepts(new InvalidArgument( + 'Argument 1 of ' . $methodId . ' expects entity class, ' + . 'non-entity ' . $type->getKey() . ' given', + new CodeLocation($source, $callArgs[0]) + )); + } + } + } + return null; + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 78c45cf..2a67680 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -1,24 +1,60 @@ loadConfiguration($config)) { + $this->loadHooks($psalm); + } + $stubs = $this->getStubFiles(); foreach ($stubs as $file) { $psalm->addStubFile($file); } } + private function loadHooks(RegistrationInterface $psalm): void + { + foreach ([ + Hooks\EntityManager\Find::class + ] as $class) { + class_exists($class, true); + $psalm->registerHooksFromClass($class); + } + } + /** @return string[] */ private function getStubFiles(): array { return glob(__DIR__ . '/' . '../stubs/*\\.php'); } + + private function loadConfiguration(?SimpleXMLElement $config): bool + { + if (!$config) { + // TODO: add warning + return false; + } + self::$doctrine = DoctrineFacade::load($config); + return true; + } + + public static function doctrine(): DoctrineFacade + { + if (null === self::$doctrine) { + throw new RuntimeException('Doctrine unavailable, this method is not expected to be called'); + } + return self::$doctrine; + } } diff --git a/tests/_support/Helper/Acceptance.php b/tests/_support/Helper/Acceptance.php index 82e6f15..0053e7f 100644 --- a/tests/_support/Helper/Acceptance.php +++ b/tests/_support/Helper/Acceptance.php @@ -1,9 +1,46 @@ getModule('\\' . Psalm\Module::class)->_getConfig('default_file'); + assert(is_string($defaultFile)); + + $dir = dirname($defaultFile) . '/mapping'; + @mkdir($dir, 0755, true); + + $filename = $dir . '/' . str_replace('\\', '.', $class) . '.dcm.xml'; + $this->fs()->writeToFile($filename, $mapping); + } + + private function fs(): Filesystem + { + if (null === $this->fs) { + $fs = $this->getModule('Filesystem'); + if (!$fs instanceof Filesystem) { + throw new ModuleRequireException($this, 'Needs Filesystem module'); + } + $this->fs = $fs; + } + return $this->fs; + } } diff --git a/tests/acceptance/EntityManager.feature b/tests/acceptance/EntityManager.feature index 7f37b8a..6948ec7 100644 --- a/tests/acceptance/EntityManager.feature +++ b/tests/acceptance/EntityManager.feature @@ -12,17 +12,35 @@ Feature: EntityManager - + + + + mapping + + + """ + And I have the following mapping for "C" + """ + + + + + + """ And I have the following code preamble """ getRepository(I::class)); + atan(em()->getRepository(C::class)); """ When I run Psalm Then I see these errors | Type | Message | - | InvalidArgument | Argument 1 of atan expects float, Doctrine\ORM\EntityRepository provided | + | InvalidArgument | Argument 1 of atan expects float, Doctrine\ORM\EntityRepository provided | And I see no other errors @EntityManager::getRepository @@ -60,12 +78,24 @@ Feature: EntityManager Scenario: Finding an entity Given I have the following code """ - atan(em()->find(I::class, 1)); + atan(em()->find(C::class, 1)); """ When I run Psalm Then I see these errors | Type | Message | - | InvalidArgument | Argument 1 of atan expects float, null\|I provided | + | InvalidArgument | Argument 1 of atan expects float, null\|C provided | + And I see no other errors + + @EntityManager::find + Scenario: Finding a non-entity + Given I have the following code + """ + em()->find(InvalidArgumentException::class, 1); + """ + When I run Psalm + Then I see these errors + | Type | Message | + | InvalidArgument | Argument 1 of Doctrine\ORM\EntityManager::find expects entity class, non-entity class-string(InvalidArgumentException) given | And I see no other errors @EntityManager::find @@ -84,12 +114,12 @@ Feature: EntityManager Scenario: Getting a reference Given I have the following code """ - atan(em()->getReference(I::class, 1)); + atan(em()->getReference(C::class, 1)); """ When I run Psalm Then I see these errors | Type | Message | - | InvalidArgument | Argument 1 of atan expects float, I provided | + | InvalidArgument | Argument 1 of atan expects float, C provided | And I see no other errors @EntityManager::getReference