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:

  1. Specify all possible types of returned objects:

    @return AbstractPlugin|IPlugin|YourPlugin
    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.

  2. Use assert

    $schoolUsersPlugin = Core::getInstance()->getPluginInstance('Users');
    assert($usersPlugin instanceof UsersPlugin);
    $companies = $usersPlugin->loadCompanies();
    I think adding assert calls throughout the code is not a very pleasant task, and the readability of the code won't be very good...

  3. 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.

  4. Write your own plugin for Phan that will solve this problem.

Creating a Plugin for Phan

  1. Add the plugin to your configuration:

    'plugins' => [
       '.phan/plugins/CorePlugin.php'
    ],
  2. 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();