Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ parameters:
editorUrlTitle: null
errorFormat: null
__validate: true
phpStormMeta:
metaPaths:
- '.phpstorm.meta.php'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this path mean?

  1. How does PhpStorm search for these files? Can they always be expected to be alongside composer.json?
  2. Is the file always named the same?

I'm clinging towards only looking at the project file for now, I've never seen a public package to include this file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's an example of one in mockery/mockery
https://github.com/mockery/mockery/blob/master/.phpstorm.meta.php

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to PHPStorm’s documentation (link), I would say it scans the entire project for files and directories with this name, and parses them all. This implementation only looks for it next to composer.json, but that is not the only place where PHPStorm would look.

I found the following packages with this file in my projects: nesbot/carbon, phpunit/phpunit, mockery/mockery, league/commonmark, nette/di, nette/utils. There are also several files with this name inside jetbrains/phpstorm-stubs, and there’s thomas-schulz/symfony-meta that provides these definitions for various Symfony components.

I think that searching the entire project (or at least vendor/) would give the closest experience to PHPStorm. But I can see arguments against this search as well, e.g. performance, metadata conflict resolution.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the way forward is to describe behaviour with advanced PHPDoc features which there are plenty of already, and PhpStorm starts supporting them too. That's why I think we don't need to look in vendor.

I think we should look for these files in %composerAutoloaderProjectPaths%. These are the paths where PHPStan looks for composer.json files.

Also, to be honest - I'm not sure I want to merge and maintain these 800 lines of code if the decided way forward for the community is advanced and already implemented PHPDoc features like conditional types, assert tags etc.

Maybe I can give you the needed hooks so that you can register these extensions in your own package instead?

Thank you for understanding.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can see your reasoning about leaving this out. Adding a hook in LazyDynamicReturnTypeExtensionRegistryProvider to register these extensions would be enough, then I could leave this in a separate package. Even without such a hook, a separate package would still work, the users would just have to declare those extensions manually.


extensions:
rules: PHPStan\DependencyInjection\RulesExtension
Expand Down Expand Up @@ -408,6 +411,9 @@ parametersSchema:
editorUrl: schema(string(), nullable())
editorUrlTitle: schema(string(), nullable())
errorFormat: schema(string(), nullable())
phpStormMeta: structure([
metaPaths: listOf(string())
])

# irrelevant Nette parameters
debugMode: bool()
Expand Down Expand Up @@ -2061,6 +2067,23 @@ services:
singleReflectionFile: %singleReflectionFile%
autowired: false

# PhpStorm meta support
cachedParserForMeta:
class: PHPStan\Parser\CachedParser
arguments:
originalParser: @currentPhpVersionRichParser
cachedNodesByStringCountMax: %cache.nodesByStringCountMax%
autowired: false
-
class: PHPStan\PhpStormMeta\MetaFileParser
arguments:
parser: @cachedParserForMeta
metaPaths: %phpStormMeta.metaPaths%
-
class: PHPStan\PhpStormMeta\OverrideParser
-
class: PHPStan\PhpStormMeta\ReturnTypeExtensionGenerator

# Error formatters

-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,40 @@
use PHPStan\Broker\Broker;
use PHPStan\Broker\BrokerFactory;
use PHPStan\DependencyInjection\Container;
use PHPStan\PhpStormMeta\MetaFileParser;
use PHPStan\PhpStormMeta\ReturnTypeExtensionGenerator;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicReturnTypeExtensionRegistry;
use function array_push;

class LazyDynamicReturnTypeExtensionRegistryProvider implements DynamicReturnTypeExtensionRegistryProvider
{

private ?DynamicReturnTypeExtensionRegistry $registry = null;

public function __construct(private Container $container)
public function __construct(private Container $container, private MetaFileParser $phpStormMetaParser, private ReturnTypeExtensionGenerator $phpStormMetaExtensionGenerator)
{
}

public function getRegistry(): DynamicReturnTypeExtensionRegistry
{
if ($this->registry === null) {
$dynamicMethodReturnTypeExtensions = $this->container->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG);
$dynamicStaticMethodReturnTypeExtensions = $this->container->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG);
$dynamicFunctionReturnTypeExtensions = $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG);

$phpStormMetaExtensions = $this->phpStormMetaExtensionGenerator->generateExtensionsBasedOnMeta($this->phpStormMetaParser->getMeta());

array_push($dynamicMethodReturnTypeExtensions, ...$phpStormMetaExtensions->nonStaticMethodExtensions);
array_push($dynamicStaticMethodReturnTypeExtensions, ...$phpStormMetaExtensions->staticMethodExtensions);
array_push($dynamicFunctionReturnTypeExtensions, ...$phpStormMetaExtensions->functionExtensions);

$this->registry = new DynamicReturnTypeExtensionRegistry(
$this->container->getByType(Broker::class),
$this->container->getByType(ReflectionProvider::class),
$this->container->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG),
$this->container->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG),
$this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG),
$dynamicMethodReturnTypeExtensions,
$dynamicStaticMethodReturnTypeExtensions,
$dynamicFunctionReturnTypeExtensions,
);
}

Expand Down
55 changes: 55 additions & 0 deletions src/PhpStormMeta/FunctionReturnTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpStormMeta;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Type;
use function array_map;
use function count;
use function in_array;
use function strtolower;

