Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
4.2 KiB
PHP
105 lines
4.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Compatibility;
|
||
|
||
use ReflectionException;
|
||
use ReflectionFunction;
|
||
use ReflectionFunctionAbstract;
|
||
use ReflectionMethod;
|
||
use ReflectionParameter;
|
||
use ReflectionType;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Fingerprint of the signature of one function or static method of a third-party plugin or theme (spec §4.2, E1 of
|
||
* the plan "Divi-Fähigkeiten"), read with Reflection from the running PHP process – so it needs the symbol to be
|
||
* loaded (in WordPress the plugin or theme is, once active) and never includes a file. The class of a method is
|
||
* autoloaded the way ManifestCheck::accepts() already does it.
|
||
*
|
||
* What the hash covers: function or method; for a method its visibility and whether it is static; per parameter the
|
||
* type (nullable and union included; no type reads as "mixed"), by reference, variadic and whether it is optional
|
||
* (has a default); the return type (none reads as "mixed"). What it leaves out:
|
||
* - Parameter names: the plugin passes every argument positionally (PHP 8 named arguments are never used),
|
||
* so a rename cannot break a call, and a harmless rename must not lock a feature.
|
||
* - Default values: they are expressions (constants, arrays) that Reflection may be unable to evaluate in some
|
||
* requests and that change spelling without changing meaning; only "has a default" matters for the number of
|
||
* arguments the plugin may omit.
|
||
* - The body: a changed body is no change of the interface (mirrors cover the code the plugin repeats).
|
||
*
|
||
* Type strings come from Reflection and differ between PHP versions (e.g. `iterable` reads as `Traversable|array`
|
||
* from 8.2). The reference was computed under PHP 8.4; another PHP version can therefore report a spurious signature
|
||
* problem – it only blocks the automatic release (fail closed), never releases; --update-reference regenerates it.
|
||
*/
|
||
final class SignatureFingerprint
|
||
{
|
||
/** sha256 of canonical(), null when the symbol does not exist or cannot be reflected. */
|
||
public static function of(Call $call): ?string
|
||
{
|
||
$canonical = self::canonical($call);
|
||
|
||
return $canonical === null ? null : hash('sha256', $canonical);
|
||
}
|
||
|
||
/**
|
||
* The text the hash is built from, e.g. "public static (int, array|string=, &mixed=, ...mixed): ?bool".
|
||
* Public for tests and diagnostics; null when the symbol does not exist.
|
||
*/
|
||
public static function canonical(Call $call): ?string
|
||
{
|
||
try {
|
||
$reflection = self::reflect($call);
|
||
} catch (ReflectionException) {
|
||
return null;
|
||
}
|
||
if ($reflection === null) {
|
||
return null;
|
||
}
|
||
|
||
return sprintf(
|
||
'%s (%s): %s',
|
||
self::kind($reflection),
|
||
implode(', ', array_map(self::parameter(...), $reflection->getParameters())),
|
||
self::type($reflection->getReturnType())
|
||
);
|
||
}
|
||
|
||
/** "function", or a method's visibility and "static" – a method that turns private or non-static breaks a call. */
|
||
private static function kind(ReflectionFunctionAbstract $reflection): string
|
||
{
|
||
if (! $reflection instanceof ReflectionMethod) {
|
||
return 'function';
|
||
}
|
||
|
||
$visibility = $reflection->isPublic() ? 'public' : ($reflection->isProtected() ? 'protected' : 'private');
|
||
|
||
return $reflection->isStatic() ? $visibility . ' static' : $visibility;
|
||
}
|
||
|
||
/** @throws ReflectionException */
|
||
private static function reflect(Call $call): ?ReflectionFunctionAbstract
|
||
{
|
||
$class = $call->className();
|
||
if ($class === null) {
|
||
return function_exists($call->member()) ? new ReflectionFunction($call->member()) : null;
|
||
}
|
||
|
||
return method_exists($class, $call->member()) ? new ReflectionMethod($class, $call->member()) : null;
|
||
}
|
||
|
||
private static function parameter(ReflectionParameter $parameter): string
|
||
{
|
||
return ($parameter->isPassedByReference() ? '&' : '')
|
||
. ($parameter->isVariadic() ? '...' : '')
|
||
. self::type($parameter->getType())
|
||
. ($parameter->isOptional() && ! $parameter->isVariadic() ? '=' : '');
|
||
}
|
||
|
||
private static function type(?ReflectionType $type): string
|
||
{
|
||
return $type === null ? 'mixed' : (string) $type;
|
||
}
|
||
}
|