class RenderCallbackRule
@implements Rule<Node\Expr\ArrayItem>
Hierarchy
- class \mglaman\PHPStanDrupal\Rules\Drupal\RenderCallbackRule implements \PHPStan\Rules\Rule
Expanded class hierarchy of RenderCallbackRule
File
-
vendor/
mglaman/ phpstan-drupal/ src/ Rules/ Drupal/ RenderCallbackRule.php, line 42
Namespace
mglaman\PHPStanDrupal\Rules\DrupalView source
final class RenderCallbackRule implements Rule {
private ReflectionProvider $reflectionProvider;
private ServiceMap $serviceMap;
private array $supportedKeys = [
'#pre_render',
'#post_render',
'#access_callback',
'#lazy_builder',
'#date_time_callbacks',
'#date_date_callbacks',
];
public function __construct(ReflectionProvider $reflectionProvider, ServiceMap $serviceMap) {
$this->reflectionProvider = $reflectionProvider;
$this->serviceMap = $serviceMap;
}
public function getNodeType() : string {
return Node\Expr\ArrayItem::class;
}
public function processNode(Node $node, Scope $scope) : array {
$key = $node->key;
if (!$key instanceof Node\Scalar\String_) {
return [];
}
// @see https://www.drupal.org/node/2966725
$keySearch = array_search($key->value, $this->supportedKeys, true);
if ($keySearch === false) {
return [];
}
$keyChecked = $this->supportedKeys[$keySearch];
$value = $node->value;
if ($keyChecked === '#access_callback') {
return $this->doProcessNode($node->value, $scope, $keyChecked, 0);
}
if ($keyChecked === '#lazy_builder') {
if ($scope->isInClass()) {
$classReflection = $scope->getClassReflection();
$classType = new ObjectType($classReflection->getName());
// These classes use #lazy_builder in array_intersect_key. With
// PHPStan 1.6, nodes do not track their parent/next/prev which
// saves a lot of memory. But makes it harder to detect if we're
// in a call to array_intersect_key. This is an easier workaround.
$allowedTypes = new UnionType([
new ObjectType(PlaceholderGenerator::class),
new ObjectType(Renderer::class),
new ObjectType('Drupal\\Tests\\Core\\Render\\RendererPlaceholdersTest'),
]);
if ($allowedTypes->isSuperTypeOf($classType)
->yes()) {
return [];
}
}
if (!$value instanceof Node\Expr\Array_) {
return [
RuleErrorBuilder::message(sprintf('The "%s" expects a callable array with arguments.', $keyChecked))->line($node->getStartLine())
->build(),
];
}
if (count($value->items) === 0) {
return [];
}
// @todo take $value->items[1] and validate parameters against the callback.
return $this->doProcessNode($value->items[0]->value, $scope, $keyChecked, 0);
}
if (!$value instanceof Node\Expr\Array_) {
return [
RuleErrorBuilder::message(sprintf('The "%s" render array value expects an array of callbacks.', $keyChecked))->line($node->getStartLine())
->build(),
];
}
if (count($value->items) === 0) {
return [];
}
$errors = [];
foreach ($value->items as $pos => $item) {
$errors[] = $this->doProcessNode($item->value, $scope, $keyChecked, $pos);
}
return array_merge(...$errors);
}
/**
@return (string|\PHPStan\Rules\RuleError)[] errors
*/
private function doProcessNode(Node\Expr $node, Scope $scope, string $keyChecked, int $pos) : array {
$checkIsCallable = true;
$trustedCallbackType = new UnionType([
new ObjectType(TrustedCallbackInterface::class),
new ObjectType(RenderCallbackInterface::class),
]);
$errors = [];
$errorLine = $node->getStartLine();
$type = $this->getType($node, $scope);
foreach ($type->getConstantStrings() as $constantStringType) {
if (!$constantStringType->isCallable()
->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->build();
}
elseif ($this->reflectionProvider
->hasFunction(new Name($constantStringType->getValue()), null)) {
// We can determine if the callback is callable through the type system. However, we cannot determine
// if it is just a function or a static class call (MyClass::staticFunc).
$errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not trusted.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->tip('Change record: https://www.drupal.org/node/2966725.')
->build();
}
else {
// @see \PHPStan\Type\Constant\ConstantStringType::isCallable
preg_match('#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#', $constantStringType->getValue(), $matches);
if (count($matches) === 0) {
$errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->build();
}
elseif (!$trustedCallbackType->isSuperTypeOf(new ObjectType($matches[1]))
->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf("%s callback class %s at key '%s' does not implement Drupal\\Core\\Security\\TrustedCallbackInterface.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->tip('Change record: https://www.drupal.org/node/2966725.')
->build();
}
}
}
foreach ($type->getConstantArrays() as $constantArrayType) {
if (!$constantArrayType->isCallable()
->yes()) {
// If the right-hand side of the array is a variable, we cannot
// determine if it is callable. Bail now.
$itemType = $constantArrayType->getItemType();
if ($itemType instanceof UnionType) {
$unionConstantStrings = array_merge(...array_map(static function (Type $type) {
return $type->getConstantStrings();
}, $itemType->getTypes()));
if (count($unionConstantStrings) === 0) {
// Right-hand side of UnionType is not a constant string. We cannot determine if the dynamic
// value is callable or not.
$checkIsCallable = false;
break;
}
}
$errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantArrayType->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->build();
continue;
}
$typeAndMethodNames = $constantArrayType->findTypeAndMethodNames();
if ($typeAndMethodNames === []) {
continue;
}
foreach ($typeAndMethodNames as $typeAndMethodName) {
$isTrustedCallbackAttribute = TrinaryLogic::createNo()->lazyOr($typeAndMethodName->getType()
->getObjectClassReflections(), function (ClassReflection $reflection) use ($typeAndMethodName) {
if (!class_exists(TrustedCallback::class)) {
return TrinaryLogic::createNo();
}
$hasAttribute = $reflection->getNativeReflection()
->getMethod($typeAndMethodName->getMethod())
->getAttributes(TrustedCallback::class);
return TrinaryLogic::createFromBoolean(count($hasAttribute) > 0);
});
$isTrustedCallbackInterfaceType = $trustedCallbackType->isSuperTypeOf($typeAndMethodName->getType())
->yes();
if (!$isTrustedCallbackInterfaceType && !$isTrustedCallbackAttribute->yes()) {
if (class_exists(TrustedCallback::class)) {
$errors[] = RuleErrorBuilder::message(sprintf("%s callback method '%s' at key '%s' does not implement attribute \\Drupal\\Core\\Security\\Attribute\\TrustedCallback.", $keyChecked, $constantArrayType->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->tip('Change record: https://www.drupal.org/node/3349470')
->build();
}
else {
$errors[] = RuleErrorBuilder::message(sprintf("%s callback class '%s' at key '%s' does not implement Drupal\\Core\\Security\\TrustedCallbackInterface.", $keyChecked, $typeAndMethodName->getType()
->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->tip('Change record: https://www.drupal.org/node/2966725.')
->build();
}
}
}
}
// @todo move to its own rule for 1.2.0, FormClosureSerializationRule.
if ($type instanceof ClosureType && $scope->isInClass()) {
$classReflection = $scope->getClassReflection();
$classType = new ObjectType($classReflection->getName());
$formType = new ObjectType('\\Drupal\\Core\\Form\\FormInterface');
if ($formType->isSuperTypeOf($classType)
->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed.", $keyChecked, $pos))->line($errorLine)
->build();
}
}
if (count($errors) === 0 && ($checkIsCallable && !$type->isCallable()
->yes())) {
$errors[] = RuleErrorBuilder::message(sprintf("%s value '%s' at key '%s' is invalid.", $keyChecked, $type->describe(VerbosityLevel::value()), $pos))
->line($errorLine)
->build();
}
return $errors;
}
// @todo move to a helper, as Drupal uses `service:method` references a lot.
private function getType(Node\Expr $node, Scope $scope) : Type {
$type = $scope->getType($node);
if ($type instanceof IntersectionType) {
// Covers concatenation of static::class . '::methodName'.
if ($node instanceof Node\Expr\BinaryOp\Concat) {
$leftType = $scope->getType($node->left);
$rightType = $scope->getType($node->right);
if ($rightType instanceof ConstantStringType && $leftType instanceof GenericClassStringType && $leftType->getGenericType() instanceof StaticType) {
return new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
], [
$leftType->getGenericType(),
new ConstantStringType(ltrim($rightType->getValue(), ':')),
]);
}
}
}
elseif ($type instanceof ConstantStringType) {
if ($type->isClassStringType()
->yes()) {
return $type;
}
// Covers \Drupal\Core\Controller\ControllerResolver::createController.
if (substr_count($type->getValue(), ':') === 1) {
[
$class_or_service,
$method,
] = explode(':', $type->getValue(), 2);
$serviceDefinition = $this->serviceMap
->getService($class_or_service);
if ($serviceDefinition === null || $serviceDefinition->getClass() === null) {
return $type;
}
return new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
], [
new ObjectType($serviceDefinition->getClass()),
new ConstantStringType($method),
]);
}
// @see \PHPStan\Type\Constant\ConstantStringType::isCallable
preg_match('#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#', $type->getValue(), $matches);
if (count($matches) > 0) {
return new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
], [
new StaticType($this->reflectionProvider
->getClass($matches[1])),
new ConstantStringType($matches[2]),
]);
}
}
return $type;
}
}
Members
Title Sort descending | Modifiers | Object type | Summary |
---|---|---|---|
RenderCallbackRule::$reflectionProvider | private | property | |
RenderCallbackRule::$serviceMap | private | property | |
RenderCallbackRule::$supportedKeys | private | property | |
RenderCallbackRule::doProcessNode | private | function | |
RenderCallbackRule::getNodeType | public | function | |
RenderCallbackRule::getType | private | function | |
RenderCallbackRule::processNode | public | function | |
RenderCallbackRule::__construct | public | function |