Skip to content

Commit

Permalink
Merge pull request #8164 from AndrolGenhald/encapsed-literal-strings
Browse files Browse the repository at this point in the history
Encapsed literal strings
  • Loading branch information
orklah authored Jun 26, 2022
2 parents a4ab664 + 5ff54bc commit b3038f0
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Psalm\Internal\Analyzer\Statements\Expression;

use PhpParser;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\BinaryOp\Equal;
use PhpParser\Node\Expr\BinaryOp\Greater;
use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;
Expand Down Expand Up @@ -103,6 +104,7 @@ class AssertionFinder
'is_iterable' => ['iterable'],
'is_countable' => ['countable'],
];

/**
* Gets all the type assertions in a conditional
*
Expand Down Expand Up @@ -1499,50 +1501,35 @@ protected static function hasNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& $conditional->left->getArgs();

$operator_greater_than_or_equal =
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;

if ($left_count
&& $conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $operator_greater_than_or_equal
&& $conditional->right->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
? 0
: 1
)
&& $conditional->left->getArgs()
&& ($conditional instanceof BinaryOp\Greater || $conditional instanceof BinaryOp\GreaterOrEqual)
) {
$min_count = $conditional->right->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0);

return self::ASSIGNMENT_TO_RIGHT;
}

$right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall
$assignment_to = self::ASSIGNMENT_TO_RIGHT;
$compare_to = $conditional->right;
$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'
&& $conditional->right->getArgs();

$operator_less_than_or_equal =
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual;
&& $conditional->right->getArgs()
&& ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual)
) {
$assignment_to = self::ASSIGNMENT_TO_LEFT;
$compare_to = $conditional->left;
$comparison_adjustment = $conditional instanceof BinaryOp\Smaller ? 1 : 0;
} else {
return false;
}

if ($right_count
&& $conditional->left instanceof PhpParser\Node\Scalar\LNumber
&& $operator_less_than_or_equal
&& $conditional->left->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 0 : 1
)
// TODO get node type provider here somehow and check literal ints and int ranges
if ($compare_to instanceof PhpParser\Node\Scalar\LNumber
&& $compare_to->value > (-1 * $comparison_adjustment)
) {
$min_count = $conditional->left->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0);
$min_count = $compare_to->value + $comparison_adjustment;

return self::ASSIGNMENT_TO_LEFT;
return $assignment_to;
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
*/
class ConcatAnalyzer
{
private const MAX_LITERALS = 64;

/**
* @param Union|null $result_type
*/
Expand Down Expand Up @@ -155,39 +157,35 @@ public static function analyze(
self::analyzeOperand($statements_analyzer, $left, $left_type, 'Left', $context);
self::analyzeOperand($statements_analyzer, $right, $right_type, 'Right', $context);

// If one of the types is a single int or string literal, and the other
// type is all string or int literals, combine them into new literal(s).
// If both types are specific literals, combine them into new literals
$literal_concat = false;

if (($left_type->allStringLiterals() || $left_type->allIntLiterals())
&& ($right_type->allStringLiterals() || $right_type->allIntLiterals())
) {
$literal_concat = true;
$result_type_parts = [];

foreach ($left_type->getAtomicTypes() as $left_type_part) {
assert($left_type_part instanceof TLiteralString || $left_type_part instanceof TLiteralInt);
foreach ($right_type->getAtomicTypes() as $right_type_part) {
assert($right_type_part instanceof TLiteralString || $right_type_part instanceof TLiteralInt);
$literal = $left_type_part->value . $right_type_part->value;
if (strlen($literal) >= $config->max_string_length) {
// Literal too long, use non-literal type instead
$literal_concat = false;
break 2;
if ($left_type->allSpecificLiterals() && $right_type->allSpecificLiterals()) {
$left_type_parts = $left_type->getAtomicTypes();
$right_type_parts = $right_type->getAtomicTypes();
$combinations = count($left_type_parts) * count($right_type_parts);
if ($combinations < self::MAX_LITERALS) {
$literal_concat = true;
$result_type_parts = [];

foreach ($left_type->getAtomicTypes() as $left_type_part) {
foreach ($right_type->getAtomicTypes() as $right_type_part) {
$literal = $left_type_part->value . $right_type_part->value;
if (strlen($literal) >= $config->max_string_length) {
// Literal too long, use non-literal type instead
$literal_concat = false;
break 2;
}

$result_type_parts[] = new TLiteralString($literal);
}

$result_type_parts[] = new TLiteralString($literal);
}
}

if (!empty($result_type_parts)) {
if ($literal_concat && count($result_type_parts) < 64) {
if ($literal_concat) {
assert(count($result_type_parts) === $combinations);
assert(count($result_type_parts) !== 0); // #8163
$result_type = new Union($result_type_parts);
} else {
$result_type = new Union([new TNonEmptyNonspecificLiteralString]);
}

return;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
Expand Down Expand Up @@ -327,7 +328,7 @@ public static function castStringAttempt(
|| $atomic_type instanceof TInt
|| $atomic_type instanceof TNumeric
) {
if ($atomic_type instanceof TLiteralInt) {
if ($atomic_type instanceof TLiteralInt || $atomic_type instanceof TLiteralFloat) {
$castable_types[] = new TLiteralString((string) $atomic_type->value);
} elseif ($atomic_type instanceof TNonspecificLiteralInt) {
$castable_types[] = new TNonspecificLiteralString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
namespace Psalm\Internal\Analyzer\Statements\Expression;

use PhpParser;
use PhpParser\Node\Scalar\EncapsedStringPart;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Atomic\TNonspecificLiteralInt;
use Psalm\Type\Atomic\TNonspecificLiteralString;
use Psalm\Type\Union;

use function assert;
use function in_array;

class EncapsulatedStringAnalyzer
Expand All @@ -29,20 +36,16 @@ public static function analyze(

$all_literals = true;

foreach ($stmt->parts as $part) {
if ($part instanceof PhpParser\Node\Scalar\EncapsedStringPart
&& $part->value
) {
$non_empty = true;
}
$literal_string = "";

foreach ($stmt->parts as $part) {
if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) {
return false;
}

$part_type = $statements_analyzer->node_data->getType($part);

if ($part_type) {
if ($part_type !== null) {
$casted_part_type = CastAnalyzer::castStringAttempt(
$statements_analyzer,
$context,
Expand All @@ -52,6 +55,28 @@ public static function analyze(

if (!$casted_part_type->allLiterals()) {
$all_literals = false;
} elseif (!$non_empty) {
// Check if all literals are nonempty
$non_empty = true;
foreach ($casted_part_type->getAtomicTypes() as $atomic_literal) {
if (!$atomic_literal instanceof TLiteralInt
&& !$atomic_literal instanceof TNonspecificLiteralInt
&& !$atomic_literal instanceof TLiteralFloat
&& !$atomic_literal instanceof TNonEmptyNonspecificLiteralString
&& !($atomic_literal instanceof TLiteralString && $atomic_literal->value !== "")
) {
$non_empty = false;
break;
}
}
}

if ($literal_string !== null) {
if ($casted_part_type->isSingleLiteral()) {
$literal_string .= $casted_part_type->getSingleLiteral()->value;
} else {
$literal_string = null;
}
}

if ($statements_analyzer->data_flow_graph
Expand Down Expand Up @@ -82,16 +107,30 @@ public static function analyze(
}
}
}
} elseif ($part instanceof EncapsedStringPart) {
if ($literal_string !== null) {
$literal_string .= $part->value;
}
$non_empty = $non_empty || $part->value !== "";
} else {
$all_literals = false;
$literal_string = null;
}
}

if ($non_empty) {
if ($all_literals) {
if ($literal_string !== null) {
$new_type = Type::getString($literal_string);
} elseif ($all_literals) {
$new_type = new Union([new TNonEmptyNonspecificLiteralString()]);
} else {
$new_type = new Union([new TNonEmptyString()]);
}

} elseif ($all_literals) {
$new_type = new Union([new TNonspecificLiteralString()]);
}
if (isset($new_type)) {
assert($new_type instanceof Union);
$new_type->parent_nodes = $stmt_type->parent_nodes;
$stmt_type = $new_type;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev

if (isset($atomic_type->properties['options'])
&& $atomic_type->properties['options']->hasArray()
&& ($options_array = $atomic_type->properties['options']->getAtomicTypes()['array'])
&& ($options_array = $atomic_type->properties['options']->getAtomicTypes()['array'] ?? null)
&& $options_array instanceof TKeyedArray
&& isset($options_array->properties['default'])
) {
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Type/Atomic/TFalse.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
*/
class TFalse extends TBool
{
/** @var false */
public $value = false;

public function __toString(): string
{
return 'false';
Expand Down
3 changes: 3 additions & 0 deletions src/Psalm/Type/Atomic/TTrue.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
*/
class TTrue extends TBool
{
/** @var true */
public $value = true;

public function __toString(): string
{
return 'true';
Expand Down
55 changes: 55 additions & 0 deletions src/Psalm/Type/Union.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ public function replaceTypes(array $types): void
}

/**
* @psalm-mutation-free
* @return non-empty-array<string, Atomic>
*/
public function getAtomicTypes(): array
Expand Down Expand Up @@ -1302,6 +1303,34 @@ public function allIntLiterals(): bool
return true;
}

/**
* @psalm-assert-if-true array<
* array-key,
* TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue
* > $this->getAtomicTypes()
*/
public function allSpecificLiterals(): bool
{
foreach ($this->types as $atomic_key_type) {
if (!$atomic_key_type instanceof TLiteralString
&& !$atomic_key_type instanceof TLiteralInt
&& !$atomic_key_type instanceof TLiteralFloat
&& !$atomic_key_type instanceof TFalse
&& !$atomic_key_type instanceof TTrue
) {
return false;
}
}

return true;
}

/**
* @psalm-assert-if-true array<
* array-key,
* TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue
* > $this->getAtomicTypes()
*/
public function allLiterals(): bool
{
foreach ($this->types as $atomic_key_type) {
Expand Down Expand Up @@ -1329,6 +1358,32 @@ public function hasLiteralValue(): bool
|| isset($this->types['true']);
}

public function isSingleLiteral(): bool
{
return count($this->types) === 1
&& count($this->literal_int_types)
+ count($this->literal_string_types)
+ count($this->literal_float_types) === 1
;
}

/**
* @return TLiteralInt|TLiteralString|TLiteralFloat
*/
public function getSingleLiteral()
{
if (!$this->isSingleLiteral()) {
throw new InvalidArgumentException("Not a single literal");
}

return ($literal = reset($this->literal_int_types)) !== false
? $literal
: (($literal = reset($this->literal_string_types)) !== false
? $literal
: reset($this->literal_float_types))
;
}

public function hasLiteralString(): bool
{
return count($this->literal_string_types) > 0;
Expand Down
Loading

0 comments on commit b3038f0

Please sign in to comment.