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 src/Internal/Ast/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ private function handleExpression(Node\Stmt\Expression $node): ?array
}
}

return $checkStmts !== [] ? array_merge([$node], $checkStmts) : null;
return $checkStmts !== [] ? [$node, ...$checkStmts] : null;
}

private function handleAssign(Node\Expr\Assign $node): void
Expand Down Expand Up @@ -517,7 +517,7 @@ private function extractDestructuringVariables(Node\Expr\List_|Node\Expr\Array_
'expr' => $item->value,
];
} elseif ($item->value instanceof Node\Expr\List_ || $item->value instanceof Node\Expr\Array_) {
$vars = array_merge($vars, $this->extractDestructuringVariables($item->value));
$vars = [...$vars, ...$this->extractDestructuringVariables($item->value)];
}
}

Expand Down
73 changes: 58 additions & 15 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?

$hasParamOut = $byRefParams !== [] && ($hasParamOutDoc || $hasInheritance);

$hasSelfOutDoc = str_contains($docText, 'self-out') || str_contains($docText, 'this-out');
$hasSelfOut = $isClassMethod && ! $node->isStatic() && ($hasSelfOutDoc || $hasInheritance);

$hasReturnDoc = str_contains($docText, '@return')
|| str_contains($docText, '@phpstan-return')
|| str_contains($docText, '@psalm-return');
Expand All @@ -86,7 +89,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
&& ! ($isNativeVoid && ! $hasReturnDoc)
&& self::hasReturnContracts($docText, $isClassMethod, $isPrivate);

if (! $hasParam && ! $hasReturn && ! $hasParamOut) {
if (! $hasParam && ! $hasReturn && ! $hasParamOut && ! $hasSelfOut) {
return;
}

Expand All @@ -100,7 +103,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
$injectedStmts = self::buildParamInjections($node->params, $docText, $thisArg);
}

if ($hasReturn || $hasParamOut) {
if ($hasReturn || $hasParamOut || $hasSelfOut) {
$node->stmts = self::isGenerator($node)
? self::wrapGeneratorReturns($node->stmts, $thisArg)
: self::wrapNonGeneratorReturns(
Expand All @@ -109,13 +112,35 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
$isNativeVoid,
$needsReturnVars,
$hasReturn,
$hasParamOut ? $byRefParams : []
$hasParamOut ? $byRefParams : [],
$hasSelfOut
);
}

$node->stmts = [...$injectedStmts, ...$node->stmts];
}

public static function buildSelfOutCheckStmt(Node\Expr $thisArg, bool $needsReturnVars = false): Node\Stmt\Expression
{
$varsArg = $needsReturnVars
? new Node\Expr\Variable('_typephpArgs')
: new Node\Expr\Array_();

$checkCall = new Node\Expr\FuncCall(
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::checkSelfOut'),
[
new Node\Arg(new Node\Scalar\MagicConst\Method()),
new Node\Arg($thisArg),
new Node\Arg($varsArg),
]
);

$stmt = new Node\Stmt\Expression($checkCall);
$stmt->setAttribute('typephp_injected', true);

return $stmt;
}

