Skip to content

Commit c673bd0

Browse files
authored
Major Bug fixes and Internal Performance Improvements (#108)
* Add F-Bounded Polymorphism tests and implement scalar class collision handling * Enhance F-Bounded Template Handling and Update Scalar Type Aliases in Tests * Refactor type node handling to replace array_map with foreach for improved and performance across internal engine * Refactor type handling to use iterative approaches for NullableType and ArrayType across multiple classes instead of recursion for optimizing performance * Impove performance on identified crititical hotpaths * Enhance type validation by adding checks for IdentifierTypeNode in ArrayValidator, GenericValidator, and UnionValidator for improve performance * Refactor ReturnChecker and RuntimeTypeChecker to improve conditional checks for better clarity and performance * Refactor TemplateManager to enhance type resolution and add Multi-Level Generic Template Forwarding tests for improved type safety * Enhance callable type handling by refining template resolution and adding tests for callable-local generics * Enhance generator return type validation by adding checks for TReturn in GeneratorChecker and RuntimeTypeChecker, and implement corresponding tests for valid and invalid return values in GenericIteratorsAndGeneratorsTest. * Enhance CallableWrapper by adding support for specialized callable caching and refining type validation for callable parameters * Enhance FileFilter and PathMatcher to exclude view templates and framework caches, and implement regression tests for constructor synthesis and expression semantics in ContractVisitor. * Refactor ViewTemplateAndConstructorExclusionTest to improve test isolationa and prevent failing ci 8.2 matrix * Enhance StreamWrapper and PathMatcher to improve path handling and bypass logic for PHP files * Enhance CacheManager and StreamWrapper to improve caching mechanisms and versioning * fix phpstan errors * Fix bug on self:: in an imported @phpstan-type alias is resolved against the importing class * Refactor InlineChecker and ReturnChecker to utilize parseClassTemplates method; add DocblockParser::parseClassTemplates for improved template extraction; introduce InheritedConstructorClassTemplateTest for bug reproduction and validation * Add tests for subclass type alias in inherited tags to validate behavior and implement green fix * Implement class template extraction in ContractVisitor and add tests for templated property defaults in subclasses and fix bug on Inherited property default is checked against the subclass's template binding, so constructing the subclass throws (PHPStan accepts it) * Enhance project root resolution in Config class and add comprehensive tests for custom vendor-dir scenarios * Add security audit job to CI workflow and update PHP version in setup * Update CI workflow to use stable versions for actions and dependencies
1 parent 4e4244e commit c673bd0

41 files changed

Lines changed: 3197 additions & 313 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ permissions:
1212
contents: read
1313

1414
jobs:
15+
security:
16+
name: Security Audit
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout Code
21+
uses: actions/checkout@v4
22+
23+
- name: Setup PHP
24+
uses: shivammathur/setup-php@v2
25+
with:
26+
php-version: '8.4'
27+
coverage: none
28+
29+
- name: Install Dependencies
30+
uses: ramsey/composer-install@v3
31+
32+
- name: Run Security Audit (Composer Audit)
33+
run: composer audit
34+
1535
style:
1636
name: Check Code Style
1737
runs-on: ubuntu-latest

‎composer.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"nikic/php-parser": "^5.3"
3131
},
3232
"require-dev": {
33+
"roave/security-advisories": "dev-latest",
3334
"laravel/pint": "^1.10",
3435
"pestphp/pest": "^2.0 || ^3.0 || ^4.0",
3536
"phpstan/phpstan": "^2.1",
@@ -90,4 +91,4 @@
9091
"phpstan/extension-installer": true
9192
}
9293
}
93-
}
94+
}

‎src/Internal/Ast/ContractVisitor.php‎

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\PhpDocParser\Parser\TokenIterator;
1414
use TypePHP\Internal\Docblock\DocblockExtractor;
1515
use TypePHP\Internal\Docblock\DocblockNormalizer;
16+
use TypePHP\Internal\Docblock\DocblockParser;
1617
use TypePHP\Internal\Util\Config;
1718

