Build a substitution map from a class's template parameters and concrete type arguments. Handles right-alignment when fewer arguments than template parameters are provided (see [`apply_generic_args`] for details on the heuristic). Returns an empty map when no substitutions can be made (e.g. when `template_params` or `type_args` is empty).
(
class: &ClassInfo,
type_args: &[PhpType],
)
| 1296 | /// Returns an empty map when no substitutions can be made (e.g. when |
| 1297 | /// `template_params` or `type_args` is empty). |
| 1298 | pub(crate) fn build_generic_subs( |
| 1299 | class: &ClassInfo, |
| 1300 | type_args: &[PhpType], |
| 1301 | ) -> HashMap<String, PhpType> { |
| 1302 | if class.template_params.is_empty() || type_args.is_empty() { |
| 1303 | return HashMap::new(); |
| 1304 | } |
| 1305 | |
| 1306 | // When fewer type arguments are provided than template parameters, |
| 1307 | // right-align the args so that trailing (value) params get bound |
| 1308 | // and leading key-like params stay unbound. This handles the |
| 1309 | // common PHP pattern of writing `Collection<Model>` instead of |
| 1310 | // `Collection<int, Model>` — the single arg should bind to |
| 1311 | // `TValue`/`TModel`, not `TKey`. |
| 1312 | // |
| 1313 | // The heuristic only activates when every skipped leading param |
| 1314 | // has an `array-key` (or `int` / `string`) bound, which is the |
| 1315 | // universal convention for collection key parameters. |
| 1316 | let offset = if type_args.len() < class.template_params.len() { |
| 1317 | let skip = class.template_params.len() - type_args.len(); |
| 1318 | let all_skipped_are_key_like = class.template_params[..skip].iter().all(|param| { |
| 1319 | class |
| 1320 | .template_param_bounds |
| 1321 | .get(param) |
| 1322 | .is_some_and(is_key_like_bound) |
| 1323 | }); |
| 1324 | if all_skipped_are_key_like { skip } else { 0 } |
| 1325 | } else { |
| 1326 | 0 |
| 1327 | }; |
| 1328 | |
| 1329 | let mut subs = HashMap::new(); |
| 1330 | for (i, param_name) in class.template_params.iter().enumerate() { |
| 1331 | if i < offset { |
| 1332 | // Skipped (right-aligned) params: fall back to their |
| 1333 | // declared upper bound or `mixed` so the raw template |
| 1334 | // name never leaks into downstream consumers. |
| 1335 | let fallback = class |
| 1336 | .template_param_bounds |
| 1337 | .get(param_name) |
| 1338 | .cloned() |
| 1339 | .unwrap_or_else(PhpType::mixed); |
| 1340 | subs.insert(param_name.to_string(), fallback); |
| 1341 | continue; |
| 1342 | } |
| 1343 | if let Some(arg) = type_args.get(i - offset) { |
| 1344 | subs.insert(param_name.to_string(), arg.clone()); |
| 1345 | } else { |
| 1346 | // Unbound param (more template params than type args and |
| 1347 | // right-alignment didn't apply): use upper bound or `mixed`. |
| 1348 | let fallback = class |
| 1349 | .template_param_bounds |
| 1350 | .get(param_name) |
| 1351 | .cloned() |
| 1352 | .unwrap_or_else(PhpType::mixed); |
| 1353 | subs.insert(param_name.to_string(), fallback); |
| 1354 | } |
| 1355 | } |
no test coverage detected