Skip to content

Commit dc17232

Browse files
committed
fix(analyzer): correct composite string handling, and improve string casting
closes #11465 closes #11466 closes #11464 Signed-off-by: azjezz <[email protected]>
1 parent cf42094 commit dc17232

File tree

4 files changed

+273
-71
lines changed

4 files changed

+273
-71
lines changed

src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -745,45 +745,41 @@ public static function castStringAttempt(
745745
continue;
746746
}
747747

748-
if ($atomic_type instanceof TNull
749-
|| $atomic_type instanceof TFalse
750-
) {
748+
if ($atomic_type instanceof TArray || $atomic_type instanceof TKeyedArray) {
749+
$valid_strings[] = Type::getAtomicStringFromLiteral('Array');
750+
continue;
751+
}
752+
753+
754+
if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) {
751755
$valid_strings[] = Type::getAtomicStringFromLiteral('');
752756
continue;
753757
}
754758

755-
if ($atomic_type instanceof TTrue
756-
) {
759+
if ($atomic_type instanceof TTrue) {
757760
$valid_strings[] = Type::getAtomicStringFromLiteral('1');
758761
continue;
759762
}
760763

761-
if ($atomic_type instanceof TBool
762-
) {
764+
if ($atomic_type instanceof TBool) {
763765
$valid_strings[] = Type::getAtomicStringFromLiteral('1');
764766
$valid_strings[] = Type::getAtomicStringFromLiteral('');
765767
continue;
766768
}
767769

768-
if ($atomic_type instanceof TClosedResource
769-
|| $atomic_type instanceof TResource
770-
) {
770+
if ($atomic_type instanceof TClosedResource || $atomic_type instanceof TResource) {
771771
$castable_types[] = new TNonEmptyString();
772772

773773
continue;
774774
}
775775

776-
if ($atomic_type instanceof TMixed
777-
|| $atomic_type instanceof Scalar
778-
) {
776+
if ($atomic_type instanceof TMixed || $atomic_type instanceof Scalar) {
779777
$castable_types[] = new TString();
780778

781779
continue;
782780
}
783781

784-
if ($atomic_type instanceof TNamedObject
785-
|| $atomic_type instanceof TObjectWithProperties
786-
) {
782+
if ($atomic_type instanceof TNamedObject || $atomic_type instanceof TObjectWithProperties) {
787783
$intersection_types = [$atomic_type];
788784

789785
if ($atomic_type->extra_types) {

src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616
use Psalm\Internal\DataFlow\TaintSource;
1717
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
1818
use Psalm\Type;
19-
use Psalm\Type\Atomic\TLiteralFloat;
20-
use Psalm\Type\Atomic\TLiteralInt;
2119
use Psalm\Type\Atomic\TLiteralString;
2220
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
2321
use Psalm\Type\Atomic\TNonEmptyString;
24-
use Psalm\Type\Atomic\TNonspecificLiteralInt;
2522
use Psalm\Type\Atomic\TNonspecificLiteralString;
2623
use Psalm\Type\Atomic\TString;
2724
use Psalm\Type\Union;
@@ -40,24 +37,28 @@ public static function analyze(
4037
Context $context,
4138
): bool {
4239
$parent_nodes = [];
43-
4440
$non_empty = false;
45-
4641
$all_literals = true;
47-
4842
$literal_string = "";
43+
$impossible = false;
4944

5045
foreach ($stmt->parts as $part) {
5146
if ($part instanceof Expr) {
47+
$was_inside_general_use = $context->inside_general_use;
48+
$context->inside_general_use = true;
5249
if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) {
50+
$context->inside_general_use = $was_inside_general_use;
5351
return false;
5452
}
53+
54+
$context->inside_general_use = $was_inside_general_use;
5555
}
5656

5757
if ($part instanceof InterpolatedStringPart) {
5858
if ($literal_string !== null) {
5959
$literal_string .= $part->value;
6060
}
61+
6162
$non_empty = $non_empty || $part->value !== "";
6263
} elseif ($part_type = $statements_analyzer->node_data->getType($part)) {
6364
$casted_part_type = CastAnalyzer::castStringAttempt(
@@ -67,30 +68,65 @@ public static function analyze(
6768
$part,
6869
);
6970

70-
if (!$casted_part_type->allLiterals()) {
71-
$all_literals = false;
72-
} elseif (!$non_empty) {
73-
// Check if all literals are nonempty
74-
$non_empty = true;
75-
foreach ($casted_part_type->getAtomicTypes() as $atomic_literal) {
76-
if (!$atomic_literal instanceof TLiteralInt
77-
&& !$atomic_literal instanceof TNonspecificLiteralInt
78-
&& !$atomic_literal instanceof TLiteralFloat
79-
&& !$atomic_literal instanceof TNonEmptyNonspecificLiteralString
80-
&& !($atomic_literal instanceof TLiteralString && $atomic_literal->value !== "")
81-
) {
82-
$non_empty = false;
83-
break;
71+
if ($casted_part_type->isNever()) {
72+
$impossible = true;
73+
74+
continue;
75+
}
76+
77+
78+
$is_non_empty_part = true;
79+
$part_is_all_literals = true;
80+
$part_literal_string = null;
81+
$could_specify_literals = true;
82+
foreach ($casted_part_type->getAtomicTypes() as $casted_part_atomic) {
83+
if (!$casted_part_atomic instanceof TString) {
84+
$part_is_all_literals = false;
85+
$could_specify_literals = false;
86+
continue;
87+
}
88+
89+
if (!$casted_part_atomic instanceof TNonEmptyString
90+
&& !$casted_part_atomic instanceof TNonEmptyNonspecificLiteralString
91+
&& !($casted_part_atomic instanceof TLiteralString && $casted_part_atomic->value !== '')
92+
) {
93+
$is_non_empty_part = false;
94+
}
95+
96+
if (!$part_is_all_literals || !$could_specify_literals) {
97+
continue;
98+
}
99+
100+
if ($casted_part_atomic instanceof TLiteralString) {
101+
if ($part_literal_string === null) {
102+
$part_literal_string = $casted_part_atomic->value;
103+
} elseif ($part_literal_string !== $casted_part_atomic->value) {
104+
$part_literal_string = null;
105+
$could_specify_literals = false;
84106
}
107+
108+
continue;
85109
}
86-
}
87110

88-
if ($literal_string !== null) {
89-
if ($casted_part_type->isSingleLiteral()) {
90-
$literal_string .= $casted_part_type->getSingleLiteral()->value;
91-
} else {
111+
if ($casted_part_atomic instanceof TNonspecificLiteralString) {
92112
$literal_string = null;
113+
$part_literal_string = null;
114+
$could_specify_literals = false;
115+
116+
continue;
93117
}
118+
119+
$part_is_all_literals = false;
120+
}
121+
122+
$non_empty = $non_empty || $is_non_empty_part;
123+
$all_literals = $all_literals && $part_is_all_literals;
124+
if (!$part_is_all_literals || !$could_specify_literals) {
125+
$literal_string = null;
126+
} else if ($part_literal_string !== null && $literal_string !== null) {
127+
$literal_string .= $part_literal_string;
128+
} else {
129+
$literal_string = null;
94130
}
95131

96132
if ($statements_analyzer->data_flow_graph
@@ -134,36 +170,26 @@ public static function analyze(
134170
}
135171
}
136172

137-
if ($non_empty) {
138-
if ($literal_string !== null) {
139-
$stmt_type = new Union(
140-
[Type::getAtomicStringFromLiteral($literal_string)],
141-
['parent_nodes' => $parent_nodes],
142-
);
143-
} elseif ($all_literals) {
144-
$stmt_type = new Union(
145-
[new TNonEmptyNonspecificLiteralString()],
146-
['parent_nodes' => $parent_nodes],
147-
);
148-
} else {
149-
$stmt_type = new Union(
150-
[new TNonEmptyString()],
151-
['parent_nodes' => $parent_nodes],
152-
);
153-
}
173+
if ($impossible) {
174+
$resulting_string = Type::getNever();
175+
} elseif ($literal_string !== null) {
176+
$resulting_string = Type::getAtomicStringFromLiteral($literal_string);
177+
} elseif ($non_empty && $all_literals) {
178+
$resulting_string = new TNonEmptyNonspecificLiteralString();
179+
} elseif ($non_empty) {
180+
$resulting_string = new TNonEmptyString();
154181
} elseif ($all_literals) {
155-
$stmt_type = new Union(
156-
[new TNonspecificLiteralString()],
157-
['parent_nodes' => $parent_nodes],
158-
);
182+
$resulting_string = new TNonspecificLiteralString();
159183
} else {
160-
$stmt_type = new Union(
161-
[new TString()],
162-
['parent_nodes' => $parent_nodes],
163-
);
184+
$resulting_string = new TString();
164185
}
165186

166-
$statements_analyzer->node_data->setType($stmt, $stmt_type);
187+
$resulting_type = new Union(
188+
[$resulting_string],
189+
['parent_nodes' => $parent_nodes],
190+
);
191+
192+
$statements_analyzer->node_data->setType($stmt, $resulting_type);
167193

168194
return true;
169195
}

tests/BinaryOperationTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -971,10 +971,10 @@ function foo(string $s1): string {
971971
],
972972
'encapsedPossiblyEmptyLiteralString' => [
973973
'code' => '<?php
974-
/** @var "foo"|"" */
975-
$foo = "";
976-
/** @var "bar"|"" */
977-
$bar = "";
974+
/**
975+
* @var ""|"foo" $foo
976+
* @var ""|"bar" $bar
977+
*/
978978
$interpolated = "{$foo}{$bar}";
979979
',
980980
'assertions' => ['$interpolated===' => 'literal-string'],

0 commit comments

Comments
 (0)