Skip to content

Commit e052de4

Browse files
authored
Merge pull request #11369 from haas-dtv/fix/11337-6.x
Backport #11364 to 6.x
2 parents 1899907 + 34de29b commit e052de4

File tree

4 files changed

+103
-63
lines changed

4 files changed

+103
-63
lines changed

psalm-baseline.xml

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="6.x-dev@049d3523d462c4e2039608d41487347b9394c70c">
2+
<files psalm-version="dev-master@354b4d49a646d88aeb9612668c15402416cc70c0">
33
<file src="examples/TemplateChecker.php">
44
<PossiblyUndefinedIntArrayOffset>
55
<code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code>
@@ -116,10 +116,6 @@
116116
</RiskyTruthyFalsyComparison>
117117
</file>
118118
<file src="src/Psalm/Config/FileFilter.php">
119-
<InvalidOperand>
120-
<code><![CDATA[$glob_index]]></code>
121-
<code><![CDATA[$glob_index]]></code>
122-
</InvalidOperand>
123119
<PossiblyUndefinedIntArrayOffset>
124120
<code><![CDATA[explode('::', $method_id)[1]]]></code>
125121
</PossiblyUndefinedIntArrayOffset>
@@ -490,11 +486,6 @@
490486
</ImplicitToStringCast>
491487
</file>
492488
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php">
493-
<PossiblyInvalidOperand>
494-
<code><![CDATA[$item_key_value]]></code>
495-
<code><![CDATA[$item_key_value]]></code>
496-
<code><![CDATA[$item_key_value]]></code>
497-
</PossiblyInvalidOperand>
498489
<RiskyTruthyFalsyComparison>
499490
<code><![CDATA[$var_id]]></code>
500491
</RiskyTruthyFalsyComparison>
@@ -546,9 +537,6 @@
546537
<code><![CDATA[$var_id+1]]></code>
547538
<code><![CDATA[$var_id+1]]></code>
548539
</InvalidOperand>
549-
<PossiblyInvalidOperand>
550-
<code><![CDATA[$literal_key]]></code>
551-
</PossiblyInvalidOperand>
552540
<PossiblyUndefinedIntArrayOffset>
553541
<code><![CDATA[$assertion->rule[0]]]></code>
554542
<code><![CDATA[$assertion->rule[0]]]></code>
@@ -670,9 +658,6 @@
670658
<code><![CDATA[$child_stmt_dim_type]]></code>
671659
<code><![CDATA[$child_stmt_dim_type]]></code>
672660
</ImplicitToStringCast>
673-
<PossiblyInvalidOperand>
674-
<code><![CDATA[$key_value->value]]></code>
675-
</PossiblyInvalidOperand>
676661
<RiskyTruthyFalsyComparison>
677662
<code><![CDATA[!$parent_var_id]]></code>
678663
<code><![CDATA[$object_id]]></code>
@@ -751,9 +736,6 @@
751736
<code><![CDATA[$context->byref_constraints[$var_id]->type]]></code>
752737
<code><![CDATA[$var_comment_type]]></code>
753738
</ImplicitToStringCast>
754-
<PossiblyInvalidOperand>
755-
<code><![CDATA[$offset_value]]></code>
756-
</PossiblyInvalidOperand>
757739
<RiskyTruthyFalsyComparison>
758740
<code><![CDATA[!$var_comment->var_id]]></code>
759741
<code><![CDATA[!strpos($root_var_id ?? '', '->')]]></code>
@@ -1158,9 +1140,6 @@
11581140
</RiskyTruthyFalsyComparison>
11591141
</file>
11601142
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php">
1161-
<PossiblyInvalidOperand>
1162-
<code><![CDATA[$var_id]]></code>
1163-
</PossiblyInvalidOperand>
11641143
<PossiblyUndefinedIntArrayOffset>
11651144
<code><![CDATA[$callable_arg->items[0]]]></code>
11661145
<code><![CDATA[$callable_arg->items[1]]]></code>
@@ -1209,10 +1188,6 @@
12091188
<code><![CDATA[$stmt->name]]></code>
12101189
<code><![CDATA[$stmt->name]]></code>
12111190
</ImplicitToStringCast>
1212-
<PossiblyInvalidOperand>
1213-
<code><![CDATA[$offset]]></code>
1214-
<code><![CDATA[$stmt->dim->value]]></code>
1215-
</PossiblyInvalidOperand>
12161191
<RiskyTruthyFalsyComparison>
12171192
<code><![CDATA[!$lhs_var_name]]></code>
12181193
<code><![CDATA[!$object_id]]></code>
@@ -1233,13 +1208,6 @@
12331208
<code><![CDATA[$array_type]]></code>
12341209
<code><![CDATA[$offset_type]]></code>
12351210
</ImplicitToStringCast>
1236-
<PossiblyInvalidOperand>
1237-
<code><![CDATA[$dim_value]]></code>
1238-
<code><![CDATA[$dim_value]]></code>
1239-
<code><![CDATA[$literal_access]]></code>
1240-
<code><![CDATA[$literal_access]]></code>
1241-
<code><![CDATA[is_int($last_key) ? $last_key : '\'' . $last_key . '\'']]></code>
1242-
</PossiblyInvalidOperand>
12431211
<ReferenceConstraintViolation>
12441212
<code><![CDATA[$stmt_type]]></code>
12451213
<code><![CDATA[$stmt_type]]></code>
@@ -2100,12 +2068,6 @@
21002068
<code><![CDATA[$method_name]]></code>
21012069
</PossiblyUndefinedIntArrayOffset>
21022070
</file>
2103-
<file src="src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php">
2104-
<PossiblyInvalidOperand>
2105-
<code><![CDATA[$option]]></code>
2106-
<code><![CDATA[$option]]></code>
2107-
</PossiblyInvalidOperand>
2108-
</file>
21092071
<file src="src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php">
21102072
<RiskyTruthyFalsyComparison>
21112073
<code><![CDATA[$fetch_class_name]]></code>
@@ -2382,9 +2344,6 @@
23822344
<InvalidOperand>
23832345
<code><![CDATA[$property_key - 1]]></code>
23842346
</InvalidOperand>
2385-
<PossiblyInvalidOperand>
2386-
<code><![CDATA[$property]]></code>
2387-
</PossiblyInvalidOperand>
23882347
<PossiblyUndefinedIntArrayOffset>
23892348
<code><![CDATA[$const_name]]></code>
23902349
<code><![CDATA[$const_name]]></code>
@@ -2652,14 +2611,6 @@
26522611
<code><![CDATA[!$intersection]]></code>
26532612
</RiskyTruthyFalsyComparison>
26542613
</file>
2655-
<file src="src/Psalm/Type/Atomic/TIntRange.php">
2656-
<PossiblyInvalidOperand>
2657-
<code><![CDATA[$this->max_bound ?? 'max']]></code>
2658-
<code><![CDATA[$this->max_bound ?? 'max']]></code>
2659-
<code><![CDATA[$this->min_bound ?? 'min']]></code>
2660-
<code><![CDATA[$this->min_bound ?? 'min']]></code>
2661-
</PossiblyInvalidOperand>
2662-
</file>
26632614
<file src="src/Psalm/Type/Atomic/TIterable.php">
26642615
<RiskyTruthyFalsyComparison>
26652616
<code><![CDATA[!$intersection]]></code>
@@ -2689,10 +2640,6 @@
26892640
<ImpurePropertyAssignment>
26902641
<code><![CDATA[$key_type->possibly_undefined]]></code>
26912642
</ImpurePropertyAssignment>
2692-
<PossiblyInvalidOperand>
2693-
<code><![CDATA[$name]]></code>
2694-
<code><![CDATA[$name]]></code>
2695-
</PossiblyInvalidOperand>
26962643
<PossiblyUndefinedIntArrayOffset>
26972644
<code><![CDATA[$this->properties[0]]]></code>
26982645
<code><![CDATA[$this->properties[0]]]></code>
@@ -2721,10 +2668,6 @@
27212668
<code><![CDATA[replace]]></code>
27222669
<code><![CDATA[replace]]></code>
27232670
</ImpureMethodCall>
2724-
<PossiblyInvalidOperand>
2725-
<code><![CDATA[$name]]></code>
2726-
<code><![CDATA[$name]]></code>
2727-
</PossiblyInvalidOperand>
27282671
<RiskyTruthyFalsyComparison>
27292672
<code><![CDATA[!$intersection]]></code>
27302673
<code><![CDATA[!$intersection]]></code>
@@ -2782,9 +2725,6 @@
27822725
<InvalidOperand>
27832726
<code><![CDATA[$array_key_offset-1]]></code>
27842727
</InvalidOperand>
2785-
<PossiblyInvalidOperand>
2786-
<code><![CDATA[$array_key]]></code>
2787-
</PossiblyInvalidOperand>
27882728
<PossiblyUndefinedIntArrayOffset>
27892729
<code><![CDATA[$const_name]]></code>
27902730
</PossiblyUndefinedIntArrayOffset>

