internal/lib/IncompatibleSignatureDetectorBase.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

declare(strict_types=1);

use Phan\CodeBase;
use Phan\Exception\FQSENException;
use Phan\Memoize;

define('ORIGINAL_SIGNATURE_PATH', dirname(__DIR__, 2) . '/src/Phan/Language/Internal/FunctionSignatureMap.php');
define('ORIGINAL_FUNCTION_DOCUMENTATION_PATH', dirname(__DIR__, 2) . '/src/Phan/Language/Internal/FunctionDocumentationMap.php');
define('ORIGINAL_CONSTANT_DOCUMENTATION_PATH', dirname(__DIR__, 2) . '/src/Phan/Language/Internal/ConstantDocumentationMap.php');
define('ORIGINAL_CLASS_DOCUMENTATION_PATH', dirname(__DIR__, 2) . '/src/Phan/Language/Internal/ClassDocumentationMap.php');
define('ORIGINAL_PROPERTY_DOCUMENTATION_PATH', dirname(__DIR__, 2) . '/src/Phan/Language/Internal/PropertyDocumentationMap.php');

/**
 * Implementations of this can be used to check Phan's function signature map.
 *
 * They do the following:
 *
 * - Load signatures from an external source
 * - Compare the signatures against Phan's to report incomplete or inaccurate signatures of Phan itself (or the external signature)
 *
 * TODO: could extend this to properties (the use of properties in extensions is rare).
 * TODO: Fix zookeeperconfig in phpdoc-en svn repo
 *
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
 * @phan-file-suppress PhanPluginRemoveDebugAny only used internally
 */
abstract class IncompatibleSignatureDetectorBase
{
    use Memoize;

    const FUNCTIONLIKE_BLACKLIST = '@' .
        '(^_*PHPSTORM)|PS_UNRESERVE_PREFIX|' .
        '(^(ereg|expression|getsession|hrtime_|imageps|mssql_|mysql_|split|sql_regcase|sybase|xmldiff_))|' .
        '(^closure_)|' .  // Phan's representation of a closure
        '\.|,' .  // a literal `.` or `,`
        '@i';

    /** @var array<string,string> maps aliases to originals - only set for xml parser */
    protected $aliases = [];

    /**
     * @return void (does not return)
     */
    protected static function printUsageAndExit(int $exit_code = 1): void
    {
        global $argv;
        $program_name = $argv[0];
        $msg = <<<EOT
Usage: $program_name command [...args]
  $program_name sort
    Sort the internal signature map in place

  $program_name help
    Print this help message

  $program_name update-stubs path/to/stubs-dir
    Update any of Phan's missing signatures based on a checkout of a directory with stubs for extensions.

  $program_name update-real-stubs path/to/php-src-or-ext-dir
    Print information about where *.stub.php files conflict with Phan's stub files.

  $program_name update-real-param-names path/to/php-src-or-ext-dir
    Update param names of functions (without alternates) based on *.stub.php files in a directory.

  $program_name update-svn path/to/phpdoc_svn_dir
    Update any of Phan's missing signatures based on a checkout of the docs.php.net source repo.

    phpdoc_svn_dir can be checked out via 'svn checkout https://svn.php.net/repository/phpdoc/modules/doc-en phpdoc-en' (subversion must be installed)
    (and updated via 'svn update')
    see http://doc.php.net/tutorial/structure.php

  $program_name update-descriptions-svn path/to/phpdoc_svn_dir
    Update Phan's descriptions for functions/methods based on the docs.php.net source repo.

  $program_name update-descriptions-stubs path/to/stubs_dir
    Update Phan's descriptions for functions/methods based on a checkout of a directory with stubs for extensions.

  $program_name compare-named-parameters path/to/stubs_dir path/to/phpdoc_svn_dir

    Compares the parameter names of the functions/methods in stub files used by php-src (or an extension) and the
    official documentation from the repo used to generate php.net function documentation.

    Treats any file ending in .php as a stub file.

EOT;
        fwrite(STDERR, $msg);
        exit($exit_code);
    }

