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
87 changes: 78 additions & 9 deletions src/Internal/Ast/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use TypePHP\Internal\Docblock\DocblockNormalizer;
use TypePHP\Internal\Docblock\DocblockParser;
use TypePHP\Internal\Util\Config;
use TypePHP\Internal\Util\StubManager;

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

/**
* Tracks whether the enclosing function or method returns by reference.
*
* @var list<bool>
*/
private array $byRefStack = [];

public function __construct()
{
$this->scopeManager = new ScopeManager();
Expand All @@ -80,6 +88,10 @@ public function enterNode(Node $node): ?array
{
$this->trackDeclarationEntry($node);

if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Expr\Closure) {
$this->byRefStack[] = $node->byRef;
}

if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
$this->scopeManager->pushScope(isIsolated: true);
} elseif ($node instanceof Node\Expr\Closure) {
Expand Down Expand Up @@ -135,6 +147,10 @@ public function enterNode(Node $node): ?array
}

if ($node instanceof Node\Stmt\Return_) {
if ($node->expr !== null && $this->byRefStack !== [] && end($this->byRefStack)) {
$this->markWriteContext($node->expr);
}

$this->handleReturn($node);

return null;
Expand Down Expand Up @@ -173,10 +189,21 @@ public function leaveNode(Node $node): ?Node
{
$this->trackDeclarationExit($node);

if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Expr\Closure) {
array_pop($this->byRefStack);
}

if ($node instanceof Node\Expr\Clone_) {
return $this->wrapClone($node);
}

if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && strtolower($node->name->getLast()) === 'clone') {
$replacement = $this->wrapCloneFunction($node);
if ($replacement !== null) {
return $replacement;
}
}

