Skip to content

Commit 7cbd2c9

Browse files
committed
Implement cache directory exclusion in FileFilter and update tests
1 parent ecbfd2b commit 7cbd2c9

4 files changed

Lines changed: 59 additions & 7 deletions

File tree

src/Contract/FileFilter.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace TypePHP\Contract;
66

7+
use TypePHP\Internal\CacheManager;
78
use TypePHP\Internal\Config;
89

910
/**
@@ -28,6 +29,11 @@ public static function isFileExcluded(string|false|null $fileName): bool
2829
return true;
2930
}
3031

32+
$normalizedCacheDir = rtrim(str_replace('\\', '/', CacheManager::getCacheDir()), '/') . '/';
33+
if (str_starts_with($normalizedPath, $normalizedCacheDir)) {
34+
return true;
35+
}
36+
3137
$config = Config::get();
3238
/** @var array<mixed> $includes */
3339
$includes = \is_array($config['include'] ?? null) ? $config['include'] : ['**'];
@@ -62,7 +68,7 @@ public static function isFileExcluded(string|false|null $fileName): bool
6268
// Equal specificity tie-breaker: Exclude wins!
6369
return $longestExcludeMatch >= $longestIncludeMatch;
6470
}
65-
71+
6672
/**
6773
* Converts a glob pattern into an absolute regex pattern.
6874
*/

src/Internal/StreamWrapper.php

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ public static function transformSource(string $source, string $filePath = ''): s
117117
return $source;
118118
}
119119

120+
$originalLineCount = substr_count($source, "\n");
121+
120122
$parser = (new ParserFactory())->createForNewestSupportedVersion();
121123

122124
try {
@@ -150,9 +152,26 @@ public static function transformSource(string $source, string $filePath = ''): s
150152
$printer = new TypePHPPrinter();
151153
$transformed = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
152154

153-
// Critical: Remove the newline and indentation preceding any injected statement.
154-
$transformed = preg_replace('/[ \t]*\r?\n[ \t]*\/\*__TYPEPHP_INJECTED__\*\//', ' /*__TYPEPHP_INJECTED__*/', $transformed) ?? $transformed;
155-
$transformed = str_replace('/*__TYPEPHP_INJECTED__*/', '', $transformed);
155+
$transformed = preg_replace('/[ \t]*\r?\n[ \t]*\/\*__TYPEPHP_INJECTED_START__\*\//', ' /*__TYPEPHP_INJECTED_START__*/', $transformed) ?? $transformed;
156+
157+
$transformedLineCount = substr_count($transformed, "\n");
158+
$drift = $transformedLineCount - $originalLineCount;
159+
160+
if ($drift > 0) {
161+
$transformed = preg_replace('/[ \t]*\r?\n[ \t]*\{[ \t]*\/\*__TYPEPHP_INJECTED_START__\*\//', ' { /*__TYPEPHP_INJECTED_START__*/', $transformed, $drift, $count1) ?? $transformed;
162+
$drift -= $count1;
163+
}
164+
165+
if ($drift > 0) {
166+
$transformed = preg_replace('/\/\*__TYPEPHP_INJECTED_END__\*\/[ \t]*\r?\n[ \t]*\}/', '/*__TYPEPHP_INJECTED_END__*/ }', $transformed, $drift, $count2) ?? $transformed;
167+
$drift -= $count2;
168+
}
169+
170+
if ($drift > 0) {
171+
$transformed = preg_replace('/\/\*__TYPEPHP_INJECTED_END__\*\/[ \t]*\r?\n[ \t]*/', '/*__TYPEPHP_INJECTED_END__*/ ', $transformed, $drift) ?? $transformed;
172+
}
173+
174+
$transformed = str_replace(['/*__TYPEPHP_INJECTED_START__*/', '/*__TYPEPHP_INJECTED_END__*/'], '', $transformed);
156175

157176
return $transformed;
158177
}
@@ -485,7 +504,7 @@ private static function isReadOnlyCall(): bool
485504
return \in_array($callerFunc, ['file_get_contents', 'file', 'readfile', 'highlight_file', 'show_source', 'token_get_all'], true);
486505
}
487506

488-
/**
507+
/**
489508
* Determines whether a target PHP file path should be intercepted using Pattern Specificity.
490509
*/
491510
private static function isApplicationFile(string $path, string|false $resolvedPath): bool
@@ -500,13 +519,20 @@ private static function isApplicationFile(string $path, string|false $resolvedPa
500519

501520
$normalizedPath = str_replace('\\', '/', $resolvedPath);
502521

522+
// Prevent parsing TypePHP's own source code
503523
$parentDir = realpath(__DIR__ . '/..');
504524
$libSrcDir = $parentDir !== false ? str_replace('\\', '/', $parentDir) : '';
505525

506526
if ($libSrcDir !== '' && str_starts_with($normalizedPath, $libSrcDir)) {
507527
return false;
508528
}
509529

530+
// Unconditionally prevent double-parsing cached files!
531+
$normalizedCacheDir = rtrim(str_replace('\\', '/', self::$cacheDir), '/') . '/';
532+
if (str_starts_with($normalizedPath, $normalizedCacheDir)) {
533+
return false;
534+
}
535+
510536
$longestIncludeMatch = 0;
511537
foreach (self::$includeRawPatterns as $pattern => $regex) {
512538
if (preg_match($regex, $normalizedPath) === 1) {

src/Internal/TypePHPPrinter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final class TypePHPPrinter extends Standard
1818
{
1919
/**
2020
* Overrides base node printing to intercept injected statements, squash their
21-
* formatting, and tag them with a unique marker for post-processing.
21+
* formatting, and tag them with unique markers for post-processing.
2222
*/
2323
protected function p(
2424
Node $node,
@@ -31,7 +31,7 @@ protected function p(
3131
if ($node instanceof Node\Stmt && $node->getAttribute('typephp_injected') === true) {
3232
$output = preg_replace('/\s+/', ' ', trim($output)) ?? $output;
3333

34-
return '/*__TYPEPHP_INJECTED__*/' . $output;
34+
return '/*__TYPEPHP_INJECTED_START__*/' . $output . '/*__TYPEPHP_INJECTED_END__*/';
3535
}
3636

3737
return $output;

tests/Contract/FileFilterTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,24 @@
137137

138138
Config::reset();
139139
});
140+
141+
test('unconditionally excludes the configured cache directory even if include pattern is **', function () {
142+
$customCacheDir = getcwd() . '/storage/typephp-cache';
143+
144+
Config::set([
145+
'cache_dir' => $customCacheDir,
146+
'include' => [
147+
'**',
148+
],
149+
'exclude' => [],
150+
]);
151+
152+
$cachedFilePath = str_replace('\\', '/', $customCacheDir . '/v0.1_hash123.php');
153+
$normalFilePath = str_replace('\\', '/', getcwd() . '/app/Models/User.php');
154+
155+
expect(FileFilter::isFileExcluded($cachedFilePath))->toBeTrue()
156+
->and(FileFilter::isFileExcluded($normalFilePath))->toBeFalse();
157+
158+
Config::reset();
159+
});
140160
});

0 commit comments

Comments
 (0)