Skip to content

Commit 0c91be7

Browse files
authored
Major Performance Improvement. (#118)
* Enhance ContractVisitor and FunctionContractInjector to support by-reference tracking and improve doc comment resolution for performance; add comprehensive tests for unannotated method return checks * Optimize validation logic in ArrayValidator and GenericValidator; introduce CollectionElementValidator for improved element validation and add support for new types in IdentifierValidator * Implement PHP 8.5 "clone with" functionality; add RuntimeTypeChecker::cloneWith method and related tests for property validation and generic type preservation * fix failing test on 8.5 * Refactor clone method in NativeCloneWithRunner to cloneWith; update tests for consistency and type validation * fix code styling
1 parent 4f18780 commit 0c91be7

13 files changed

Lines changed: 1275 additions & 355 deletions

‎src/Internal/Ast/ContractVisitor.php‎

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use TypePHP\Internal\Docblock\DocblockNormalizer;
1616
use TypePHP\Internal\Docblock\DocblockParser;
1717
use TypePHP\Internal\Util\Config;
18+
use TypePHP\Internal\Util\StubManager;
1819

1920
/**
2021
* @internal AST Node Visitor that injects contract checks, scope tracking, property hook validation, and parameter/return wrappers into functions and methods.
@@ -66,6 +67,13 @@ final class ContractVisitor extends NodeVisitorAbstract
6667
*/
6768
private array $thisAvailableStack = [];
6869

70+
/**
71+
* Tracks whether the enclosing function or method returns by reference.
72+
*
73+
* @var list<bool>
74+
*/
75+
private array $byRefStack = [];
76+
6977
public function __construct()
7078
{
7179
$this->scopeManager = new ScopeManager();
@@ -80,6 +88,10 @@ public function enterNode(Node $node): ?array
8088
{
8189
$this->trackDeclarationEntry($node);
8290

91+
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Expr\Closure) {
92+
$this->byRefStack[] = $node->byRef;
93+
}
94+
8395
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
8496
$this->scopeManager->pushScope(isIsolated: true);
8597
} elseif ($node instanceof Node\Expr\Closure) {
@@ -135,6 +147,10 @@ public function enterNode(Node $node): ?array
135147
}
136148

137149
if ($node instanceof Node\Stmt\Return_) {
150+
if ($node->expr !== null && $this->byRefStack !== [] && end($this->byRefStack)) {
151+
$this->markWriteContext($node->expr);
152+
}
153+
138154
$this->handleReturn($node);
139155

140156
return null;
@@ -173,10 +189,21 @@ public function leaveNode(Node $node): ?Node
173189
{
174190
$this->trackDeclarationExit($node);
175191

192+
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Expr\Closure) {
193+
array_pop($this->byRefStack);
194+
}
195+
176196
if ($node instanceof Node\Expr\Clone_) {
177197
return $this->wrapClone($node);
178198
}
179199

200+
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && strtolower($node->name->getLast()) === 'clone') {
201+
$replacement = $this->wrapCloneFunction($node);
202+
if ($replacement !== null) {
203+
return $replacement;
204+
}
205+
}
206+
180207
if ($node instanceof Node\Expr\AssignOp) {
181208
$replacement = $this->handleAssignOp($node);
182209
if ($replacement !== null) {
@@ -195,13 +222,6 @@ public function leaveNode(Node $node): ?Node
195222
}
196223
}
197224

198-
if ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) {
199-
$replacement = $this->handleIncDec($node);
200-
if ($replacement !== null) {
201-
return $replacement;
202-
}
203-
}
204-
205225
if ($node instanceof Node\Expr\StaticPropertyFetch && $node->name instanceof Node\VarLikeIdentifier) {
206226
$propName = $node->name->toString();
207227

@@ -232,6 +252,46 @@ public function leaveNode(Node $node): ?Node
232252
return null;
233253
}
234254