class FunctionReturnTypeResolver implements DynamicFunctionReturnTypeExtension
{

/** @var list<string> */
private readonly array $functionNames;

/**
* @param list<string> $functionNames
*/
public function __construct(
private readonly TypeFromMetaResolver $metaResolver,
array $functionNames,
)
{
$this->functionNames = array_map(strtolower(...), $functionNames);
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
$functionName = strtolower($functionReflection->getName());

return in_array($functionName, $this->functionNames, true);
}

public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): ?Type
{
$functionName = strtolower($functionReflection->getName());
$args = $functionCall->getArgs();

if (count($args) > 0) {
return $this->metaResolver->resolveReferencedType($functionName, ...$args);
}

return null;
}

}
127 changes: 127 additions & 0 deletions src/PhpStormMeta/MetaFileParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpStormMeta;

use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Expression;
use PHPStan\File\FileHelper;
use PHPStan\Parser\CachedParser;
use PHPStan\PhpStormMeta\TypeMapping\CallReturnOverrideCollection;
use PHPStan\PhpStormMeta\TypeMapping\FunctionCallTypeOverride;
use PHPStan\PhpStormMeta\TypeMapping\MethodCallTypeOverride;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
use function array_key_exists;
use function count;
use function file_exists;
use function is_dir;

class MetaFileParser
{

private readonly CallReturnOverrideCollection $parsedMeta;

/** @var array<string, string> */
private array $parsedMetaPaths = [];

private bool $metaParsed;

/**
* @param list<string> $metaPaths
*/
public function __construct(
private readonly CachedParser $parser,
private readonly OverrideParser $overrideParser,
private readonly FileHelper $fileHelper,
private readonly array $metaPaths,
)
{
$this->parsedMeta = new CallReturnOverrideCollection();
$this->metaParsed = $this->metaPaths === [];
}

public function getMeta(): CallReturnOverrideCollection
{
if (!$this->metaParsed) {
$this->parseMeta($this->parsedMeta, ...$this->metaPaths);
$this->metaParsed = true;
}

return $this->parsedMeta;
}

private function parseMeta(
CallReturnOverrideCollection $resultCollector,
string ...$metaPaths,
): void
{
$metaFiles = [];

/** @var array<string, string> $newlyParsedMetaPaths */
$newlyParsedMetaPaths = [];
foreach ($metaPaths as $relativePath) {
if (array_key_exists($relativePath, $this->parsedMetaPaths)) {
continue;
}

$path = $this->fileHelper->absolutizePath($relativePath);

if (is_dir($path)) {
$finder = (new Finder())->in($path)->files()->ignoreDotFiles(false);
foreach ($finder as $fileInfo) {
$metaFiles [] = $fileInfo->getPathname();
}
} elseif (file_exists($path)) {
$singleFile = new SplFileInfo($path);
$metaFiles[] = $singleFile->getPathname();
}

$newlyParsedMetaPaths[$relativePath] = $path;
}

foreach ($metaFiles as $metaFile) {
$stmts = $this->parser->parseFile($metaFile);

foreach ($stmts as $topStmt) {
if (!($topStmt instanceof Stmt\Namespace_)) {
continue;
}

if ($topStmt->name === null || $topStmt->name->toString() !== 'PHPSTORM_META') {
continue;
}

foreach ($topStmt->stmts as $metaStmt) {
if (!($metaStmt instanceof Expression)
|| !($metaStmt->expr instanceof FuncCall)
|| !($metaStmt->expr->name instanceof Name)
|| $metaStmt->expr->name->toString() !== 'override'
) {
continue;
}

$args = $metaStmt->expr->getArgs();
if (count($args) < 2) {
continue;
}

[$callableArg, $overrideArg] = $args;
$parsedOverride = $this->overrideParser->parseOverride($callableArg, $overrideArg);

if ($parsedOverride instanceof MethodCallTypeOverride) {
$resultCollector->addMethodCallOverride($parsedOverride);
} elseif ($parsedOverride instanceof FunctionCallTypeOverride) {
$resultCollector->addFunctionCallOverride($parsedOverride);
}
}
}

foreach ($newlyParsedMetaPaths as $relativePath => $path) {
$this->parsedMetaPaths[$relativePath] = $path;
}
}
}

}
63 changes: 63 additions & 0 deletions src/PhpStormMeta/NonStaticMethodReturnTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpStormMeta;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use function array_map;
use function count;
use function in_array;
use function sprintf;
use function strtolower;

class NonStaticMethodReturnTypeResolver implements DynamicMethodReturnTypeExtension
{

/** @var list<string> */
private readonly array $methodNames;

/**
* @param list<string> $methodNames
*/
public function __construct(
private readonly TypeFromMetaResolver $metaResolver,
private readonly string $className,
array $methodNames,
)
{
$this->methodNames = array_map(strtolower(...), $methodNames);
}

public function getClass(): string
{
return $this->className;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
$methodName = strtolower($methodReflection->getName());

return in_array($methodName, $this->methodNames, true);
}

public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
): ?Type
{
$methodName = strtolower($methodReflection->getName());
$args = $methodCall->getArgs();

if (count($args) > 0) {
$fqn = sprintf('%s::%s', $this->className, $methodName);
return $this->metaResolver->resolveReferencedType($fqn, ...$args);
}

return null;
}

}
Loading