This mirrors [`crate::inheritance::build_substitution_map`] but is scoped to the virtual-member provider so it does not need to be public on the inheritance module.
(
current: &ClassInfo,
parent: &ClassInfo,
active_subs: &HashMap<String, PhpType>,
)
| 953 | /// scoped to the virtual-member provider so it does not need to be public |
| 954 | /// on the inheritance module. |
| 955 | fn build_mixin_substitution_map( |
| 956 | current: &ClassInfo, |
| 957 | parent: &ClassInfo, |
| 958 | active_subs: &HashMap<String, PhpType>, |
| 959 | ) -> HashMap<String, PhpType> { |
| 960 | if parent.template_params.is_empty() { |
| 961 | return active_subs.clone(); |
| 962 | } |
| 963 | |
| 964 | let parent_short = short_name(&parent.name); |
| 965 | |
| 966 | // Find `@extends`/`@implements` generics matching this parent. |
| 967 | let type_args = current |
| 968 | .extends_generics |
| 969 | .iter() |
| 970 | .chain(current.implements_generics.iter()) |
| 971 | .find(|(name, _)| { |
| 972 | let name_short = short_name(name); |
| 973 | name_short == parent_short |
| 974 | }) |
| 975 | .map(|(_, args)| args); |
| 976 | |
| 977 | let type_args = match type_args { |
| 978 | Some(args) => args, |
| 979 | None => return active_subs.clone(), |
| 980 | }; |
| 981 | |
| 982 | // Check whether the parent has any @mixin whose name is itself a |
| 983 | // template parameter (e.g. `@mixin TNode` on a class with |
| 984 | // `@template TNode`). When this is the case and a substitution |
| 985 | // still resolves to a raw template parameter name on the child |
| 986 | // class, we fall back to the template bound. This handles the |
| 987 | // PHPMD pattern where `AbstractNode<TNode>` has `@mixin TNode` |
| 988 | // and `ASTNode extends AbstractNode<TNode>` — without the |
| 989 | // fallback, `TNode` stays as an unresolvable class name. |
| 990 | // |
| 991 | // We do NOT apply this fallback when the mixin is a concrete |
| 992 | // class with template arguments (e.g. `@mixin Builder<TModel>`), |
| 993 | // because the template param may be resolved later by a concrete |
| 994 | // caller through the generic substitution chain. |
| 995 | let parent_has_template_param_mixin = parent.mixins.iter().any(|m| { |
| 996 | parent |
| 997 | .template_params |
| 998 | .iter() |
| 999 | .any(|t| t.as_str() == m.as_str()) |
| 1000 | }); |
| 1001 | |
| 1002 | let mut map = HashMap::new(); |
| 1003 | for (i, param_name) in parent.template_params.iter().enumerate() { |
| 1004 | if let Some(arg) = type_args.get(i) { |
| 1005 | let mut resolved = if active_subs.is_empty() { |
| 1006 | arg.clone() |
| 1007 | } else { |
| 1008 | arg.substitute(active_subs) |
| 1009 | }; |
| 1010 | |
| 1011 | // Fall back to the template bound only when the parent |
| 1012 | // uses the template param directly as a mixin name. |