src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ private static function analyzeOperand(
466466

467467
if (!$operand_type_match
468468
&& (!$comparison_result->scalar_type_match_found
469-
|| (!$operand_type->isInt() && $config->strict_binary_operands)
469+
|| (!$operand_type->isConcatSafe() && $config->strict_binary_operands)
470470
)
471471
) {
472472
if ($has_valid_operand) {

src/Psalm/Type/UnionTrait.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Psalm\StatementsSource;
1919
use Psalm\Storage\FileStorage;
2020
use Psalm\Type\Atomic\TArray;
21+
use Psalm\Type\Atomic\TArrayKey;
2122
use Psalm\Type\Atomic\TCallable;
2223
use Psalm\Type\Atomic\TClassString;
2324
use Psalm\Type\Atomic\TClassStringMap;
@@ -766,7 +767,7 @@ public function hasTemplate(): bool
766767
if ($t instanceof TTemplateParam) {
767768
return true;
768769
}
769-
770+
770771
if ($t instanceof TNamedObject) {
771772
foreach ($t->extra_types as $sub) {
772773
if ($sub instanceof TTemplateParam) {
@@ -1117,6 +1118,23 @@ public function isSingleStringLiteral(): bool
11171118
return count($this->types) === 1 && count($this->literal_string_types) === 1;
11181119
}
11191120

1121+
1122+
/**
1123+
* @psalm-mutation-free
1124+
* @return bool true if this type is a safe operand for string concatenation (int|string|array-key)
1125+
*/
1126+
public function isConcatSafe(): bool
1127+
{
1128+
foreach ($this->types as $type) {
1129+
if (!($type instanceof TInt)
1130+
&& !($type instanceof TString)
1131+
&& !($type instanceof TArrayKey)) {
1132+
return false;
1133+
}
1134+
}
1135+
return true;
1136+
}
1137+
11201138
/**
11211139
* @throws InvalidArgumentException if isSingleStringLiteral is false
11221140
* @psalm-mutation-free

tests/BinaryOperationTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,88 @@ public function testDifferingNumericTypesAdditionInStrictMode(): void
275275
$this->analyzeFile('somefile.php', new Context());
276276
}
277277

278+
public function testConcatenationWithIntInStrictMode(): void
279+
{
280+
$config = Config::getInstance();
281+
$config->strict_binary_operands = true;
282+
283+
$this->addFile(
284+
'somefile.php',
285+
'<?php
286+
$a = "hi" . 5;',
287+
);
288+
289+
$this->analyzeFile('somefile.php', new Context());
290+
}
291+
292+
public function testConcatenationWithArrayKeyInStrictMode(): void
293+
{
294+
$config = Config::getInstance();
295+
$config->strict_binary_operands = true;
296+
297+
$this->addFile(
298+
'somefile.php',
299+
'<?php
300+
interface I {
301+
/** @return array-key */
302+
public function t();
303+
}
304+
305+
function test(I $i): void {
306+
$a = "hi" . $i->t();
307+
}
308+
',
309+
);
310+
311+
$this->analyzeFile('somefile.php', new Context());
312+
}
313+
314+
public function testConcatenationWithAllowedUnionTypeInStrictMode(): void
315+
{
316+
$config = Config::getInstance();
317+
$config->strict_binary_operands = true;
318+
319+
$this->addFile(
320+
'somefile.php',
321+
'<?php
322+
interface I {
323+
/** @return int|string */
324+
public function t();
325+
}
326+
327+
function test(I $i): void {
328+
$a = "hi" . $i->t();
329+
}
330+
',
331+
);
332+
333+
$this->analyzeFile('somefile.php', new Context());
334+
}
335+
336+
public function testConcatenationWithBadUnionTypeInStrictMode(): void
337+
{
338+
$config = Config::getInstance();
339+
$config->strict_binary_operands = true;
340+
341+
$this->addFile(
342+
'somefile.php',
343+
'<?php
344+
interface I {
345+
/** @return int|float */
346+
public function t();
347+
}
348+
349+
function test(I $i): void {
350+
$a = "hi" . $i->t();
351+
}
352+
',
353+
);
354+
$this->expectException(CodeException::class);
355+
$this->expectExceptionMessage('InvalidOperand');
356+
357+
$this->analyzeFile('somefile.php', new Context());
358+
}
359+
278360
public function testImplicitStringConcatenation(): void
279361
{
280362
$config = Config::getInstance();

0 commit comments

Comments
 (0)