From 710444e88dd523584ae39c2078756963a39c3c80 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sun, 18 Oct 2026 22:01:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Signatur-Fingerabdruck=20der=20Divi-Auf?= =?UTF-8?q?rufe=20(Reflection);=20fingerprints.json=20enth=C3=A4lt=20sig:-?= =?UTF-8?q?Eintr=C3=A4ge,=20divi-check=20--update-reference=20schreibt=20s?= =?UTF-8?q?ie;=20Referenz=20f=C3=BCr=20Divi=205.15.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- src/Cli/DiviCheckCommand.php | 25 ++- .../Divi/Release/DiviAdapters.php | 13 ++ .../Divi/Release/DiviReference.php | 45 +++- .../Divi/Release/SignatureFingerprint.php | 99 +++++++++ .../Divi/Release/fingerprints.json | 29 +++ tests/Unit/Cli/DiviCheckCommandTest.php | 46 +++- .../Divi/Release/DiviReferenceTest.php | 86 +++++++- .../Divi/Release/SignatureFingerprintTest.php | 202 ++++++++++++++++++ 8 files changed, 525 insertions(+), 20 deletions(-) create mode 100644 src/Integrations/Divi/Release/SignatureFingerprint.php create mode 100644 tests/Unit/Integrations/Divi/Release/SignatureFingerprintTest.php diff --git a/src/Cli/DiviCheckCommand.php b/src/Cli/DiviCheckCommand.php index f91dc7d..3fecd35 100644 --- a/src/Cli/DiviCheckCommand.php +++ b/src/Cli/DiviCheckCommand.php @@ -35,6 +35,8 @@ final class DiviCheckCommand * @param (Closure(): ?string)|null $themeDir null = DiviEnvironment::themeDir(). * @param string|null $expectedFile null = DiviReference::FILE. * @param (Closure(): ?string)|null $sourceDir null = DiviReference::sourceDir(). + * @param (Closure(): array)|null $signatures "sig:" => sha256 of every Divi call in the + * running process; null = DiviReference::currentSignatures(). */ public function __construct( private Closure $check, @@ -42,7 +44,8 @@ final class DiviCheckCommand ?Closure $out = null, private ?Closure $themeDir = null, private ?string $expectedFile = null, - private ?Closure $sourceDir = null + private ?Closure $sourceDir = null, + private ?Closure $signatures = null ) { $this->out = $out ?? static function (string $line): void { \WP_CLI::line($line); @@ -67,7 +70,8 @@ final class DiviCheckCommand * : Zeigt für jede geänderte nachgebaute Divi-Funktion den Unterschied zur Referenz. * * [--update-reference] - * : Schreibt Soll-Fingerabdrücke und Referenzquellen neu – nur wenn alle übrigen Schritte grün sind. + * : Schreibt die Soll-Fingerabdrücke (Nachbauten und Signaturen der Divi-Aufrufe) und die Referenzquellen neu – + * nur wenn alle übrigen Schritte grün sind. * * ## EXAMPLES * @@ -179,6 +183,16 @@ final class DiviCheckCommand return 1; } + try { + $signatures = ($this->signatures ?? DiviReference::currentSignatures(...))(); + } catch (RuntimeException $e) { + ($this->out)( + 'Referenz nicht aktualisiert – die Signatur eines Divi-Aufrufs ist nicht lesbar: ' . $e->getMessage() + ); + + return 1; + } + try { // Sources first, fingerprints.json last: the fingerprints decide the release gate, so a write that // fails before this last one leaves the gate exactly as it was. @@ -186,7 +200,7 @@ final class DiviCheckCommand DiviReference::writeSource($id, $print->source, $sourceDir); } DiviReference::writeExpected( - array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints), + array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints) + $signatures, $this->expectedFile ); } catch (RuntimeException $e) { @@ -197,8 +211,9 @@ final class DiviCheckCommand delete_transient(ReleaseResolver::CACHE); ReleaseGate::forget(); ($this->out)(sprintf( - 'Referenz aktualisiert (%d Fingerabdrücke). Smoke-Skripte laufen lassen, dann committen.', - count($prints) + 'Referenz aktualisiert (%d Fingerabdrücke, %d Signaturen). Smoke-Skripte laufen lassen, dann committen.', + count($prints), + count($signatures) )); return 0; diff --git a/src/Integrations/Divi/Release/DiviAdapters.php b/src/Integrations/Divi/Release/DiviAdapters.php index 4182a8e..37f6fb3 100644 --- a/src/Integrations/Divi/Release/DiviAdapters.php +++ b/src/Integrations/Divi/Release/DiviAdapters.php @@ -42,6 +42,19 @@ final class DiviAdapters return $manifests; } + /** @return list Every Divi call of every manifest, each symbol once (the first entry wins). */ + public static function calls(): array + { + $calls = []; + foreach (self::manifests() as $manifest) { + foreach ($manifest->calls as $call) { + $calls[ltrim($call->symbol, '\\')] ??= $call; + } + } + + return array_values($calls); + } + /** @return list */ public static function mirrors(): array { diff --git a/src/Integrations/Divi/Release/DiviReference.php b/src/Integrations/Divi/Release/DiviReference.php index ac7893b..27eb3f4 100644 --- a/src/Integrations/Divi/Release/DiviReference.php +++ b/src/Integrations/Divi/Release/DiviReference.php @@ -9,15 +9,22 @@ use RuntimeException; defined('ABSPATH') || exit; /** - * The expected fingerprints of the mirrors (fingerprints.json, shipped with the plugin) and their reference sources - * (tests/Fixtures/divi/reference/.txt, development only – not in the plugin package). Written by - * wp lit-mcp divi-check --update-reference. + * The expected fingerprints (fingerprints.json, shipped with the plugin): the source fingerprint of every mirror under + * its ID, and the signature fingerprint of every Divi call under "sig:" (SignatureFingerprint) – plus the + * reference sources of the mirrors (tests/Fixtures/divi/reference/.txt, development only – not in the plugin + * package). Written by wp lit-mcp divi-check --update-reference. */ final class DiviReference { public const FILE = __DIR__ . '/fingerprints.json'; - /** @return array Mirror ID => sha256; [] when the file is missing or broken. */ + /** Key prefix of the signature fingerprints; no mirror ID starts with it. */ + public const SIGNATURE_PREFIX = 'sig:'; + + /** + * @return array Mirror ID or "sig:" => sha256, sorted by key; [] when the file is + * missing or broken. + */ public static function expected(?string $file = null): array { $file ??= self::FILE; @@ -40,7 +47,7 @@ final class DiviReference } /** - * @param array $hashes Mirror ID => sha256. + * @param array $hashes Mirror ID or "sig:" => sha256; written sorted by key. * @throws RuntimeException */ public static function writeExpected(array $hashes, ?string $file = null): void @@ -54,6 +61,34 @@ final class DiviReference } } + /** The key of $call's signature fingerprint: "sig:" and the symbol without a leading backslash. */ + public static function signatureKey(Call $call): string + { + return self::SIGNATURE_PREFIX . ltrim($call->symbol, '\\'); + } + + /** + * Signature fingerprints of the calls in the running PHP process (Divi loaded). + * + * @param list|null $calls null = DiviAdapters::calls(). + * @return array "sig:" => sha256, sorted by key. + * @throws RuntimeException When a symbol is not loaded. + */ + public static function currentSignatures(?array $calls = null): array + { + $signatures = []; + foreach ($calls ?? DiviAdapters::calls() as $call) { + $hash = SignatureFingerprint::of($call); + if ($hash === null) { + throw new RuntimeException(sprintf('signature of %s: symbol is not loaded', $call->label())); + } + $signatures[self::signatureKey($call)] = $hash; + } + ksort($signatures); + + return $signatures; + } + /** tests/Fixtures/divi/reference of a development checkout; null in an installed plugin. */ public static function sourceDir(): ?string { diff --git a/src/Integrations/Divi/Release/SignatureFingerprint.php b/src/Integrations/Divi/Release/SignatureFingerprint.php new file mode 100644 index 0000000..a2307e0 --- /dev/null +++ b/src/Integrations/Divi/Release/SignatureFingerprint.php @@ -0,0 +1,99 @@ +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; + } +} diff --git a/src/Integrations/Divi/Release/fingerprints.json b/src/Integrations/Divi/Release/fingerprints.json index 698a3f8..87eaf8d 100644 --- a/src/Integrations/Divi/Release/fingerprints.json +++ b/src/Integrations/Divi/Release/fingerprints.json @@ -1,6 +1,35 @@ { "library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02", "preset-sync": "9586f8005b8065893c0350977a58ee9171bf715fff21ac0c84663cc512f45522", + "sig:ET\\Builder\\Framework\\UserRole\\UserRole::can_current_user_use_visual_builder": "76d199c13adc6600c8a456225a8eb9db8037064f20eca9db597c4e45ccecf8d5", + "sig:ET\\Builder\\Packages\\Conversion\\Conversion::get_preset_attrs": "5b0ace397bc5e171b73b9c50df5d27942719dffe5344fb4bc36af0f35616a75b", + "sig:ET\\Builder\\Packages\\Conversion\\Conversion::get_preset_attrs_mapping": "b5c3403666ff92be1dd7bfa6e6589c32f6662b37282f2fd40922f8d50151745d", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::get_global_colors": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalData::get_global_variables": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_data": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_deleted_preset_ids": "d36bfe7698116a94f7c6bc5ff00e3f66f3cd4267b68b5e61f0a5ec01e051b450", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::get_group_preset_default_attr": "427ee58858537cf02a9d6dbbf58aafd779f8424c9c840806b5d7af8f25fbbda5", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::prepare_data": "94f109150a8d6db57c309de00f4f4e369b4cd4a7e2a0682d5612dddeb65c8861", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::remove_preset_from_legacy_option": "11f8d1b50b8f113f96c9d8b2fb79454d45938a9d1613f42f5089e35767597aed", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::save_data": "7cfacdb2d6231f4baff0bea176081bf3237b5c3fb2a3f8e76fcd8b7d156d4375", + "sig:ET\\Builder\\Packages\\GlobalData\\GlobalPreset::validate_preset_count": "09765c551d2f4fb3d5841644f79aa304b7406d92aed6a1cc33821f80dc8b6592", + "sig:ET\\Builder\\Packages\\GlobalLayout\\GlobalLayout::is_global_layout_template": "6fe723bf98c5fa7580f9f52af29adf08a037520081ced8363063125270ae3384", + "sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_all_core_modules_metadata": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae", + "sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_default_attrs": "728c9cddae0184ffccaff5b22a392b7a4e9c5f078b5d12d4c9717aca6af70e69", + "sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_module_settings": "f61b3693b0df17be1f068b8be424dcf56a67324372a92f576724cd564d992036", + "sig:ET\\Builder\\Packages\\Module\\Layout\\Components\\DynamicContent\\DynamicContentOptions::get_options": "6d3932cbf7921be4f353ab71d2bd7200c2bf500577b401b32e9ca31809f07489", + "sig:ET_Core_PageResource::remove_static_resources": "eaa39fd2c037c2d399578f71862da5c04b68d58a30657916482f95e94e5d8a96", + "sig:et_": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966", + "sig:et_builder_enabled_for_post_type": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc", + "sig:et_builder_get_public_post_types": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966", + "sig:et_pb_is_allowed": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799", + "sig:et_theme_builder_clear_wp_cache": "3ac3132305f3818c13df78416ae13e34c36af17a0ba9b66d652fe9babc6c051d", + "sig:et_theme_builder_get_flat_template_settings_options": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966", + "sig:et_theme_builder_get_template": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc", + "sig:et_theme_builder_get_theme_builder_post_id": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799", + "sig:et_theme_builder_get_theme_builder_template_ids": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799", + "sig:et_theme_builder_insert_layout": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc", + "sig:et_theme_builder_store_template": "6c84ec472739a2dbb87bcf21777c4dc977fd2233142eacfefe60eba462a4c219", "tb-api-save": "7ba452f4d73c9b676281ca6ddcfddd669502965db868bf478ef7804937e5bfc4", "tb-load-setting-options": "2ea0a6925a1da5522489a417104b384b2bd77a16b25f4122b9503873a4c8a24e", "tb-store-template": "3bf4ac6028ed99ed8ac3523c659392a5fce7818ef8a80ea226e01f5404eb441d", diff --git a/tests/Unit/Cli/DiviCheckCommandTest.php b/tests/Unit/Cli/DiviCheckCommandTest.php index ce03b91..d88a56d 100644 --- a/tests/Unit/Cli/DiviCheckCommandTest.php +++ b/tests/Unit/Cli/DiviCheckCommandTest.php @@ -13,6 +13,7 @@ use LIT\MCP\Integrations\Divi\Release\CompatibilityCheck; use LIT\MCP\Integrations\Divi\Release\DiviReference; use LIT\MCP\Tests\Unit\TestCase; use Mockery; +use RuntimeException; final class DiviCheckCommandTest extends TestCase { @@ -23,10 +24,18 @@ final class DiviCheckCommandTest extends TestCase private string $sourceDir = ''; private string $expectedFile = ''; + /** @var array What the command reads as the signatures of the running Divi. */ + private array $signatures = []; + + /** Set to true to make the signature read fail like a Divi call that is not loaded. */ + private bool $signaturesUnreadable = false; + protected function setUp(): void { parent::setUp(); $this->lines = []; + $this->signatures = ['sig:et_pb_is_allowed' => str_repeat('a', 64), 'sig:A::b' => str_repeat('b', 64)]; + $this->signaturesUnreadable = false; $base = sys_get_temp_dir() . '/lit-cli-' . uniqid(); $this->themeDir = $base . '/Divi'; $this->sourceDir = $base . '/reference'; @@ -75,7 +84,14 @@ final class DiviCheckCommandTest extends TestCase }, fn (): ?string => $this->themeDir, $this->expectedFile, - $noSourceDir ? static fn (): ?string => null : fn (): ?string => $this->sourceDir + $noSourceDir ? static fn (): ?string => null : fn (): ?string => $this->sourceDir, + function (): array { + if ($this->signaturesUnreadable) { + throw new RuntimeException('signature of et_pb_is_allowed(): symbol is not loaded'); + } + + return $this->signatures; + } ); } @@ -176,7 +192,24 @@ final class DiviCheckCommandTest extends TestCase } } - public function testUpdateReferenceWritesFingerprintsAndSources(): void + public function testUpdateReferenceWritesNothingWhenASignatureIsUnreadable(): void + { + $this->writeDivi(); + $this->signaturesUnreadable = true; + + $code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]); + + self::assertSame(1, $code); + self::assertStringContainsString( + 'Referenz nicht aktualisiert – die Signatur eines Divi-Aufrufs ist nicht lesbar: ' + . 'signature of et_pb_is_allowed(): symbol is not loaded', + implode("\n", $this->lines) + ); + self::assertFileDoesNotExist($this->expectedFile, 'all or nothing'); + self::assertSame([], glob($this->sourceDir . '/*.txt'), 'no source written either'); + } + + public function testUpdateReferenceWritesFingerprintsSignaturesAndSources(): void { $this->writeDivi(); @@ -185,11 +218,16 @@ final class DiviCheckCommandTest extends TestCase self::assertSame(0, $code); self::assertSame( [ - 'library-submit-layout', 'preset-sync', 'tb-api-save', 'tb-load-setting-options', 'tb-store-template', - 'vb-sync-flags', + 'library-submit-layout', 'preset-sync', 'sig:A::b', 'sig:et_pb_is_allowed', 'tb-api-save', + 'tb-load-setting-options', 'tb-store-template', 'vb-sync-flags', ], array_keys(DiviReference::expected($this->expectedFile)) ); + self::assertSame(str_repeat('b', 64), DiviReference::expected($this->expectedFile)['sig:A::b']); + self::assertContains( + 'Referenz aktualisiert (6 Fingerabdrücke, 2 Signaturen). Smoke-Skripte laufen lassen, dann committen.', + $this->lines + ); self::assertStringStartsWith( 'function et_pb_submit_layout(', (string) DiviReference::readSource('library-submit-layout', $this->sourceDir) diff --git a/tests/Unit/Integrations/Divi/Release/DiviReferenceTest.php b/tests/Unit/Integrations/Divi/Release/DiviReferenceTest.php index 72d4a84..f7af2af 100644 --- a/tests/Unit/Integrations/Divi/Release/DiviReferenceTest.php +++ b/tests/Unit/Integrations/Divi/Release/DiviReferenceTest.php @@ -4,25 +4,99 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release; +use LIT\MCP\Integrations\Divi\Release\Call; use LIT\MCP\Integrations\Divi\Release\DiviAdapters; use LIT\MCP\Integrations\Divi\Release\DiviReference; +use LIT\MCP\Integrations\Divi\Release\SignatureFingerprint; use PHPUnit\Framework\TestCase; +use RuntimeException; final class DiviReferenceTest extends TestCase { - public function testEveryMirrorHasAnExpectedFingerprintAndAReferenceSource(): void + public function testEveryMirrorAndEveryCallHasAnExpectedFingerprintAndEveryMirrorAReferenceSource(): void { - $ids = array_map(static fn ($mirror): string => $mirror->id, DiviAdapters::mirrors()); - sort($ids); + $mirrorIds = array_map(static fn ($mirror): string => $mirror->id, DiviAdapters::mirrors()); + $signatureKeys = []; + foreach (DiviAdapters::manifests() as $manifest) { + foreach ($manifest->calls as $call) { + $signatureKeys[] = 'sig:' . ltrim($call->symbol, '\\'); + } + } + $keys = array_values(array_unique(array_merge($mirrorIds, $signatureKeys))); + sort($keys); $expected = DiviReference::expected(); - self::assertSame($ids, array_keys($expected)); - foreach ($expected as $id => $hash) { - self::assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $hash, $id); + self::assertSame($keys, array_keys($expected)); + foreach ($expected as $key => $hash) { + self::assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $hash, $key); + } + foreach ($mirrorIds as $id) { self::assertNotNull(DiviReference::readSource($id, (string) DiviReference::sourceDir()), $id); } } + public function testEverySignatureKeyStartsWithThePrefixNoMirrorIdUses(): void + { + foreach (DiviAdapters::mirrors() as $mirror) { + self::assertStringStartsNotWith(DiviReference::SIGNATURE_PREFIX, $mirror->id); + } + self::assertSame('sig:et_pb_is_allowed', DiviReference::signatureKey(new Call('et_pb_is_allowed', 1))); + self::assertSame( + 'sig:ET_Core_PageResource::remove_static_resources', + DiviReference::signatureKey(new Call('\\ET_Core_PageResource::remove_static_resources', 3)) + ); + } + + public function testCallsListsEverySymbolOnceAcrossTheManifests(): void + { + $symbols = array_map(static fn (Call $call): string => ltrim($call->symbol, '\\'), DiviAdapters::calls()); + + self::assertSame($symbols, array_values(array_unique($symbols))); + self::assertContains('ET_Core_PageResource::remove_static_resources', $symbols); + self::assertCount(1, array_keys($symbols, 'ET_Core_PageResource::remove_static_resources', true)); + self::assertContains('et_pb_is_allowed', $symbols); + } + + public function testCurrentSignaturesAreSortedAndKeyedLikeTheReference(): void + { + $signatures = DiviReference::currentSignatures([ + new Call('strlen', 1), + new Call('\\str_replace', 3), + new Call('strlen', 1), + ]); + + self::assertSame(['sig:str_replace', 'sig:strlen'], array_keys($signatures)); + self::assertSame(SignatureFingerprint::of(new Call('strlen')), $signatures['sig:strlen']); + } + + public function testCurrentSignaturesRefuseASymbolThatIsNotLoaded(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('signature of lit_mcp_missing_function(): symbol is not loaded'); + + DiviReference::currentSignatures([new Call('strlen', 1), new Call('lit_mcp_missing_function', 0)]); + } + + public function testSignatureKeysAreWrittenAndReadLikeMirrorIds(): void + { + $file = (string) tempnam(sys_get_temp_dir(), 'lit'); + + try { + DiviReference::writeExpected([ + 'sig:Z\\Klass::run' => str_repeat('c', 64), + 'tb-api-save' => str_repeat('b', 64), + 'sig:a_function' => str_repeat('a', 64), + ], $file); + + self::assertSame( + ['sig:Z\\Klass::run', 'sig:a_function', 'tb-api-save'], + array_keys(DiviReference::expected($file)) + ); + } finally { + unlink($file); + } + } + public function testWritesAndReadsExpectedFingerprintsSorted(): void { $file = (string) tempnam(sys_get_temp_dir(), 'lit'); diff --git a/tests/Unit/Integrations/Divi/Release/SignatureFingerprintTest.php b/tests/Unit/Integrations/Divi/Release/SignatureFingerprintTest.php new file mode 100644 index 0000000..17336d4 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Release/SignatureFingerprintTest.php @@ -0,0 +1,202 @@ + */ + public static function changedSignatures(): array + { + return [ + 'extra required parameter' => ['extraRequired'], + 'fewer parameters' => ['fewer'], + 'parameter type' => ['typeChanged'], + 'wider union' => ['unionWidened'], + 'return type loses null' => ['notNullable'], + 'by reference lost' => ['byValue'], + 'variadic lost' => ['notVariadic'], + 'default removed' => ['defaultRemoved'], + 'return type' => ['returnChanged'], + 'no longer static' => ['notStatic'], + 'no longer public' => ['notPublic'], + ]; + } + + /** @dataProvider changedSignatures */ + public function testAnyChangeOfTheInterfaceChangesTheHash(string $variant): void + { + self::assertNotSame( + SignatureFingerprint::of(self::method('base')), + SignatureFingerprint::of(self::method($variant)), + $variant + ); + } + + public function testRenamedParametersAndOtherDefaultValuesKeepTheHash(): void + { + self::assertSame( + SignatureFingerprint::of(self::method('base')), + SignatureFingerprint::of(self::method('renamed')) + ); + } + + public function testNoTypeReadsAsMixed(): void + { + self::assertSame( + SignatureFingerprint::of(self::method('untyped')), + SignatureFingerprint::of(self::method('mixedTyped')) + ); + } + + public function testTheCallsOwnArgumentCountAndFeaturesAreNotPartOfIt(): void + { + self::assertSame( + SignatureFingerprint::of(new Call(self::FIXTURE . '::base', 2)), + SignatureFingerprint::of(new Call('\\' . self::FIXTURE . '::base', null, [])) + ); + } + + public function testAFunctionAndAMethodOfTheSameShapeDiffer(): void + { + self::assertNotSame( + SignatureFingerprint::of(new Call('strlen')), + SignatureFingerprint::of(new Call(ArrayObject::class . '::count')) + ); + } + + public function testTheClassNeverIncludesAFile(): void + { + $source = (string) file_get_contents( + dirname(__DIR__, 5) . '/src/Integrations/Divi/Release/SignatureFingerprint.php' + ); + $forbidden = [T_INCLUDE, T_INCLUDE_ONCE, T_REQUIRE, T_REQUIRE_ONCE, T_EVAL]; + + foreach (token_get_all($source) as $token) { + self::assertFalse(is_array($token) && in_array($token[0], $forbidden, true), 'no include, require or eval'); + } + } +}