Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/1-bug-report.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Bug report
description: If something isn't working as expected.
title: ""
title: "[Bug]: "
labels: ["bug"]
body:
- type: textarea
Expand All @@ -18,7 +18,7 @@ body:
label: Code snippet that reproduces the problem
description: |
Please reproduce the issue with the [on-line TypePHP playground](https://typephp-php.github.io/docs/playground), get a unique URL with the Share button and post it here.
placeholder: https://typephp-php.github.io/docs/playground#code=<unique_id>
placeholder: "https://typephp-php.github.io/docs/playground#code=<unique_id>"
validations:
required: false

Expand Down
3 changes: 0 additions & 3 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Report a security vulnerability
url: https://github.com/typephp-php/typephp/security/policy
about: Please review our security policy for more details.
- name: Support question
url: https://github.com/typephp-php/typephp/discussions/new?category=q-a
about: Please open a new discussion instead. Thank you.
13 changes: 8 additions & 5 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ private static function resolveThisArg(bool $isClassMethod, Node\Stmt\Function_|

private static function isGenerator(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): bool
{
$visitor = new class() extends NodeVisitorAbstract {
$visitor = new class () extends NodeVisitorAbstract {
public bool $isGen = false;

public function enterNode(Node $n): ?int
Expand Down Expand Up @@ -678,8 +678,10 @@ public static function buildWrappedYieldNode(Node\Expr\Yield_ $n, Node\Expr $thi
private static function wrapGeneratorReturns(array $stmts, Node\Expr $thisArg): array
{
$traverser = new NodeTraverser();
$traverser->addVisitor(new class($thisArg) extends NodeVisitorAbstract {
public function __construct(private Node\Expr $thisArg) {}
$traverser->addVisitor(new class ($thisArg) extends NodeVisitorAbstract {
public function __construct(private Node\Expr $thisArg)
{
}

public function enterNode(Node $n): int|Node|null
{
Expand Down Expand Up @@ -738,7 +740,7 @@ private static function wrapNonGeneratorReturns(
bool $hasSelfOut = false
): array {
$traverser = new NodeTraverser();
$traverser->addVisitor(new class($thisArg, $isNativeVoid, $needsReturnVars, $hasReturn, $byRefParams, $hasSelfOut) extends NodeVisitorAbstract {
$traverser->addVisitor(new class ($thisArg, $isNativeVoid, $needsReturnVars, $hasReturn, $byRefParams, $hasSelfOut) extends NodeVisitorAbstract {
/**
* @param array<string> $byRefParams
*/
Expand All @@ -749,7 +751,8 @@ public function __construct(
private bool $hasReturn,
private array $byRefParams,
private bool $hasSelfOut
) {}
) {
}

public function enterNode(Node $n): int|array|null
{
Expand Down
38 changes: 22 additions & 16 deletions src/Internal/Ast/ScopeManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,40 @@
namespace TypePHP\Internal\Ast;

use PhpParser\Node;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use TypePHP\Internal\Docblock\DocblockExtractor;
use TypePHP\Internal\Docblock\DocblockNormalizer;

/**
* @internal Manages lexical scope stack frames and extracts local @var variable annotations.
*/
final class ScopeManager
{
/**
* @var array<int, array<string, string>>
* @var list<array<string, string>>
*/
private array $scopeStack = [[]];

/**
* Pushes a new scope frame, inheriting variables from the parent scope.
* Tracks the current scope frame depth.
*/
private int $depth = 0;

/**
* Pushes a new empty lexical scope frame (O(1)).
*/
public function pushScope(): void
{
$currentScope = end($this->scopeStack);
$this->scopeStack[] = $currentScope !== false ? $currentScope : [];
$this->depth++;
$this->scopeStack[] = [];
}

/**
* Pops the top scope frame, restoring variables back to the parent scope.
* Pops the top scope frame, restoring the previous lexical scope (O(1)).
*/
public function popScope(): void
{
if (\count($this->scopeStack) > 1) {
if ($this->depth > 0) {
array_pop($this->scopeStack);
$this->depth--;
}
}

Expand All @@ -44,12 +48,12 @@ public function popScope(): void
*/
public function extractVarDocblock(string $docText, ?Node\Expr $expr = null): void
{
try {
$docText = DocblockNormalizer::normalize($docText);
[$phpDocParser, $lexer] = DocblockExtractor::getParserComponents();
if (! str_contains($docText, 'var')) {
return;
}

$tokens = new TokenIterator($lexer->tokenize($docText));
$phpDocNode = $phpDocParser->parse($tokens);
try {
$phpDocNode = DocblockExtractor::parseDocString($docText);
$varTags = DocblockExtractor::getVarTags($phpDocNode);

foreach ($varTags as $varTag) {
Expand All @@ -63,18 +67,20 @@ public function extractVarDocblock(string $docText, ?Node\Expr $expr = null): vo
}

if ($varName !== '') {
$currentScopeIndex = \count($this->scopeStack) - 1;
$this->scopeStack[$currentScopeIndex][$varName] = $typeString;
$this->scopeStack[$this->depth][$varName] = $typeString;
}
}
} catch (\Throwable $e) {
// Silently ignore malformed docblocks
}
}

/**
* Resolves a variable type by walking upward through the lexical scope chain.
*/
public function getVarTypeFromScope(string $varName): ?string
{
for ($i = \count($this->scopeStack) - 1; $i >= 0; $i--) {
for ($i = $this->depth; $i >= 0; $i--) {
if (isset($this->scopeStack[$i][$varName])) {
return $this->scopeStack[$i][$varName];
}
Expand Down
106 changes: 89 additions & 17 deletions src/Internal/Checker/GeneratorChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@
*/
final class GeneratorChecker
{
/**
* O(1) Fast-path cache for generators determined to have no return contracts.
*
* @var array<string, true>
*/
private static array $noContractCache = [];

/**
* Cache for resolved yield & send types of static / non-generic generators:
* [$function] => array{0: ?TypeNode, 1: ?TypeNode, 2: ?TypeNode}.
*
* @var array<string, array{0: ?TypeNode, 1: ?TypeNode, 2: ?TypeNode}>
*/
private static array $staticYieldTypeCache = [];

/**
* 2D Cache for resolved yield & send types of generic generators:
* [$function][$templateSignature] => array{0: ?TypeNode, 1: ?TypeNode, 2: ?TypeNode}.
*
* @var array<string, array<string, array{0: ?TypeNode, 1: ?TypeNode, 2: ?TypeNode}>>
*/
private static array $genericYieldTypeCache = [];

/**
* Validates a value sent into a generator via $gen->send() against TSend.
*/
Expand All @@ -32,12 +55,12 @@ public static function checkSend(
return null;
}

$returnTypeNode = self::resolveGeneratorReturnType($function, $thisOrClass);
if (! ($returnTypeNode instanceof GenericTypeNode)) {
$types = self::resolveYieldAndSendTypes($function, $thisOrClass);
if ($types === null) {
return $sendValue;
}

$sendTypeNode = $returnTypeNode->genericTypes[2] ?? null;
$sendTypeNode = $types[2];
if ($sendTypeNode === null) {
return $sendValue;
}
Expand All @@ -48,7 +71,7 @@ public static function checkSend(
}

/**
* Validates yielded keys and values from a generator function against TKey and TValue.
* Validates yielded keys and values from a generator function against TKey and TValue with zero-allocation caching.
*/
public static function checkYield(
string $function,
Expand All @@ -57,12 +80,12 @@ public static function checkYield(
TypeValidatorRegistry $registry,
object|string|null $thisOrClass = null
): mixed {
$returnTypeNode = self::resolveGeneratorReturnType($function, $thisOrClass);
if ($returnTypeNode === null) {
$types = self::resolveYieldAndSendTypes($function, $thisOrClass);
if ($types === null) {
return $value;
}

[$keyTypeNode, $itemTypeNode] = self::extractYieldTypes($returnTypeNode);
[$keyTypeNode, $itemTypeNode] = $types;

if ($key !== null && $keyTypeNode !== null) {
$err = $registry->validate($key, $keyTypeNode, "$function(): Return iterator key");
Expand All @@ -82,43 +105,91 @@ public static function checkYield(
}

/**
* Resolves the generator's return contract, applying alias expansion, template substitution, and special types.
* Resolves and caches the generator's yielded key, value, and sent types in memory.
*
* @return array{0: ?TypeNode, 1: ?TypeNode, 2: ?TypeNode}|null
*/
private static function resolveGeneratorReturnType(string $function, object|string|null $thisOrClass): ?TypeNode
private static function resolveYieldAndSendTypes(string $function, object|string|null $thisOrClass): ?array
{
if (isset(self::$noContractCache[$function])) {
return null;
}

if (isset(self::$staticYieldTypeCache[$function])) {
return self::$staticYieldTypeCache[$function];
}

$contract = DocblockParser::parse($function);
$returnTypeNode = $contract['return'] ?? null;

if ($returnTypeNode === null) {
self::$noContractCache[$function] = true;

return null;
}

$allTemplates = [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];
$aliases = $contract['aliases'] ?? [];
$hasGenerics = \count($allTemplates) > 0;
$hasAliases = \count($aliases) > 0;
$thisObj = \is_object($thisOrClass) ? $thisOrClass : null;

// Static / non-generic generator: cache permanently for this function
if (! $hasGenerics && ! $hasAliases && $thisObj === null) {
$resolvedNode = SpecialTypeResolver::resolve($returnTypeNode, $function, null);

return self::$staticYieldTypeCache[$function] = self::extractYieldTypes($resolvedNode);
}

// Generic generator: check signature-based 2D cache
$boundTemplates = $hasGenerics ? TemplateManager::getBoundTemplates($function, $thisObj, $allTemplates) : [];

$sig = null;
$boundCount = \count($boundTemplates);
if ($boundCount > 0 && $boundCount <= 2 && ! $hasAliases && $thisObj === null) {
if ($boundCount === 1) {
$first = reset($boundTemplates);
$sig = $first instanceof IdentifierTypeNode ? $first->name : (string) $first;
} else {
$sig = '';
foreach ($boundTemplates as $v) {
$sig .= ($v instanceof IdentifierTypeNode ? $v->name : (string) $v) . '|';
}
}

if (isset(self::$genericYieldTypeCache[$function][$sig])) {
return self::$genericYieldTypeCache[$function][$sig];
}
}

if ($returnTypeNode instanceof IdentifierTypeNode && isset($aliases[$returnTypeNode->name])) {
$returnTypeNode = $aliases[$returnTypeNode->name];
}

$thisObj = \is_object($thisOrClass) ? $thisOrClass : null;
$allTemplates = [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];
$boundTemplates = TemplateManager::getBoundTemplates($function, $thisObj, $allTemplates);

if (\count($boundTemplates) > 0 || \count($allTemplates) > 0) {
$returnTypeNode = TemplateSubstitutor::substitute($returnTypeNode, $boundTemplates, $allTemplates);
$returnTypeNode = SpecialTypeResolver::resolve($returnTypeNode, $function, $thisObj);
}

return $returnTypeNode;
$types = self::extractYieldTypes($returnTypeNode);

if ($sig !== null) {
self::$genericYieldTypeCache[$function][$sig] = $types;
}

return $types;
}

/**
* Extracts yielded key and item TypeNodes from a resolved generator/array AST node.
* Extracts yielded key, item, and sent (TSend) TypeNodes from a resolved generator/array AST node.
*
* @return array{0: ?TypeNode, 1: ?TypeNode}
* @return array{0: ?TypeNode, 1: ?TypeNode, 2: ?TypeNode}
*/
private static function extractYieldTypes(TypeNode $returnTypeNode): array
{
$itemTypeNode = null;
$keyTypeNode = null;
$sendTypeNode = null;

if ($returnTypeNode instanceof GenericTypeNode) {
$typesCount = \count($returnTypeNode->genericTypes);
Expand All @@ -127,11 +198,12 @@ private static function extractYieldTypes(TypeNode $returnTypeNode): array
} elseif ($typesCount >= 2) {
$keyTypeNode = $returnTypeNode->genericTypes[0];
$itemTypeNode = $returnTypeNode->genericTypes[1];
$sendTypeNode = $returnTypeNode->genericTypes[2] ?? null;
}
} elseif ($returnTypeNode instanceof ArrayTypeNode) {
$itemTypeNode = $returnTypeNode->type;
}

return [$keyTypeNode, $itemTypeNode];
return [$keyTypeNode, $itemTypeNode, $sendTypeNode];
}
}
Loading
Loading