Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
26deb91
Add F-Bounded Polymorphism tests and implement scalar class collision…
rcalicdan Oct 8, 2026
e55904c
Enhance F-Bounded Template Handling and Update Scalar Type Aliases in…
rcalicdan Oct 8, 2026
c74f4db
Refactor type node handling to replace array_map with foreach for imp…
rcalicdan Oct 8, 2026
867fb31
Refactor type handling to use iterative approaches for NullableType a…
rcalicdan Oct 8, 2026
86b2ada
Impove performance on identified crititical hotpaths
rcalicdan Oct 8, 2026
3deea66
Enhance type validation by adding checks for IdentifierTypeNode in Ar…
rcalicdan Oct 8, 2026
98cf42c
Refactor ReturnChecker and RuntimeTypeChecker to improve conditional …
rcalicdan Oct 8, 2026
34dfb08
Refactor TemplateManager to enhance type resolution and add Multi-Lev…
rcalicdan Oct 9, 2026
1e94af9
Enhance callable type handling by refining template resolution and ad…
rcalicdan Oct 9, 2026
e9ae8e9
Enhance generator return type validation by adding checks for TReturn…
rcalicdan Oct 9, 2026
7eb8eeb
Enhance CallableWrapper by adding support for specialized callable ca…
rcalicdan Oct 9, 2026
bc261ed
Enhance FileFilter and PathMatcher to exclude view templates and fram…
rcalicdan Oct 9, 2026
316ab8a
Refactor ViewTemplateAndConstructorExclusionTest to improve test isol…
rcalicdan Oct 9, 2026
a4f2756
Enhance StreamWrapper and PathMatcher to improve path handling and by…
rcalicdan Oct 9, 2026
50a4993
Enhance CacheManager and StreamWrapper to improve caching mechanisms …
rcalicdan Oct 10, 2026
b2c8bdb
fix phpstan errors
rcalicdan Oct 10, 2026
47c6751
Fix bug on self:: in an imported @phpstan-type alias is resolved agai…
rcalicdan Oct 10, 2026
eeb1c8c
Refactor InlineChecker and ReturnChecker to utilize parseClassTemplat…
rcalicdan Oct 10, 2026
8aedb0f
Add tests for subclass type alias in inherited tags to validate behav…
rcalicdan Oct 10, 2026
c147982
Implement class template extraction in ContractVisitor and add tests …
rcalicdan Oct 10, 2026
3606c92
Enhance project root resolution in Config class and add comprehensive…
rcalicdan Oct 10, 2026
d8cfbee
Add security audit job to CI workflow and update PHP version in setup
rcalicdan Oct 10, 2026
26256cc
Update CI workflow to use stable versions for actions and dependencies
rcalicdan Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ permissions:
contents: read

jobs:
security:
name: Security Audit
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none

- name: Install Dependencies
uses: ramsey/composer-install@v3

- name: Run Security Audit (Composer Audit)
run: composer audit

style:
name: Check Code Style
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"nikic/php-parser": "^5.3"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"laravel/pint": "^1.10",
"pestphp/pest": "^2.0 || ^3.0 || ^4.0",
"phpstan/phpstan": "^2.1",
Expand Down Expand Up @@ -90,4 +91,4 @@
"phpstan/extension-installer": true
}
}
}
}
90 changes: 75 additions & 15 deletions src/Internal/Ast/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDocParser\Parser\TokenIterator;
use TypePHP\Internal\Docblock\DocblockExtractor;
use TypePHP\Internal\Docblock\DocblockNormalizer;
use TypePHP\Internal\Docblock\DocblockParser;
use TypePHP\Internal\Util\Config;

