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
426 changes: 312 additions & 114 deletions src/Internal/Ast/ContractVisitor.php

Large diffs are not rendered by default.

387 changes: 200 additions & 187 deletions src/Internal/Ast/FunctionContractInjector.php

Large diffs are not rendered by default.

30 changes: 22 additions & 8 deletions src/Internal/Ast/NodeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,30 @@ public static function createPropertyCheckCall(Node\Expr $valueExpr, Node\Expr $
);
}

public static function createVariableCheckCall(Node\Expr $valueExpr, string $typeString, string $varName): Node\Expr\FuncCall
{
public static function createVariableCheckCall(
Node\Expr $valueExpr,
string $typeString,
string $varName,
?Node\Expr $callerExpr = null,
?Node\Expr $thisArg = null
): Node\Expr\FuncCall {
$args = [
new Node\Arg($valueExpr),
new Node\Arg(new Node\Scalar\String_($typeString)),
new Node\Arg(new Node\Scalar\String_($varName)),
new Node\Arg(new Node\Scalar\MagicConst\File()),
];

if ($callerExpr !== null) {
$args[] = new Node\Arg($callerExpr);
if ($thisArg !== null) {
$args[] = new Node\Arg($thisArg);
}
}

return new Node\Expr\FuncCall(
new Node\Name('\TypePHP\Internal\RuntimeTypeChecker::checkVariable'),
[
new Node\Arg($valueExpr),
new Node\Arg(new Node\Scalar\String_($typeString)),
new Node\Arg(new Node\Scalar\String_($varName)),
new Node\Arg(new Node\Scalar\MagicConst\File()),
]
$args
);
}

Expand Down
180 changes: 69 additions & 111 deletions src/Internal/Checker/InlineChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,27 @@ final class InlineChecker
private static array $parsedTypeNodeCache = [];

/**
* Memoized cache for PHP internal function determinations.
* In-memory cache for resolved class contexts with static bounds.
*
* @var array<string, bool>
* @var array<string, TypeNode>
*/
private static array $internalFunctionsCache = [];
private static array $resolvedClassContextCache = [];

/**
* In-memory cache for properties known to have no DocBlock annotations.
*
* @var array<string, true>
*/
public static array $nullPropertyCache = [];

/**
* Resets internal type node and function caches. Useful for test isolation.
*/
public static function reset(): void
{
self::$parsedTypeNodeCache = [];
self::$internalFunctionsCache = [];
self::$resolvedClassContextCache = [];
self::$nullPropertyCache = [];
}

/**
Expand Down Expand Up @@ -113,8 +121,15 @@ public static function reset(): void
/**
* Evaluates inline variable validation dynamically based on configuration.
*/
public static function checkVariable(mixed $value, string $typeString, string $varName, string $file, TypeValidatorRegistry $registry): mixed
{
public static function checkVariable(
mixed $value,
string $typeString,
string $varName,
string $file,
TypeValidatorRegistry $registry,
?string $caller = null,
mixed $thisOrClass = null
): mixed {
$rawConfig = Config::get()['inline_vars'] ?? [];
/** @var array<string, bool> $config */
$config = \is_array($rawConfig) ? $rawConfig : [];
Expand All @@ -131,8 +146,8 @@ public static function checkVariable(mixed $value, string $typeString, string $v
$typeNode = SpecialTypeResolver::resolveForFile($typeNode, $file);
}

if ($needsContext) {
$typeNode = self::resolveCallerContext($typeNode);
if ($needsContext && $caller !== null) {
$typeNode = self::resolveCallerContext($typeNode, $caller, $thisOrClass);
}

if (! self::shouldValidateType($typeNode, $config)) {
Expand Down Expand Up @@ -203,6 +218,13 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string
return $value;
}

$className = \is_string($objectOrClass) ? $objectOrClass : \get_class($objectOrClass);
$cacheKey = $className . '::$' . $propName;

if (isset(self::$nullPropertyCache[$cacheKey])) {
return $value;
}

$rawConfig = Config::get()['inline_vars'] ?? [];
/** @var array<string, bool> $config */
$config = \is_array($rawConfig) ? $rawConfig : [];
Expand All @@ -211,10 +233,10 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string
return $value;
}

$className = \is_string($objectOrClass) ? $objectOrClass : \get_class($objectOrClass);

$typeNode = DocblockParser::parseProperty($className, $propName);
if ($typeNode === null) {
self::$nullPropertyCache[$cacheKey] = true;

return $value;
}

Expand Down Expand Up @@ -255,113 +277,29 @@ private static function hasActiveInlineChecks(array $config): bool
/**
* Resolves caller class or function context and applies templates & type aliases to the AST.
*/
private static function resolveCallerContext(TypeNode $typeNode): TypeNode
{
$frameInfo = self::findCallerFrame();

if ($frameInfo['functionName'] !== null) {
return self::resolveFunctionContext($typeNode, $frameInfo['functionName']);
}

if ($frameInfo['className'] !== null) {
return self::resolveClassContext(
$typeNode,
$frameInfo['className'],
$frameInfo['methodName'],
$frameInfo['thisObj']
);
}

return $typeNode;
}

/**
* Inspects the backtrace to find the nearest non-internal caller frame.
*
* @return array{className: ?string, methodName: ?string, functionName: ?string, thisObj: ?object}
*/
private static function findCallerFrame(): array
private static function resolveCallerContext(TypeNode $typeNode, ?string $caller = null, mixed $thisOrClass = null): TypeNode
{
$className = null;
$methodName = null;
$functionName = null;
$thisObj = null;

$trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 15);

foreach ($trace as $frame) {
$classCandidate = $frame['class'] ?? null;
$funcCandidate = $frame['function'];

if ($classCandidate === 'Closure' || $classCandidate === 'Generator') {
if ($thisObj === null && isset($frame['object']) && ! ($frame['object'] instanceof \Closure) && ! ($frame['object'] instanceof \Generator)) {
$thisObj = $frame['object'];
}

continue;
if ($caller !== null) {
if ($caller === '') {
return $typeNode;
}

if ($funcCandidate === '{closure}' || str_starts_with($funcCandidate, '{closure')) {
if ($thisObj === null && isset($frame['object']) && ! ($frame['object'] instanceof \Closure) && ! ($frame['object'] instanceof \Generator)) {
$thisObj = $frame['object'];
}
if (str_contains($caller, '::')) {
[$className, $methodName] = explode('::', $caller, 2);
$thisObj = \is_object($thisOrClass) ? $thisOrClass : null;

continue;
return self::resolveClassContext(
$typeNode,
$className,
$methodName,
$thisObj
);
}

if ($classCandidate !== null) {
if (! str_starts_with($classCandidate, 'TypePHP\\Internal\\') && ! str_starts_with($classCandidate, 'TypePHP\\Wrapper\\')) {
$className = $classCandidate;
$methodName = $funcCandidate;
if ($thisObj === null) {
$thisObj = $frame['object'] ?? null;
}

break;
}
} else {
if (! str_starts_with($funcCandidate, 'TypePHP\\')) {
if (! \in_array($funcCandidate, ['include', 'include_once', 'require', 'require_once', 'eval'], true)) {
if (self::isInternalFunction($funcCandidate)) {
continue;
}

$functionName = $funcCandidate;

break;
}
}
}
}

return [
'className' => $className,
'methodName' => $methodName,
'functionName' => $functionName,
'thisObj' => $thisObj,
];
}

/**
* Fast check if a function name represents an internal PHP built-in function.
*/
private static function isInternalFunction(string $funcName): bool
{
if (! \function_exists($funcName)) {
return false;
}

if (isset(self::$internalFunctionsCache[$funcName])) {
return self::$internalFunctionsCache[$funcName];
return self::resolveFunctionContext($typeNode, $caller);
}

try {
$rf = new \ReflectionFunction($funcName);

return self::$internalFunctionsCache[$funcName] = $rf->isInternal();
} catch (\ReflectionException $e) {
return self::$internalFunctionsCache[$funcName] = false;
}
return $typeNode;
}

/**
Expand Down Expand Up @@ -408,21 +346,37 @@ private static function resolveClassContext(
return $typeNode;
}

$cacheKey = null;
if ($thisObj === null) {
$cacheKey = ((string) $typeNode) . '|' . $className . '|' . ($methodName ?? '');
if (isset(self::$resolvedClassContextCache[$cacheKey])) {
return self::$resolvedClassContextCache[$cacheKey];
}
}

try {
/** @var class-string<object> $className */
$refClass = new \ReflectionClass($className);
$typeNode = SpecialTypeResolver::resolve($typeNode, $refClass);

$classAliases = DocblockParser::parseClassAliases($className);

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

$contract = DocblockParser::parse($targetFunc);
$declaredTemplates = $contract['allTemplates'] ?? ($contract['classTemplates'] ?? []);
$boundTemplates = TemplateManager::getBoundTemplates($targetFunc, $thisObj, $declaredTemplates);

if (\count($classAliases) === 0 && \count($declaredTemplates) === 0) {
if ($cacheKey !== null) {
return self::$resolvedClassContextCache[$cacheKey] = $typeNode;
}

return $typeNode;
}

$boundTemplates = TemplateManager::getBoundTemplates($targetFunc, $thisObj, $declaredTemplates);
$activeBindings = [...$classAliases, ...$boundTemplates];

if (\count($activeBindings) > 0 || \count($declaredTemplates) > 0) {
Expand All @@ -433,6 +387,10 @@ private static function resolveClassContext(
// Silently continue if reflection fails
}

if ($cacheKey !== null) {
return self::$resolvedClassContextCache[$cacheKey] = $typeNode;
}

return $typeNode;
}

Expand Down
Loading
Loading