1819
/**
@@ -140,9 +141,15 @@ public function enterNode(Node $node): ?array
140141
}
141142

142143
if ($node instanceof Node\Stmt\Expression) {
144+
$node->expr->setAttribute('typephp_is_standalone', true);
145+
143146
return $this->handleExpression($node);
144147
}
145148

149+
if ($node instanceof Node\Expr\ArrowFunction) {
150+
$node->expr->setAttribute('typephp_is_standalone', true);
151+
}
152+
146153
if ($node instanceof Node\Stmt\Foreach_) {
147154
$doc = $node->getDocComment();
148155
if ($doc !== null && str_contains($doc->getText(), '@var')) {
@@ -177,6 +184,17 @@ public function leaveNode(Node $node): ?Node
177184
}
178185
}
179186

187+
if ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) {
188+
if (($node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PostDec) && $node->getAttribute('typephp_is_standalone') !== true) {
189+
return null;
190+
}
191+
192+
$replacement = $this->handleIncDec($node);
193+
if ($replacement !== null) {
194+
return $replacement;
195+
}
196+
}
197+
180198
if ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) {
181199
$replacement = $this->handleIncDec($node);
182200
if ($replacement !== null) {
@@ -216,10 +234,14 @@ public function leaveNode(Node $node): ?Node
216234

217235
private function markWriteContext(Node $node): void
218236
{
237+
while ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
238+
$node->setAttribute('typephp_write_context', true);
239+
$node = $node->var;
240+
}
241+
219242
$node->setAttribute('typephp_write_context', true);
220-
if ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
221-
$this->markWriteContext($node->var);
222-
} elseif ($node instanceof Node\Expr\Array_ || $node instanceof Node\Expr\List_) {
243+
244+
if ($node instanceof Node\Expr\Array_ || $node instanceof Node\Expr\List_) {
223245
foreach ($node->items as $item) {
224246
if ($item !== null) {
225247
$this->markWriteContext($item->value);
@@ -237,6 +259,35 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void
237259
$defaultProps = [];
238260
$hasConstructor = false;
239261

262+
$classTemplates = [];
263+
$nodeDoc = $node->getDocComment();
264+
if ($nodeDoc !== null && (str_contains($nodeDoc->getText(), '@template') || str_contains($nodeDoc->getText(), '@phpstan-template') || str_contains($nodeDoc->getText(), '@psalm-template'))) {
265+
try {
266+
$phpDocNode = DocblockExtractor::parseDocString($nodeDoc->getText());
267+
$classTemplates = DocblockExtractor::extractTemplates($phpDocNode);
268+
} catch (\Throwable) {
269+
}
270+
}
271+
272+
if ($node->name !== null) {
273+
$className = $this->resolveQualifiedName($node->name);
274+
if ($className !== null) {
275+
try {
276+
$loadedTemplates = DocblockParser::parseClassTemplates($className);
277+
$classTemplates = [...$classTemplates, ...$loadedTemplates];
278+
} catch (\Throwable) {
279+
}
280+
}
281+
}
282+
283+
if ($node->extends !== null) {
284+
try {
285+
$parentTemplates = DocblockParser::parseClassTemplates($node->extends->toString());
286+
$classTemplates = [...$classTemplates, ...$parentTemplates];
287+
} catch (\Throwable) {
288+
}
289+
}
290+
240291
foreach ($node->stmts as $stmt) {
241292
if ($stmt instanceof Node\Stmt\ClassMethod && strtolower($stmt->name->toString()) === '__construct') {
242293
$hasConstructor = true;
@@ -247,6 +298,22 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void
247298

248299
$doc = $stmt->getDocComment();
249300
if ($doc !== null && str_contains($doc->getText(), '@var') && ! str_contains($doc->getText(), '@typephp-ignore')) {
301+
if ($classTemplates !== []) {
302+
$varTag = DocblockExtractor::extractVarTagFromDoc($doc->getText());
303+
if ($varTag !== null) {
304+
try {
305+
[$typeParser, $lexer] = DocblockExtractor::getTypeParserComponents();
306+
$tokens = new TokenIterator($lexer->tokenize(DocblockNormalizer::normalize($varTag[0])));
307+
$propTypeNode = $typeParser->parse($tokens);
308+
309+
if (DocblockParser::typeReferencesTemplate($propTypeNode, $classTemplates)) {
310+
continue;
311+
}
312+
} catch (\Throwable) {
313+
}
314+
}
315+
}
316+
250317
foreach ($stmt->props as $p) {
251318
$isExplicitNull = $p->default instanceof Node\Expr\ConstFetch && strtolower($p->default->name->toString()) === 'null';
252319
if ($p->default !== null && ! $isExplicitNull) {
@@ -265,19 +332,12 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void
265332
}
266333

267334
if (! $hasConstructor) {
268-
$ctorStmts = [];
269-
if ($node->extends !== null) {
270-
$ctorStmts[] = new Node\Stmt\If_(
271-
new Node\Expr\FuncCall(new Node\Name('method_exists'), [
272-
new Node\Arg(new Node\Expr\ClassConstFetch(new Node\Name('parent'), 'class')),
273-
new Node\Arg(new Node\Scalar\String_('__construct')),
274-
]),
275-
['stmts' => [
276-
new Node\Stmt\Expression(new Node\Expr\StaticCall(new Node\Name('parent'), '__construct', [new Node\Arg(new Node\Expr\Variable('_typephp_ctor_args'), false, true)])),
277-
]]
278-
);
335+
if ($node->extends !== null || $node->isAbstract()) {
336+
return;
279337
}
280338

339+
$ctorStmts = [];
340+
281341
foreach ($defaultProps as $dp) {
282342
$checkCall = NodeBuilder::createPropertyCheckCall(
283343
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $dp['name']),
@@ -291,7 +351,7 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void
291351

292352
$ctor = new Node\Stmt\ClassMethod('__construct', [
293353
'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
294-
'params' => [new Node\Param(new Node\Expr\Variable('_typephp_ctor_args'), null, null, false, true)],
354+
'params' => [],
295355
'stmts' => $ctorStmts,
296356
]);
297357
$ctor->setAttribute('typephp_injected', true);

‎src/Internal/Ast/FunctionContractInjector.php‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,10 @@ private static function isIterableCandidate(Node\Param $param, string $docText):
557557
*/
558558
private static function typeMatchesName(Node\Identifier|Node\Name|Node\ComplexType|null $type, array $targetNames): bool
559559
{
560+
while ($type instanceof Node\NullableType) {
561+
$type = $type->type;
562+
}
563+
560564
if ($type instanceof Node\Identifier) {
561565
return isset($targetNames[strtolower($type->name)]);
562566
}
@@ -757,13 +761,54 @@ public function enterNode(Node $n): int|Node|null
757761
);
758762
}
759763

