Skip to content

Commit 69d3559

Browse files
committed
fix(codex): fix strings combiner
closes #733 Signed-off-by: azjezz <[email protected]>
1 parent 9fc0ff2 commit 69d3559

File tree

5 files changed

+72
-8
lines changed

5 files changed

+72
-8
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
final class Package
6+
{
7+
private const string EMPTY_VALUE = 'k. A.';
8+
9+
/** @var numeric-string|self::EMPTY_VALUE */
10+
private string $quantity1;
11+
12+
/** @var numeric-string|'Empty' */
13+
private string $quantity2;
14+
15+
public function __construct(int|null $quantity1, int|null $quantity2)
16+
{
17+
$this->quantity1 = $quantity1 !== null ? (string) $quantity1 : self::EMPTY_VALUE;
18+
$this->quantity2 = $quantity2 !== null ? (string) $quantity2 : 'Empty';
19+
}
20+
21+
public function quantity1(): int|null
22+
{
23+
return $this->quantity1 !== self::EMPTY_VALUE ? (int) $this->quantity1 : null;
24+
}
25+
26+
public function quantity2(): int|null
27+
{
28+
return $this->quantity2 !== 'Empty' ? (int) $this->quantity2 : null;
29+
}
30+
}

crates/analyzer/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ test_case!(issue_722);
441441
test_case!(issue_725);
442442
test_case!(issue_728_symfony_reference);
443443
test_case!(issue_731);
444+
test_case!(issue_733);
444445
test_case!(issue_736);
445446
test_case!(issue_737);
446447
test_case!(issue_739);

crates/codex/src/ttype/atomic/scalar/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ impl TScalar {
108108
/// Creates the `numeric-string` type.
109109
#[inline]
110110
pub const fn numeric_string() -> Self {
111-
TScalar::String(TString::general_with_props(true, false, false, true))
111+
TScalar::String(TString::general_with_props(true, false, false, false))
112112
}
113113

114114
/// Creates the general `string` type.

crates/codex/src/ttype/atomic/scalar/string.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,9 @@ impl TString {
8787
is_numeric: bool,
8888
is_truthy: bool,
8989
mut is_non_empty: bool,
90-
mut is_lowercase: bool,
90+
is_lowercase: bool,
9191
) -> Self {
9292
is_non_empty |= is_numeric || is_truthy;
93-
is_lowercase |= is_numeric;
9493

9594
Self { literal, is_numeric, is_truthy, is_non_empty, is_lowercase }
9695
}
@@ -278,8 +277,8 @@ impl TString {
278277
literal: if retain_literal { self.literal.clone() } else { None },
279278
is_numeric: true,
280279
is_truthy: self.is_truthy,
281-
is_non_empty: true,
282-
is_lowercase: true, // Numeric strings are considered lowercase
280+
is_non_empty: true, // Numeric strings are always non-empty
281+
is_lowercase: self.is_lowercase,
283282
}
284283
}
285284
}

crates/codex/src/ttype/combiner.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -926,12 +926,35 @@ fn scrape_type_properties(
926926
if let TAtomic::Scalar(TScalar::String(mut string_scalar)) = atomic {
927927
if let Some(existing_string_type) = combination.value_types.get_mut(&*ATOM_STRING) {
928928
if let TAtomic::Scalar(TScalar::String(existing_string_type)) = existing_string_type {
929-
*existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
929+
if let Some(lit_atom) = string_scalar.get_known_literal_atom() {
930+
let lit_value = lit_atom.as_str();
931+
let is_incompatible = (existing_string_type.is_numeric && !str_is_numeric(lit_value))
932+
|| (existing_string_type.is_truthy && (lit_value.is_empty() || lit_value == "0"))
933+
|| (existing_string_type.is_non_empty && lit_value.is_empty())
934+
|| (existing_string_type.is_lowercase && lit_value.chars().any(|c| c.is_uppercase()));
935+
936+
if is_incompatible {
937+
combination.literal_strings.insert(lit_atom);
938+
} else {
939+
*existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
940+
}
941+
} else {
942+
*existing_string_type = combine_string_scalars(existing_string_type, string_scalar);
943+
}
930944
};
931945
} else if let Some(atom) = string_scalar.get_known_literal_atom() {
932946
combination.literal_strings.insert(atom);
933947
} else {
934-
if string_scalar.is_truthy || string_scalar.is_non_empty || string_scalar.is_numeric {
948+
// When we have a constrained string type (like numeric-string) and literals,
949+
// we need to decide whether to merge them or keep them separate.
950+
// If the non-literal is numeric-string, keep non-numeric literals separate.
951+
let mut literals_to_keep = Vec::new();
952+
953+
if string_scalar.is_truthy
954+
|| string_scalar.is_non_empty
955+
|| string_scalar.is_numeric
956+
|| string_scalar.is_lowercase
957+
{
935958
for value in &combination.literal_strings {
936959
if value.is_empty() {
937960
string_scalar.is_non_empty = false;
@@ -942,12 +965,23 @@ fn scrape_type_properties(
942965
string_scalar.is_truthy = false;
943966
}
944967

945-
string_scalar.is_numeric = string_scalar.is_numeric && str_is_numeric(value);
968+
// If the string is numeric but the literal is not, keep the literal separate
969+
if string_scalar.is_numeric && !str_is_numeric(value) {
970+
literals_to_keep.push(*value);
971+
} else {
972+
string_scalar.is_numeric = string_scalar.is_numeric && str_is_numeric(value);
973+
}
974+
975+
string_scalar.is_lowercase = string_scalar.is_lowercase && value.chars().all(|c| !c.is_uppercase());
946976
}
947977
}
948978

949979
combination.value_types.insert(*ATOM_STRING, TAtomic::Scalar(TScalar::String(string_scalar)));
980+
950981
combination.literal_strings.clear();
982+
for lit in literals_to_keep {
983+
combination.literal_strings.insert(lit);
984+
}
951985
}
952986

953987
return;

0 commit comments

Comments
 (0)