private static function resolveDocComment(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): ?Doc
{
$doc = $node->getDocComment();
Expand Down Expand Up @@ -699,10 +724,11 @@ private static function wrapNonGeneratorReturns(
bool $isNativeVoid,
bool $needsReturnVars = false,
bool $hasReturn = true,
array $byRefParams = []
array $byRefParams = [],
bool $hasSelfOut = false
): array {
$traverser = new NodeTraverser();
$traverser->addVisitor(new class ($thisArg, $isNativeVoid, $needsReturnVars, $hasReturn, $byRefParams) extends NodeVisitorAbstract {
$traverser->addVisitor(new class ($thisArg, $isNativeVoid, $needsReturnVars, $hasReturn, $byRefParams, $hasSelfOut) extends NodeVisitorAbstract {
/**
* @param array<string> $byRefParams
*/
Expand All @@ -711,7 +737,8 @@ public function __construct(
private bool $isNativeVoid,
private bool $needsReturnVars,
private bool $hasReturn,
private array $byRefParams
private array $byRefParams,
private bool $hasSelfOut
) {
}

Expand All @@ -726,12 +753,21 @@ public function enterNode(Node $n): int|array|null
return null;
}

$exitStmts = [];
if ($this->hasSelfOut) {
$exitStmts[] = FunctionContractInjector::buildSelfOutCheckStmt($this->thisArg, $this->needsReturnVars);
}

$paramOutStmts = $this->byRefParams !== []
? FunctionContractInjector::buildParamOutCheckStmts($this->byRefParams, $this->thisArg)
: [];

if ($paramOutStmts !== []) {
$exitStmts = [...$exitStmts, ...$paramOutStmts];
}

if (! $this->hasReturn) {
return $paramOutStmts !== [] ? [...$paramOutStmts, $n] : null;
return $exitStmts !== [] ? [...$exitStmts, $n] : null;
}

$exprToWrap = $n->expr ?? new Node\Expr\ConstFetch(new Node\Name('null'));
Expand All @@ -740,7 +776,7 @@ public function enterNode(Node $n): int|array|null
$checkCall = FunctionContractInjector::buildReturnCheckCall($exprToWrap, $this->thisArg, $this->needsReturnVars);
$voidGuardStmts = FunctionContractInjector::buildVoidReturnGuard($checkCall);

return [...$paramOutStmts, ...$voidGuardStmts];
return [...$exitStmts, ...$voidGuardStmts];
}

// Call-site cache bypass for return checks
Expand All @@ -764,7 +800,7 @@ public function enterNode(Node $n): int|array|null
$ternaryExpr
);

return $paramOutStmts !== [] ? [...$paramOutStmts, $n] : null;
return $exitStmts !== [] ? [...$exitStmts, $n] : null;
}

return null;
Expand All @@ -776,21 +812,28 @@ public function enterNode(Node $n): int|array|null

$lastStmt = end($newStmts);
if (! $lastStmt instanceof Node\Stmt\Return_ && ! ($lastStmt instanceof Node\Stmt\Expression && $lastStmt->expr instanceof Node\Expr\Throw_)) {
$exitStmts = [];
if ($hasSelfOut) {
$exitStmts[] = self::buildSelfOutCheckStmt($thisArg, $needsReturnVars);
}

$paramOutStmts = $byRefParams !== []
? self::buildParamOutCheckStmts($byRefParams, $thisArg)
: [];

if ($paramOutStmts !== []) {
$exitStmts = [...$exitStmts, ...$paramOutStmts];
}

if (! $hasReturn) {
if ($paramOutStmts !== []) {
$retStmt = new Node\Stmt\Return_(null);
$retStmt->setAttribute('typephp_injected', true);
$newStmts = [...$newStmts, ...$paramOutStmts, $retStmt];
if ($exitStmts !== []) {
$newStmts = [...$newStmts, ...$exitStmts];
}
} else {
$checkCall = self::buildReturnCheckCall(new Node\Expr\ConstFetch(new Node\Name('null')), $thisArg, $needsReturnVars);

if ($isNativeVoid) {
$newStmts = [...$newStmts, ...$paramOutStmts, ...self::buildVoidReturnGuard($checkCall)];
$newStmts = [...$newStmts, ...$exitStmts, ...self::buildVoidReturnGuard($checkCall)];
} else {
$cacheKeyExpr = new Node\Scalar\MagicConst\Method();
$cacheCheck = new Node\Expr\Isset_([
Expand All @@ -812,7 +855,7 @@ public function enterNode(Node $n): int|array|null

$retStmt = new Node\Stmt\Return_($fallbackExpr);
$retStmt->setAttribute('typephp_injected', true);
$newStmts = [...$newStmts, ...$paramOutStmts, $retStmt];
$newStmts = [...$newStmts, ...$exitStmts, $retStmt];
}
}
}
Expand Down
24 changes: 13 additions & 11 deletions src/Internal/Checker/InlineChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public static function checkVariable(

$checkGenerics = (bool) ($config['generics'] ?? true);
if ($typeNode instanceof GenericTypeNode && $checkGenerics && \is_object($value)) {
$err = TemplateManager::bindInstanceFromNode($value, $typeNode, $context, forceBind: true);
$err = TemplateManager::bindInstanceFromNode($value, $typeNode, $context);
if ($err !== null) {
return $err;
}
Expand Down Expand Up @@ -346,8 +346,15 @@ private static function resolveClassContext(
return $typeNode;
}

$targetFunc = ($methodName !== '{closure}' && $methodName !== null && ! str_starts_with($methodName, '{closure'))
? $className . '::' . $methodName
: $className . '::__construct';

$contract = DocblockParser::parse($targetFunc);
$hasMethodTemplates = ($contract['templates'] ?? []) !== [];

$cacheKey = null;
if ($thisObj === null) {
if ($thisObj === null && ! $hasMethodTemplates) {
$cacheKey = ((string) $typeNode) . '|' . $className . '|' . ($methodName ?? '');
if (isset(self::$resolvedClassContextCache[$cacheKey])) {
return self::$resolvedClassContextCache[$cacheKey];
Expand All @@ -360,15 +367,10 @@ private static function resolveClassContext(
$typeNode = SpecialTypeResolver::resolve($typeNode, $refClass);

$classAliases = DocblockParser::parseClassAliases($className);
$allTemplates = $contract['allTemplates'] ?? [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];
$declaredTemplates = $allTemplates;

$targetFunc = ($methodName !== '{closure}' && $methodName !== null && ! str_starts_with($methodName, '{closure'))
? $className . '::' . $methodName
: $className . '::__construct';

$contract = DocblockParser::parse($targetFunc);
$declaredTemplates = $contract['allTemplates'] ?? ($contract['classTemplates'] ?? []);

if (\count($classAliases) === 0 && \count($declaredTemplates) === 0) {
if ($classAliases === [] && $declaredTemplates === []) {
if ($cacheKey !== null) {
return self::$resolvedClassContextCache[$cacheKey] = $typeNode;
}
Expand All @@ -379,7 +381,7 @@ private static function resolveClassContext(
$boundTemplates = TemplateManager::getBoundTemplates($targetFunc, $thisObj, $declaredTemplates);
$activeBindings = [...$classAliases, ...$boundTemplates];

if (\count($activeBindings) > 0 || \count($declaredTemplates) > 0) {
if ($activeBindings !== [] || $declaredTemplates !== []) {
$typeNode = TemplateSubstitutor::substitute($typeNode, $activeBindings, $declaredTemplates);
$typeNode = SpecialTypeResolver::resolve($typeNode, $refClass);
}
Expand Down
12 changes: 5 additions & 7 deletions src/Internal/Checker/ParamChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ public static function checkParams(
return null;
}

// Use pre-resolved contract or parse
$contract ??= DocblockParser::parse($effectiveFunction);

if (! $contract['hasParamContract']) {
Expand Down Expand Up @@ -387,7 +386,7 @@ public static function resolveEffectiveFunction(string $function, object|string|
}

if ($isTargetOfAlias) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6);
foreach ($trace as $frame) {
$frameFunc = $frame['function'];
$frameClass = $frame['class'] ?? '';
Expand Down Expand Up @@ -507,7 +506,7 @@ private static function inferTemplatesFromClosures(
foreach ($cTypeNode->parameters as $idx => $pNode) {
if ($pNode->type instanceof IdentifierTypeNode && isset($templates[$pNode->type->name]) && isset($closureParams[$idx])) {
$tName = $pNode->type->name;
$isClassLevel = isset($classTemplates[$tName]);
$isClassLevel = ! TemplateManager::isMethodTemplate($effectiveFunction, $tName) && isset($classTemplates[$tName]);
$targetObj = $isClassLevel ? $thisObj : null;

$inferredCandidate = self::extractTypeFromClosureParameter($closureParams[$idx]);
Expand Down Expand Up @@ -639,7 +638,6 @@ private static function inferArrayTemplatesFromAllElements(
}

$sampleItems = self::getSampleArraySlice($arrVal);

$genericCount = \count($typeNode->genericTypes);

if ($genericCount === 1 && $typeNode->genericTypes[0] instanceof IdentifierTypeNode) {
Expand Down Expand Up @@ -737,7 +735,7 @@ private static function bindTemplateIfUnbound(
): void {
$contract = DocblockParser::parse($effectiveFunction);
$classTemplates = $contract['classTemplates'] ?? [];
$isClassLevelTemplate = isset($classTemplates[$templateName]);
$isClassLevelTemplate = ! TemplateManager::isMethodTemplate($effectiveFunction, $templateName) && isset($classTemplates[$templateName]);
$targetObj = $isClassLevelTemplate ? $thisObj : null;

if (isset($templates[$templateName]) && ! TemplateManager::isBound($effectiveFunction, $targetObj, $templateName)) {
Expand Down Expand Up @@ -941,7 +939,7 @@ private static function resolveClassStringTemplate(
$innerType = $typeNode->genericTypes[0];
$templateName = $innerType->name;
$templateNode = $templates[$templateName];
$isClassLevelTemplate = isset($classTemplates[$templateName]);
$isClassLevelTemplate = ! TemplateManager::isMethodTemplate($function, $templateName) && isset($classTemplates[$templateName]);
$targetObj = $isClassLevelTemplate ? $thisObj : null;

if (! TemplateManager::isBound($function, $targetObj, $templateName)) {
Expand Down Expand Up @@ -1067,7 +1065,7 @@ private static function resolveTemplateParam(
$templateNode = $templates[$templateName];
$isVariadic = $typeNode instanceof ArrayTypeNode;
$isNullable = ($typeNode instanceof NullableTypeNode) || ($typeNode instanceof UnionTypeNode && self::typeContainsNull($typeNode));
$isClassLevelTemplate = isset($classTemplates[$templateName]);
$isClassLevelTemplate = ! TemplateManager::isMethodTemplate($function, $templateName) && isset($classTemplates[$templateName]);
$targetObj = $isClassLevelTemplate ? $thisObj : null;
$allowsNullInBound = ($templateNode->bound !== null && self::typeContainsNull($templateNode->bound));

Expand Down
93 changes: 93 additions & 0 deletions src/Internal/Checker/SelfOutChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace TypePHP\Internal\Checker;

use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use TypePHP\Internal\Docblock\DocblockParser;
use TypePHP\Internal\Generics\TemplateManager;
use TypePHP\Internal\Generics\TemplateSubstitutor;
use TypePHP\Internal\Resolver\SpecialTypeResolver;
use TypePHP\Internal\Util\Config;
use TypePHP\Internal\Validator\TypeValidatorRegistry;

/**
* @internal Evaluates generic state transitions on $this (@self-out, @phpstan-self-out, @psalm-self-out).
*/
final class SelfOutChecker
{
/**
* O(1) Fast-path cache for methods determined to have no self-out contracts.
*
* @var array<string, true>
*/
public static array $noSelfOutContractCache = [];

public static function reset(): void
{
self::$noSelfOutContractCache = [];
}

/**
* @param array<int|string, mixed> $vars
*/
public static function checkSelfOut(
string $function,
object $thisObj,
array $vars,
TypeValidatorRegistry $registry,
string $effectiveFunction = ''
): void {
if (! Config::isEnabled() || ! Config::isSelfOutEnabled()) {
return;
}

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

if ($effectiveFunction === '') {
$effectiveFunction = ParamChecker::resolveEffectiveFunction($function, $thisObj, $thisObj);
}

if (isset(self::$noSelfOutContractCache[$effectiveFunction])) {
self::$noSelfOutContractCache[$function] = true;

return;
}

$contract = DocblockParser::parse($effectiveFunction);

if (! ($contract['hasSelfOutContract'] ?? false) || $contract['selfOut'] === null) {
self::$noSelfOutContractCache[$effectiveFunction] = true;
self::$noSelfOutContractCache[$function] = true;

return;
}

$selfOutNode = $contract['selfOut'];
$allTemplates = [...($contract['classTemplates'] ?? []), ...($contract['templates'] ?? [])];
$boundTemplates = (\count($allTemplates) > 0)
? TemplateManager::getBoundTemplates($effectiveFunction, $thisObj, $allTemplates)
: [];

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

if (
$selfOutNode instanceof ConditionalTypeForParameterNode ||
$selfOutNode instanceof ConditionalTypeNode
) {
$selfOutNode = ConditionalChecker::resolve($selfOutNode, $vars, $boundTemplates, $registry, $effectiveFunction);
}

if ($selfOutNode instanceof GenericTypeNode) {
TemplateManager::bindInstanceFromNode($thisObj, $selfOutNode, forceBind: true);
}
}
}
Loading
Loading