Merge another scope into `self`. For each variable: - Present in both: union the type sets (variable was assigned in both branches). - Present in only one: keep it with the existing types (variable was assigned in only one branch — it *might* have those types). After merging, subsumed entries are removed. When one entry's type is a subset of another (e.g. `string|null` ⊆ `int|string|null`, or `
(&mut self, other: &ScopeState)
| 1898 | /// narrowed types from non-exiting if-branches leak into the |
| 1899 | /// post-merge scope and pollute subsequent narrowing operations. |
| 1900 | pub fn merge_branch(&mut self, other: &ScopeState) { |
| 1901 | for (name, other_types) in &other.locals { |
| 1902 | let entry = self.locals.entry(*name).or_default(); |
| 1903 | |
| 1904 | // Merge other_types into entry. When an incoming entry |
| 1905 | // shares a class name with an existing entry but has a |
| 1906 | // broader type_string (e.g. `?A` vs `A`), widen the |
| 1907 | // existing entry's type_string instead of discarding |
| 1908 | // the incoming one. This prevents post-loop merges from |
| 1909 | // losing nullable information. |
| 1910 | for rt in other_types.iter() { |
| 1911 | let mut merged_into_existing = false; |
| 1912 | if let Some(ref rt_cls) = rt.class_info { |
| 1913 | for existing in entry.iter_mut() { |
| 1914 | if let Some(ref ex_cls) = existing.class_info |
| 1915 | && ex_cls.name == rt_cls.name |
| 1916 | { |
| 1917 | // Same class. If the incoming type is |
| 1918 | // broader, adopt it. |
| 1919 | if existing.type_string != rt.type_string |
| 1920 | && existing.type_string.is_subset_of(&rt.type_string) |
| 1921 | { |
| 1922 | existing.type_string = rt.type_string.clone(); |
| 1923 | } |
| 1924 | merged_into_existing = true; |
| 1925 | break; |
| 1926 | } |
| 1927 | } |
| 1928 | } |
| 1929 | if !merged_into_existing { |
| 1930 | ResolvedType::push_unique(entry, rt.clone()); |
| 1931 | } |
| 1932 | } |
| 1933 | |
| 1934 | // Remove entries whose type is subsumed by a broader entry. |
| 1935 | // E.g. `string|null` ⊆ `int|string|null` → drop the former. |
| 1936 | if entry.len() > 1 { |
| 1937 | let types: Vec<crate::php_type::PhpType> = |
| 1938 | entry.iter().map(|rt| rt.type_string.clone()).collect(); |
| 1939 | let mut keep = vec![true; types.len()]; |
| 1940 | for i in 0..types.len() { |
| 1941 | if !keep[i] { |
| 1942 | continue; |
| 1943 | } |
| 1944 | for j in 0..types.len() { |
| 1945 | if i == j || !keep[j] { |
| 1946 | continue; |
| 1947 | } |
| 1948 | // If j is a strict subset of i, drop j. |
| 1949 | if types[j] != types[i] && types[j].is_subset_of(&types[i]) { |
| 1950 | keep[j] = false; |
| 1951 | } |
| 1952 | } |
| 1953 | } |
| 1954 | let mut idx = 0; |
| 1955 | entry.retain(|_| { |
| 1956 | let k = keep[idx]; |
| 1957 | idx += 1; |
no test coverage detected