if ($node instanceof Node\Expr\AssignOp) {
$replacement = $this->handleAssignOp($node);
if ($replacement !== null) {
Expand All @@ -195,13 +222,6 @@ 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) {
$replacement = $this->handleIncDec($node);
if ($replacement !== null) {
return $replacement;
}
}

if ($node instanceof Node\Expr\StaticPropertyFetch && $node->name instanceof Node\VarLikeIdentifier) {
$propName = $node->name->toString();

Expand Down Expand Up @@ -232,6 +252,46 @@ public function leaveNode(Node $node): ?Node
return null;
}

private function wrapCloneFunction(Node\Expr\FuncCall $node): ?Node\Expr
{
if ($node->getAttribute('typephp_wrapped') === true) {
return null;
}

$firstArg = $node->args[0] ?? null;
if (! $firstArg instanceof Node\Arg) {
return null;
}

$node->setAttribute('typephp_wrapped', true);

$originalExpr = $firstArg->value;

$secondArg = $node->args[1] ?? null;
$propertiesExpr = $secondArg instanceof Node\Arg
? $secondArg->value
: new Node\Expr\ConstFetch(new Node\Name('null'));

$node->args[0] = new Node\Arg(
new Node\Expr\FuncCall(
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker::prepareClone'),
[new Node\Arg($originalExpr)],
),
);

return new Node\Expr\StaticCall(
new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker'),
'cloneWith',
[
new Node\Arg($node),
new Node\Arg($originalExpr),
new Node\Arg($propertiesExpr),
new Node\Arg(new Node\Scalar\MagicConst\File()),
new Node\Arg(new Node\Scalar\LNumber($node->getStartLine())),
],
);
}

private function markWriteContext(Node $node): void
{
while ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) {
Expand Down Expand Up @@ -429,13 +489,23 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
$hasRealInheritance = false;
$hasPropertyWithDoc = false;
$isReadonly = false;
$isFinal = false;
$hasTemplates = false;

$doc = $node->getDocComment();
if ($doc === null && $typeName !== null && StubManager::hasClassStub($typeName)) {
$stubDocText = StubManager::getClassDoc($typeName);
if ($stubDocText !== null) {
$doc = new \PhpParser\Comment\Doc($stubDocText);
}
}

if ($node instanceof Node\Stmt\Class_) {
$hasExtends = $node->extends !== null;
$hasImplements = $node->implements !== [];
$hasTraits = false;
$isReadonly = ($node->flags & Node\Stmt\Class_::MODIFIER_READONLY) !== 0;
$isFinal = ($node->flags & Node\Stmt\Class_::MODIFIER_FINAL) !== 0;

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

$doc = $node->getDocComment();
$hasTemplates = $doc !== null && (
str_contains($doc->getText(), '@template')
|| str_contains($doc->getText(), '@phpstan-template')
Expand All @@ -473,7 +542,6 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
$hasRealInheritance = $node->implements !== [];
$hasInheritance = $hasRealInheritance;
} else {
$doc = $node->getDocComment();
$hasTemplates = $doc !== null && (
str_contains($doc->getText(), '@template')
|| str_contains($doc->getText(), '@phpstan-template')
Expand All @@ -488,6 +556,7 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
'hasRealInheritance' => $hasRealInheritance,
'hasPropertyWithDoc' => $hasPropertyWithDoc,
'isReadonly' => $isReadonly,
'isFinal' => $isFinal,
'hasTemplates' => $hasTemplates,
];
}
Expand Down
95 changes: 86 additions & 9 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use TypePHP\Internal\Util\StubManager;

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

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

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

$hasInheritance = $classContext['hasInheritance'] ?? true;
$hasRealInheritance = $classContext['hasRealInheritance'] ?? $hasInheritance;
$hasPropertyWithDoc = $classContext['hasPropertyWithDoc'] ?? true;
$isReadonlyClass = $classContext['isReadonly'] ?? false;
$isFinalClass = $classContext['isFinal'] ?? false;
$hasClassTemplates = $classContext['hasTemplates'] ?? false;

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

$hasMethodReturnStatements = self::hasReturnStatements($node);

$hasReturn = ! $isMagicLifecycle
&& ! $isNativeNever
&& ! ($isNativeVoid && ! $hasReturnDoc)
&& ($isMagicGet || $isMagicCall || self::hasReturnContracts($docText, $isClassMethod, $isPrivate));
&& ($isMagicGet || $isMagicCall || self::hasReturnContracts(
$docText,
$isClassMethod,
$hasInheritance,
$isPrivate,
$isFinalClass,
$node->returnType !== null,
$hasMethodReturnStatements,
$classContext === null,
));

$byRefParams = [];
foreach ($node->params as $p) {
Expand Down Expand Up @@ -104,7 +117,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
$isConstructor,
$hasPropertyWithDoc,
$classContext === null,
$node->attrGroups !== []
$node->attrGroups !== [],
);

if (! $hasParam && ! $hasReturn && ! $hasParamOut && ! $hasSelfOut) {
Expand Down Expand Up @@ -132,7 +145,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?
$hasReturn,
$hasParamOut ? $byRefParams : [],
$hasSelfOut,
$node->byRef
$node->byRef,
);
}

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

private static function resolveDocComment(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): ?Doc
{
/**
* @param array{name?: ?string, hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, isFinal?: bool, hasTemplates?: bool}|null $classContext
*/
private static function resolveDocComment(
Node\Stmt\Function_|Node\Stmt\ClassMethod $node,
?array $classContext = null,
): ?Doc {
$doc = $node->getDocComment();
if ($doc !== null) {
return $doc;
Expand All @@ -176,6 +194,25 @@ private static function resolveDocComment(Node\Stmt\Function_|Node\Stmt\ClassMet
}
}

$className = $classContext['name'] ?? null;
if ($node instanceof Node\Stmt\ClassMethod && $className !== null) {
$methodName = $node->name->toString();
if (StubManager::hasMethodStub($className, $methodName)) {
$stubText = StubManager::getMethodDoc($className, $methodName);
if ($stubText !== null) {
return new Doc($stubText);
}
}
} elseif ($node instanceof Node\Stmt\Function_) {
$funcName = $node->name->toString();
if (StubManager::hasFunctionStub($funcName)) {
$stubText = StubManager::getFunctionDoc($funcName);
if ($stubText !== null) {
return new Doc($stubText);
}
}
}

return null;
}

Expand Down Expand Up @@ -248,8 +285,17 @@ private static function hasNonMixedParam(string $docText): bool
private static function hasReturnContracts(
string $docText,
bool $isClassMethod,
bool $isPrivate
bool $hasInheritance,
bool $isPrivate,
bool $isFinalClass = false,
bool $hasNativeReturnType = false,
bool $hasReturnStatements = false,
bool $isDirectUnitTest = false,
): bool {
if ($isDirectUnitTest && $isClassMethod) {
return true;
}

if ($isPrivate && $docText === '') {
return false;
}
Expand All @@ -275,7 +321,38 @@ private static function hasReturnContracts(
return true;
}

return $isClassMethod && ! $isPrivate;
if (! $isClassMethod) {
return false;
}

if ($hasInheritance && ! $isPrivate) {
return true;
}

if ($isFinalClass) {
return false;
}

if (! $hasNativeReturnType && ! $hasReturnStatements) {
return false;
}

return ! $isPrivate;
}

private static function hasReturnStatements(Node\Stmt\Function_|Node\Stmt\ClassMethod $node): bool
{
if ($node->stmts === null || $node->stmts === []) {
return false;
}

foreach ($node->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr !== null) {
return true;
}
}

return false;
}

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