Skip to content

Commit ecbfd2b

Browse files
committed
Enhance caching configuration documentation and add cache directory option
1 parent 7a0aed3 commit ecbfd2b

5 files changed

Lines changed: 147 additions & 21 deletions

File tree

src/Command/ConfigInitCommand.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,18 @@ private static function getTemplate(): string
8787
8888
/*
8989
|--------------------------------------------------------------------------
90-
| Enable Caching
90+
| Enable Caching & Cache Directory
9191
|--------------------------------------------------------------------------
9292
| When enabled, transformed PHP files are cached on disk for speed.
9393
| Set to false to run AST transformations purely in RAM (php://memory).
94+
|
95+
| 'cache_dir' determines where these files are stored. By default (null),
96+
| it uses your system's temp directory. You can change this to a path
97+
| inside your project, e.g., __DIR__ . '/storage/framework/typephp'.
98+
| TypePHP will automatically protect this directory from being re-transformed.
9499
*/
95100
'cache' => true,
101+
'cache_dir' => null,
96102
97103
/*
98104
|--------------------------------------------------------------------------

src/Internal/Visitor/PropertyHookInjector.php

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,49 @@ public static function process(Node\Stmt\Property $node): void
4343
: 'value';
4444

4545
$checkCall = NodeBuilder::createPropertyCheckCall(new Node\Expr\Variable($paramName), new Node\Expr\Variable('this'), $propertyName);
46-
$paramCheckStmt = new Node\Stmt\Expression(
47-
new Node\Expr\Assign(
48-
new Node\Expr\Variable($paramName),
49-
NodeBuilder::createTernaryThrowExpr($checkCall)
50-
)
51-
);
52-
$paramCheckStmt->setAttribute('typephp_injected', true);
5346

5447
if (\is_array($hook->body)) {
48+
$paramCheckStmt = new Node\Stmt\Expression(
49+
new Node\Expr\Assign(
50+
new Node\Expr\Variable($paramName),
51+
NodeBuilder::createTernaryThrowExpr($checkCall)
52+
)
53+
);
54+
$paramCheckStmt->setAttribute('typephp_injected', true);
5555
array_unshift($hook->body, $paramCheckStmt);
5656
} elseif ($hook->body instanceof Node\Expr) {
57-
$hook->body = [
58-
$paramCheckStmt,
59-
new Node\Stmt\Expression($hook->body),
60-
];
57+
// Bypass php-parser formatting bugs by keeping short hooks as Expressions
58+
$hook->body = new Node\Expr\Ternary(
59+
new Node\Expr\Instanceof_(
60+
new Node\Expr\Assign(
61+
new Node\Expr\Variable('__typephpVal'),
62+
$checkCall
63+
),
64+
new Node\Name('\TypePHP\Internal\ErrorMessage')
65+
),
66+
new Node\Expr\Throw_(
67+
new Node\Expr\StaticCall(
68+
new Node\Name('\TypePHP\Internal\ErrorFactory'),
69+
'prepareException',
70+
[
71+
new Node\Arg(
72+
new Node\Expr\New_(
73+
new Node\Name('\TypePHP\Exception\TypeError'),
74+
[
75+
new Node\Arg(
76+
new Node\Expr\MethodCall(
77+
new Node\Expr\Variable('__typephpVal'),
78+
'getMessage'
79+
)
80+
),
81+
]
82+
)
83+
),
84+
]
85+
)
86+
),
87+
$hook->body // False branch evaluates the original assignment
88+
);
6189
}
6290
}
6391
}
@@ -96,4 +124,4 @@ public function enterNode(Node $node): int|null
96124

97125
return $newStmts;
98126
}
99-
}
127+
}

tests/Unit/LineNumberPreservationTest.php

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function number(int $number): int
2222
number(-5);
2323
PHP;
2424

25-
$transformed = StreamWrapper::transformSource($source, 'test4.php');
25+
$transformed = StreamWrapper::transformSource($source, 'test_params.php');
2626

2727
$origLines = explode("\n", str_replace("\r\n", "\n", $source));
2828
$transLines = explode("\n", str_replace("\r\n", "\n", $transformed));
@@ -56,7 +56,7 @@ public function __construct(public array $numbers)
5656
new Numbers(['a', 'b', 'c', 1]);
5757
PHP;
5858

59-
$transformed = StreamWrapper::transformSource($source, 'test8.php');
59+
$transformed = StreamWrapper::transformSource($source, 'test_cpm.php');
6060

6161
$origLines = explode("\n", str_replace("\r\n", "\n", $source));
6262
$transLines = explode("\n", str_replace("\r\n", "\n", $transformed));
@@ -68,4 +68,69 @@ public function __construct(public array $numbers)
6868

6969
expect($transCallLine)->toBe($origCallLine);
7070
});
71+
72+
test('transforms single-line empty methods and constructors without shifting line numbers', function () {
73+
$source = <<<'PHP'
74+
<?php
75+
76+
declare(strict_types=1);
77+
78+
class SingleLineBlocks
79+
{
80+
public function __construct() {}
81+
82+
public function emptyMethod(): void {}
83+
84+
/** @param string $val */
85+
public function doNothing(string $val) {}
86+
}
87+
88+
$obj = new SingleLineBlocks();
89+
PHP;
90+
91+
$transformed = StreamWrapper::transformSource($source, 'test_single_line.php');
92+
93+
$origLines = explode("\n", str_replace("\r\n", "\n", $source));
94+
$transLines = explode("\n", str_replace("\r\n", "\n", $transformed));
95+
96+
expect(\count($transLines))->toBe(\count($origLines));
97+
98+
$origCallLine = array_search('$obj = new SingleLineBlocks();', array_map('trim', $origLines), true);
99+
$transCallLine = array_search('$obj = new SingleLineBlocks();', array_map('trim', $transLines), true);
100+
101+
expect($transCallLine)->toBe($origCallLine);
102+
});
103+
104+
test('transforms generic single-line constructors perfectly (Edge Case Reproduction)', function () {
105+
$source = <<<'PHP'
106+
<?php
107+
108+
namespace App;
109+
110+
/**
111+
* Covariant Producer Wrapper with single-line constructor
112+
*
113+
* @template-covariant T
114+
*/
115+
class Producer
116+
{
117+
/** @param T $item */
118+
public function __construct(public mixed $item) {}
119+
}
120+
121+
$p = new Producer('test');
122+
PHP;
123+
124+
$transformed = StreamWrapper::transformSource($source, 'test_producer.php');
125+
126+
$origLines = explode("\n", str_replace("\r\n", "\n", $source));
127+
$transLines = explode("\n", str_replace("\r\n", "\n", $transformed));
128+
129+
expect(\count($transLines))->toBe(\count($origLines));
130+
131+
$origCallLine = array_search("\$p = new Producer('test');", array_map('trim', $origLines), true);
132+
$transCallLine = array_search("\$p = new Producer('test');", array_map('trim', $transLines), true);
133+
134+
expect($transCallLine)->toBe($origCallLine);
135+
});
71136
});