/**
Expand Down Expand Up @@ -140,9 +141,15 @@ public function enterNode(Node $node): ?array
}

if ($node instanceof Node\Stmt\Expression) {
$node->expr->setAttribute('typephp_is_standalone', true);

return $this->handleExpression($node);
}

if ($node instanceof Node\Expr\ArrowFunction) {
$node->expr->setAttribute('typephp_is_standalone', true);
}

if ($node instanceof Node\Stmt\Foreach_) {
$doc = $node->getDocComment();
if ($doc !== null && str_contains($doc->getText(), '@var')) {
Expand Down Expand Up @@ -177,6 +184,17 @@ public function leaveNode(Node $node): ?Node
}
}

if ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) {
if (($node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PostDec) && $node->getAttribute('typephp_is_standalone') !== true) {
return null;
}

$replacement = $this->handleIncDec($node);
if ($replacement !== null) {
return $replacement;
}
}

if ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) {
$replacement = $this->handleIncDec($node);
if ($replacement !== null) {
Expand Down Expand Up @@ -216,10 +234,14 @@ public function leaveNode(Node $node): ?Node

private function markWriteContext(Node $node): void
{
while ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
$node->setAttribute('typephp_write_context', true);
$node = $node->var;
}

$node->setAttribute('typephp_write_context', true);
if ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
$this->markWriteContext($node->var);
} elseif ($node instanceof Node\Expr\Array_ || $node instanceof Node\Expr\List_) {

if ($node instanceof Node\Expr\Array_ || $node instanceof Node\Expr\List_) {
foreach ($node->items as $item) {
if ($item !== null) {
$this->markWriteContext($item->value);
Expand All @@ -237,6 +259,35 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void
$defaultProps = [];
$hasConstructor = false;

$classTemplates = [];
$nodeDoc = $node->getDocComment();
if ($nodeDoc !== null && (str_contains($nodeDoc->getText(), '@template') || str_contains($nodeDoc->getText(), '@phpstan-template') || str_contains($nodeDoc->getText(), '@psalm-template'))) {
try {
$phpDocNode = DocblockExtractor::parseDocString($nodeDoc->getText());
$classTemplates = DocblockExtractor::extractTemplates($phpDocNode);
} catch (\Throwable) {
}
}

if ($node->name !== null) {
$className = $this->resolveQualifiedName($node->name);
if ($className !== null) {
try {
$loadedTemplates = DocblockParser::parseClassTemplates($className);
$classTemplates = [...$classTemplates, ...$loadedTemplates];
} catch (\Throwable) {
}
}
}

if ($node->extends !== null) {
try {
$parentTemplates = DocblockParser::parseClassTemplates($node->extends->toString());
$classTemplates = [...$classTemplates, ...$parentTemplates];
} catch (\Throwable) {
}
}

foreach ($node->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\ClassMethod && strtolower($stmt->name->toString()) === '__construct') {
$hasConstructor = true;
Expand All @@ -247,6 +298,22 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void

$doc = $stmt->getDocComment();
if ($doc !== null && str_contains($doc->getText(), '@var') && ! str_contains($doc->getText(), '@typephp-ignore')) {
if ($classTemplates !== []) {
$varTag = DocblockExtractor::extractVarTagFromDoc($doc->getText());
if ($varTag !== null) {
try {
[$typeParser, $lexer] = DocblockExtractor::getTypeParserComponents();
$tokens = new TokenIterator($lexer->tokenize(DocblockNormalizer::normalize($varTag[0])));
$propTypeNode = $typeParser->parse($tokens);

if (DocblockParser::typeReferencesTemplate($propTypeNode, $classTemplates)) {
continue;
}
} catch (\Throwable) {
}
}
}

foreach ($stmt->props as $p) {
$isExplicitNull = $p->default instanceof Node\Expr\ConstFetch && strtolower($p->default->name->toString()) === 'null';
if ($p->default !== null && ! $isExplicitNull) {
Expand All @@ -265,19 +332,12 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void
}

if (! $hasConstructor) {
$ctorStmts = [];
if ($node->extends !== null) {
$ctorStmts[] = new Node\Stmt\If_(
new Node\Expr\FuncCall(new Node\Name('method_exists'), [
new Node\Arg(new Node\Expr\ClassConstFetch(new Node\Name('parent'), 'class')),
new Node\Arg(new Node\Scalar\String_('__construct')),
]),
['stmts' => [
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)])),
]]
);
if ($node->extends !== null || $node->isAbstract()) {
return;
}

$ctorStmts = [];

foreach ($defaultProps as $dp) {
$checkCall = NodeBuilder::createPropertyCheckCall(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $dp['name']),
Expand All @@ -291,7 +351,7 @@ private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void

$ctor = new Node\Stmt\ClassMethod('__construct', [
'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC,
'params' => [new Node\Param(new Node\Expr\Variable('_typephp_ctor_args'), null, null, false, true)],
'params' => [],
'stmts' => $ctorStmts,
]);
$ctor->setAttribute('typephp_injected', true);
Expand Down
45 changes: 45 additions & 0 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@ private static function isIterableCandidate(Node\Param $param, string $docText):
*/
private static function typeMatchesName(Node\Identifier|Node\Name|Node\ComplexType|null $type, array $targetNames): bool
{
while ($type instanceof Node\NullableType) {
$type = $type->type;
}

if ($type instanceof Node\Identifier) {
return isset($targetNames[strtolower($type->name)]);
}
Expand Down Expand Up @@ -757,13 +761,54 @@ public function enterNode(Node $n): int|Node|null
);
}

