Skip to content

Commit

Permalink
#7387 Add asserting non-empty-string by strlen
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoVie committed Nov 25, 2022
1 parent 255a152 commit 1d06d0f
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 11 deletions.
230 changes: 219 additions & 11 deletions src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ private static function scrapeEqualityAssertions(

$count = null;
$count_equality_position = self::hasCountEqualityCheck($conditional, $count);
$strlen = null;
$strlen_equality_position = self::hasStrlenEqualityCheck($conditional, $strlen);

if ($count_equality_position) {
$if_types = [];
Expand Down Expand Up @@ -498,6 +500,55 @@ private static function scrapeEqualityAssertions(
return $if_types ? [$if_types] : [];
}

if ($strlen_equality_position) {
$if_types = [];

if ($strlen_equality_position === self::ASSIGNMENT_TO_RIGHT) {
$strlen_expr = $conditional->left;
} elseif ($strlen_equality_position === self::ASSIGNMENT_TO_LEFT) {
$strlen_expr = $conditional->right;
} else {
throw new UnexpectedValueException('$strlen_equality_position value');
}

/** @var PhpParser\Node\Expr\FuncCall $strlen_expr */
$var_name = ExpressionIdentifier::getExtendedVarId(
$strlen_expr->getArgs()[0]->value,
$this_class_name,
$source
);

if ($source instanceof StatementsAnalyzer) {
$var_type = $source->node_data->getType($conditional->left);
$other_type = $source->node_data->getType($conditional->right);

if ($codebase
&& $other_type
&& $var_type
&& $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical
) {
self::handleParadoxicalAssertions(
$source,
$var_type,
$this_class_name,
$other_type,
$codebase,
$conditional
);
}
}

if ($var_name) {
if ($strlen > 0) {
$if_types[$var_name] = [[new Assertion\NonEmpty()]];
} else {
$if_types[$var_name] = [[new Empty_()]];
}
}

return $if_types ? [$if_types] : [];
}

if (!$source instanceof StatementsAnalyzer) {
return [];
}
Expand Down Expand Up @@ -670,6 +721,9 @@ private static function scrapeInequalityAssertions(
$count = null;
$count_inequality_position = self::hasCountEqualityCheck($conditional, $count);

$strlen = null;
$strlen_inequality_position = self::hasStrlenEqualityCheck($conditional, $strlen);

if ($count_inequality_position) {
$if_types = [];

Expand Down Expand Up @@ -719,6 +773,55 @@ private static function scrapeInequalityAssertions(
return $if_types ? [$if_types] : [];
}

if ($strlen_inequality_position) {
$if_types = [];

if ($strlen_inequality_position === self::ASSIGNMENT_TO_RIGHT) {
$count_expr = $conditional->left;
} elseif ($strlen_inequality_position === self::ASSIGNMENT_TO_LEFT) {
$count_expr = $conditional->right;
} else {
throw new UnexpectedValueException('$strlen_inequality_position value');
}

/** @var PhpParser\Node\Expr\FuncCall $count_expr */
$var_name = ExpressionIdentifier::getExtendedVarId(
$count_expr->getArgs()[0]->value,
$this_class_name,
$source
);

if ($source instanceof StatementsAnalyzer) {
$var_type = $source->node_data->getType($conditional->left);
$other_type = $source->node_data->getType($conditional->right);

if ($codebase
&& $other_type
&& $var_type
&& $conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical
) {
self::handleParadoxicalAssertions(
$source,
$var_type,
$this_class_name,
$other_type,
$codebase,
$conditional
);
}
}

if ($var_name) {
if ($strlen > 0) {
$if_types[$var_name] = [[new Assertion\Empty_()]];
} else {
$if_types[$var_name] = [[new Assertion\NonEmpty()]];
}
}

return $if_types ? [$if_types] : [];
}

if (!$source instanceof StatementsAnalyzer) {
return [];
}
Expand Down Expand Up @@ -1539,10 +1642,34 @@ protected static function hasGetClassCheck(
protected static function hasNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count
) {
return self::hasNonEmptyCountOrStrlenEqualityCheck($conditional, $min_count, ['count']);
}

/**
* @param Greater|GreaterOrEqual|Smaller|SmallerOrEqual $conditional
* @return false|int
*/
protected static function hasNonEmptyStrlenEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count
) {
return self::hasNonEmptyCountOrStrlenEqualityCheck($conditional, $min_count, ['strlen', 'mb_strlen']);
}

/**
* @param Greater|GreaterOrEqual|Smaller|SmallerOrEqual $conditional
* @param array<string> $funcCallNames
* @return false|int
*/
protected static function hasNonEmptyCountOrStrlenEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_value,
array $funcCallNames
) {
if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->left->name->parts[0]), $funcCallNames)
&& $conditional->left->getArgs()
&& ($conditional instanceof BinaryOp\Greater || $conditional instanceof BinaryOp\GreaterOrEqual)
) {
Expand All @@ -1551,7 +1678,7 @@ protected static function hasNonEmptyCountEqualityCheck(
$comparison_adjustment = $conditional instanceof BinaryOp\Greater ? 1 : 0;
} elseif ($conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->right->name->parts[0]), $funcCallNames)
&& $conditional->right->getArgs()
&& ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual)
) {
Expand All @@ -1566,7 +1693,7 @@ protected static function hasNonEmptyCountEqualityCheck(
if ($compare_to instanceof PhpParser\Node\Scalar\LNumber
&& $compare_to->value > (-1 * $comparison_adjustment)
) {
$min_count = $compare_to->value + $comparison_adjustment;
$min_value = $compare_to->value + $comparison_adjustment;

return $assignment_to;
}
Expand Down Expand Up @@ -1630,25 +1757,49 @@ protected static function hasLessThanCountEqualityCheck(
protected static function hasCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$count
) {
return self::hasCountOrStrlenEqualityCheck($conditional, $count, ['count']);
}

/**
* @param Equal|Identical|NotEqual|NotIdentical $conditional
* @return false|int
*/
protected static function hasStrlenEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$count
) {
return self::hasCountOrStrlenEqualityCheck($conditional, $count, ['strlen', 'mb_strlen']);
}

/**
* @param Equal|Identical|NotEqual|NotIdentical $conditional
* @param array<string> $funcCallNames
* @return false|int
*/
protected static function hasCountOrStrlenEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$resultValue,
array $funcCallNames
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->left->name->parts[0]), $funcCallNames)
&& $conditional->left->getArgs();