764+
if ($n instanceof Node\Stmt\Return_) {
765+
if ($n->getAttribute('typephp_var_wrapped') === true || $n->getAttribute('typephp_generator_return_wrapped') === true) {
766+
return null;
767+
}
768+
$n->setAttribute('typephp_generator_return_wrapped', true);
769+
770+
$exprToWrap = $n->expr ?? new Node\Expr\ConstFetch(new Node\Name('null'));
771+
$checkCall = new Node\Expr\FuncCall(
772+
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::checkGeneratorReturn'),
773+
[
774+
new Node\Arg(new Node\Scalar\MagicConst\Method()),
775+
new Node\Arg($exprToWrap),
776+
new Node\Arg($this->thisArg),
777+
]
778+
);
779+
780+
$n->expr = FunctionContractInjector::buildTernaryReturnExpr($checkCall, $n->getStartLine());
781+
782+
return $n;
783+
}
784+
760785
return null;
761786
}
762787
});
763788

764789
/** @var array<Node\Stmt> $newStmts */
765790
$newStmts = $traverser->traverse($stmts);
766791

792+
$lastStmt = end($newStmts);
793+
if (! $lastStmt instanceof Node\Stmt\Return_ && ! ($lastStmt instanceof Node\Stmt\Expression && $lastStmt->expr instanceof Node\Expr\Throw_)) {
794+
$checkCall = new Node\Expr\FuncCall(
795+
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::checkGeneratorReturn'),
796+
[
797+
new Node\Arg(new Node\Scalar\MagicConst\Method()),
798+
new Node\Arg(new Node\Expr\ConstFetch(new Node\Name('null'))),
799+
new Node\Arg($thisArg),
800+
]
801+
);
802+
803+
$fallbackLine = $lastStmt instanceof Node\Stmt ? $lastStmt->getStartLine() : null;
804+
$ternaryExpr = self::buildTernaryReturnExpr($checkCall, $fallbackLine);
805+
806+
$retStmt = new Node\Stmt\Return_($ternaryExpr);
807+
$retStmt->setAttribute('typephp_injected', true);
808+
$retStmt->setAttribute('typephp_generator_return_wrapped', true);
809+
$newStmts[] = $retStmt;
810+
}
811+
767812
return $newStmts;
768813
}
769814

