internal/reflection_completeness_check.php

Summary

Maintainability
B
4 hrs
Test Coverage
#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
 * This checks that the function signatures are complete.
 * TODO: Expand to checking classes (methods, and properties)
 * TODO: Refactor the scripts in internal/ to reuse more code.
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
 * @phan-file-suppress PhanPluginRemoveDebugAny
 */

require_once dirname(__DIR__) . '/vendor/autoload.php';

use Phan\Config;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\FQSEN\FullyQualifiedMethodName;
use Phan\Language\UnionType;

/**
 * Checks if Phan has internal signatures for all elements of PHP modules
 * (e.g. php-ast, redis)
 * that the running PHP binary has Reflection information for.
 */
class ReflectionCompletenessCheck
{
    const EXCLUDED_FUNCTIONS = [
        'zend_test_array_return' => true,
        'zend_test_nullable_array_return' => true,
        'zend_test_void_return' => true,
        'zend_create_unterminated_string' => true,
        'zend_terminate_string' => true,
        'zend_leak_bytes' => true,
        'zend_leak_variable' => true,
    ];

    const EXCLUDED_CLASS_NAMES = [
        '_zendtestclass' => true,
        '_zendtestchildclass' => true,
        '_zendtestclassalias' => true,
    ];

    private static function checkForUndeclaredTypeFunctions(): void
    {
        foreach (get_defined_functions() as $unused_ext => $group) {
            foreach ($group as $function_name) {
                $reflection_function = new ReflectionFunction($function_name);
                if (!$reflection_function->isInternal()) {
                    continue;
                }
                if (array_key_exists($function_name, self::EXCLUDED_FUNCTIONS)) {
                    continue;
                }
                try {
                    $fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($function_name);
                } catch (Exception $e) {
                    echo "Obtained invalid reflection function '$function_name' from reflection, ignoring: " . $e->getMessage() . "\n";
                    continue;
                }
                $map_list = UnionType::internalFunctionSignatureMapForFQSEN($fqsen);
                if (!$map_list) {
                    $stub_signature = self::stubSignatureToString(self::createStubSignature($reflection_function));
                    echo "Missing signatures for $function_name : should be $stub_signature\n";
                }
            }
        }
    }

    /**
     * @return Generator<string,ReflectionClass>
     */
    private static function getInternalClasses(): Generator
    {
        $classes = array_merge(
            get_declared_classes(),
            get_declared_interfaces(),
            get_declared_traits()
        );
        sort($classes);
        foreach ($classes as $class_name) {
            if (array_key_exists(strtolower($class_name), self::EXCLUDED_CLASS_NAMES)) {
                continue;
            }
            $reflection_class = (new ReflectionClass($class_name));
            if (!$reflection_class->isInternal()) {
                continue;
            }
            yield $class_name => $reflection_class;
        }
    }

    private static function checkForUndeclaredTypeMethods(): void
    {
        foreach (self::getInternalClasses() as $class_name => $reflection_class) {
            foreach ($reflection_class->getMethods() as $reflection_method) {
                if ($reflection_method->class !== $class_name) {
                    continue;
                }
                $method_name = $reflection_method->getName();
                // @phan-suppress-next-line PhanThrowTypeAbsentForCall should not happen for FQSENs of internal modules
                $method_fqsen = FullyQualifiedMethodName::fromFullyQualifiedString($class_name . '::' . $method_name);
                $map_list = UnionType::internalFunctionSignatureMapForFQSEN($method_fqsen);
                if (!$map_list) {
                    $stub_signature = self::stubSignatureToString(self::createStubSignature($reflection_method));
                    echo "Missing signatures for $method_fqsen : Should be $stub_signature\n";
                }
            }
        }
    }

    /**
     * @return string[]
     */
    public static function createStubSignature(ReflectionFunctionAbstract $reflection_method): array
    {
        $signature = [];
        $signature[] = (string)UnionType::fromReflectionType($reflection_method->getReturnType());
        foreach ($reflection_method->getParameters() as $parameter) {
            $key = $parameter->getName();
            if ($parameter->isVariadic()) {
                $key = "...$key";
            } elseif ($parameter->isOptional()) {
                $key = "$key=";
            }
            if ($parameter->isPassedByReference()) {
                $key = "&$key";
            }
            $type = (string)UnionType::fromReflectionType($reflection_method->getReturnType());
            $signature[$key] = $type;
        }
        return $signature;
    }

    /**
     * @param array<int|string,string> $stub Phan's internal signature info
     */
    private static function stubSignatureToString(array $stub): string
    {
        $result = "['$stub[0]'";
        unset($stub[0]);
        foreach ($stub as $key => $value) {
            $result .= ", '$key'=>'$value'";
        }

        $result .= ']';
        return $result;
    }

    private static function checkForUndeclaredTypeProperties(): void
    {
        foreach (self::getInternalClasses() as $class_name => $reflection_class) {
            $map_for_class = UnionType::internalPropertyMapForClassName($class_name);
            foreach ($reflection_class->getProperties(ReflectionProperty::IS_PUBLIC) as $reflection_property) {
                $property_name = $reflection_property->getName();
                if (!array_key_exists($property_name, $map_for_class)) {
                    printf(
                        "Failed to find signature for property %s%s%s.\n",
                        $class_name,
                        $reflection_property->isStatic() ? '::$' : '->',
                        $property_name
                    );
                }
            }
        }
    }

    /**
     * Run all checks that the function signatures are complete.
     */
    public static function main(): void
    {
        Config::setValue('target_php_version', sprintf("%d.%d", PHP_MAJOR_VERSION, PHP_MINOR_VERSION));
        error_reporting(E_ALL);

        self::checkForUndeclaredTypeFunctions();
        self::checkForUndeclaredTypeMethods();
        self::checkForUndeclaredTypeProperties();
    }
}
ReflectionCompletenessCheck::main();