    /**
     * Update markdown summaries of elements with the docs from php.net
     */
    protected function updatePHPDocSummaries(): void
    {
        $this->updatePHPDocPropertySummaries();
        $this->updatePHPDocFunctionSummaries();
        $this->updatePHPDocConstantSummaries();
        $this->updatePHPDocClassSummaries();
    }

    /**
     * Merge signatures from $new into $old if the case-insensitive signatures don't already exist in $old.
     *
     * Returns the resulting sorted signature map.
     *
     * @template T
     * @param array<string,T> $old
     * @param array<string,T> $new
     * @return array<string,T>
     */
    public static function mergeSignatureMaps(array $old, array $new): array
    {
        $normalized_old = [];
        foreach ($old as $key => $_) {
            // NOTE: This won't work for the name part of constants, but low importance.
            $normalized_old[strtolower($key)] = true;
        }
        foreach ($new as $key => $value) {
            if (isset($normalized_old[strtolower($key)])) {
                continue;
            }
            $old[$key] = $value;
        }
        self::sortSignatureMap($old);
        return $old;
    }

    protected function updatePHPDocPropertySummaries(): void
    {
        $old_property_documentation = $this->readPropertyDocumentationMap();
        $new_property_documentation = $this->getAvailablePropertyPHPDocSummaries();
        $new_property_documentation = self::mergeSignatureMaps($old_property_documentation, $new_property_documentation);

        $new_property_documentation_path = ORIGINAL_PROPERTY_DOCUMENTATION_PATH . '.new';
        static::info("Saving modified property descriptions to $new_property_documentation_path\n");
        static::savePropertyDocumentationMap($new_property_documentation_path, $new_property_documentation);
    }

    /**
     * Returns short markdown summaries of property signatures
     *
     * @return array<string,string>
     */
    abstract protected function getAvailablePropertyPHPDocSummaries(): array;

    protected function updatePHPDocFunctionSummaries(): void
    {
        $old_function_documentation = $this->readFunctionDocumentationMap();
        $new_function_documentation = $this->getAvailableMethodPHPDocSummaries();
        $new_function_documentation = self::mergeSignatureMaps($old_function_documentation, $new_function_documentation);

        $new_function_documentation_path = ORIGINAL_FUNCTION_DOCUMENTATION_PATH . '.new';
        static::info("Saving modified function descriptions to $new_function_documentation_path\n");
        static::saveFunctionDocumentationMap($new_function_documentation_path, $new_function_documentation);
    }

    /**
     * Returns short markdown summaries of function and method signatures
     *
     * @return array<string,string>
     */
    abstract protected function getAvailableMethodPHPDocSummaries(): array;

    protected function updatePHPDocConstantSummaries(): void
    {
        $old_constant_documentation = $this->readConstantDocumentationMap();
        $new_constant_documentation = $this->getAvailableConstantPHPDocSummaries();
        $new_constant_documentation = self::mergeSignatureMaps($old_constant_documentation, $new_constant_documentation);

        self::sortSignatureMap($new_constant_documentation);

        $new_constant_documentation_path = ORIGINAL_CONSTANT_DOCUMENTATION_PATH . '.new';
        static::info("Saving modified constant descriptions to $new_constant_documentation_path\n");
        static::saveConstantDocumentationMap($new_constant_documentation_path, $new_constant_documentation);
    }

    /**
     * @return array<string,string>
     */
    abstract protected function getAvailableConstantPHPDocSummaries(): array;

    protected function updatePHPDocClassSummaries(): void
    {
        $old_class_documentation = $this->readClassDocumentationMap();
        $new_class_documentation = $this->getAvailableClassPHPDocSummaries();
        $new_class_documentation = self::mergeSignatureMaps($old_class_documentation, $new_class_documentation);

        self::sortSignatureMap($new_class_documentation);

        $new_class_documentation_path = ORIGINAL_CLASS_DOCUMENTATION_PATH . '.new';
        static::info("Saving modified class descriptions to $new_class_documentation_path\n");
        static::saveClassDocumentationMap($new_class_documentation_path, $new_class_documentation);
    }