if ($n instanceof Node\Stmt\Return_) {
if ($n->getAttribute('typephp_var_wrapped') === true || $n->getAttribute('typephp_generator_return_wrapped') === true) {
return null;
}
$n->setAttribute('typephp_generator_return_wrapped', true);

$exprToWrap = $n->expr ?? new Node\Expr\ConstFetch(new Node\Name('null'));
$checkCall = new Node\Expr\FuncCall(
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::checkGeneratorReturn'),
[
new Node\Arg(new Node\Scalar\MagicConst\Method()),
new Node\Arg($exprToWrap),
new Node\Arg($this->thisArg),
]
);

$n->expr = FunctionContractInjector::buildTernaryReturnExpr($checkCall, $n->getStartLine());

return $n;
}

return null;
}
});

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

$lastStmt = end($newStmts);
if (! $lastStmt instanceof Node\Stmt\Return_ && ! ($lastStmt instanceof Node\Stmt\Expression && $lastStmt->expr instanceof Node\Expr\Throw_)) {
$checkCall = new Node\Expr\FuncCall(
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::checkGeneratorReturn'),
[
new Node\Arg(new Node\Scalar\MagicConst\Method()),
new Node\Arg(new Node\Expr\ConstFetch(new Node\Name('null'))),
new Node\Arg($thisArg),
]
);

$fallbackLine = $lastStmt instanceof Node\Stmt ? $lastStmt->getStartLine() : null;
$ternaryExpr = self::buildTernaryReturnExpr($checkCall, $fallbackLine);

$retStmt = new Node\Stmt\Return_($ternaryExpr);
$retStmt->setAttribute('typephp_injected', true);
$retStmt->setAttribute('typephp_generator_return_wrapped', true);
$newStmts[] = $retStmt;
}

return $newStmts;
}

Expand Down
46 changes: 25 additions & 21 deletions src/Internal/Checker/ConditionalChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ final class ConditionalChecker
*/
public static function containsConditional(TypeNode $node): bool
{
if ($node instanceof ConditionalTypeNode || $node instanceof ConditionalTypeForParameterNode) {
return true;
while ($node instanceof NullableTypeNode || $node instanceof ArrayTypeNode) {
$node = $node->type;
}

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

if ($node instanceof GenericTypeNode) {
Expand Down Expand Up @@ -145,10 +145,10 @@ public static function resolve(

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

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

if ($typeNode instanceof UnionTypeNode) {
return new UnionTypeNode(array_map(
fn ($t) => self::resolve($t, $vars, $boundTemplates, $registry, $function),
$typeNode->types
));
$types = [];
foreach ($typeNode->types as $t) {
$types[] = self::resolve($t, $vars, $boundTemplates, $registry, $function);
}

return new UnionTypeNode($types);
}

if ($typeNode instanceof IntersectionTypeNode) {
return new IntersectionTypeNode(array_map(
fn ($t) => self::resolve($t, $vars, $boundTemplates, $registry, $function),
$typeNode->types
));
$types = [];
foreach ($typeNode->types as $t) {
$types[] = self::resolve($t, $vars, $boundTemplates, $registry, $function);
}

return new IntersectionTypeNode($types);
}

if ($typeNode instanceof ArrayShapeNode) {
Expand Down Expand Up @@ -211,16 +215,16 @@ public static function resolve(
}

if ($typeNode instanceof CallableTypeNode) {
$parameters = array_map(
fn (CallableTypeParameterNode $param) => new CallableTypeParameterNode(
$parameters = [];
foreach ($typeNode->parameters as $param) {
$parameters[] = new CallableTypeParameterNode(
self::resolve($param->type, $vars, $boundTemplates, $registry, $function),
$param->isReference,
$param->isVariadic,
$param->parameterName,
$param->isOptional
),
$typeNode->parameters
);
);
}

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

Expand Down
Loading
Loading