Skip to content

Commit

Permalink
Merge branch '5.x' into update-master
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Mar 2, 2024
2 parents b940c7e + 20e8604 commit e9c595d
Show file tree
Hide file tree
Showing 24 changed files with 549 additions and 125 deletions.
16 changes: 15 additions & 1 deletion src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2640,8 +2640,22 @@ public function getPHPVersionFromComposerJson(): ?string
$version_parser = new VersionParser();

$constraint = $version_parser->parseConstraints($php_version);
$php_versions = [
'5.4',
'5.5',
'5.6',
'7.0',
'7.1',
'7.2',
'7.3',
'7.4',
'8.0',
'8.1',
'8.2',
'8.3',
];

foreach (['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] as $candidate) {
foreach ($php_versions as $candidate) {
if ($constraint->matches(new Constraint('<=', "$candidate.0.0-dev"))
|| $constraint->matches(new Constraint('<=', "$candidate.999"))
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,15 +308,6 @@ public static function verifyReturnType(
$source->getParentFQCLN(),
);

// hack until we have proper yield type collection
if ($function_like_storage
&& $function_like_storage->has_yield
&& !$inferred_yield_type
&& !$inferred_return_type->isVoid()
) {
$inferred_return_type = new Union([new TNamedObject('Generator')]);
}

if ($is_to_string) {
$union_comparison_results = new TypeComparisonResult();
if (!$inferred_return_type->hasMixed() &&
Expand Down
55 changes: 52 additions & 3 deletions src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ public static function checkIteratorType(
$key_type,
$value_type,
$has_valid_iterator,
$invalid_iterator_types,
);
} else {
$raw_object_types[] = $iterator_atomic_type->value;
Expand Down Expand Up @@ -720,6 +721,7 @@ public static function checkIteratorType(
return null;
}

/** @param list<string> $invalid_iterator_types */
public static function handleIterable(
StatementsAnalyzer $statements_analyzer,
TNamedObject $iterator_atomic_type,
Expand All @@ -729,6 +731,7 @@ public static function handleIterable(
?Union &$key_type,
?Union &$value_type,
bool &$has_valid_iterator,
array &$invalid_iterator_types = [],
): void {
if ($iterator_atomic_type->extra_types) {
$iterator_atomic_types = [
Expand All @@ -747,9 +750,6 @@ public static function handleIterable(
throw new UnexpectedValueException('Shouldn’t get a generic param here');
}


$has_valid_iterator = true;

if ($iterator_atomic_type instanceof TIterable
|| (strtolower($iterator_atomic_type->value) === 'traversable'
|| $codebase->classImplements(
Expand All @@ -776,6 +776,8 @@ public static function handleIterable(
)
)
) {
$has_valid_iterator = true;

$old_data_provider = $statements_analyzer->node_data;

$statements_analyzer->node_data = clone $statements_analyzer->node_data;
Expand Down Expand Up @@ -862,6 +864,7 @@ public static function handleIterable(
$key_type,
$value_type,
$has_valid_iterator,
$invalid_iterator_types,
);

continue;
Expand Down Expand Up @@ -894,6 +897,51 @@ public static function handleIterable(
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
}
}
} elseif ($iterator_atomic_type instanceof TGenericObject
&& strtolower($iterator_atomic_type->value) === 'generator'
) {
$type_params = $iterator_atomic_type->type_params;
if (isset($type_params[2]) && !$type_params[2]->isNullable() && !$type_params[2]->isMixed()) {
$invalid_iterator_types[] = $iterator_atomic_type->getKey();
} else {
$has_valid_iterator = true;
}

$iterator_value_type = self::getFakeMethodCallType(
$statements_analyzer,
$foreach_expr,
$context,
'current',
);

$iterator_key_type = self::getFakeMethodCallType(
$statements_analyzer,
$foreach_expr,
$context,
'key',
);

if ($iterator_value_type && !$iterator_value_type->isMixed()) {
// remove null coming from current() to signify invalid iterations
// we're in a foreach context, so we know we're not going iterate past the end
if (isset($type_params[1]) && !$type_params[1]->isNullable()) {
$iterator_value_type = $iterator_value_type->getBuilder();
$iterator_value_type->removeType('null');
$iterator_value_type = $iterator_value_type->freeze();
}
$value_type = Type::combineUnionTypes($value_type, $iterator_value_type);
}

if ($iterator_key_type && !$iterator_key_type->isMixed()) {
// remove null coming from key() to signify invalid iterations
// we're in a foreach context, so we know we're not going iterate past the end
if (isset($type_params[0]) && !$type_params[0]->isNullable()) {
$iterator_key_type = $iterator_key_type->getBuilder();
$iterator_key_type->removeType('null');
$iterator_key_type = $iterator_key_type->freeze();
}
$key_type = Type::combineUnionTypes($key_type, $iterator_key_type);
}
} elseif ($codebase->classImplements(
$iterator_atomic_type->value,
'Iterator',
Expand All @@ -906,6 +954,7 @@ public static function handleIterable(
)
)
) {
$has_valid_iterator = true;
$iterator_value_type = self::getFakeMethodCallType(
$statements_analyzer,
$foreach_expr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@
use Psalm\Issue\DocblockTypeContradiction;
use Psalm\Issue\RedundantCondition;
use Psalm\Issue\RedundantConditionGivenDocblockType;
use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\Issue\TypeDoesNotContainType;
use Psalm\IssueBuffer;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Reconciler;

use function array_diff_key;
use function array_filter;
use function array_keys;
use function array_key_first;
use function array_merge;
use function array_values;
use function count;
Expand Down Expand Up @@ -82,7 +80,7 @@ public static function analyze(
$entry_clauses,
static fn(Clause $c): bool => count($c->possibilities) > 1
|| $c->wedge
|| !isset($changed_var_ids[array_keys($c->possibilities)[0]])
|| !isset($changed_var_ids[array_key_first($c->possibilities)])
),
);
}
Expand Down Expand Up @@ -373,33 +371,7 @@ public static function handleParadoxicalCondition(
} elseif (!($stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical)
&& !($stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical)
&& !($stmt instanceof PhpParser\Node\Expr\BooleanNot)) {
if (count($type->getAtomicTypes()) > 1) {
$has_truthy_or_falsy_exclusive_type = false;
$both_types = $type->getBuilder();
foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
if ($atomic_type->isTruthy()
|| $atomic_type->isFalsy()
|| $atomic_type instanceof TBool) {
$both_types->removeType($key);
$has_truthy_or_falsy_exclusive_type = true;
}
}

if (count($both_types->getAtomicTypes()) > 0 && $has_truthy_or_falsy_exclusive_type) {
$both_types = $both_types->freeze();
IssueBuffer::maybeAdd(
new RiskyTruthyFalsyComparison(
'Operand of type ' . $type->getId() . ' contains ' .
'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
$both_types->getId() . ', which can be falsy and truthy. ' .
'This can cause possibly unexpected behavior. Use strict comparison instead.',
new CodeLocation($statements_analyzer, $stmt),
$type->getId(),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
ExpressionAnalyzer::checkRiskyTruthyFalsyComparison($type, $statements_analyzer, $stmt);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@
namespace Psalm\Internal\Analyzer\Statements\Expression;

use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TTrue;
use Psalm\Type\Union;

use function count;

/**
* @internal
*/
Expand Down Expand Up @@ -47,34 +42,7 @@ public static function analyze(
} elseif ($expr_type->isAlwaysFalsy()) {
$stmt_type = new TTrue($expr_type->from_docblock);
} else {
if (count($expr_type->getAtomicTypes()) > 1) {
$has_truthy_or_falsy_exclusive_type = false;
$both_types = $expr_type->getBuilder();
foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
if ($atomic_type->isTruthy()
|| $atomic_type->isFalsy()
|| $atomic_type instanceof TBool) {
$both_types->removeType($key);
$has_truthy_or_falsy_exclusive_type = true;
}
}

if (count($both_types->getAtomicTypes()) > 0 && $has_truthy_or_falsy_exclusive_type) {
$both_types = $both_types->freeze();
IssueBuffer::maybeAdd(
new RiskyTruthyFalsyComparison(
'Operand of type ' . $expr_type->getId() . ' contains ' .
'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
$both_types->getId() . ', which can be falsy and truthy. ' .
'This can cause possibly unexpected behavior. Use strict comparison instead.',
new CodeLocation($statements_analyzer, $stmt),
$expr_type->getId(),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}

ExpressionAnalyzer::checkRiskyTruthyFalsyComparison($expr_type, $statements_analyzer, $stmt);
$stmt_type = new TBool();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ public static function analyze(
$was_inside_call = $context->inside_call;
$context->inside_call = true;

$was_inside_isset = $context->inside_isset;
$context->inside_isset = false;

if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$arg->value,
Expand All @@ -240,11 +243,13 @@ public static function analyze(
null,
$high_order_template_result,
) === false) {
$context->inside_isset = $was_inside_isset;
$context->inside_call = $was_inside_call;

return false;
}

$context->inside_isset = $was_inside_isset;
$context->inside_call = $was_inside_call;

if ($high_order_callable_info && $high_order_template_result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,19 +723,27 @@ public static function analyzeAssignment(

// Check assigned type matches docblock type
if ($assigned_type = $statements_analyzer->node_data->getType($const->value)) {
if ($const_storage->type !== null
$const_storage_type = $const_storage->type;

if ($const_storage_type !== null
&& $const_storage->stmt_location !== null
&& $assigned_type !== $const_storage->type
&& $assigned_type !== $const_storage_type
// Check if this type was defined via a dockblock or type hint otherwise the inferred type
// should always match the assigned type and we don't even need to do additional checks
// There is an issue with constants over a certain length where additional values
// are added to fallback_params in the assigned_type but not in const_storage_type
// which causes a false flag for this error to appear. Usually happens with arrays
&& ($const_storage_type->from_docblock || $const_storage_type->from_property)
&& !UnionTypeComparator::isContainedBy(
$statements_analyzer->getCodebase(),
$assigned_type,
$const_storage->type,
$const_storage_type,
)
) {
IssueBuffer::maybeAdd(
new InvalidConstantAssignmentValue(
"{$class_storage->name}::{$const->name->name} with declared type "
. "{$const_storage->type->getId()} cannot be assigned type {$assigned_type->getId()}",
. "{$const_storage_type->getId()} cannot be assigned type {$assigned_type->getId()}",
$const_storage->stmt_location,
"{$class_storage->name}::{$const->name->name}",
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TTrue;
use Psalm\Type\Union;

use function count;

/**
* @internal
*/
Expand Down Expand Up @@ -66,34 +64,7 @@ public static function analyze(
} elseif ($expr_type->isAlwaysFalsy()) {
$stmt_type = new TTrue($expr_type->from_docblock);
} else {
if (count($expr_type->getAtomicTypes()) > 1) {
$has_truthy_or_falsy_exclusive_type = false;
$both_types = $expr_type->getBuilder();
foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
if ($atomic_type->isTruthy()
|| $atomic_type->isFalsy()
|| $atomic_type instanceof TBool) {
$both_types->removeType($key);
$has_truthy_or_falsy_exclusive_type = true;
}
}

if (count($both_types->getAtomicTypes()) > 0 && $has_truthy_or_falsy_exclusive_type) {
$both_types = $both_types->freeze();
IssueBuffer::maybeAdd(
new RiskyTruthyFalsyComparison(
'Operand of type ' . $expr_type->getId() . ' contains ' .
'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
$both_types->getId() . ', which can be falsy and truthy. ' .
'This can cause possibly unexpected behavior. Use strict comparison instead.',
new CodeLocation($statements_analyzer, $stmt),
$expr_type->getId(),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}

ExpressionAnalyzer::checkRiskyTruthyFalsyComparison($expr_type, $statements_analyzer, $stmt);
$stmt_type = new TBool();
}

Expand Down
Loading

0 comments on commit e9c595d

Please sign in to comment.