    /**
     * @return array<string,string>
     */
    abstract protected function getAvailableClassPHPDocSummaries(): array;

    /**
     * Update the function/method signatures using the subclass's source of type information
     */
    public function updateFunctionSignatures(): void
    {
        $phan_signatures = static::readSignatureMap();
        $new_signatures = [];
        foreach ($phan_signatures as $method_name => $arguments) {
            if (strpos($method_name, "'") !== false || isset($phan_signatures["$method_name'1"])) {
                // Don't update functions/methods with alternate
                $new_signatures[$method_name] = $arguments;
                continue;
            }
            try {
                $new_signatures[$method_name] = static::updateSignature($method_name, $arguments);
            } catch (FQSENException | InvalidArgumentException $e) {
                static::info("Skipping invalid signature for $method_name: $e\n");
                $new_signatures[$method_name] = $arguments;
            }
        }
        $new_signature_path = ORIGINAL_SIGNATURE_PATH . '.new';
        static::info("Saving modified function signatures to $new_signature_path (updating param and return types)\n");
        static::saveSignatureMap($new_signature_path, $new_signatures);
    }

    /**
     * @param array<mixed,string> $arguments_from_phan
     * @return array<mixed,string>
     */
    protected function updateSignature(string $function_like_name, array $arguments_from_phan): array
    {
        $return_type = $arguments_from_phan[0];
        $arguments_from_svn = $this->parseFunctionLikeSignature($function_like_name);
        if (is_null($arguments_from_svn)) {
            return $arguments_from_phan;
        }
        if ($return_type === '') {
            $svn_return_type = $arguments_from_svn[0] ?? '';
            if ($svn_return_type !== '') {
                static::debug("A better Phan return type for $function_like_name is " . $svn_return_type . "\n");
                $arguments_from_phan[0] = $svn_return_type;
            }
        }
        $param_index = 0;
        $arguments_from_svn_list = array_values($arguments_from_svn);  // keys are 0, 1, 2,...
        $arguments_from_svn_names = array_keys($arguments_from_svn);  // keys are 0, 1, 2,...
        foreach ($arguments_from_phan as $param_name => $param_type_from_phan) {
            if ($param_name === 0) {
                continue;
            }
            $param_index++;

            // after incrementing param_index
            $param_from_svn_name = $arguments_from_svn_names[$param_index] ?? null;
            if (is_string($param_from_svn_name)) {
                $param_name = preg_replace('/^(rw|r|w)_/', '', trim((string)$param_name, '.=&'));
                $param_from_svn_name = trim($param_from_svn_name, '.=&');
                if ($param_from_svn_name !== $param_name) {
                    echo "Name mismatch for $function_like_name: #$param_index is \$$param_name in Phan, \$$param_from_svn_name in source\n";
                }
            }
            if ($param_type_from_phan !== '') {
                continue;
            }
            $param_from_svn = $arguments_from_svn_list[$param_index] ?? '';
            if ($param_from_svn !== '') {
                static::debug("A better Phan param type for $function_like_name (for param #$param_index called \$$param_name) is $param_from_svn\n");
                $arguments_from_phan[$param_name] = $param_from_svn;
            }
        }
        // TODO: Update param types
        // @see IncompatibleRealStubsDetector
        return $arguments_from_phan;
    }

    /**
     * Save a file with suffix .extra_signatures using information from the type source
     * that is not in Phan's signature map.
     */
    public function addMissingFunctionLikeSignatures(): void
    {
        $phan_signatures = static::readSignatureMap();
        $this->addMissingGlobalFunctionSignatures($phan_signatures);
        $this->addMissingMethodSignatures($phan_signatures);
        $new_signature_path = ORIGINAL_SIGNATURE_PATH . '.extra_signatures';
        static::info("Saving function signatures with added missing signatures to $new_signature_path (updating param and return types)\n");
        static::sortSignatureMap($phan_signatures);
        static::saveSignatureMap($new_signature_path, $phan_signatures);
    }