tests/Visitor/PropertyHookInjectorTest.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use TypePHP\Internal\Visitor\PropertyHookInjector;
1111

1212
describe('PropertyHookInjector Unit Tests', function () {
13-
test('wraps short get property hooks (get => $expr)', function () {
13+
test('wraps short get property hooks (get => $expr) in ternary', function () {
1414
$hook = new Node\PropertyHook(
1515
name: 'get',
1616
body: new Node\Scalar\String_('invalid')
@@ -27,7 +27,28 @@
2727
expect($prop->hooks[0]->body)->toBeInstanceOf(Node\Expr\Ternary::class);
2828
});
2929

30-
test('wraps set property hooks and injects paramCheckStmt with typephp_injected attribute', function () {
30+
test('wraps short set property hooks (set => $expr) in ternary to avoid parser bugs', function () {
31+
$hook = new Node\PropertyHook(
32+
name: 'set',
33+
body: new Node\Expr\Assign(
34+
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), 'title'),
35+
new Node\Expr\Variable('value')
36+
)
37+
);
38+
39+
$prop = new Node\Stmt\Property(
40+
flags: Node\Stmt\Class_::MODIFIER_PUBLIC,
41+
props: [new Node\PropertyItem('title')],
42+
hooks: [$hook]
43+
);
44+
45+
PropertyHookInjector::process($prop);
46+
47+
// The body should remain an expression (Ternary), not an array of statements
48+
expect($prop->hooks[0]->body)->toBeInstanceOf(Node\Expr\Ternary::class);
49+
});
50+
51+
test('injects paramCheckStmt into block set property hooks', function () {
3152
$hook = new Node\PropertyHook(
3253
name: 'set',
3354
body: [
@@ -54,4 +75,4 @@
5475
->and($body[0]->getAttribute('typephp_injected'))->toBeTrue()
5576
;
5677
});
57-
});
78+
});

typephp.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,20 @@
4242
*/
4343
'respect_ignore_tags' => true,
4444

45-
/*
45+
/*
4646
|--------------------------------------------------------------------------
47-
| Enable Caching
47+
| Enable Caching & Cache Directory
4848
|--------------------------------------------------------------------------
4949
| When enabled, transformed PHP files are cached on disk for speed.
5050
| Set to false to run AST transformations purely in RAM (php://memory).
51+
|
52+
| 'cache_dir' determines where these files are stored. By default (null),
53+
| it uses your system's temp directory. You can change this to a path
54+
| inside your project, e.g., __DIR__ . '/storage/framework/typephp'.
55+
| TypePHP will automatically protect this directory from being re-transformed.
5156
*/
5257
'cache' => true,
58+
'cache_dir' => null,
5359

5460
/*
5561
|--------------------------------------------------------------------------
@@ -99,7 +105,7 @@
99105
| You can use "*" glob to match any file.
100106
*/
101107
'include' => [
102-
'src/**',
108+
'*',
103109
'app/**',
104110
'internals/**',
105111
'tests/**',

0 commit comments

Comments
 (0)