Phan
Phan is one of the popular static code analyzers for PHP 7+.
Solving Phan Static Analyzer Problems by Creating Plugins
The basic description of how to write plugins for Phan can be found in the official documentation, but it's often unclear how to solve specific problems in your project.
PhanUndeclaredMethod
Let's consider a problem where you have a factory or factory method that returns an object instance based on dynamic class name formation, for example:
class Core
{
...
/**
* @param string $pluginName
* @return AbstractPlugin|IPlugin
*/
public function getPluginInstance(string $pluginName)
{
$classPostfix = 'Plugin';
$className = $pluginName.$classPostfix;
...
$pluginInstance = new $className();
...
return $pluginInstance;
}
...
}
In most cases, the analyzer will consider that the return type is specified directly in the return type or PHPDoc. Usually, following SOLID principles, you'll specify an interface or an abstract class, or sometimes nothing at all, as is often the case in loosely typed languages. And when running the static analyzer Phan, you get an error:
PhanUndeclaredMethod Call to undeclared method \{YourInterfaceOrAbstractClassName}::{YourMethodName}
For example:
Core::getInstance()->getPluginInstance('Users')->loadCompanies();
XXXXXX.php:84 PhanUndeclaredMethod Call to undeclared method \AbstractPlugin::loadCompanies
There are at least four solutions:
-
Specify all possible types of returned objects:
This option is suitable if you have access or the ability to change the PHPDoc for this method, but if it's a library, this option won't work.@return AbstractPlugin|IPlugin|YourPlugin
-
Use
assert
I think adding$schoolUsersPlugin = Core::getInstance()->getPluginInstance('Users'); assert($usersPlugin instanceof UsersPlugin); $companies = $usersPlugin->loadCompanies();
assert
calls throughout the code is not a very pleasant task, and the readability of the code won't be very good... -
Inherit from Core and override
getPluginInstance
and add to the PHPDoc as in option 1, but this option is also poor in terms of both code readability and may cause other problems like a large number of inheritances or calls. -
Write your own plugin for Phan that will solve this problem.
Creating a Plugin for Phan
-
Add the plugin to your configuration:
'plugins' => [ '.phan/plugins/CorePlugin.php' ],
-
Add
.phan/plugins/CorePlugin.php
declare(strict_types=1);
use ast\Node;
use Phan\CodeBase;
use Phan\Language\Context;
use Phan\Language\Element\Clazz;
use Phan\Language\UnionType;
use Phan\Language\Element\Method;
use Phan\Language\Element\Property;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
use Phan\Language\Type;
class CorePlugin extends PluginV3 implements AnalyzeFunctionCallCapability
{
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
$closureCallback = static function (
CodeBase $codeBase,
Context $context,
Method $method,
array $args,
?Node $node
): void {
$pluginClassName = "\\".$args[0].'Plugin';
$realReturnType = UnionType::fromStringInContext($pluginClassName, $context, Type::FROM_TYPE);
$method->setRealReturnType($realReturnType);
$analyzer = static function (CodeBase $codeBase, Context $context, FunctionInterface $function, array $args) use ($realReturnType) : UnionType {
return $realReturnType;
};
$method->setDependentReturnTypeClosure($analyzer);
};
return [
'\\Core::getPluginInstance' => $closureCallback,
];
}
}
return new CorePlugin();