    /**
     * @param array<string,array<int|string,string>> &$phan_signatures
     */
    protected function addMissingGlobalFunctionSignatures(array &$phan_signatures): void
    {
        $phan_signatures_lc = static::getLowercaseSignatureMap($phan_signatures);
        foreach ($this->getAvailableGlobalFunctionSignatures() as $function_name => $method_signature) {
            if (isset($phan_signatures_lc[strtolower($function_name)])) {
                continue;
            }
            if (\preg_match(static::FUNCTIONLIKE_BLACKLIST, $function_name)) {
                continue;
            }
            $phan_signatures[$function_name] = $method_signature;
        }
    }

    /**
     * @return array<string,array<int|string,string>>
     */
    abstract public function getAvailableGlobalFunctionSignatures(): array;

    /**
     * @param array<string,array<int|string,string>> &$phan_signatures
     */
    protected function addMissingMethodSignatures(array &$phan_signatures): void
    {
        $phan_signatures_lc = static::getLowercaseSignatureMap($phan_signatures);
        foreach ($this->getAvailableMethodSignatures() as $method_name => $method_signature) {
            if (isset($phan_signatures_lc[strtolower($method_name)])) {
                continue;
            }
            if (\preg_match(static::FUNCTIONLIKE_BLACKLIST, $method_name)) {
                continue;
            }
            $phan_signatures[$method_name] = $method_signature;
        }
    }

    /**
     * @return array<string,array<int|string,string>>
     */
    abstract public function getAvailableMethodSignatures(): array;

    /**
     * @param array<string,array<int|string,string>> $phan_signatures
     * @return array<string,array<int|string,string>>
     */
    protected static function getLowercaseSignatureMap(array $phan_signatures): array
    {
        $phan_signatures_lc = [];
        foreach ($phan_signatures as $key => $signature) {
            $phan_signatures_lc[\strtolower($key)] = $signature;
        }
        return $phan_signatures_lc;
    }
    /**
     * @return ?array<mixed,string>
     * @throws InvalidArgumentException
     */
    public function parseFunctionLikeSignature(string $method_name): ?array
    {
        if (isset($this->aliases[$method_name])) {
            $method_name = $this->aliases[$method_name];
        }
        if (strpos($method_name, '::') !== false) {
            $parts = \explode('::', $method_name);
            if (\count($parts) !== 2) {
                throw new InvalidArgumentException("Wrong number of parts in $method_name");
            }

            return $this->parseMethodSignature($parts[0], $parts[1]);
        }
        return $this->parseFunctionSignature($method_name);
    }

    /** @return ?array<mixed,string> */
    abstract public function parseMethodSignature(string $class, string $method): ?array;

    /** @return ?array<mixed,string> */
    abstract public function parseFunctionSignature(string $function_name): ?array;

    /**
     * @param string $msg @phan-unused-param
     * @suppress PhanPluginUseReturnValueNoopVoid implementation is usually commented out
     */
    protected static function debug(string $msg): void
    {
        // uncomment the below line to see debug output
        // fwrite(STDERR, $msg);
    }

    protected static function info(string $msg): void
    {
        // comment out the below line to hide debug output
        fwrite(STDERR, $msg);
    }

    /**
     * @param array<string,mixed> &$phan_signatures
     */
    public static function sortSignatureMap(array &$phan_signatures): void
    {
        uksort($phan_signatures, static function (string $a, string $b): int {
            $a = strtolower(str_replace("'", "\x0", $a));
            $b = strtolower(str_replace("'", "\x0", $b));
            return $a <=> $b;
        });
    }

    /** @return array<string,array<int|string,string>> */
    public static function readSignatureMap(): array
    {
        return require(ORIGINAL_SIGNATURE_PATH);
    }