if ($left_count && $conditional->right instanceof PhpParser\Node\Scalar\LNumber) {
$count = $conditional->right->value;
$resultValue = $conditional->right->value;

return self::ASSIGNMENT_TO_RIGHT;
}

$right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& in_array(strtolower($conditional->right->name->parts[0]), $funcCallNames)
&& $conditional->right->getArgs();

if ($right_count && $conditional->left instanceof PhpParser\Node\Scalar\LNumber) {
$count = $conditional->left->value;
$resultValue = $conditional->left->value;

return self::ASSIGNMENT_TO_LEFT;
}
Expand Down Expand Up @@ -1783,15 +1934,37 @@ protected static function hasInferiorNumberCheck(
protected static function hasReconcilableNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
return self::hasReconcilableNonEmptyCountOrStrlenEqualityCheck($conditional, ['count']);
}

/**
* @param PhpParser\Node\Expr\BinaryOp\Greater|PhpParser\Node\Expr\BinaryOp\GreaterOrEqual $conditional
* @return false|int
*/
protected static function hasReconcilableNonEmptyStrlenEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional
) {
return self::hasReconcilableNonEmptyCountOrStrlenEqualityCheck($conditional, ['strlen', 'mb_strlen']);
}

/**
* @param PhpParser\Node\Expr\BinaryOp\Greater|PhpParser\Node\Expr\BinaryOp\GreaterOrEqual $conditional
* @param array<string> $funcCallNames
* @return false|int
*/
protected static function hasReconcilableNonEmptyCountOrStrlenEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
array $funcCallNames
) {
$left_value = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count';
&& in_array(strtolower($conditional->left->name->parts[0]), $funcCallNames);

$right_number = $conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $conditional->right->value === (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 0 : 1);
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 0 : 1);

if ($left_count && $right_number) {
if ($left_value && $right_number) {
return self::ASSIGNMENT_TO_RIGHT;
}

Expand Down Expand Up @@ -3806,6 +3979,8 @@ private static function getGreaterAssertions(
$conditional,
$superior_value_comparison
);
$min_strlen = null;
$strlen_equality_position = self::hasNonEmptyStrlenEqualityCheck($conditional, $min_strlen);

if ($count_equality_position) {
if ($count_equality_position === self::ASSIGNMENT_TO_RIGHT) {
Expand Down Expand Up @@ -3836,6 +4011,39 @@ private static function getGreaterAssertions(
return $if_types ? [$if_types] : [];
}

if ($strlen_equality_position) {
if ($strlen_equality_position === self::ASSIGNMENT_TO_RIGHT) {
$strlened_expr = $conditional->left;
} else {
throw new UnexpectedValueException('$count_equality_position value');
}

/** @var PhpParser\Node\Expr\FuncCall $strlened_expr */
$var_name = ExpressionIdentifier::getExtendedVarId(
$strlened_expr->getArgs()[0]->value,
$this_class_name,
$source
);

if ($var_name) {
if (self::hasReconcilableNonEmptyStrlenEqualityCheck($conditional)) {
$if_types[$var_name] = [[new Assertion\NonEmpty()]];
} else {
if ($min_strlen > 0) {
$if_types[$var_name] = [[new Assertion\NonEmpty()]];
} else {
$if_types[$var_name] = [[new Empty_()]];
}
}
}

if ($var_name) {
$if_types[$var_name] = [[new Assertion\NonEmpty()]];
}

return $if_types ? [$if_types] : [];
}

if ($count_inequality_position) {
if ($count_inequality_position === self::ASSIGNMENT_TO_LEFT) {
$count_expr = $conditional->right;
Expand Down

0 comments on commit 1d06d0f

Please sign in to comment.