255+
private function wrapCloneFunction(Node\Expr\FuncCall $node): ?Node\Expr
256+
{
257+
if ($node->getAttribute('typephp_wrapped') === true) {
258+
return null;
259+
}
260+
261+
$firstArg = $node->args[0] ?? null;
262+
if (! $firstArg instanceof Node\Arg) {
263+
return null;
264+
}
265+
266+
$node->setAttribute('typephp_wrapped', true);
267+
268+
$originalExpr = $firstArg->value;
269+
270+
$secondArg = $node->args[1] ?? null;
271+
$propertiesExpr = $secondArg instanceof Node\Arg
272+
? $secondArg->value
273+
: new Node\Expr\ConstFetch(new Node\Name('null'));
274+
275+
$node->args[0] = new Node\Arg(
276+
new Node\Expr\FuncCall(
277+
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::prepareClone'),
278+
[new Node\Arg($originalExpr)],
279+
),
280+
);
281+
282+
return new Node\Expr\StaticCall(
283+
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker'),
284+
'cloneWith',
285+
[
286+
new Node\Arg($node),
287+
new Node\Arg($originalExpr),
288+
new Node\Arg($propertiesExpr),
289+
new Node\Arg(new Node\Scalar\MagicConst\File()),
290+
new Node\Arg(new Node\Scalar\LNumber($node->getStartLine())),
291+
],
292+
);
293+
}
294+
235295
private function markWriteContext(Node $node): void
236296
{
237297
while ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
@@ -429,13 +489,23 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
429489
$hasRealInheritance = false;
430490
$hasPropertyWithDoc = false;
431491
$isReadonly = false;
492+
$isFinal = false;
432493
$hasTemplates = false;
433494

495+
$doc = $node->getDocComment();
496+
if ($doc === null && $typeName !== null && StubManager::hasClassStub($typeName)) {
497+
$stubDocText = StubManager::getClassDoc($typeName);
498+
if ($stubDocText !== null) {
499+
$doc = new \PhpParser\Comment\Doc($stubDocText);
500+
}
501+
}
502+
434503
if ($node instanceof Node\Stmt\Class_) {
435504
$hasExtends = $node->extends !== null;
436505
$hasImplements = $node->implements !== [];
437506
$hasTraits = false;
438507
$isReadonly = ($node->flags & Node\Stmt\Class_::MODIFIER_READONLY) !== 0;
508+
$isFinal = ($node->flags & Node\Stmt\Class_::MODIFIER_FINAL) !== 0;
439509

440510
foreach ($node->stmts as $stmt) {
441511
if ($stmt instanceof Node\Stmt\TraitUse) {
@@ -453,7 +523,6 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
453523
}
454524
}
455525

456-
$doc = $node->getDocComment();
457526
$hasTemplates = $doc !== null && (
458527
str_contains($doc->getText(), '@template')
459528
|| str_contains($doc->getText(), '@phpstan-template')
@@ -473,7 +542,6 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
473542
$hasRealInheritance = $node->implements !== [];
474543
$hasInheritance = $hasRealInheritance;
475544
} else {
476-
$doc = $node->getDocComment();
477545
$hasTemplates = $doc !== null && (
478546
str_contains($doc->getText(), '@template')
479547
|| str_contains($doc->getText(), '@phpstan-template')
@@ -488,6 +556,7 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
488556
'hasRealInheritance' => $hasRealInheritance,
489557
'hasPropertyWithDoc' => $hasPropertyWithDoc,
490558
'isReadonly' => $isReadonly,
559+
'isFinal' => $isFinal,
491560
'hasTemplates' => $hasTemplates,
492561
];
493562
}

‎src/Internal/Ast/FunctionContractInjector.php‎

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpParser\Node;
99
use PhpParser\NodeTraverser;
1010
use PhpParser\NodeVisitorAbstract;
11+
use TypePHP\Internal\Util\StubManager;
1112

1213
/**
1314
* @internal Injects parameter checks, return checks, param-out checks, and generator interceptors into functions and methods.
@@ -28,7 +29,7 @@ final class FunctionContractInjector
2829
];
2930

3031
/**
31-
* @param array{hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, hasTemplates?: bool}|null $classContext
32+
* @param array{name?: ?string, hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, isFinal?: bool, hasTemplates?: bool}|null $classContext
3233
*/
3334
public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?array $classContext = null): void
3435
{
@@ -37,13 +38,14 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
3738
}
3839

3940
$isClassMethod = $node instanceof Node\Stmt\ClassMethod;
40-
$doc = self::resolveDocComment($node);
41+
$doc = self::resolveDocComment($node, $classContext);
4142
$docText = $doc !== null ? $doc->getText() : '';
4243

4344
$hasInheritance = $classContext['hasInheritance'] ?? true;
4445
$hasRealInheritance = $classContext['hasRealInheritance'] ?? $hasInheritance;
4546
$hasPropertyWithDoc = $classContext['hasPropertyWithDoc'] ?? true;
4647
$isReadonlyClass = $classContext['isReadonly'] ?? false;
48+
$isFinalClass = $classContext['isFinal'] ?? false;
4749
$hasClassTemplates = $classContext['hasTemplates'] ?? false;
4850

4951
$methodName = $isClassMethod ? strtolower($node->name->toString()) : '';
@@ -72,10 +74,21 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
7274
|| str_contains($docText, '@phpstan-return')
7375
|| str_contains($docText, '@psalm-return');
7476

77+
$hasMethodReturnStatements = self::hasReturnStatements($node);
78+
7579
$hasReturn = ! $isMagicLifecycle
7680
&& ! $isNativeNever
7781
&& ! ($isNativeVoid && ! $hasReturnDoc)
78-
&& ($isMagicGet || $isMagicCall || self::hasReturnContracts($docText, $isClassMethod, $isPrivate));
82+
&& ($isMagicGet || $isMagicCall || self::hasReturnContracts(
83+
$docText,
84+
$isClassMethod,
85+
$hasInheritance,
86+
$isPrivate,
87+
$isFinalClass,
88+
$node->returnType !== null,
89+
$hasMethodReturnStatements,
90+
$classContext === null,
91+
));
7992

8093
$byRefParams = [];
8194
foreach ($node->params as $p) {
@@ -104,7 +117,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
104117
$isConstructor,
105118
$hasPropertyWithDoc,
106119
$classContext === null,
107-
$node->attrGroups !== []
120+
$node->attrGroups !== [],
108121
);
109122

110123
if (! $hasParam && ! $hasReturn && ! $hasParamOut && ! $hasSelfOut) {
@@ -132,7 +145,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
132145
$hasReturn,
133146
$hasParamOut ? $byRefParams : [],
134147
$hasSelfOut,
135-
$node->byRef
148+
$node->byRef,
136149
);
137150
}
138151

