From 8a2c80e4cc3f9e7a3c81e87715426d53b4dc0540 Mon Sep 17 00:00:00 2001 From: dnnsjsk Date: Tue, 19 May 2026 00:41:04 +0200 Subject: [PATCH] Raise PHPStan to level 10 Tighten types across the codebase so static analysis runs at the strictest level. Replace unsafe casts of mixed values with explicit narrowing, surface stdlib failure modes (unpack, zlib_encode, hex2bin) as typed exceptions, and rewrite the recursive nested-array tree builders in Notes/Stash/Repository as flat path-grouping passes that phpstan can reason about. Replace SplPriorityQueue in CommitWalker with a typed array-based heap (preserves newest-first + FIFO tiebreak) because the stdlib stub is too loose at level 10. Co-Authored-By: Claude Opus 4.7 (1M context) --- phpstan.neon.dist | 2 +- src/Diff/MyersDiff.php | 3 + src/Diff/TreeDiff.php | 15 +- src/Encoding/BinaryReader.php | 40 ++++- src/Encoding/VarInt.php | 4 +- src/Graph/Blame.php | 8 + src/Graph/CommitWalker.php | 52 +++---- src/Graph/Grep.php | 1 + src/Lfs/LfsClient.php | 29 +++- src/Merge/MergeBase.php | 4 +- src/Object/ObjectId.php | 7 +- src/Pack/DeltaResolver.php | 10 +- src/Pack/PackFile.php | 18 ++- src/Pack/PackIndexer.php | 4 +- src/Pack/PackWriter.php | 10 +- src/Protocol/Bundle.php | 3 + src/Protocol/GitProtocolClient.php | 8 +- src/Protocol/PktLine.php | 8 +- src/Protocol/ProtocolV1.php | 1 + src/Protocol/SmartHttpClient.php | 17 ++- src/Protocol/SshClient.php | 8 +- src/Protocol/UploadPackClient.php | 2 +- src/Ref/Notes.php | 76 +++++----- src/Ref/PackedRefStore.php | 11 +- src/Ref/Reflog.php | 2 + src/Repository.php | 231 +++++++++++++++++++---------- src/Stash/Stash.php | 76 +++++----- src/Status/WorkingTreeStatus.php | 16 +- src/Storage/ObjectSerializer.php | 7 +- src/Support/Json.php | 6 + src/Support/PathFingerprint.php | 2 +- 31 files changed, 445 insertions(+), 236 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 26697bef..24c2c5c7 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 10 treatPhpDocTypesAsCertain: false paths: - src diff --git a/src/Diff/MyersDiff.php b/src/Diff/MyersDiff.php index 9d455ab9..58cc8df1 100644 --- a/src/Diff/MyersDiff.php +++ b/src/Diff/MyersDiff.php @@ -142,6 +142,9 @@ private static function myers(array $a, array $b, int $n, int $m): array * rounds 0..d-1 finished. For backtrace at step s, we need V after * round s-1 finished = trace[s] (NOT trace[s-1]). * + * @param array> $trace + * @param array $a + * @param array $b * @return array */ private static function backtrace( diff --git a/src/Diff/TreeDiff.php b/src/Diff/TreeDiff.php index d0313f0a..a7ecc6d9 100644 --- a/src/Diff/TreeDiff.php +++ b/src/Diff/TreeDiff.php @@ -66,11 +66,15 @@ private function detectRenames(array $results): array $matched = []; foreach ($deleted as $di => $del) { + if ($del->oldHash === null) { + continue; + } + $bestScore = 0; $bestIdx = null; foreach ($added as $ai => $add) { - if (isset($matched[$ai])) { + if (isset($matched[$ai]) || $add->newHash === null) { continue; } @@ -101,6 +105,11 @@ private function detectRenames(array $results): array if ($bestIdx !== null) { $add = $added[$bestIdx]; + + if ($add->newHash === null) { + continue; + } + $oldContent = $this->readBlobContent($del->oldHash); $newContent = $this->readBlobContent($add->newHash); $hunks = DiffAlgorithm::diff($oldContent, $newContent, $this->algorithm); @@ -220,6 +229,10 @@ private function diffInto(?ObjectId $oldTree, ?ObjectId $newTree, string $prefix continue; } + if ($oldEntry === null || $newEntry === null) { + continue; + } + if ($oldEntry['hash'] === $newEntry['hash']) { continue; } diff --git a/src/Encoding/BinaryReader.php b/src/Encoding/BinaryReader.php index 1acc4bb8..5401238b 100644 --- a/src/Encoding/BinaryReader.php +++ b/src/Encoding/BinaryReader.php @@ -96,9 +96,8 @@ public function readByte(): int public function readUint32(): int { $data = $this->read(4); - $unpacked = unpack('N', $data); - return (int) $unpacked[1]; + return $this->unpackOne('N', $data); } /** @@ -117,9 +116,9 @@ public function readUint24(): int public function readUint64(): int { $data = $this->read(8); - $parts = unpack('Nhigh/Nlow', $data); + $parts = $this->unpackPair('Nhigh/Nlow', $data); - return ((int) $parts['high'] << 32) | (int) $parts['low']; + return ($parts['high'] << 32) | $parts['low']; } /** @@ -128,9 +127,38 @@ public function readUint64(): int public function readUint16(): int { $data = $this->read(2); - $unpacked = unpack('n', $data); - return (int) $unpacked[1]; + return $this->unpackOne('n', $data); + } + + private function unpackOne(string $format, string $data): int + { + $unpacked = unpack($format, $data); + + if ($unpacked === false || !isset($unpacked[1]) || !is_int($unpacked[1])) { + throw new RuntimeException("Failed to unpack {$format}"); + } + + return $unpacked[1]; + } + + /** + * @return array{high: int, low: int} + */ + private function unpackPair(string $format, string $data): array + { + $unpacked = unpack($format, $data); + + if ( + $unpacked === false + || !isset($unpacked['high'], $unpacked['low']) + || !is_int($unpacked['high']) + || !is_int($unpacked['low']) + ) { + throw new RuntimeException("Failed to unpack {$format}"); + } + + return ['high' => $unpacked['high'], 'low' => $unpacked['low']]; } /** diff --git a/src/Encoding/VarInt.php b/src/Encoding/VarInt.php index 02fe82a2..6512bb96 100644 --- a/src/Encoding/VarInt.php +++ b/src/Encoding/VarInt.php @@ -73,12 +73,12 @@ public static function encodePackSize(int $type, int $size): string $result = ''; while ($size > 0) { - $result .= chr($byte | 0x80); + $result .= chr(($byte | 0x80) & 0xFF); $byte = $size & 0x7F; $size >>= 7; } - $result .= chr($byte); + $result .= chr($byte & 0xFF); return $result; } diff --git a/src/Graph/Blame.php b/src/Graph/Blame.php index 63240da3..e9f3e0d7 100644 --- a/src/Graph/Blame.php +++ b/src/Graph/Blame.php @@ -206,6 +206,10 @@ private function matchedLineIndexes(array $olderLines, array $newerLines): array return array_keys($matchedNewIndexes); } + /** + * @param array $olderLines + * @param array $newerLines + */ private function isPrefixMatch(array $olderLines, array $newerLines, int $length): bool { for ($index = 0; $index < $length; $index++) { @@ -217,6 +221,10 @@ private function isPrefixMatch(array $olderLines, array $newerLines, int $length return true; } + /** + * @param array $olderLines + * @param array $newerLines + */ private function isSuffixMatch(array $olderLines, array $newerLines, int $oldCount, int $newCount): bool { $offset = $newCount - $oldCount; diff --git a/src/Graph/CommitWalker.php b/src/Graph/CommitWalker.php index b01b7dfb..d8bea873 100644 --- a/src/Graph/CommitWalker.php +++ b/src/Graph/CommitWalker.php @@ -28,23 +28,7 @@ public function __construct(private readonly ObjectDatabase $objects) */ public function walk(ObjectId $from, int $limit = 50): array { - $commits = []; - $visited = []; - $queue = new \SplPriorityQueue(); - - $this->enqueue($queue, $from, $visited); - - while (!$queue->isEmpty() && count($commits) < $limit) { - ['id' => $id, 'commit' => $object] = $queue->extract(); - - $commits[] = $object; - - foreach ($object->parents as $parentId) { - $this->enqueue($queue, $parentId, $visited); - } - } - - return $commits; + return $this->walkAll([$from], $limit); } /** @@ -57,15 +41,15 @@ public function walkAll(array $from, int $limit = 50): array { $commits = []; $visited = []; - $queue = new \SplPriorityQueue(); + $queue = []; foreach ($from as $id) { $this->enqueue($queue, $id, $visited); } - while (!$queue->isEmpty() && count($commits) < $limit) { - ['id' => $id, 'commit' => $object] = $queue->extract(); - + while ($queue !== [] && count($commits) < $limit) { + $entry = array_shift($queue); + $object = $entry['commit']; $commits[] = $object; foreach ($object->parents as $parentId) { @@ -77,9 +61,13 @@ public function walkAll(array $from, int $limit = 50): array } /** + * Insert by descending timestamp (newest first). + * + * @param array $queue * @param array $visited + * @param-out array $queue */ - private function enqueue(\SplPriorityQueue $queue, ObjectId $id, array &$visited): void + private function enqueue(array &$queue, ObjectId $id, array &$visited): void { if (isset($visited[$id->hex])) { return; @@ -89,9 +77,23 @@ private function enqueue(\SplPriorityQueue $queue, ObjectId $id, array &$visited $object = $this->objects->read($id); - if ($object instanceof Commit) { - $timestamp = $object->committerTimestamp() ?? 0; - $queue->insert(['id' => $id, 'commit' => $object], $timestamp); + if (!$object instanceof Commit) { + return; } + + $timestamp = $object->committerTimestamp() ?? 0; + $entry = ['timestamp' => $timestamp, 'commit' => $object]; + + $count = count($queue); + + for ($i = 0; $i < $count; $i++) { + if ($queue[$i]['timestamp'] < $timestamp) { + array_splice($queue, $i, 0, [$entry]); + + return; + } + } + + $queue[] = $entry; } } diff --git a/src/Graph/Grep.php b/src/Graph/Grep.php index c4e85d01..a0d98427 100644 --- a/src/Graph/Grep.php +++ b/src/Graph/Grep.php @@ -26,6 +26,7 @@ public function __construct(private readonly ObjectDatabase $objects) /** * Search for a pattern in all files in a tree. * + * @param array{regex?: bool, ignore_case?: bool} $options * @return array */ public function grep(ObjectId $treeId, string $pattern, string $prefix = '', array $options = []): array diff --git a/src/Lfs/LfsClient.php b/src/Lfs/LfsClient.php index daffbedd..7273dfc0 100644 --- a/src/Lfs/LfsClient.php +++ b/src/Lfs/LfsClient.php @@ -122,24 +122,41 @@ private function batch(string $operation, array $objects): array $data = json_decode($response, true); - if (!is_array($data) || !isset($data['objects'])) { + if (!is_array($data) || !isset($data['objects']) || !is_array($data['objects'])) { throw new ProtocolException('Invalid LFS batch response'); } $results = []; foreach ($data['objects'] as $obj) { + if (!is_array($obj)) { + continue; + } + $href = null; + $actions = $obj['actions'] ?? null; + + if (is_array($actions) && isset($actions[$operation]) && is_array($actions[$operation])) { + $candidate = $actions[$operation]['href'] ?? null; + + if (is_string($candidate)) { + $href = $candidate; + } + } + + $errorBag = $obj['error'] ?? null; + $error = null; - if (isset($obj['actions'][$operation]['href'])) { - $href = $obj['actions'][$operation]['href']; + if (is_array($errorBag) && isset($errorBag['message']) && is_string($errorBag['message'])) { + $error = $errorBag['message']; } - $error = $obj['error']['message'] ?? null; + $oid = $obj['oid'] ?? ''; + $size = $obj['size'] ?? 0; $results[] = [ - 'oid' => $obj['oid'] ?? '', - 'size' => $obj['size'] ?? 0, + 'oid' => is_string($oid) ? $oid : '', + 'size' => is_int($size) ? $size : 0, 'href' => $href, 'error' => $error, ]; diff --git a/src/Merge/MergeBase.php b/src/Merge/MergeBase.php index a322f734..0b1db57e 100644 --- a/src/Merge/MergeBase.php +++ b/src/Merge/MergeBase.php @@ -192,9 +192,9 @@ private function parentsOf(string $hex): array return []; } - return array_map( + return array_values(array_map( static fn (ObjectId $parent): string => $parent->hex, $commit->parents, - ); + )); } } diff --git a/src/Object/ObjectId.php b/src/Object/ObjectId.php index 0a3e49e3..801a4b70 100644 --- a/src/Object/ObjectId.php +++ b/src/Object/ObjectId.php @@ -83,8 +83,13 @@ public static function compute(ObjectType $type, string $content, string $algo = { $header = $type->value . ' ' . strlen($content) . "\0"; $hex = hash($algo, $header . $content); + $binary = hex2bin($hex); - return new self($hex, hex2bin($hex), $algo); + if ($binary === false) { + throw new \RuntimeException("hash({$algo}) produced invalid hex: {$hex}"); + } + + return new self($hex, $binary, $algo); } /** diff --git a/src/Pack/DeltaResolver.php b/src/Pack/DeltaResolver.php index 5a89228f..efbb7215 100644 --- a/src/Pack/DeltaResolver.php +++ b/src/Pack/DeltaResolver.php @@ -17,8 +17,12 @@ final class DeltaResolver { public static function maxChainDepth(): int { - return defined('PITMASTER_MAX_DELTA_CHAIN') - ? (int) constant('PITMASTER_MAX_DELTA_CHAIN') - : 50; + if (!defined('PITMASTER_MAX_DELTA_CHAIN')) { + return 50; + } + + $value = constant('PITMASTER_MAX_DELTA_CHAIN'); + + return is_int($value) ? $value : 50; } } diff --git a/src/Pack/PackFile.php b/src/Pack/PackFile.php index a265c1c9..db42df37 100644 --- a/src/Pack/PackFile.php +++ b/src/Pack/PackFile.php @@ -107,9 +107,7 @@ public function readAtOffset(int $offset, ?string $expectedHash = null): GitObje */ private function resolveAtOffset(int $offset, int $depth): array { - $maxDepth = defined('PITMASTER_MAX_DELTA_CHAIN') - ? (int) constant('PITMASTER_MAX_DELTA_CHAIN') - : 50; + $maxDepth = DeltaResolver::maxChainDepth(); if ($depth > $maxDepth) { throw PackParseException::deltaChainTooDeep($depth, $maxDepth); @@ -119,16 +117,28 @@ private function resolveAtOffset(int $offset, int $depth): array if (!$entry->isDelta()) { $type = $entry->objectType(); + + if ($type === null) { + throw PackParseException::invalidDeltaBase("invalid base type at offset {$offset}"); + } + $content = $this->readCompressedData($entry->dataOffset, $entry->uncompressedSize); return ['type' => $type, 'content' => $content]; } if ($entry->isOfsDelta()) { + if ($entry->baseOffset === null) { + throw PackParseException::invalidDeltaBase("ofs-delta missing baseOffset at {$offset}"); + } + $baseOffset = $entry->entryOffset - $entry->baseOffset; $base = $this->resolveAtOffset($baseOffset, $depth + 1); } else { - // REF_DELTA: look up the base by hash + if ($entry->baseHash === null) { + throw PackParseException::invalidDeltaBase("ref-delta missing baseHash at {$offset}"); + } + $basePackOffset = $this->index->findOffset($entry->baseHash); if ($basePackOffset === null) { diff --git a/src/Pack/PackIndexer.php b/src/Pack/PackIndexer.php index ecad5a5b..592b05c1 100644 --- a/src/Pack/PackIndexer.php +++ b/src/Pack/PackIndexer.php @@ -191,9 +191,7 @@ private static function resolveObjectAtOffset( array &$hashToOffset, int $depth = 0, ): array { - $maxDepth = defined('PITMASTER_MAX_DELTA_CHAIN') - ? (int) constant('PITMASTER_MAX_DELTA_CHAIN') - : 50; + $maxDepth = DeltaResolver::maxChainDepth(); if ($depth > $maxDepth) { throw PackParseException::deltaChainTooDeep($depth, $maxDepth); diff --git a/src/Pack/PackWriter.php b/src/Pack/PackWriter.php index 75fbf605..5b776b42 100644 --- a/src/Pack/PackWriter.php +++ b/src/Pack/PackWriter.php @@ -6,6 +6,7 @@ use Pitmaster\Object\GitObject; use Pitmaster\Storage\ObjectSerializer; +use RuntimeException; /** * Pack file writer. Creates .pack and .idx files from a set of objects. @@ -72,13 +73,18 @@ private static function buildPack(array $objects): array $entryHeader = ''; while ($size > 0) { - $entryHeader .= chr($byte | 0x80); + $entryHeader .= chr(($byte | 0x80) & 0xFF); $byte = $size & 0x7F; $size >>= 7; } - $entryHeader .= chr($byte); + $entryHeader .= chr($byte & 0xFF); $compressed = zlib_encode($raw, ZLIB_ENCODING_DEFLATE); + + if ($compressed === false) { + throw new RuntimeException("Failed to zlib-encode pack entry for {$object->id->hex}"); + } + $entryData = $entryHeader . $compressed; $crc = crc32($entryData); diff --git a/src/Protocol/Bundle.php b/src/Protocol/Bundle.php index 60a978a8..a6a86b15 100644 --- a/src/Protocol/Bundle.php +++ b/src/Protocol/Bundle.php @@ -102,6 +102,9 @@ public static function parse(string $data): self /** * Create a bundle from refs and pack data. + * + * @param array $refs + * @param array $prerequisites */ public static function create(array $refs, string $packData, array $prerequisites = []): self { diff --git a/src/Protocol/GitProtocolClient.php b/src/Protocol/GitProtocolClient.php index 45390b7d..48e6daa7 100644 --- a/src/Protocol/GitProtocolClient.php +++ b/src/Protocol/GitProtocolClient.php @@ -224,7 +224,13 @@ private function readUntilFlush($socket): string $remaining = $lineLen - 4; while (strlen($payload) < $remaining) { - $chunk = fread($socket, $remaining - strlen($payload)); + $toRead = $remaining - strlen($payload); + + if ($toRead < 1) { + break; + } + + $chunk = fread($socket, $toRead); if ($chunk === false) { break 2; diff --git a/src/Protocol/PktLine.php b/src/Protocol/PktLine.php index 70d5ba6e..542610e4 100644 --- a/src/Protocol/PktLine.php +++ b/src/Protocol/PktLine.php @@ -143,7 +143,13 @@ public static function readFromStream($stream): array $payload = ''; while (strlen($payload) < $payloadLen) { - $chunk = fread($stream, $payloadLen - strlen($payload)); + $remaining = $payloadLen - strlen($payload); + + if ($remaining < 1) { + break; + } + + $chunk = fread($stream, $remaining); if ($chunk === false || $chunk === '') { throw new ProtocolException('Truncated pkt-line stream'); diff --git a/src/Protocol/ProtocolV1.php b/src/Protocol/ProtocolV1.php index 7246aaa3..27f23618 100644 --- a/src/Protocol/ProtocolV1.php +++ b/src/Protocol/ProtocolV1.php @@ -103,6 +103,7 @@ public static function buildFetchRequest( * Build a v1 push request for receive-pack. * * @param array $updates + * @param array $capabilities */ public static function buildPushRequest(array $updates, array $capabilities = self::DEFAULT_PUSH_CAPABILITIES): string { diff --git a/src/Protocol/SmartHttpClient.php b/src/Protocol/SmartHttpClient.php index 60b499c3..385b0986 100644 --- a/src/Protocol/SmartHttpClient.php +++ b/src/Protocol/SmartHttpClient.php @@ -18,9 +18,14 @@ final class SmartHttpClient implements UploadPackTransport, ReceivePackTransport public function __construct(?int $timeout = null) { - $this->timeout = $timeout ?? (defined('PITMASTER_HTTP_TIMEOUT') - ? (int) constant('PITMASTER_HTTP_TIMEOUT') - : 30); + if ($timeout !== null) { + $this->timeout = $timeout; + } elseif (defined('PITMASTER_HTTP_TIMEOUT')) { + $value = constant('PITMASTER_HTTP_TIMEOUT'); + $this->timeout = is_int($value) ? $value : 30; + } else { + $this->timeout = 30; + } } /** @@ -130,6 +135,9 @@ private function discoverServiceRefs(string $url, string $service, string $expec return RefDiscovery::parse($filtered); } + /** + * @param array $extraHeaders + */ private function get(string $url, string $expectedContentType, array $extraHeaders = []): string { $headers = array_merge(['User-Agent: Pitmaster/1.0'], $extraHeaders); @@ -164,6 +172,9 @@ private function get(string $url, string $expectedContentType, array $extraHeade return $response; } + /** + * @param array $extraHeaders + */ private function post( string $url, string $body, diff --git a/src/Protocol/SshClient.php b/src/Protocol/SshClient.php index 1d73cf00..dec8e59b 100644 --- a/src/Protocol/SshClient.php +++ b/src/Protocol/SshClient.php @@ -388,7 +388,13 @@ private function readExact($stream, int $bytes): ?string $buffer = ''; while (strlen($buffer) < $bytes) { - $chunk = fread($stream, $bytes - strlen($buffer)); + $toRead = $bytes - strlen($buffer); + + if ($toRead < 1) { + break; + } + + $chunk = fread($stream, $toRead); if ($chunk === false || $chunk === '') { return null; diff --git a/src/Protocol/UploadPackClient.php b/src/Protocol/UploadPackClient.php index 27dda431..84d3e43e 100644 --- a/src/Protocol/UploadPackClient.php +++ b/src/Protocol/UploadPackClient.php @@ -86,7 +86,7 @@ public function fetchV2Result(string $url, array $wants, array $haves = [], ?int ]; } - $request = ProtocolV2::buildFetchRequest($wants, $haves, ProtocolV2::DEFAULT_FETCH_FEATURES, true, [], $depth); + $request = ProtocolV2::buildFetchRequest(array_values($wants), array_values($haves), ProtocolV2::DEFAULT_FETCH_FEATURES, true, [], $depth); if (!$this->transport instanceof SmartHttpClient) { throw new ProtocolException('Protocol v2 fetch requires smart HTTP transport'); } diff --git a/src/Ref/Notes.php b/src/Ref/Notes.php index b16311d1..589045c6 100644 --- a/src/Ref/Notes.php +++ b/src/Ref/Notes.php @@ -248,6 +248,9 @@ private function findNoteBlobId(ObjectId $treeId, ObjectId $commitId): ?ObjectId return null; } + /** + * @param array $parts + */ private function findBlobAtPath(ObjectId $treeId, array $parts): ?ObjectId { $tree = $this->objects->read($treeId); @@ -256,7 +259,13 @@ private function findBlobAtPath(ObjectId $treeId, array $parts): ?ObjectId return null; } - $entry = $tree->entry(array_shift($parts)); + $name = array_shift($parts); + + if ($name === null) { + return null; + } + + $entry = $tree->entry($name); if ($entry === null) { return null; @@ -302,59 +311,48 @@ private function flattenNotesTreeInto(ObjectId $treeId, string $prefix, array &$ */ private function buildNotesTree(array $entries): Tree { - $tree = []; ksort($entries); - foreach ($entries as $path => $blobId) { - $this->insertTreeEntry($tree, explode('/', $path), $blobId); - } - - return $this->writeTreeNode($tree); + return $this->writeTreeFromPaths($entries); } /** - * @param array $node - * @param array $parts + * @param array $entries Path -> blob ID */ - private function insertTreeEntry(array &$node, array $parts, ObjectId $blobId): void + private function writeTreeFromPaths(array $entries): Tree { - $name = array_shift($parts); + $direct = []; + $subDirs = []; - if ($name === null) { - return; - } - - if ($parts === []) { - $node[$name] = $blobId; - return; - } + foreach ($entries as $path => $blobId) { + $slashPos = strpos($path, '/'); - $node[$name] ??= []; - $this->insertTreeEntry($node[$name], $parts, $blobId); - } + if ($slashPos === false) { + $direct[$path] = $blobId; + continue; + } - /** - * @param array $node - */ - private function writeTreeNode(array $node): Tree - { - ksort($node); - $entries = []; + $dirName = substr($path, 0, $slashPos); + $rest = substr($path, $slashPos + 1); + $subDirs[$dirName] ??= []; + $subDirs[$dirName][$rest] = $blobId; + } - foreach ($node as $name => $value) { - $name = (string) $name; + $treeEntries = []; - if ($value instanceof ObjectId) { - $entries[] = new TreeEntry('100644', $name, $value); - continue; - } + foreach ($direct as $name => $blobId) { + $treeEntries[$name] = new TreeEntry('100644', $name, $blobId); + } - $tree = $this->writeTreeNode($value); - $this->objects->write($tree); - $entries[] = new TreeEntry('40000', $name, $tree->id); + foreach ($subDirs as $dirName => $subEntries) { + $subTree = $this->writeTreeFromPaths($subEntries); + $this->objects->write($subTree); + $treeEntries[$dirName] = new TreeEntry('40000', $dirName, $subTree->id); } - return Tree::fromEntries($entries); + ksort($treeEntries); + + return Tree::fromEntries(array_values($treeEntries)); } private function currentIdentity(): string diff --git a/src/Ref/PackedRefStore.php b/src/Ref/PackedRefStore.php index 3167a34e..f6ea0200 100644 --- a/src/Ref/PackedRefStore.php +++ b/src/Ref/PackedRefStore.php @@ -16,12 +16,14 @@ */ final class PackedRefStore implements RefStore { - /** @var array|null */ - private ?array $refs = null; + /** @var array */ + private array $refs = []; /** @var array Peeled values for tag refs */ private array $peeled = []; + private bool $loaded = false; + public function __construct(private readonly string $gitDir) { } @@ -119,15 +121,16 @@ public function replace(array $refs, array $peeled = []): void { $this->refs = $refs; $this->peeled = $peeled; + $this->loaded = true; } private function ensureLoaded(): void { - if ($this->refs !== null) { + if ($this->loaded) { return; } - $this->refs = []; + $this->loaded = true; $path = $this->gitDir . '/packed-refs'; if (!is_file($path)) { diff --git a/src/Ref/Reflog.php b/src/Ref/Reflog.php index fbd15ce1..7f3795e9 100644 --- a/src/Ref/Reflog.php +++ b/src/Ref/Reflog.php @@ -104,6 +104,8 @@ public function entries(): array /** * Get the most recent entry. + * + * @return array{old: string, new: string, identity: string, message: string}|null */ public function latest(): ?array { diff --git a/src/Repository.php b/src/Repository.php index bcb83a08..a6263864 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -779,10 +779,10 @@ public function logOneline(int $limit = 50, bool $all = false, ?string $path = n ? $this->logPath($path, $limit) : ($all ? $this->logAll($limit) : $this->log($limit)); - return array_map( + return array_values(array_map( fn (Commit $commit): string => substr($commit->id->hex, 0, 7) . ' ' . $this->subjectLine($commit->message), $commits, - ); + )); } /** @@ -943,7 +943,7 @@ public function mv(string $source, string $destination): void */ public function remove(string ...$paths): void { - ['cached' => $cached, 'recursive' => $recursive, 'paths' => $paths] = $this->parseRemoveArguments($paths); + ['cached' => $cached, 'recursive' => $recursive, 'paths' => $paths] = $this->parseRemoveArguments(array_values($paths)); if ($paths === []) { throw new \RuntimeException('No pathspec given for remove'); @@ -957,7 +957,7 @@ public function remove(string ...$paths): void $index = $this->index(); $headId = $this->refs->resolveHead(); $headEntries = $this->flattenTreeEntries($headId !== null ? $this->getCommitTree($headId) : null); - $trackedEntries = $this->trackedEntriesForPaths($index, $paths); + $trackedEntries = $this->trackedEntriesForPaths($index, array_values($paths)); $pathsToRemove = []; foreach ($paths as $path) { @@ -999,7 +999,7 @@ public function removeCached(string ...$paths): void $index = $this->index(); $headId = $this->refs->resolveHead(); $headEntries = $this->flattenTreeEntries($headId !== null ? $this->getCommitTree($headId) : null); - $trackedEntries = $this->trackedEntriesForPaths($index, $paths); + $trackedEntries = $this->trackedEntriesForPaths($index, array_values($paths)); $pathsToRemove = []; foreach ($paths as $path) { @@ -1149,7 +1149,12 @@ public function cherryPick(string $revision): ObjectId $id = $this->resolve($revision); $commit = $this->objects->read($id); $headId = $this->refs->resolveHead(); - $headCommit = $headId !== null ? $this->objects->read($headId) : null; + + if ($headId === null) { + throw new \RuntimeException('Cannot cherry-pick: HEAD is not set'); + } + + $headCommit = $this->objects->read($headId); if (!$commit instanceof Commit || !$headCommit instanceof Commit) { throw new \RuntimeException("Not a commit: {$revision}"); @@ -1227,7 +1232,12 @@ public function revert(string $revision): ObjectId $id = $this->resolve($revision); $commit = $this->objects->read($id); $headId = $this->refs->resolveHead(); - $headCommit = $headId !== null ? $this->objects->read($headId) : null; + + if ($headId === null) { + throw new \RuntimeException('Cannot revert: HEAD is not set'); + } + + $headCommit = $this->objects->read($headId); if (!$commit instanceof Commit || !$headCommit instanceof Commit) { throw new \RuntimeException("Not a commit: {$revision}"); @@ -1238,6 +1248,11 @@ public function revert(string $revision): ObjectId } $parentTree = $this->getCommitTree($commit->parents[0]); + + if ($parentTree === null) { + throw new \RuntimeException("Cannot revert: parent tree of {$commit->parents[0]->hex} not found"); + } + $message = "Revert \"{$this->subjectLine($commit->message)}\"\n\nThis reverts commit {$commit->id->hex}.\n"; $trackedPaths = $this->index()->paths(); $merge = $this->mergeTreeEntries( @@ -2535,69 +2550,62 @@ private function mergeBaseFinder(): MergeBase */ private function buildTreeFromIndex(Index $index): ObjectId { - // Group entries by directory - $root = []; + $entries = []; foreach ($index->entries() as $entry) { - $parts = explode('/', $entry->path); - $this->insertIntoTree($root, $parts, $entry); + $entries[$entry->path] = $entry; } - return $this->writeTreeRecursive($root); + return $this->writeTreeFromIndexEntries($entries); } /** - * @param array $node - * @param array $parts + * @param array $entries Path -> IndexEntry */ - private function insertIntoTree(array &$node, array $parts, IndexEntry $entry): void + private function writeTreeFromIndexEntries(array $entries): ObjectId { - if (count($parts) === 1) { - $node[$parts[0]] = $entry; + $direct = []; + $subDirs = []; - return; - } + foreach ($entries as $path => $entry) { + $slashPos = strpos($path, '/'); - $dir = array_shift($parts); + if ($slashPos === false) { + $direct[$path] = $entry; + continue; + } - if (!isset($node[$dir]) || !is_array($node[$dir])) { - $node[$dir] = []; + $dirName = substr($path, 0, $slashPos); + $rest = substr($path, $slashPos + 1); + $subDirs[$dirName] ??= []; + $subDirs[$dirName][$rest] = $entry; } - $this->insertIntoTree($node[$dir], $parts, $entry); - } + $treeEntries = []; - /** - * @param array $node - */ - private function writeTreeRecursive(array $node): ObjectId - { - $entries = []; + foreach ($direct as $name => $entry) { + $mode = match ($entry->mode) { + 0100755 => '100755', + 0120000 => '120000', + 0160000 => '160000', + default => '100644', + }; + $treeEntries[] = new TreeEntry($mode, $name, $entry->hash); + } - foreach ($node as $name => $value) { - if ($value instanceof IndexEntry) { - $mode = match ($value->mode) { - 0100755 => '100755', - 0120000 => '120000', - 0160000 => '160000', - default => '100644', - }; - $entries[] = new TreeEntry($mode, (string) $name, $value->hash); - } elseif (is_array($value)) { - $subtreeId = $this->writeTreeRecursive($value); - $entries[] = new TreeEntry('40000', (string) $name, $subtreeId); - } + foreach ($subDirs as $dirName => $subEntries) { + $subtreeId = $this->writeTreeFromIndexEntries($subEntries); + $treeEntries[] = new TreeEntry('40000', $dirName, $subtreeId); } - // Sort entries (git sorts trees with trailing / for directories) - usort($entries, function (TreeEntry $a, TreeEntry $b): int { + usort($treeEntries, function (TreeEntry $a, TreeEntry $b): int { $nameA = $a->isTree() ? $a->name . '/' : $a->name; $nameB = $b->isTree() ? $b->name . '/' : $b->name; return strcmp($nameA, $nameB); }); - $tree = Tree::fromEntries($entries, $this->objectHashAlgo()); + $tree = Tree::fromEntries($treeEntries, $this->objectHashAlgo()); $this->objects->write($tree); return $tree->id; @@ -2710,8 +2718,8 @@ private function flattenTreeEntriesInto(?ObjectId $treeId, string $prefix, array /** * Reset worktree and index to match a commit. - */ - /** + * + * @param array $pathsToPrune * @param array $preserveRefs */ private function resetWorktree(ObjectId $commitId, array $pathsToPrune = [], array $preserveRefs = []): void @@ -2868,6 +2876,8 @@ private function materializedTreeEntries(array $treeEntries, string $targetDir): /** * @param array{hash: string, mode: int} $treeEntry + * + * @phpstan-assert-if-true !null $entry */ private function canReuseResetEntry(?IndexEntry $entry, array $treeEntry, int $extendedFlags, ?int $scanTimeSec): bool { @@ -2976,18 +2986,18 @@ private function worktreeDiffersFromIndex(IndexEntry $entry, ?int $scanTimeSec = } /** - * @param array $stat + * @param array{ctime: int, mtime: int, dev: int, ino: int, uid: int, gid: int, size: int} $stat */ private function statMatchesIndexEntry(IndexEntry $entry, array $stat, int $mode): bool { - return $entry->ctimeSec === (int) $stat['ctime'] - && $entry->mtimeSec === (int) $stat['mtime'] - && $entry->dev === (int) $stat['dev'] - && $entry->ino === (int) $stat['ino'] + return $entry->ctimeSec === $stat['ctime'] + && $entry->mtimeSec === $stat['mtime'] + && $entry->dev === $stat['dev'] + && $entry->ino === $stat['ino'] && $entry->mode === $mode - && $entry->uid === (int) $stat['uid'] - && $entry->gid === (int) $stat['gid'] - && $entry->fileSize === (int) $stat['size']; + && $entry->uid === $stat['uid'] + && $entry->gid === $stat['gid'] + && $entry->fileSize === $stat['size']; } /** @@ -3373,6 +3383,9 @@ private function worktreeMode(string $path): ?int return is_executable($fullPath) ? 0100755 : 0100644; } + /** + * @param array $entries + */ private function buildTreeFromEntries(array $entries): ObjectId { $index = new Index($this->objectHashBytes()); @@ -3441,7 +3454,13 @@ private function buildVirtualMergeBaseTree(MergeBase $mergeBaseFinder, array $ba } if ($baseIds === []) { - return $this->getCommitTree($current); + $tree = $this->getCommitTree($current); + + if ($tree === null) { + throw new \RuntimeException("Cannot resolve commit tree for {$current->hex}"); + } + + return $tree; } $next = array_shift($baseIds); @@ -3480,7 +3499,13 @@ private function mergeBaseTreesRecursively( ObjectId $theirsId, ): ObjectId { if ($oursId->equals($theirsId)) { - return $this->getCommitTree($oursId); + $tree = $this->getCommitTree($oursId); + + if ($tree === null) { + throw new \RuntimeException("Cannot resolve commit tree for {$oursId->hex}"); + } + + return $tree; } $nestedBaseIds = $mergeBaseFinder->findAll($oursId, $theirsId); @@ -3528,7 +3553,12 @@ private function materializeMergeEntriesWithConflicts(array $merge): array $blob = Blob::fromContent($content, $this->objectHashAlgo()); $this->objects->write($blob); $stages = $merge['conflictEntries'][$path] ?? []; - $mode = $stages[2]['mode'] ?? $stages[3]['mode'] ?? $stages[1]['mode'] ?? 0100644; + $mode = match (true) { + isset($stages[2]) => $stages[2]['mode'], + isset($stages[3]) => $stages[3]['mode'], + isset($stages[1]) => $stages[1]['mode'], + default => 0100644, + }; $entries[$path] = [ 'hash' => $blob->id->hex, 'mode' => $mode, @@ -3551,7 +3581,7 @@ private function buildOctopusMergeMessage(array $branches): string } /** - * @return array{parents: array, author?: string, message?: string, type?: string}|null + * @return array{parents: array, author?: string|null, message?: string|null, type?: string}|null */ private function pendingOperationState(?ObjectId $headId): ?array { @@ -3609,7 +3639,7 @@ private function pendingOperationState(?ObjectId $headId): ?array } /** - * @param array{message?: string}|null $state + * @param array{message?: string|null}|null $state */ private function resolveCommitMessage(?string $message, ?array $state): string { @@ -3761,9 +3791,13 @@ private function mergeTreeEntries( if ($isRenameDestination && $base !== null && ($ours === null || $theirs === null)) { $conflictPaths[] = $path; $conflictEntries[$path] = $this->conflictStageEntries($base, $ours, $theirs); - $conflictContents[$path] = $ours !== null - ? $this->readBlobContent(ObjectId::fromHex($ours['hash'])) - : $this->readBlobContent(ObjectId::fromHex($theirs['hash'])); + $survivor = $ours ?? $theirs; + + if ($survivor === null) { + continue; + } + + $conflictContents[$path] = $this->readBlobContent(ObjectId::fromHex($survivor['hash'])); continue; } @@ -3794,15 +3828,19 @@ private function mergeTreeEntries( if ($ours === null || $theirs === null) { $conflictPaths[] = $path; $conflictEntries[$path] = $this->conflictStageEntries($base, $ours, $theirs); - $conflictContents[$path] = $ours !== null - ? $this->readBlobContent(ObjectId::fromHex($ours['hash'])) - : $this->readBlobContent(ObjectId::fromHex($theirs['hash'])); + $survivor = $ours ?? $theirs; + + if ($survivor === null) { + continue; + } + + $conflictContents[$path] = $this->readBlobContent(ObjectId::fromHex($survivor['hash'])); continue; } $baseContent = $baseHash !== null ? $this->readBlobContent(ObjectId::fromHex($baseHash)) : ''; - $oursContent = $this->readBlobContent(ObjectId::fromHex($oursHash)); - $theirsContent = $this->readBlobContent(ObjectId::fromHex($theirsHash)); + $oursContent = $this->readBlobContent(ObjectId::fromHex($ours['hash'])); + $theirsContent = $this->readBlobContent(ObjectId::fromHex($theirs['hash'])); if ( MyersDiff::isBinary($baseContent) @@ -4252,7 +4290,12 @@ private function continueRebaseSequence(): array while ($state['current'] < count($state['commits'])) { $headId = $this->refs->resolveHead(); - $headCommit = $headId !== null ? $this->objects->read($headId) : null; + + if ($headId === null) { + throw new \RuntimeException('Cannot continue rebase: HEAD is not set'); + } + + $headCommit = $this->objects->read($headId); $replayId = $state['commits'][$state['current']]; $commit = $this->objects->read($replayId); @@ -4413,8 +4456,13 @@ private function readRebaseState(): ?array } $commits = []; + $rawCommits = $state['commits'] ?? []; - foreach (($state['commits'] ?? []) as $hex) { + if (!is_array($rawCommits)) { + return null; + } + + foreach ($rawCommits as $hex) { if (!is_string($hex)) { return null; } @@ -4422,15 +4470,20 @@ private function readRebaseState(): ?array $commits[] = ObjectId::fromHex($hex); } - if (!is_string($state['headName'] ?? null) || !is_string($state['origHead'] ?? null) || !is_string($state['onto'] ?? null)) { + $headName = $state['headName'] ?? null; + $origHead = $state['origHead'] ?? null; + $onto = $state['onto'] ?? null; + $current = $state['current'] ?? 0; + + if (!is_string($headName) || !is_string($origHead) || !is_string($onto)) { return null; } return [ - 'headName' => $state['headName'], - 'origHead' => ObjectId::fromHex($state['origHead']), - 'onto' => ObjectId::fromHex($state['onto']), - 'current' => (int) ($state['current'] ?? 0), + 'headName' => $headName, + 'origHead' => ObjectId::fromHex($origHead), + 'onto' => ObjectId::fromHex($onto), + 'current' => is_int($current) ? $current : 0, 'commits' => $commits, ]; } @@ -4497,6 +4550,10 @@ private function clearRebaseState(): void ); foreach ($iterator as $path) { + if (!$path instanceof \SplFileInfo) { + continue; + } + if ($path->isDir()) { rmdir($path->getPathname()); continue; @@ -5128,8 +5185,14 @@ private function identityName(string $role): string } foreach ($this->identityNameConstants($role) as $constant) { - if (defined($constant) && trim((string) constant($constant)) !== '') { - return trim((string) constant($constant)); + if (!defined($constant)) { + continue; + } + + $value = constant($constant); + + if (is_string($value) && trim($value) !== '') { + return trim($value); } } @@ -5147,8 +5210,14 @@ private function identityEmail(string $role): string } foreach ($this->identityEmailConstants($role) as $constant) { - if (defined($constant) && trim((string) constant($constant)) !== '') { - return trim((string) constant($constant)); + if (!defined($constant)) { + continue; + } + + $value = constant($constant); + + if (is_string($value) && trim($value) !== '') { + return trim($value); } } @@ -5206,7 +5275,7 @@ private function identityEmailConstants(string $role): array } /** - * @param array{message?: string, type?: string}|null $state + * @param array{message?: string|null, type?: string}|null $state */ private function prepareCommitMessage(string $message, ?array $state): string { diff --git a/src/Stash/Stash.php b/src/Stash/Stash.php index 8ac3a108..6e51043e 100644 --- a/src/Stash/Stash.php +++ b/src/Stash/Stash.php @@ -62,7 +62,8 @@ public function push(string $message = '', bool $includeUntracked = false): Obje } $branch = $this->currentBranch(); - $headSummary = substr($headId->hex, 0, 7) . ' ' . trim(strtok($headCommit->message, "\n")); + $firstLine = strtok($headCommit->message, "\n"); + $headSummary = substr($headId->hex, 0, 7) . ' ' . trim($firstLine === false ? '' : $firstLine); $stashMessage = $message !== '' ? "On {$branch}: {$message}" : "WIP on {$branch}: {$headSummary}"; @@ -270,14 +271,13 @@ private function normalizeDate(string $date): string private function buildTreeFromIndex(Index $index): ObjectId { - $root = []; + $entries = []; foreach ($index->entries() as $entry) { - $parts = explode('/', $entry->path); - $this->insertIntoTreeNode($root, $parts, $entry); + $entries[$entry->path] = $entry; } - return $this->writeTreeNode($root); + return $this->writeTreeFromPaths($entries); } /** @@ -291,7 +291,7 @@ private function buildTreeFromWorktree( array &$dirtyPaths = [], array &$includedUntrackedPaths = [], ): ObjectId { - $root = []; + $entries = []; $modified = []; $deleted = []; $untracked = []; @@ -331,8 +331,7 @@ private function buildTreeFromWorktree( } if (!isset($modified[$entry->path])) { - $parts = explode('/', $entry->path); - $this->insertIntoTreeNode($root, $parts, $entry); + $entries[$entry->path] = $entry; continue; } @@ -343,8 +342,7 @@ private function buildTreeFromWorktree( && $pathStatus->worktree === FileStatus::Unmodified && $pathStatus->index !== FileStatus::Deleted ) { - $parts = explode('/', $entry->path); - $this->insertIntoTreeNode($root, $parts, $entry); + $entries[$entry->path] = $entry; continue; } @@ -357,8 +355,7 @@ private function buildTreeFromWorktree( $content = file_get_contents($fullPath); $blob = Blob::fromContent($content !== false ? $content : '', $this->hashAlgo()); $this->objects->write($blob); - $parts = explode('/', $entry->path); - $this->insertIntoTreeNode($root, $parts, IndexEntry::create($entry->path, $blob->id, $entry->mode)); + $entries[$entry->path] = IndexEntry::create($entry->path, $blob->id, $entry->mode); } if ($includeUntracked) { @@ -372,52 +369,53 @@ private function buildTreeFromWorktree( $content = file_get_contents($fullPath); $blob = Blob::fromContent($content !== false ? $content : '', $this->hashAlgo()); $this->objects->write($blob); - $worktreeEntry = IndexEntry::fromStat($path, $blob->id, $fullPath); - $parts = explode('/', $path); - $this->insertIntoTreeNode($root, $parts, $worktreeEntry); + $entries[$path] = IndexEntry::fromStat($path, $blob->id, $fullPath); } } - return $this->writeTreeNode($root); + return $this->writeTreeFromPaths($entries); } - private function insertIntoTreeNode(array &$node, array $parts, IndexEntry $entry): void + /** + * @param array $entries Path -> IndexEntry + */ + private function writeTreeFromPaths(array $entries): ObjectId { - if (count($parts) === 1) { - $node[$parts[0]] = $entry; + $direct = []; + $subDirs = []; - return; - } + foreach ($entries as $path => $entry) { + $slashPos = strpos($path, '/'); - $dir = array_shift($parts); + if ($slashPos === false) { + $direct[$path] = $entry; + continue; + } - if (!isset($node[$dir]) || !is_array($node[$dir])) { - $node[$dir] = []; + $dirName = substr($path, 0, $slashPos); + $rest = substr($path, $slashPos + 1); + $subDirs[$dirName] ??= []; + $subDirs[$dirName][$rest] = $entry; } - $this->insertIntoTreeNode($node[$dir], $parts, $entry); - } + $treeEntries = []; - private function writeTreeNode(array $node): ObjectId - { - $entries = []; + foreach ($direct as $name => $entry) { + $mode = $entry->mode === 0100755 ? '100755' : '100644'; + $treeEntries[] = new TreeEntry($mode, $name, $entry->hash); + } - foreach ($node as $name => $value) { - if ($value instanceof IndexEntry) { - $mode = $value->mode === 0100755 ? '100755' : '100644'; - $entries[] = new TreeEntry($mode, (string) $name, $value->hash); - } elseif (is_array($value)) { - $subtreeId = $this->writeTreeNode($value); - $entries[] = new TreeEntry('40000', (string) $name, $subtreeId); - } + foreach ($subDirs as $dirName => $subEntries) { + $subtreeId = $this->writeTreeFromPaths($subEntries); + $treeEntries[] = new TreeEntry('40000', $dirName, $subtreeId); } - usort($entries, fn (TreeEntry $a, TreeEntry $b) => strcmp( + usort($treeEntries, fn (TreeEntry $a, TreeEntry $b) => strcmp( $a->isTree() ? $a->name . '/' : $a->name, $b->isTree() ? $b->name . '/' : $b->name, )); - $tree = Tree::fromEntries($entries, $this->hashAlgo()); + $tree = Tree::fromEntries($treeEntries, $this->hashAlgo()); $this->objects->write($tree); return $tree->id; diff --git a/src/Status/WorkingTreeStatus.php b/src/Status/WorkingTreeStatus.php index c655c3a8..4ee09688 100644 --- a/src/Status/WorkingTreeStatus.php +++ b/src/Status/WorkingTreeStatus.php @@ -177,18 +177,18 @@ private function worktreeFileChanged(IndexEntry $entry, string $path): bool } /** - * @param array $stat + * @param array{ctime: int, mtime: int, dev: int, ino: int, uid: int, gid: int, size: int} $stat */ private function statMatchesIndexEntry(IndexEntry $entry, array $stat, int $mode): bool { - return $entry->ctimeSec === (int) $stat['ctime'] - && $entry->mtimeSec === (int) $stat['mtime'] - && $entry->dev === (int) $stat['dev'] - && $entry->ino === (int) $stat['ino'] + return $entry->ctimeSec === $stat['ctime'] + && $entry->mtimeSec === $stat['mtime'] + && $entry->dev === $stat['dev'] + && $entry->ino === $stat['ino'] && $entry->mode === $mode - && $entry->uid === (int) $stat['uid'] - && $entry->gid === (int) $stat['gid'] - && $entry->fileSize === (int) $stat['size']; + && $entry->uid === $stat['uid'] + && $entry->gid === $stat['gid'] + && $entry->fileSize === $stat['size']; } /** diff --git a/src/Storage/ObjectSerializer.php b/src/Storage/ObjectSerializer.php index 98141343..1a18b480 100644 --- a/src/Storage/ObjectSerializer.php +++ b/src/Storage/ObjectSerializer.php @@ -28,8 +28,13 @@ final class ObjectSerializer public static function encode(GitObject $object): string { $raw = self::encodeRaw($object); + $compressed = zlib_encode($raw, ZLIB_ENCODING_DEFLATE); - return zlib_encode($raw, ZLIB_ENCODING_DEFLATE); + if ($compressed === false) { + throw new \RuntimeException("Failed to zlib-encode object {$object->id->hex}"); + } + + return $compressed; } /** diff --git a/src/Support/Json.php b/src/Support/Json.php index 29e06b8a..36fd6f67 100644 --- a/src/Support/Json.php +++ b/src/Support/Json.php @@ -8,6 +8,9 @@ final class Json { + /** + * @return array + */ public static function decodeFile(string $path): array { $content = @file_get_contents($path); @@ -25,6 +28,9 @@ public static function decodeFile(string $path): array return $decoded; } + /** + * @param array $payload + */ public static function encodeFile(string $path, array $payload): void { $directory = dirname($path); diff --git a/src/Support/PathFingerprint.php b/src/Support/PathFingerprint.php index aa9fbc21..e6fc1635 100644 --- a/src/Support/PathFingerprint.php +++ b/src/Support/PathFingerprint.php @@ -62,7 +62,7 @@ private static function updateHashForPath($hash, string $path): void ); foreach ($iterator as $fileInfo) { - if (!$fileInfo->isFile()) { + if (!$fileInfo instanceof \SplFileInfo || !$fileInfo->isFile()) { continue; }