    /**
     * @throws RuntimeException if the file could not be read
     */
    public static function readSignatureHeader(?string $path = null): string
    {
        return self::readArrayFileHeader($path ?? ORIGINAL_SIGNATURE_PATH);
    }

    /**
     * @throws RuntimeException if the file could not be read
     */
    public static function readFunctionDocumentationHeader(): string
    {
        return self::readArrayFileHeader(ORIGINAL_FUNCTION_DOCUMENTATION_PATH);
    }

    /**
     * @throws RuntimeException if the file could not be read
     */
    public static function readConstantDocumentationHeader(): string
    {
        return self::readArrayFileHeader(ORIGINAL_CONSTANT_DOCUMENTATION_PATH);
    }

    /**
     * @throws RuntimeException if the file could not be read
     */
    public static function readPropertyDocumentationHeader(): string
    {
        return self::readArrayFileHeader(ORIGINAL_PROPERTY_DOCUMENTATION_PATH);
    }

    /**
     * @throws RuntimeException if the file could not be read
     */
    public static function readClassDocumentationHeader(): string
    {
        return self::readArrayFileHeader(ORIGINAL_CLASS_DOCUMENTATION_PATH);
    }

    /**
     * @throws RuntimeException if the file could not be read
     */
    private static function readArrayFileHeader(string $path): string
    {
        $fin = fopen($path, 'r');
        if (!$fin) {
            throw new RuntimeException("Failed to start reading header\n");
        }
        $header = '';
        try {
            while (($line = fgets($fin)) !== false) {
                if (preg_match('/^\s*return\b/', $line)) {
                    return $header;
                }
                $header .= $line;
            }
        } finally {
            fclose($fin);
        }
        return '';
    }

    /**
     * @return array<string,string>
     */
    public static function readPropertyDocumentationMap(): array
    {
        return require(ORIGINAL_PROPERTY_DOCUMENTATION_PATH);
    }

    /**
     * @return array<string,string>
     */
    public static function readFunctionDocumentationMap(): array
    {
        return require(ORIGINAL_FUNCTION_DOCUMENTATION_PATH);
    }

    /**
     * @return array<string,string>
     */
    public static function readConstantDocumentationMap(): array
    {
        return require(ORIGINAL_CONSTANT_DOCUMENTATION_PATH);
    }

    /**
     * @return array<string,string>
     */
    public static function readClassDocumentationMap(): array
    {
        return require(ORIGINAL_CLASS_DOCUMENTATION_PATH);
    }

    /**
     * @param array<string,array<int|string,string>> $phan_signatures
     */
    public static function saveSignatureMap(string $new_signature_path, array $phan_signatures, bool $include_header = true): void
    {
        $contents = static::serializeSignatures($phan_signatures);
        if ($include_header) {
            $contents = static::readSignatureHeader() . $contents;
        }
        file_put_contents($new_signature_path, $contents);
    }

    /**
     * @param array<string,array<string,array<int|string,string>>> $deltas
     */
    public static function saveSignatureDeltaMap(string $new_delta_path, string $original_delta_path, array $deltas, bool $include_header = true): void
    {
        $contents = static::serializeSignatureDeltas($deltas);
        if ($include_header) {
            $contents = static::readSignatureHeader($original_delta_path) . $contents;
        }
        file_put_contents($new_delta_path, $contents);
    }

    /**
     * @param array<string,array<int|string,string>> $signatures
     */
    public static function serializeSignatures(array $signatures): string
    {
        $parts = "return [\n";
        foreach ($signatures as $function_like_name => $arguments) {
            $parts .= static::encodeSingleSignature($function_like_name, $arguments);
        }
        $parts .= "];\n";
        return $parts;
    }

    /**
     * @param array<string,array<string,array<int|string,string>>> $deltas
     */
    public static function serializeSignatureDeltas(array $deltas): string
    {
        $parts = "return [\n";
        foreach ($deltas as $section_name => $signatures) {
            $parts .= "'$section_name' => [\n";
            foreach ($signatures as $function_like_name => $arguments) {
                $parts .= static::encodeSingleSignature($function_like_name, $arguments);
            }
            $parts .= "],\n";
        }
        $parts .= "];\n";
        return $parts;
    }