‎src/Internal/Checker/ConditionalChecker.php‎

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ final class ConditionalChecker
3838
*/
3939
public static function containsConditional(TypeNode $node): bool
4040
{
41-
if ($node instanceof ConditionalTypeNode || $node instanceof ConditionalTypeForParameterNode) {
42-
return true;
41+
while ($node instanceof NullableTypeNode || $node instanceof ArrayTypeNode) {
42+
$node = $node->type;
4343
}
4444

45-
if ($node instanceof NullableTypeNode || $node instanceof ArrayTypeNode) {
46-
return self::containsConditional($node->type);
45+
if ($node instanceof ConditionalTypeNode || $node instanceof ConditionalTypeForParameterNode) {
46+
return true;
4747
}
4848

4949
if ($node instanceof GenericTypeNode) {
@@ -145,10 +145,10 @@ public static function resolve(
145145

146146
if ($typeNode instanceof GenericTypeNode) {
147147
$genericType = self::resolve($typeNode->type, $vars, $boundTemplates, $registry, $function);
148-
$genericTypes = array_map(
149-
fn ($t) => self::resolve($t, $vars, $boundTemplates, $registry, $function),
150-
$typeNode->genericTypes
151-
);
148+
$genericTypes = [];
149+
foreach ($typeNode->genericTypes as $gt) {
150+
$genericTypes[] = self::resolve($gt, $vars, $boundTemplates, $registry, $function);
151+
}
152152

153153
return new GenericTypeNode(
154154
$genericType instanceof IdentifierTypeNode ? $genericType : $typeNode->type,
@@ -158,17 +158,21 @@ public static function resolve(
158158
}
159159

160160
if ($typeNode instanceof UnionTypeNode) {
161-
return new UnionTypeNode(array_map(
162-
fn ($t) => self::resolve($t, $vars, $boundTemplates, $registry, $function),
163-
$typeNode->types
164-
));
161+
$types = [];
162+
foreach ($typeNode->types as $t) {
163+
$types[] = self::resolve($t, $vars, $boundTemplates, $registry, $function);
164+
}
165+
166+
return new UnionTypeNode($types);
165167
}
166168

167169
if ($typeNode instanceof IntersectionTypeNode) {
168-
return new IntersectionTypeNode(array_map(
169-
fn ($t) => self::resolve($t, $vars, $boundTemplates, $registry, $function),
170-
$typeNode->types
171-
));
170+
$types = [];
171+
foreach ($typeNode->types as $t) {
172+
$types[] = self::resolve($t, $vars, $boundTemplates, $registry, $function);
173+
}
174+
175+
return new IntersectionTypeNode($types);
172176
}
173177

174178
if ($typeNode instanceof ArrayShapeNode) {
@@ -211,16 +215,16 @@ public static function resolve(
211215
}
212216

213217
if ($typeNode instanceof CallableTypeNode) {
214-
$parameters = array_map(
215-
fn (CallableTypeParameterNode $param) => new CallableTypeParameterNode(
218+
$parameters = [];
219+
foreach ($typeNode->parameters as $param) {
220+
$parameters[] = new CallableTypeParameterNode(
216221
self::resolve($param->type, $vars, $boundTemplates, $registry, $function),
217222
$param->isReference,
218223
$param->isVariadic,
219224
$param->parameterName,
220225
$param->isOptional
221-
),
222-
$typeNode->parameters
223-
);
226+
);
227+
}
224228

225229
$returnType = self::resolve($typeNode->returnType, $vars, $boundTemplates, $registry, $function);
226230

0 commit comments

Comments
 (0)