TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

365 lines
14 KiB

<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Immutable;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\FunctionDef;
use TypePhp\Type;
/** Compile-time effect checks; successful checks emit no runtime code. */
trait ImmutableSupportTrait
{
protected function immutableTypeNodeMayBeObject(?NodeAbstract $type): bool
{
if ($type === null) {
return true;
}
if ($type instanceof Node\NullableType) {
return $this->immutableTypeNodeMayBeObject($type->type);
}
if ($type instanceof Node\UnionType || $type instanceof Node\IntersectionType) {
foreach ($type->types as $member) {
if ($this->immutableTypeNodeMayBeObject($member)) {
return true;
}
}
return false;
}
if ($type instanceof Node\Name) {
return true;
}
if ($type instanceof Node\Identifier) {
return in_array(strtolower($type->toString()), ['mixed', 'object', 'iterable', 'callable'], true);
}
return false;
}
protected function initializeImmutableFunctionContext(): void
{
if ($this->functionDef?->immutable && $this->methodDef !== null) {
$this->context->immutableVars['this_'] = true;
$this->context->immutableObjectVars['this_'] = true;
}
foreach ($this->functionDef?->argInfoList ?? [] as $argument) {
if (!$argument->immutable) {
continue;
}
$this->context->immutableVars[$argument->name] = true;
if ($argument->type === Type::OBJECT || $argument->type === Type::VAR) {
$this->context->immutableObjectVars[$argument->name] = true;
}
}
}
protected function immutableRootName(NodeAbstract $expression): ?string
{
if ($this->context->immutableVars === []) {
return null;
}
if ($expression instanceof Node\Expr\ErrorSuppress) {
return $this->immutableRootName($expression->expr);
}
if ($expression instanceof Node\Expr\Variable && is_string($expression->name)) {
$name = $this->parseVariable($expression);
return isset($this->context->immutableVars[$name]) ? $name : null;
}
if ($expression instanceof Node\Expr\PropertyFetch
|| $expression instanceof Node\Expr\NullsafePropertyFetch
|| $expression instanceof Node\Expr\ArrayDimFetch
) {
return $this->immutableRootName($expression->var);
}
if (($expression instanceof Node\Expr\MethodCall
|| $expression instanceof Node\Expr\NullsafeMethodCall)
&& $this->immutableRootName($expression->var) !== null
&& $this->immutableCalledMethod($expression)?->immutable
) {
return $this->immutableRootName($expression->var);
}
return null;
}
protected function immutableDisplayName(string $name): string
{
return $name === 'this_' ? '$this' : '$' . $this->unescapeVarName($name);
}
protected function immutableValueMayBeObject(NodeAbstract $expression): bool
{
$root = $this->immutableRootName($expression);
if ($root === null) {
return false;
}
$type = $this->detectTypeOfExpr($expression);
// An immutable receiver may produce an ordinary scalar/COW value.
// Preserve constness only when object identity is possible.
if ($type !== Type::OBJECT && $type !== Type::VAR && $type !== Type::REF) {
return false;
}
if (isset($this->context->immutableObjectVars[$root])) {
return true;
}
return $this->detectClassOfExpr($expression) !== ''
|| $type === Type::OBJECT;
}
protected function assertImmutableMutationTarget(NodeAbstract $target): void
{
if ($target instanceof Node\Expr\List_ || $target instanceof Node\Expr\Array_) {
foreach ($target->items as $item) {
if ($item !== null) {
$this->assertImmutableMutationTarget($item->value);
}
}
return;
}
$root = $this->immutableRootName($target);
if ($root !== null) {
$this->fatalError(
$target,
'Cannot modify immutable value `' . $this->immutableDisplayName($root) . '`',
);
}
}
protected function recordImmutableAlias(NodeAbstract $left, NodeAbstract $right): void
{
if ($right instanceof Node\Expr\Clone_) {
return;
}
$root = $this->immutableRootName($right);
if ($root === null || !$this->immutableValueMayBeObject($right)) {
return;
}
if (!$left instanceof Node\Expr\Variable || !is_string($left->name)) {
$this->fatalError(
$right,
'Immutable object `' . $this->immutableDisplayName($root)
. '` cannot be stored in mutable state',
);
}
$name = $this->parseVariable($left);
if ($this->hasScopeGlobalVar($name) || $this->hasStaticVar($name)) {
$this->fatalError(
$right,
'Immutable object `' . $this->immutableDisplayName($root)
. '` cannot be stored in mutable state',
);
}
$this->context->immutableVars[$name] = true;
$this->context->immutableObjectVars[$name] = true;
$class = $this->detectClassOfExpr($right);
if ($class !== '') {
$this->addObject($name, $class);
}
}
protected function assertImmutableObjectDoesNotEscape(NodeAbstract $expression, string $destination): void
{
$root = $this->immutableRootName($expression);
if ($root !== null && $this->immutableValueMayBeObject($expression)) {
$this->fatalError(
$expression,
'Immutable object `' . $this->immutableDisplayName($root)
. '` cannot escape through ' . $destination,
);
}
}
protected function assertImmutableValueMethodDoesNotMutate(
Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $call,
string $root,
): void {
if (!$call->name instanceof Node\Identifier) {
return;
}
$type = $this->detectTypeOfExpr($call->var);
$method = $call->name->toString();
$definition = self::UNIVERSAL_METHODS[$type][$method] ?? null;
if ($definition !== null && in_array($definition['handler'], self::MUTATING_HANDLERS, true)) {
$this->fatalError(
$call,
"Cannot call mutating method `{$method}()` on immutable value `"
. $this->immutableDisplayName($root) . '`',
);
}
}
protected function immutableExtensionAcceptsReceiver(
Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $call,
): bool {
if (!$call->name instanceof Node\Identifier) {
return false;
}
$method = $call->name->toString();
$class = $this->detectClassOfExpr($call->var);
if ($class !== '') {
$definition = $this->findObjectExtensionMethod($class, $method, true);
} else {
$definition = $this->findExtensionMethod($this->detectTypeOfExpr($call->var), $method);
}
$definition ??= $this->findKeywordExtensionMethod($method);
return (bool) ($definition['receiver_immutable'] ?? false);
}
protected function immutableCalledMethod(
Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $call,
): ?FunctionDef {
$ordinary = $call instanceof Node\Expr\NullsafeMethodCall
? new Node\Expr\MethodCall($call->var, $call->name, $call->args, $call->getAttributes())
: $call;
return $this->resolveCalledFunctionDef($ordinary);
}
protected function immutableClassName(Node\Name $name): string
{
$class = $this->parseIdentifier($name);
if ($class === 'self' || $class === 'static') {
return $this->getFullClassName();
}
if ($class === 'parent') {
return $this->classDef?->extends ?? '';
}
return $this->getNamespacedClassName($class);
}
protected function immutableArgInfo(
FunctionDef $function,
Node\Arg $argument,
int $index,
): ?ArgInfo {
if ($argument->name === null) {
return $this->getArgInfoByIndex($function, $index);
}
if (!$argument->name instanceof Node\Identifier) {
return null;
}
$name = $argument->name->toString();
$variadic = null;
foreach ($function->argInfoList as $info) {
if ($info->variadic) {
$variadic = $info;
}
if (($info->phpName ?: $this->unescapeVarName($info->name)) === $name) {
return $info;
}
}
return $variadic;
}
/** @return array{string, string} function/method name and class name */
protected function immutableCallableName(Node\Expr\CallLike $call): array
{
if ($call instanceof Node\Expr\FuncCall && $call->name instanceof Node\Name) {
return [ltrim($this->parseIdentifier($call->name), '\\'), ''];
}
if (($call instanceof Node\Expr\MethodCall || $call instanceof Node\Expr\NullsafeMethodCall)
&& $call->name instanceof Node\Identifier
) {
$class = $this->detectClassOfExpr($call->var);
if ($class === '' && $call->var instanceof Node\Expr\Variable && is_string($call->var->name)) {
$name = $this->parseVariable($call->var);
$class = $name === 'this_' ? $this->getFullClassName() : $this->getDeclaredObjectType($name);
}
return [$call->name->toString(), $class];
}
if ($call instanceof Node\Expr\StaticCall
&& $call->class instanceof Node\Name
&& $call->name instanceof Node\Identifier
) {
$class = $this->immutableClassName($call->class);
return [$call->name->toString(), $class];
}
if ($call instanceof Node\Expr\New_ && $call->class instanceof Node\Name) {
return ['__construct', $this->immutableClassName($call->class)];
}
return ['', self::DYNAMIC_CALLED_CLASS];
}
protected function validateImmutableCall(Node\Expr\CallLike $call): void
{
if ($this->context->immutableVars === []) {
return;
}
if ($call->getAttribute('typephpImmutableValidated', false)) {
return;
}
$call->setAttribute('typephpImmutableValidated', true);
$function = $this->resolveCalledFunctionDef($call);
if ($call instanceof Node\Expr\NullsafeMethodCall) {
$function = $this->immutableCalledMethod($call);
} elseif ($call instanceof Node\Expr\New_ && $call->class instanceof Node\Name) {
$class = $this->immutableClassName($call->class);
$function = $class === '' ? null : $this->findAotMethodFunctionDef($class, '__construct');
}
if (($call instanceof Node\Expr\MethodCall || $call instanceof Node\Expr\NullsafeMethodCall)
&& $call->name instanceof Node\Identifier
&& ($root = $this->immutableRootName($call->var)) !== null
) {
// A named call must be proven immutable. A variable method name
// is an explicit escape hatch, similar to const_cast in C++;
// #[Immutable] deliberately has no runtime component.
if (!$this->immutableValueMayBeObject($call->var)) {
$this->assertImmutableValueMethodDoesNotMutate($call, $root);
} elseif (($function === null || !$function->immutable)
&& !$this->immutableExtensionAcceptsReceiver($call)
) {
$method = $call->name->toString();
$class = $this->detectClassOfExpr($call->var) ?: 'object';
$this->fatalError(
$call,
"Cannot call mutable method `{$class}::{$method}()` on immutable value `"
. $this->immutableDisplayName($root) . '`',
);
}
}
[$callable, $class] = $this->immutableCallableName($call);
$staticallyResolved = $function !== null
|| ($call instanceof Node\Expr\FuncCall && $callable !== '')
|| ($callable !== '' && $class !== '' && $class !== self::DYNAMIC_CALLED_CLASS);
if (!$staticallyResolved) {
return;
}
foreach ($call->args as $index => $argument) {
if ($argument instanceof Node\VariadicPlaceholder) {
continue;
}
$root = $this->immutableRootName($argument->value);
if ($root === null) {
continue;
}
$info = $function === null ? null : $this->immutableArgInfo($function, $argument, $index);
$byRef = $info?->byRef ?? false;
if ($function === null) {
$byRef = $argument->name instanceof Node\Identifier
? $this->isReferenceNamedArgument($callable, $class, $argument->name->toString())
: $this->isReferenceArgument($callable, $class, $index);
}
if ($byRef && !($info?->immutable ?? false)) {
$this->fatalError(
$argument,
'Cannot pass immutable value `' . $this->immutableDisplayName($root)
. '` to reference parameter ' . ($index + 1) . ' of ' . $callable . '()',
);
}
if ($this->immutableValueMayBeObject($argument->value) && !($info?->immutable ?? false)) {
$this->fatalError(
$argument,
'Immutable object `' . $this->immutableDisplayName($root)
. '` requires an #[Immutable] parameter',
);
}
}
}
}