Skip to content

Commit 08fa491

Browse files
authored
Internal improvements 37 (#106)
* Add Split Include template property tests and implement related classes * Enhance ViolationCollector and ViolationRecord to support occurrence counting and caller tracking; update tests to verify new metadata fields * Add report:merge command to merge JSON report files and update CommandRunner and HelpCommand; enhance tests for new functionality * Fix missing newlines at the end of several PHP files and update visibility of findDeclaredPropertyDoc method in DocblockParser * Refactor ViolationCollector to improve violation handling and reporting; consolidate shard content and enhance metadata extraction * Enhance ViolationCollector with property line caching and improve declaration line retrieval logic * fix code styling * Refactor ViolationRecord to use pre 8.2 readonly syntax without marking the class as readonly * Fix directory case sensitivity in phpunit.xml exclusion for Internal/Cli
1 parent 8c3b01d commit 08fa491

18 files changed

Lines changed: 1116 additions & 133 deletions

‎phpunit.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</include>
1515
<exclude>
1616
<file>src/bootstrap.php</file>
17-
<directory suffix=".php">src/internal/Cli</directory>
17+
<directory>src/Internal/Cli</directory>
1818
</exclude>
1919
</source>
2020
</phpunit>

‎src/Internal/Cli/CommandRunner.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final class CommandRunner
99
private const KNOWN_COMMANDS = [
1010
'config:init',
1111
'report',
12+
'report:merge',
1213
'report:clear',
1314
'cache:clear',
1415
'cache:warm',
@@ -47,6 +48,10 @@ public static function run(array $args, $outputStream = STDOUT, $errorStream = S
4748
return (new ReportCommand())->execute($args, $outputStream, $errorStream);
4849
}
4950

51+
if ($firstArg === 'report:merge') {
52+
return (new ReportMergeCommand())->execute($args, $outputStream, $errorStream);
53+
}
54+
5055
if ($firstArg === 'report:clear') {
5156
return (new ReportClearCommand())->execute($args, $outputStream, $errorStream);
5257
}

‎src/Internal/Cli/HelpCommand.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
1616
fwrite($outputStream, ' ' . $c('COMMANDS', 'yellow') . "\n");
1717
fwrite($outputStream, ' ' . $c('config:init', 'green') . " Generate default typephp.php configuration file\n");
1818
fwrite($outputStream, ' ' . $c('report', 'green') . " Display audit report summary in terminal\n");
19+
fwrite($outputStream, ' ' . $c('report:merge', 'green') . " Merge multiple JSON report files into a unified report\n");
1920
fwrite($outputStream, ' ' . $c('report:clear', 'green') . " Delete generated report file and shards\n");
2021
fwrite($outputStream, ' ' . $c('cache:clear', 'green') . " Clear all cached transformed files\n");
2122
fwrite($outputStream, ' ' . $c('cache:warm', 'green') . " Pre-transform and warm up cache for included files\n");
@@ -24,6 +25,7 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
2425
fwrite($outputStream, ' ' . $c('EXAMPLES', 'yellow') . "\n");
2526
fwrite($outputStream, " vendor/bin/typephp config:init\n");
2627
fwrite($outputStream, " vendor/bin/typephp report\n");
28+
fwrite($outputStream, " vendor/bin/typephp report:merge module1.json module2.json --output=all.json\n");
2729
fwrite($outputStream, " vendor/bin/typephp index.php\n");
2830
fwrite($outputStream, " vendor/bin/typephp cache:rebuild\n\n");
2931

‎src/Internal/Cli/ReportCommand.php‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,24 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
8181
$target = isset($item['target']) && \is_string($item['target']) ? $item['target'] : '';
8282
$expected = isset($item['expected']) && \is_string($item['expected']) ? $item['expected'] : '';
8383
$given = isset($item['given']) && \is_string($item['given']) ? $item['given'] : '';
84+
$count = isset($item['count']) && \is_int($item['count']) ? $item['count'] : 1;
85+
$caller = isset($item['caller']) && \is_string($item['caller']) ? $item['caller'] : null;
86+
$declaredIn = isset($item['declared_in']) && \is_string($item['declared_in']) ? $item['declared_in'] : null;
8487

8588
$lineFormatted = str_pad("Line {$line}", 10);
8689
$kindFormatted = str_pad($kind, 11);
8790
$targetFormatted = str_pad($target, 12);
91+
$countFormatted = $count > 1 ? $c(" (x{$count})", 'yellow') : '';
8892

89-
fwrite($outputStream, ' ' . $c($lineFormatted, 'gray') . ' ' . $c($kindFormatted, 'yellow') . ' ' . $c($targetFormatted, 'cyan') . ' expected ' . $c($expected, 'green') . ', ' . $c($given, 'red') . "\n");
93+
fwrite($outputStream, ' ' . $c($lineFormatted, 'gray') . ' ' . $c($kindFormatted, 'yellow') . ' ' . $c($targetFormatted, 'cyan') . ' expected ' . $c($expected, 'green') . ', ' . $c($given, 'red') . $countFormatted . "\n");
94+
95+
if ($caller !== null) {
96+
fwrite($outputStream, ' ' . $c('↳ caller:', 'gray') . ' ' . $c($caller, 'gray') . "\n");
97+
}
98+
99+
if ($declaredIn !== null) {
100+
fwrite($outputStream, ' ' . $c('↳ declared in:', 'gray') . ' ' . $c($declaredIn, 'gray') . "\n");
101+
}
90102
}
91103

92104
fwrite($outputStream, "\n");
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Internal\Cli;
6+
7+
use TypePHP\Internal\Reporting\ViolationRecord;
8+
9+
/**
10+
* @internal Merges multiple JSON report files into a unified report with aggregated occurrence counts.
11+
*/
12+
final class ReportMergeCommand implements CommandInterface
13+
{
14+
public function execute(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
15+
{
16+
$c = [CliFormatter::class, 'color'];
17+
18+
$inputFiles = [];
19+
$outputFile = null;
20+
21+
for ($i = 0; $i < \count($args); $i++) {
22+
$arg = $args[$i];
23+
24+
if ($arg === 'report:merge') {
25+
continue;
26+
}
27+
28+
if (str_starts_with($arg, '--output=')) {
29+
$outputFile = substr($arg, 9);
30+
31+
continue;
32+
}
33+
34+
if ($arg === '-o' && isset($args[$i + 1])) {
35+
$outputFile = $args[$i + 1];
36+
$i++;
37+
38+
continue;
39+
}
40+
41+
if (! str_starts_with($arg, '-')) {
42+
$inputFiles[] = $arg;
43+
}
44+
}
45+
46+
if ($inputFiles === []) {
47+
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
48+
fwrite($errorStream, ' ' . $c('✗', 'red') . " No input report files specified.\n\n");
49+
fwrite($errorStream, ' ' . $c('Usage:', 'yellow') . "\n");
50+
fwrite($errorStream, " vendor/bin/typephp report:merge <file1.json> <file2.json> [--output=<merged.json>]\n\n");
51+
52+
return 1;
53+
}
54+
55+
/** @var array<string, ViolationRecord> $mergedViolations */
56+
$mergedViolations = [];
57+
$validFileCount = 0;
58+
59+
foreach ($inputFiles as $file) {
60+
if (! file_exists($file)) {
61+
fwrite($errorStream, ' ' . $c('!', 'yellow') . ' Warning: Report file ' . $c('"' . $file . '"', 'bold') . " not found, skipping.\n");
62+
63+
continue;
64+
}
65+
66+
$rawContent = @file_get_contents($file);
67+
if ($rawContent === false || trim($rawContent) === '') {
68+
continue;
69+
}
70+
71+
/** @var array{violations?: list<array<string, mixed>>}|null $data */
72+
$data = json_decode($rawContent, true);
73+
if (! \is_array($data) || ! isset($data['violations']) || ! \is_array($data['violations'])) {
74+
continue;
75+
}
76+
77+
$validFileCount++;
78+
79+
foreach ($data['violations'] as $v) {
80+
if (\is_array($v)) {
81+
$record = ViolationRecord::fromArray($v);
82+
if ($record !== null) {
83+
$hash = $record->getHash();
84+
if (isset($mergedViolations[$hash])) {
85+
$mergedViolations[$hash] = $mergedViolations[$hash]->withIncrementedCount($record->count);
86+
} else {
87+
$mergedViolations[$hash] = $record;
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
$violationsList = array_values($mergedViolations);
95+
$filesAffected = [];
96+
foreach ($violationsList as $v) {
97+
$filesAffected[$v->file] = true;
98+
}
99+
100+
$document = [
101+
'version' => '1.0',
102+
'generated_at' => gmdate('c'),
103+
'summary' => [
104+
'total_violations' => \count($violationsList),
105+
'files_affected' => \count($filesAffected),
106+
],
107+
'violations' => $violationsList,
108+
];
109+
110+
$json = json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
111+
112+
if ($outputFile !== null && $json !== false) {
113+
$outputDir = \dirname($outputFile);
114+
if (! is_dir($outputDir)) {
115+
@mkdir($outputDir, 0777, true);
116+
}
117+
118+
file_put_contents($outputFile, $json);
119+
120+
fwrite($outputStream, "\n " . $c(' TYPEPHP ', 'badge') . ' ' . $c('Report Merge', 'bold') . "\n\n");
121+
fwrite($outputStream, ' ' . $c('✓', 'green') . ' Merged ' . $c((string) $validFileCount, 'bold') . ' report file(s) into ' . $c('"' . $outputFile . '"', 'bold') . ' (' . $c((string) \count($violationsList), 'bold') . " unique violation(s)).\n\n");
122+
123+
return 0;
124+
}
125+
126+
if ($json !== false) {
127+
fwrite($outputStream, $json . "\n");
128+
}
129+
130+
return 0;
131+
}
132+
}

‎src/Internal/Docblock/DocblockParser.php‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ public static function parseMagicPropertyContract(string $className, string $pro
594594
*
595595
* @return array{doc: string, declaringClass: \ReflectionClass<object>}|null
596596
*/
597-
private static function findDeclaredPropertyDoc(\ReflectionClass $refClass, string $propertyName): ?array
597+
public static function findDeclaredPropertyDoc(\ReflectionClass $refClass, string $propertyName): ?array
598598
{
599599
$current = $refClass;
600600
while ($current !== false) {
@@ -1227,14 +1227,9 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar
12271227

12281228
foreach ($classHierarchy as $hierClass) {
12291229
$hierClassName = $hierClass->getName();
1230-
$fileName = $hierClass->getFileName();
12311230
$stubDoc = StubManager::getClassDoc($hierClassName);
1232-
1233-
if ($stubDoc === null && FileFilter::isFileExcluded($fileName !== false ? $fileName : null)) {
1234-
continue;
1235-
}
1236-
12371231
$classDoc = $stubDoc ?? $hierClass->getDocComment();
1232+
12381233
if ($classDoc !== false && $classDoc !== null) {
12391234
$classPhpDocNode = DocblockExtractor::parseDocString($classDoc);
12401235

‎src/Internal/Generics/TemplateManager.php‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
use TypePHP\Internal\Resolver\SpecialTypeResolver;
3131
use TypePHP\Internal\Util\ClassNameValidator;
3232
use TypePHP\Internal\Util\Config;
33-
use TypePHP\Internal\Util\FileFilter;
3433
use TypePHP\Internal\Util\StubManager;
3534
use WeakMap;
3635

@@ -1033,14 +1032,6 @@ private static function computeClassInheritedBindings(string $actualClassName):
10331032
$classHierarchy = HierarchyResolver::getClassHierarchy($ref);
10341033

10351034
foreach ($classHierarchy as $hierClass) {
1036-
$fileName = $hierClass->getFileName();
1037-
$hierClassName = $hierClass->getName();
1038-
$stubDoc = StubManager::getClassDoc($hierClassName);
1039-
1040-
if ($stubDoc === null && $hierClass->getName() !== $actualClassName && FileFilter::isFileExcluded($fileName !== false ? $fileName : null)) {
1041-
continue;
1042-
}
1043-
10441035
$docsToInspect = self::collectDocsForClassHierarchyMember($hierClass);
10451036

10461037
foreach ($docsToInspect as $rawDoc) {

0 commit comments

Comments
 (0)