    /**
     * @param array<string,string> $phan_documentation
     */
    public static function savePropertyDocumentationMap(string $new_documentation_path, array $phan_documentation, bool $include_header = true): void
    {
        $contents = static::serializeDocumentation($phan_documentation);
        if ($include_header) {
            $contents = static::readPropertyDocumentationHeader() . $contents;
        }
        file_put_contents($new_documentation_path, $contents);
    }

    /**
     * @param array<string,string> $phan_documentation
     */
    public static function saveFunctionDocumentationMap(string $new_documentation_path, array $phan_documentation, bool $include_header = true): void
    {
        $contents = static::serializeDocumentation($phan_documentation);
        if ($include_header) {
            $contents = static::readFunctionDocumentationHeader() . $contents;
        }
        file_put_contents($new_documentation_path, $contents);
    }

    /**
     * @param array<string,string> $phan_documentation
     */
    public static function saveConstantDocumentationMap(string $new_documentation_path, array $phan_documentation, bool $include_header = true): void
    {
        $contents = static::serializeDocumentation($phan_documentation);
        if ($include_header) {
            $contents = static::readConstantDocumentationHeader() . $contents;
        }
        file_put_contents($new_documentation_path, $contents);
    }

    /**
     * @param array<string,string> $phan_documentation
     */
    public static function saveClassDocumentationMap(string $new_documentation_path, array $phan_documentation, bool $include_header = true): void
    {
        $contents = static::serializeDocumentation($phan_documentation);
        if ($include_header) {
            $contents = static::readClassDocumentationHeader() . $contents;
        }
        file_put_contents($new_documentation_path, $contents);
    }

    /**
     * @param array<string,string> $signatures
     */
    public static function serializeDocumentation(array $signatures): string
    {
        $parts = "return [\n";
        foreach ($signatures as $function_like_name => $arguments) {
            $parts .= static::encodeSingleDocumentation($function_like_name, $arguments);
        }
        $parts .= "];\n";
        return $parts;
    }

    /** @param int|string|float $scalar */
    protected static function encodeScalar($scalar): string
    {
        if (is_string($scalar)) {
            return "'" . addcslashes($scalar, "'") . "'";
        }
        return (string)$scalar;
    }

    /**
     * @param array<mixed,string> $arguments the return type and parameter signatures

     */
    public static function encodeSingleSignature(string $function_like_name, array $arguments): string
    {
        $result = static::encodeScalar($function_like_name) . ' => ';
        $result .= static::encodeSignatureArguments($arguments);
        $result .= ",\n";
        return $result;
    }

    /**
     * Encodes a single line with documentation of internal functions/methods

     */
    public static function encodeSingleDocumentation(string $function_like_name, string $description): string
    {
        $result = static::encodeScalar($function_like_name) . ' => ';
        $result .= static::encodeScalar($description);
        $result .= ",\n";
        return $result;
    }

    /**
     * @param array<mixed,string> $arguments
     */
    public static function encodeSignatureArguments(array $arguments): string
    {
        $result = '[';
        foreach ($arguments as $key => $arg) {
            if ($key !== 0) {
                $result .= ', ' . static::encodeScalar($key) . '=>';
            }
            $result .= static::encodeScalar($arg);
        }
        $result .= "]";
        return $result;
    }

    /**
     * Indicate that all functions parsed from stubs with no return statements are non-void
     */
    public static function markAllStubsAsNonVoid(CodeBase $code_base): void
    {
        static::info("Marking all stubs as non-void\n");
        foreach ($code_base->getFunctionMap() as $func) {
            if (!$func->isPHPInternal()) {
                $func->setHasReturn(true);
            }
        }
        foreach ($code_base->getMethodSet() as $func) {
            if (!$func->isPHPInternal()) {
                $func->setHasReturn(true);
            }
        }
    }
}