@@ -160,8 +173,13 @@ public static function buildSelfOutCheckStmt(Node\Expr $thisArg, bool $needsRetu
160173
return $stmt;
161174
}
162175

163-
private static function resolveDocComment(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): ?Doc
164-
{
176+
/**
177+
* @param array{name?: ?string, hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, isFinal?: bool, hasTemplates?: bool}|null $classContext
178+
*/
179+
private static function resolveDocComment(
180+
Node\Stmt\Function_|Node\Stmt\ClassMethod $node,
181+
?array $classContext = null,
182+
): ?Doc {
165183
$doc = $node->getDocComment();
166184
if ($doc !== null) {
167185
return $doc;
@@ -176,6 +194,25 @@ private static function resolveDocComment(Node\Stmt\Function_|Node\Stmt\ClassMet
176194
}
177195
}
178196

197+
$className = $classContext['name'] ?? null;
198+
if ($node instanceof Node\Stmt\ClassMethod && $className !== null) {
199+
$methodName = $node->name->toString();
200+
if (StubManager::hasMethodStub($className, $methodName)) {
201+
$stubText = StubManager::getMethodDoc($className, $methodName);
202+
if ($stubText !== null) {
203+
return new Doc($stubText);
204+
}
205+
}
206+
} elseif ($node instanceof Node\Stmt\Function_) {
207+
$funcName = $node->name->toString();
208+
if (StubManager::hasFunctionStub($funcName)) {
209+
$stubText = StubManager::getFunctionDoc($funcName);
210+
if ($stubText !== null) {
211+
return new Doc($stubText);
212+
}
213+
}
214+
}
215+
179216
return null;
180217
}
181218

@@ -248,8 +285,17 @@ private static function hasNonMixedParam(string $docText): bool
248285
private static function hasReturnContracts(
249286
string $docText,
250287
bool $isClassMethod,
251-
bool $isPrivate
288+
bool $hasInheritance,
289+
bool $isPrivate,
290+
bool $isFinalClass = false,
291+
bool $hasNativeReturnType = false,
292+
bool $hasReturnStatements = false,
293+
bool $isDirectUnitTest = false,
252294
): bool {
295+
if ($isDirectUnitTest && $isClassMethod) {
296+
return true;
297+
}
298+
253299
if ($isPrivate && $docText === '') {
254300
return false;
255301
}
@@ -275,7 +321,38 @@ private static function hasReturnContracts(
275321
return true;
276322
}
277323

278-
return $isClassMethod && ! $isPrivate;
324+
if (! $isClassMethod) {
325+
return false;
326+
}
327+
328+
if ($hasInheritance && ! $isPrivate) {
329+
return true;
330+
}
331+
332+
if ($isFinalClass) {
333+
return false;
334+
}
335+
336+
if (! $hasNativeReturnType && ! $hasReturnStatements) {
337+
return false;
338+
}
339+
340+
return ! $isPrivate;
341+
}
342+
343+
private static function hasReturnStatements(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): bool
344+
{
345+
if ($node->stmts === null || $node->stmts === []) {
346+
return false;
347+
}
348+
349+
foreach ($node->stmts as $stmt) {
350+
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr !== null) {
351+
return true;
352+
}
353+
}
354+
355+
return false;
279356
}
280357

281358
private static function resolveThisArg(bool $isClassMethod, Node\Stmt\Function_|Node\Stmt\ClassMethod $node): Node\Expr

0 commit comments

Comments
 (0)