1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 03:17:02 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php

509 lines
22 KiB
PHP
Raw Normal View History

2020-05-19 00:57:02 +02:00
<?php
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
use PhpParser;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
2020-07-22 01:40:35 +02:00
use Psalm\Internal\Type\Comparator\AtomicTypeComparator;
2020-05-19 00:57:02 +02:00
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
use Psalm\Issue\FalseOperand;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\ImpureMethodCall;
use Psalm\Issue\InvalidOperand;
use Psalm\Issue\MixedOperand;
use Psalm\Issue\NullOperand;
use Psalm\Issue\PossiblyFalseOperand;
use Psalm\Issue\PossiblyInvalidOperand;
use Psalm\Issue\PossiblyNullOperand;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use function strtolower;
use function strlen;
/**
* @internal
*/
class ConcatAnalyzer
{
/**
* @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr $left
* @param PhpParser\Node\Expr $right
* @param Type\Union|null &$result_type
*
* @return void
*/
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $left,
PhpParser\Node\Expr $right,
Context $context,
Type\Union &$result_type = null
) {
$codebase = $statements_analyzer->getCodebase();
$left_type = $statements_analyzer->node_data->getType($left);
$right_type = $statements_analyzer->node_data->getType($right);
$config = Config::getInstance();
if ($left_type && $right_type) {
$result_type = Type::getString();
if ($left_type->hasMixed() || $right_type->hasMixed()) {
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
}
if ($left_type->hasMixed()) {
if (IssueBuffer::accepts(
new MixedOperand(
'Left operand cannot be mixed',
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new MixedOperand(
'Right operand cannot be mixed',
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
return;
}
if (!$context->collect_initializations
&& !$context->collect_mutations
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
&& (!(($parent_source = $statements_analyzer->getSource())
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
) {
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
}
if ($left_type->isNull()) {
if (IssueBuffer::accepts(
new NullOperand(
'Cannot concatenate with a ' . $left_type,
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($right_type->isNull()) {
if (IssueBuffer::accepts(
new NullOperand(
'Cannot concatenate with a ' . $right_type,
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($left_type->isFalse()) {
if (IssueBuffer::accepts(
new FalseOperand(
'Cannot concatenate with a ' . $left_type,
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($right_type->isFalse()) {
if (IssueBuffer::accepts(
new FalseOperand(
'Cannot concatenate with a ' . $right_type,
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($left_type->isNullable() && !$left_type->ignore_nullable_issues) {
if (IssueBuffer::accepts(
new PossiblyNullOperand(
'Cannot concatenate with a possibly null ' . $left_type,
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($right_type->isNullable() && !$right_type->ignore_nullable_issues) {
if (IssueBuffer::accepts(
new PossiblyNullOperand(
'Cannot concatenate with a possibly null ' . $right_type,
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($left_type->isFalsable() && !$left_type->ignore_falsable_issues) {
if (IssueBuffer::accepts(
new PossiblyFalseOperand(
'Cannot concatenate with a possibly false ' . $left_type,
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($right_type->isFalsable() && !$right_type->ignore_falsable_issues) {
if (IssueBuffer::accepts(
new PossiblyFalseOperand(
'Cannot concatenate with a possibly false ' . $right_type,
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
$left_type_match = true;
$right_type_match = true;
$has_valid_left_operand = false;
$has_valid_right_operand = false;
2020-07-22 01:40:35 +02:00
$left_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
$right_comparison_result = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
2020-05-19 00:57:02 +02:00
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
2020-05-19 00:57:02 +02:00
foreach ($left_type->getAtomicTypes() as $left_type_part) {
if ($left_type_part instanceof Type\Atomic\TTemplateParam) {
if (IssueBuffer::accepts(
new MixedOperand(
'Left operand cannot be mixed',
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($left_type_part instanceof Type\Atomic\TNull || $left_type_part instanceof Type\Atomic\TFalse) {
continue;
}
2020-07-22 01:40:35 +02:00
$left_type_part_match = AtomicTypeComparator::isContainedBy(
2020-05-19 00:57:02 +02:00
$codebase,
$left_type_part,
new Type\Atomic\TString,
false,
false,
$left_comparison_result
);
$left_type_match = $left_type_match && $left_type_part_match;
$has_valid_left_operand = $has_valid_left_operand || $left_type_part_match;
if ($left_comparison_result->to_string_cast && $config->strict_binary_operands) {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
'Left side of concat op expects string, '
. '\'' . $left_type . '\' provided with a __toString method',
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
foreach ($left_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
$to_string_method_id = new \Psalm\Internal\MethodIdentifier(
$atomic_type->value,
'__tostring'
);
if ($codebase->methods->methodExists(
$to_string_method_id,
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($statements_analyzer->getSource(), $left)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)) {
try {
$storage = $codebase->methods->getStorage($to_string_method_id);
} catch (\UnexpectedValueException $e) {
continue;
}
if ($context->mutation_free && !$storage->mutation_free) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call a possibly-mutating method '
. $atomic_type->value . '::__toString from a pure context',
new CodeLocation($statements_analyzer, $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
2020-05-19 00:57:02 +02:00
}
}
}
}
}
foreach ($right_type->getAtomicTypes() as $right_type_part) {
if ($right_type_part instanceof Type\Atomic\TTemplateParam) {
if (IssueBuffer::accepts(
new MixedOperand(
'Right operand cannot be a template param',
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($right_type_part instanceof Type\Atomic\TNull || $right_type_part instanceof Type\Atomic\TFalse) {
continue;
}
2020-07-22 01:40:35 +02:00
$right_type_part_match = AtomicTypeComparator::isContainedBy(
2020-05-19 00:57:02 +02:00
$codebase,
$right_type_part,
new Type\Atomic\TString,
false,
false,
$right_comparison_result
);
$right_type_match = $right_type_match && $right_type_part_match;
$has_valid_right_operand = $has_valid_right_operand || $right_type_part_match;
if ($right_comparison_result->to_string_cast && $config->strict_binary_operands) {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
'Right side of concat op expects string, '
. '\'' . $right_type . '\' provided with a __toString method',
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
foreach ($right_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
$to_string_method_id = new \Psalm\Internal\MethodIdentifier(
$atomic_type->value,
'__tostring'
);
if ($codebase->methods->methodExists(
$to_string_method_id,
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($statements_analyzer->getSource(), $right)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)) {
try {
$storage = $codebase->methods->getStorage($to_string_method_id);
} catch (\UnexpectedValueException $e) {
continue;
}
if ($context->mutation_free && !$storage->mutation_free) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call a possibly-mutating method '
. $atomic_type->value . '::__toString from a pure context',
new CodeLocation($statements_analyzer, $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
2020-05-19 00:57:02 +02:00
}
}
}
}
}
if (!$left_type_match
&& (!$left_comparison_result->scalar_type_match_found || $config->strict_binary_operands)
) {
if ($has_valid_left_operand) {
if (IssueBuffer::accepts(
new PossiblyInvalidOperand(
'Cannot concatenate with a ' . $left_type,
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot concatenate with a ' . $left_type,
new CodeLocation($statements_analyzer->getSource(), $left)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
if (!$right_type_match
&& (!$right_comparison_result->scalar_type_match_found || $config->strict_binary_operands)
) {
if ($has_valid_right_operand) {
if (IssueBuffer::accepts(
new PossiblyInvalidOperand(
'Cannot concatenate with a ' . $right_type,
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot concatenate with a ' . $right_type,
new CodeLocation($statements_analyzer->getSource(), $right)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}
// When concatenating two known string literals (with only one possibility),
// put the concatenated string into $result_type
if ($left_type && $right_type && $left_type->isSingleStringLiteral() && $right_type->isSingleStringLiteral()) {
$literal = $left_type->getSingleStringLiteral()->value . $right_type->getSingleStringLiteral()->value;
if (strlen($literal) <= 1000) {
// Limit these to 10000 bytes to avoid extremely large union types from repeated concatenations, etc
$result_type = Type::getString($literal);
}
} else {
if ($left_type
&& $right_type
) {
$left_type_literal_value = $left_type->isSingleStringLiteral()
? $left_type->getSingleStringLiteral()->value
: null;
$right_type_literal_value = $right_type->isSingleStringLiteral()
? $right_type->getSingleStringLiteral()->value
: null;
if (($left_type->getId() === 'lowercase-string'
|| $left_type->getId() === 'non-empty-lowercase-string'
|| $left_type->isInt()
|| ($left_type_literal_value !== null
&& strtolower($left_type_literal_value) === $left_type_literal_value))
&& ($right_type->getId() === 'lowercase-string'
|| $right_type->getId() === 'non-empty-lowercase-string'
|| $right_type->isInt()
|| ($right_type_literal_value !== null
&& strtolower($right_type_literal_value) === $right_type_literal_value))
) {
if ($left_type->getId() === 'non-empty-lowercase-string'
|| $left_type->isInt()
|| ($left_type_literal_value !== null
&& strtolower($left_type_literal_value) === $left_type_literal_value)
|| $right_type->getId() === 'non-empty-lowercase-string'
|| $right_type->isInt()
|| ($right_type_literal_value !== null
&& strtolower($right_type_literal_value) === $right_type_literal_value)
) {
$result_type = new Type\Union([new Type\Atomic\TNonEmptyLowercaseString()]);
} else {
$result_type = new Type\Union([new Type\Atomic\TLowercaseString()]);
}
} elseif ($left_type->getId() === 'non-empty-string'
|| $right_type->getId() === 'non-empty-string'
|| $left_type_literal_value
|| $right_type_literal_value
) {
$result_type = new Type\Union([new Type\Atomic\TNonEmptyString()]);
}
}
}
}
}