2018-01-14 18:09:40 +01:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Checker\Statements\Expression\Assignment;
|
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
use Psalm\Checker\Statements\Expression\Fetch\ArrayFetchChecker;
|
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
|
|
|
use Psalm\Checker\StatementsChecker;
|
|
|
|
use Psalm\Context;
|
|
|
|
use Psalm\Type;
|
|
|
|
use Psalm\Type\Atomic\ObjectLike;
|
|
|
|
use Psalm\Type\Atomic\TArray;
|
2018-11-09 16:56:27 +01:00
|
|
|
use Psalm\Type\Atomic\TNonEmptyArray;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
|
|
class ArrayAssignmentChecker
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
* @param PhpParser\Node\Expr\ArrayDimFetch $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @param Type\Union $assignment_value_type
|
|
|
|
*
|
2018-03-07 17:16:56 +01:00
|
|
|
* @return void
|
2018-01-14 18:09:40 +01:00
|
|
|
* @psalm-suppress MixedMethodCall - some funky logic here
|
|
|
|
*/
|
|
|
|
public static function analyze(
|
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
PhpParser\Node\Expr\ArrayDimFetch $stmt,
|
|
|
|
Context $context,
|
|
|
|
Type\Union $assignment_value_type
|
|
|
|
) {
|
|
|
|
$nesting = 0;
|
|
|
|
$var_id = ExpressionChecker::getVarId(
|
|
|
|
$stmt->var,
|
|
|
|
$statements_checker->getFQCLN(),
|
|
|
|
$statements_checker,
|
|
|
|
$nesting
|
|
|
|
);
|
|
|
|
|
|
|
|
self::updateArrayType(
|
|
|
|
$statements_checker,
|
|
|
|
$stmt,
|
|
|
|
$assignment_value_type,
|
|
|
|
$context
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!isset($stmt->var->inferredType) && $var_id) {
|
|
|
|
$context->vars_in_scope[$var_id] = Type::getMixed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
* @param PhpParser\Node\Expr\ArrayDimFetch $stmt
|
|
|
|
* @param Type\Union $assignment_type
|
|
|
|
* @param Context $context
|
|
|
|
*
|
|
|
|
* @return false|null
|
2018-05-31 22:49:01 +02:00
|
|
|
*
|
|
|
|
* @psalm-suppress UnusedVariable due to Psalm bug
|
2018-01-14 18:09:40 +01:00
|
|
|
*/
|
|
|
|
public static function updateArrayType(
|
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
PhpParser\Node\Expr\ArrayDimFetch $stmt,
|
|
|
|
Type\Union $assignment_type,
|
|
|
|
Context $context
|
|
|
|
) {
|
|
|
|
$root_array_expr = $stmt;
|
|
|
|
|
|
|
|
$child_stmts = [];
|
|
|
|
|
|
|
|
while ($root_array_expr->var instanceof PhpParser\Node\Expr\ArrayDimFetch) {
|
|
|
|
$child_stmts[] = $root_array_expr;
|
|
|
|
$root_array_expr = $root_array_expr->var;
|
|
|
|
}
|
|
|
|
|
|
|
|
$child_stmts[] = $root_array_expr;
|
|
|
|
$root_array_expr = $root_array_expr->var;
|
|
|
|
|
|
|
|
if (ExpressionChecker::analyze(
|
|
|
|
$statements_checker,
|
|
|
|
$root_array_expr,
|
|
|
|
$context,
|
|
|
|
true
|
|
|
|
) === false) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
|
|
|
|
$root_type = isset($root_array_expr->inferredType) ? $root_array_expr->inferredType : Type::getMixed();
|
|
|
|
|
|
|
|
if ($root_type->isMixed()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$child_stmts = array_reverse($child_stmts);
|
|
|
|
|
|
|
|
$current_type = $root_type;
|
|
|
|
|
|
|
|
$current_dim = $stmt->dim;
|
|
|
|
|
|
|
|
$reversed_child_stmts = [];
|
|
|
|
|
|
|
|
// gets a variable id that *may* contain array keys
|
|
|
|
$root_var_id = ExpressionChecker::getRootVarId(
|
|
|
|
$root_array_expr,
|
|
|
|
$statements_checker->getFQCLN(),
|
|
|
|
$statements_checker
|
|
|
|
);
|
|
|
|
|
|
|
|
$var_id_additions = [];
|
|
|
|
|
2018-08-21 06:28:39 +02:00
|
|
|
$parent_var_id = null;
|
|
|
|
|
2018-02-17 18:32:19 +01:00
|
|
|
$full_var_id = true;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
|
|
$child_stmt = null;
|
|
|
|
|
|
|
|
// First go from the root element up, and go as far as we can to figure out what
|
|
|
|
// array types there are
|
|
|
|
while ($child_stmts) {
|
|
|
|
$child_stmt = array_shift($child_stmts);
|
|
|
|
|
|
|
|
if (count($child_stmts)) {
|
|
|
|
array_unshift($reversed_child_stmts, $child_stmt);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($child_stmt->dim) {
|
|
|
|
if (ExpressionChecker::analyze(
|
|
|
|
$statements_checker,
|
|
|
|
$child_stmt->dim,
|
|
|
|
$context
|
|
|
|
) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($child_stmt->dim->inferredType)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-31 22:49:01 +02:00
|
|
|
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| ($child_stmt->dim instanceof PhpParser\Node\Expr\ConstFetch
|
|
|
|
&& $child_stmt->dim->inferredType->isSingleStringLiteral())
|
|
|
|
) {
|
|
|
|
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\String_) {
|
|
|
|
$value = $child_stmt->dim->value;
|
|
|
|
} else {
|
2018-08-09 03:31:13 +02:00
|
|
|
$value = $child_stmt->dim->inferredType->getSingleStringLiteral()->value;
|
2018-05-31 22:49:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (preg_match('/^(0|[1-9][0-9]*)$/', $value)) {
|
|
|
|
$var_id_additions[] = '[' . $value . ']';
|
|
|
|
}
|
|
|
|
$var_id_additions[] = '[\'' . $value . '\']';
|
|
|
|
} elseif ($child_stmt->dim instanceof PhpParser\Node\Scalar\LNumber
|
|
|
|
|| ($child_stmt->dim instanceof PhpParser\Node\Expr\ConstFetch
|
|
|
|
&& $child_stmt->dim->inferredType->isSingleIntLiteral())
|
|
|
|
) {
|
|
|
|
if ($child_stmt->dim instanceof PhpParser\Node\Scalar\LNumber) {
|
|
|
|
$value = $child_stmt->dim->value;
|
|
|
|
} else {
|
2018-08-09 03:31:13 +02:00
|
|
|
$value = $child_stmt->dim->inferredType->getSingleIntLiteral()->value;
|
2018-05-03 19:56:30 +02:00
|
|
|
}
|
2018-05-31 22:49:01 +02:00
|
|
|
|
|
|
|
$var_id_additions[] = '[' . $value . ']';
|
2018-02-17 17:24:08 +01:00
|
|
|
} elseif ($child_stmt->dim instanceof PhpParser\Node\Expr\Variable
|
|
|
|
&& is_string($child_stmt->dim->name)
|
|
|
|
) {
|
|
|
|
$var_id_additions[] = '[$' . $child_stmt->dim->name . ']';
|
2018-01-14 18:09:40 +01:00
|
|
|
} else {
|
|
|
|
$var_id_additions[] = '[' . $child_stmt->dim->inferredType . ']';
|
2018-02-17 18:32:19 +01:00
|
|
|
$full_var_id = false;
|
2018-01-14 18:09:40 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$var_id_additions[] = '';
|
2018-02-17 18:32:19 +01:00
|
|
|
$full_var_id = false;
|
2018-01-14 18:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($child_stmt->var->inferredType)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($child_stmt->var->inferredType->isEmpty()) {
|
|
|
|
$child_stmt->var->inferredType = Type::getEmptyArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
$array_var_id = $root_var_id . implode('', $var_id_additions);
|
|
|
|
|
2018-08-21 06:28:39 +02:00
|
|
|
if ($parent_var_id && isset($context->vars_in_scope[$parent_var_id])) {
|
|
|
|
$child_stmt->var->inferredType = clone $context->vars_in_scope[$parent_var_id];
|
|
|
|
}
|
|
|
|
|
|
|
|
$parent_var_id = $array_var_id;
|
|
|
|
|
2018-01-14 18:09:40 +01:00
|
|
|
$child_stmt->inferredType = ArrayFetchChecker::getArrayAccessTypeGivenOffset(
|
|
|
|
$statements_checker,
|
|
|
|
$child_stmt,
|
|
|
|
$child_stmt->var->inferredType,
|
|
|
|
isset($child_stmt->dim->inferredType) ? $child_stmt->dim->inferredType : Type::getInt(),
|
|
|
|
true,
|
|
|
|
$array_var_id,
|
|
|
|
$child_stmts ? null : $assignment_type
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!$child_stmts) {
|
|
|
|
$child_stmt->inferredType = $assignment_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
$current_type = $child_stmt->inferredType;
|
|
|
|
$current_dim = $child_stmt->dim;
|
|
|
|
|
|
|
|
if ($child_stmt->var->inferredType->isMixed()) {
|
2018-02-17 18:32:19 +01:00
|
|
|
$full_var_id = false;
|
2018-01-14 18:09:40 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($root_var_id
|
2018-02-17 18:32:19 +01:00
|
|
|
&& $full_var_id
|
2018-01-14 18:09:40 +01:00
|
|
|
&& isset($child_stmt->var->inferredType)
|
|
|
|
&& !$child_stmt->var->inferredType->hasObjectType()
|
|
|
|
) {
|
|
|
|
$array_var_id = $root_var_id . implode('', $var_id_additions);
|
|
|
|
$context->vars_in_scope[$array_var_id] = clone $assignment_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
// only update as many child stmts are we were able to process above
|
|
|
|
foreach ($reversed_child_stmts as $child_stmt) {
|
|
|
|
if (!isset($child_stmt->inferredType)) {
|
|
|
|
throw new \InvalidArgumentException('Should never get here');
|
|
|
|
}
|
|
|
|
|
2018-05-31 22:49:01 +02:00
|
|
|
$is_single_string_literal = false;
|
|
|
|
|
2018-01-14 18:09:40 +01:00
|
|
|
if ($current_dim instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| $current_dim instanceof PhpParser\Node\Scalar\LNumber
|
2018-05-31 22:49:01 +02:00
|
|
|
|| ($current_dim instanceof PhpParser\Node\Expr\ConstFetch
|
|
|
|
&& isset($current_dim->inferredType)
|
|
|
|
&& (($is_single_string_literal = $current_dim->inferredType->isSingleStringLiteral())
|
|
|
|
|| $current_dim->inferredType->isSingleIntLiteral()))
|
2018-01-14 18:09:40 +01:00
|
|
|
) {
|
2018-05-31 22:49:01 +02:00
|
|
|
if ($current_dim instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| $current_dim instanceof PhpParser\Node\Scalar\LNumber
|
|
|
|
) {
|
|
|
|
$key_value = $current_dim->value;
|
|
|
|
} elseif ($is_single_string_literal) {
|
2018-08-09 03:31:13 +02:00
|
|
|
$key_value = $current_dim->inferredType->getSingleStringLiteral()->value;
|
2018-05-31 22:49:01 +02:00
|
|
|
} else {
|
2018-08-09 03:31:13 +02:00
|
|
|
$key_value = $current_dim->inferredType->getSingleIntLiteral()->value;
|
2018-05-31 22:49:01 +02:00
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
|
|
$has_matching_objectlike_property = false;
|
|
|
|
|
|
|
|
foreach ($child_stmt->inferredType->getTypes() as $type) {
|
|
|
|
if ($type instanceof ObjectLike) {
|
|
|
|
if (isset($type->properties[$key_value])) {
|
|
|
|
$has_matching_objectlike_property = true;
|
|
|
|
|
|
|
|
$type->properties[$key_value] = clone $current_type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$has_matching_objectlike_property) {
|
|
|
|
$array_assignment_type = new Type\Union([
|
|
|
|
new ObjectLike([$key_value => $current_type]),
|
|
|
|
]);
|
|
|
|
|
|
|
|
$new_child_type = Type::combineUnionTypes(
|
|
|
|
$child_stmt->inferredType,
|
|
|
|
$array_assignment_type
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$new_child_type = $child_stmt->inferredType; // noop
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$array_assignment_type = new Type\Union([
|
|
|
|
new TArray([
|
|
|
|
isset($current_dim->inferredType) ? $current_dim->inferredType : Type::getInt(),
|
|
|
|
$current_type,
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
|
|
|
|
$new_child_type = Type::combineUnionTypes(
|
|
|
|
$child_stmt->inferredType,
|
|
|
|
$array_assignment_type
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$new_child_type->removeType('null');
|
2018-04-10 00:02:45 +02:00
|
|
|
$new_child_type->possibly_undefined = false;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
|
|
if (!$child_stmt->inferredType->hasObjectType()) {
|
|
|
|
$child_stmt->inferredType = $new_child_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
$current_type = $child_stmt->inferredType;
|
|
|
|
$current_dim = $child_stmt->dim;
|
|
|
|
|
|
|
|
array_pop($var_id_additions);
|
|
|
|
|
|
|
|
if ($root_var_id) {
|
|
|
|
$array_var_id = $root_var_id . implode('', $var_id_additions);
|
|
|
|
$context->vars_in_scope[$array_var_id] = clone $child_stmt->inferredType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-18 17:02:50 +02:00
|
|
|
$root_is_string = $root_type->isString();
|
2018-05-31 22:49:01 +02:00
|
|
|
$is_single_string_literal = false;
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
|
|
if (($current_dim instanceof PhpParser\Node\Scalar\String_
|
2018-05-31 22:49:01 +02:00
|
|
|
|| $current_dim instanceof PhpParser\Node\Scalar\LNumber
|
|
|
|
|| ($current_dim instanceof PhpParser\Node\Expr\ConstFetch
|
|
|
|
&& isset($current_dim->inferredType)
|
|
|
|
&& (($is_single_string_literal = $current_dim->inferredType->isSingleStringLiteral())
|
|
|
|
|| $current_dim->inferredType->isSingleIntLiteral())))
|
2018-01-14 18:09:40 +01:00
|
|
|
&& ($current_dim instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| !$root_is_string)
|
|
|
|
) {
|
2018-05-31 22:49:01 +02:00
|
|
|
if ($current_dim instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| $current_dim instanceof PhpParser\Node\Scalar\LNumber
|
|
|
|
) {
|
|
|
|
$key_value = $current_dim->value;
|
|
|
|
} elseif ($is_single_string_literal) {
|
2018-08-09 03:31:13 +02:00
|
|
|
$key_value = $current_dim->inferredType->getSingleStringLiteral()->value;
|
2018-05-31 22:49:01 +02:00
|
|
|
} else {
|
2018-08-09 03:31:13 +02:00
|
|
|
$key_value = $current_dim->inferredType->getSingleIntLiteral()->value;
|
2018-05-31 22:49:01 +02:00
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
|
|
|
|
$has_matching_objectlike_property = false;
|
|
|
|
|
|
|
|
foreach ($root_type->getTypes() as $type) {
|
|
|
|
if ($type instanceof ObjectLike) {
|
|
|
|
if (isset($type->properties[$key_value])) {
|
|
|
|
$has_matching_objectlike_property = true;
|
|
|
|
|
|
|
|
$type->properties[$key_value] = clone $current_type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$has_matching_objectlike_property) {
|
|
|
|
$array_assignment_type = new Type\Union([
|
|
|
|
new ObjectLike([$key_value => $current_type]),
|
|
|
|
]);
|
|
|
|
|
|
|
|
$new_child_type = Type::combineUnionTypes(
|
|
|
|
$root_type,
|
|
|
|
$array_assignment_type
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$new_child_type = $root_type; // noop
|
|
|
|
}
|
|
|
|
} elseif (!$root_is_string) {
|
2018-05-03 19:56:30 +02:00
|
|
|
if ($current_dim) {
|
|
|
|
if (isset($current_dim->inferredType)) {
|
|
|
|
$array_atomic_key_type = ArrayFetchChecker::replaceOffsetTypeWithInts(
|
|
|
|
$current_dim->inferredType
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$array_atomic_key_type = Type::getMixed();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// todo: this can be improved I think
|
|
|
|
$array_atomic_key_type = Type::getInt();
|
|
|
|
}
|
|
|
|
|
|
|
|
$array_atomic_type = new TArray([
|
|
|
|
$array_atomic_key_type,
|
|
|
|
$current_type,
|
|
|
|
]);
|
|
|
|
|
|
|
|
$from_countable_object_like = false;
|
|
|
|
|
|
|
|
if (!$current_dim && !$context->inside_loop) {
|
|
|
|
$atomic_root_types = $root_type->getTypes();
|
|
|
|
|
|
|
|
if (isset($atomic_root_types['array'])) {
|
2018-11-09 16:56:27 +01:00
|
|
|
if ($atomic_root_types['array'] instanceof TNonEmptyArray
|
|
|
|
&& $array_atomic_type instanceof TNonEmptyArray
|
|
|
|
) {
|
2018-05-03 19:56:30 +02:00
|
|
|
$array_atomic_type->count = $atomic_root_types['array']->count;
|
|
|
|
} elseif ($atomic_root_types['array'] instanceof ObjectLike
|
2018-11-09 16:56:27 +01:00
|
|
|
&& $array_atomic_type instanceof TNonEmptyArray
|
2018-05-03 19:56:30 +02:00
|
|
|
&& $atomic_root_types['array']->sealed
|
|
|
|
) {
|
2018-05-18 17:02:50 +02:00
|
|
|
$array_atomic_type->count = count($atomic_root_types['array']->properties);
|
2018-05-03 19:56:30 +02:00
|
|
|
$from_countable_object_like = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-14 18:09:40 +01:00
|
|
|
$array_assignment_type = new Type\Union([
|
2018-05-03 19:56:30 +02:00
|
|
|
$array_atomic_type,
|
2018-01-14 18:09:40 +01:00
|
|
|
]);
|
|
|
|
|
|
|
|
$new_child_type = Type::combineUnionTypes(
|
|
|
|
$root_type,
|
|
|
|
$array_assignment_type
|
|
|
|
);
|
2018-05-03 19:56:30 +02:00
|
|
|
|
|
|
|
if ($from_countable_object_like) {
|
|
|
|
$atomic_root_types = $new_child_type->getTypes();
|
|
|
|
|
|
|
|
if (isset($atomic_root_types['array'])
|
2018-11-09 16:56:27 +01:00
|
|
|
&& $atomic_root_types['array'] instanceof TNonEmptyArray
|
2018-05-18 17:02:50 +02:00
|
|
|
&& $atomic_root_types['array']->count !== null
|
2018-05-03 19:56:30 +02:00
|
|
|
) {
|
2018-05-18 17:02:50 +02:00
|
|
|
$atomic_root_types['array']->count++;
|
2018-05-03 19:56:30 +02:00
|
|
|
}
|
|
|
|
}
|
2018-01-14 18:09:40 +01:00
|
|
|
} else {
|
|
|
|
$new_child_type = $root_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
$new_child_type->removeType('null');
|
|
|
|
|
|
|
|
if (!$root_type->hasObjectType()) {
|
|
|
|
$root_type = $new_child_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
$root_array_expr->inferredType = $root_type;
|
|
|
|
|
|
|
|
if ($root_array_expr instanceof PhpParser\Node\Expr\PropertyFetch) {
|
2018-04-17 18:16:25 +02:00
|
|
|
if ($root_array_expr->name instanceof PhpParser\Node\Identifier) {
|
2018-01-14 18:09:40 +01:00
|
|
|
PropertyAssignmentChecker::analyzeInstance(
|
|
|
|
$statements_checker,
|
|
|
|
$root_array_expr,
|
2018-04-17 18:16:25 +02:00
|
|
|
$root_array_expr->name->name,
|
2018-01-14 18:09:40 +01:00
|
|
|
null,
|
|
|
|
$root_type,
|
|
|
|
$context,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
if (ExpressionChecker::analyze($statements_checker, $root_array_expr->name, $context) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ExpressionChecker::analyze($statements_checker, $root_array_expr->var, $context) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif ($root_var_id) {
|
2018-01-28 23:28:34 +01:00
|
|
|
if ($context->hasVariable($root_var_id, $statements_checker)) {
|
2018-01-14 18:09:40 +01:00
|
|
|
$context->vars_in_scope[$root_var_id] = $root_type;
|
|
|
|
} else {
|
|
|
|
$context->vars_in_scope[$root_var_id] = $root_type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|