Skip to content

Commit

Permalink
Improve PerfectHasher and its coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
vudaltsov committed Sep 4, 2024
1 parent 771713a commit 0ceab8b
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 14 deletions.
19 changes: 9 additions & 10 deletions src/Internal/PerfectHasher.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static function global(): self
private \Closure $defaultObjectHasher;

/**
* @var array<non-empty-string, callable(object, self): non-empty-string>
* @var array<non-empty-string, \Closure(object, self): non-empty-string>
*/
private array $objectHashers = [];

Expand Down Expand Up @@ -110,7 +110,7 @@ public function hash(mixed $value): int|string
}

if (\is_object($value)) {
return $this->objectHashes[$value] ??= $this->hashObject($value);
return $this->objectHashes[$value] ??= $this->objectHasher($value::class)($value, $this);
}

if ($value === null) {
Expand Down Expand Up @@ -153,28 +153,27 @@ public function hash(mixed $value): int|string
}

/**
* @return non-empty-string
* @param class-string $class
* @return \Closure(object, self): non-empty-string
*/
private function hashObject(object $object): string
private function objectHasher(string $class): \Closure
{
$class = $object::class;

if (isset($this->objectHashers[$class])) {
return $this->objectHashers[$class]($object, $this);
return $this->objectHashers[$class];
}

foreach (class_parents($class) as $parent) {
if (isset($this->objectHashers[$parent])) {
return ($this->objectHashers[$class] = $this->objectHashers[$parent])($object, $this);
return $this->objectHashers[$class] = $this->objectHashers[$parent];
}
}

foreach (class_implements($class) as $interface) {
if (isset($this->objectHashers[$interface])) {
return ($this->objectHashers[$class] = $this->objectHashers[$interface])($object, $this);
return $this->objectHashers[$class] = $this->objectHashers[$interface];
}
}

return ($this->objectHashers[$class] = $this->defaultObjectHasher)($object);
return $this->objectHashers[$class] = $this->defaultObjectHasher;
}
}
43 changes: 39 additions & 4 deletions tests/Internal/PerfectHasherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function testObject(): void
self::assertSame('#' . spl_object_id($this), $hash);
}

public function testObjectWithCustomEncoder(): void
public function testObjectWithCustomNormalizer(): void
{
$hasher = new PerfectHasher();
$hasher->registerObjectNormalizer(\Throwable::class, static fn(\Throwable $exception): string => $exception->getMessage());
Expand All @@ -74,14 +74,49 @@ public function testObjectWithCustomEncoder(): void
self::assertSame('RangeException|UnexpectedValueException@`unexpected_value`', $hasher->hash(new \UnexpectedValueException('unexpected_value')));
}

public function testObjectHashIsMemoizedForSameInstance(): void
{
$hasher = new PerfectHasher();
$hasher->registerObjectNormalizer(\stdClass::class, static function (): int {

Check failure on line 80 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (locked)

MixedInferredReturnType

tests/Internal/PerfectHasherTest.php:80:81: MixedInferredReturnType: Could not verify return type 'int' for /home/runner/work/data-structures/data-structures/tests/internal/perfecthashertest.php:80:2987:-:closure (see https://psalm.dev/047)

Check failure on line 80 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (highest)

MixedInferredReturnType

tests/Internal/PerfectHasherTest.php:80:81: MixedInferredReturnType: Could not verify return type 'int' for /home/runner/work/data-structures/data-structures/tests/internal/perfecthashertest.php:80:2987:-:closure (see https://psalm.dev/047)
static $hash = 0;

return $hash++;

Check failure on line 83 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (locked)

MixedOperand

tests/Internal/PerfectHasherTest.php:83:20: MixedOperand: Left operand cannot be mixed (see https://psalm.dev/059)

Check failure on line 83 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (locked)

MixedReturnStatement

tests/Internal/PerfectHasherTest.php:83:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 83 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (highest)

MixedOperand

tests/Internal/PerfectHasherTest.php:83:20: MixedOperand: Left operand cannot be mixed (see https://psalm.dev/059)

Check failure on line 83 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (highest)

MixedReturnStatement

tests/Internal/PerfectHasherTest.php:83:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
});
$object = new \stdClass();

$hash1 = $hasher->hash($object);
$hash2 = $hasher->hash($object);

self::assertSame('stdClass@0', $hash1);
self::assertSame('stdClass@0', $hash2);
}

public function testMemoizedObjectHashIsNotSameForDifferentInstancesOfTheSameClass(): void
{
$hasher = new PerfectHasher();
$hasher->registerObjectNormalizer(\stdClass::class, static function (): int {

Check failure on line 97 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (locked)

MixedInferredReturnType

tests/Internal/PerfectHasherTest.php:97:81: MixedInferredReturnType: Could not verify return type 'int' for /home/runner/work/data-structures/data-structures/tests/internal/perfecthashertest.php:97:3506:-:closure (see https://psalm.dev/047)

Check failure on line 97 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (highest)

MixedInferredReturnType

tests/Internal/PerfectHasherTest.php:97:81: MixedInferredReturnType: Could not verify return type 'int' for /home/runner/work/data-structures/data-structures/tests/internal/perfecthashertest.php:97:3506:-:closure (see https://psalm.dev/047)
static $hash = 0;

return $hash++;

Check failure on line 100 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (locked)

MixedOperand

tests/Internal/PerfectHasherTest.php:100:20: MixedOperand: Left operand cannot be mixed (see https://psalm.dev/059)

Check failure on line 100 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (locked)

MixedReturnStatement

tests/Internal/PerfectHasherTest.php:100:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)

Check failure on line 100 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (highest)

MixedOperand

tests/Internal/PerfectHasherTest.php:100:20: MixedOperand: Left operand cannot be mixed (see https://psalm.dev/059)

Check failure on line 100 in tests/Internal/PerfectHasherTest.php

View workflow job for this annotation

GitHub Actions / psalm (highest)

MixedReturnStatement

tests/Internal/PerfectHasherTest.php:100:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
});
$object1 = new \stdClass();
$object2 = clone $object1;

$hash1 = $hasher->hash($object1);
$hash2 = $hasher->hash($object2);

self::assertSame('stdClass@0', $hash1);
self::assertSame('stdClass@1', $hash2);
}

/**
* @param non-empty-string $prefix
*/
#[TestWith(['abc'])]
#[TestWith([self::class])]
#[TestWith(['123'])]
#[TestWith(['a.b.c'])]
public function testRegisterObjectEncoderAcceptsValidPrefix(string $prefix): void
public function testRegisterObjectNormalizerAcceptsValidPrefix(string $prefix): void
{
$hasher = new PerfectHasher();

Expand All @@ -98,7 +133,7 @@ public function testRegisterObjectEncoderAcceptsValidPrefix(string $prefix): voi
#[TestWith(['#'])]
#[TestWith(['@'])]
#[TestWith([','])]
public function testRegisterObjectEncoderThrowsOnInvalidPrefix(string $prefix): void
public function testRegisterObjectNormalizerThrowsOnInvalidPrefix(string $prefix): void
{
$hasher = new PerfectHasher();

Expand All @@ -107,7 +142,7 @@ public function testRegisterObjectEncoderThrowsOnInvalidPrefix(string $prefix):
$hasher->registerObjectNormalizer(self::class, static fn(): bool => true, $prefix);
}

public function testRegisterObjectEncoderThrowsAfterEncoding(): void
public function testRegisterObjectNormalizerThrowsAfterHashing(): void
{
$hasher = new PerfectHasher();
$hasher->hash(1);
Expand Down

0 comments on commit 0ceab8b

Please sign in to comment.