|
4 | 4 | //! parameter types, descriptions), this module propagates richer type information |
5 | 5 | //! from ancestors through inheritance chains. |
6 | 6 |
|
| 7 | +use std::sync::Arc; |
| 8 | + |
7 | 9 | use crate::php_type::PhpType; |
8 | 10 | use crate::types::{MethodInfo, ParameterInfo, PropertyInfo}; |
9 | 11 |
|
@@ -45,6 +47,116 @@ fn ancestor_has_richer_type(effective: &Option<PhpType>, native: &Option<PhpType |
45 | 47 | } |
46 | 48 | } |
47 | 49 |
|
| 50 | +/// Copy-on-write wrapper around [`enrich_method_from_ancestor`]. |
| 51 | +/// |
| 52 | +/// Most class/ancestor method overlaps have nothing to propagate (the |
| 53 | +/// child already carries its own docblock types, or the ancestor has |
| 54 | +/// nothing richer to offer). Checking first means the shared |
| 55 | +/// `Arc<MethodInfo>` is only deep-cloned when the enrichment actually |
| 56 | +/// changes a field, instead of once per overlap per class. |
| 57 | +pub(crate) fn enrich_method_arc_from_ancestor( |
| 58 | + existing: &mut Arc<MethodInfo>, |
| 59 | + ancestor: &MethodInfo, |
| 60 | +) { |
| 61 | + if method_enrichment_would_change(existing, ancestor) { |
| 62 | + enrich_method_from_ancestor(Arc::make_mut(existing), ancestor); |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +/// Whether [`enrich_method_from_ancestor`] would change any field of |
| 67 | +/// `existing`. |
| 68 | +/// |
| 69 | +/// Mirrors the apply function's conditions exactly (keep the two in |
| 70 | +/// sync), with one refinement: a copy that would overwrite a field with |
| 71 | +/// an equal value counts as "no change". A `false` here guarantees the |
| 72 | +/// enrichment is semantically a no-op, so the caller can skip the |
| 73 | +/// copy-on-write clone. |
| 74 | +fn method_enrichment_would_change(existing: &MethodInfo, ancestor: &MethodInfo) -> bool { |
| 75 | + // Return type. |
| 76 | + if (existing.return_type.is_none() && ancestor.return_type.is_some() |
| 77 | + || lacks_docblock_override(&existing.return_type, &existing.native_return_type) |
| 78 | + && ancestor_has_richer_type(&ancestor.return_type, &ancestor.native_return_type)) |
| 79 | + && existing.return_type != ancestor.return_type |
| 80 | + { |
| 81 | + return true; |
| 82 | + } |
| 83 | + |
| 84 | + // Template parameters (copies bounds and bindings alongside). |
| 85 | + if existing.template_params.is_empty() && !ancestor.template_params.is_empty() { |
| 86 | + return true; |
| 87 | + } |
| 88 | + |
| 89 | + // Conditional return type. |
| 90 | + if existing.conditional_return.is_none() && ancestor.conditional_return.is_some() { |
| 91 | + return true; |
| 92 | + } |
| 93 | + |
| 94 | + // Type assertions. |
| 95 | + if existing.type_assertions.is_empty() && !ancestor.type_assertions.is_empty() { |
| 96 | + return true; |
| 97 | + } |
| 98 | + |
| 99 | + // Parameters (constructors match by name, everything else by position). |
| 100 | + let params_would_change = if existing.name == "__construct" { |
| 101 | + constructor_parameters_would_change(&existing.parameters, &ancestor.parameters) |
| 102 | + } else { |
| 103 | + positional_parameters_would_change(&existing.parameters, &ancestor.parameters) |
| 104 | + }; |
| 105 | + if params_would_change { |
| 106 | + return true; |
| 107 | + } |
| 108 | + |
| 109 | + // Descriptions. |
| 110 | + (existing.description.is_none() && ancestor.description.is_some()) |
| 111 | + || (existing.return_description.is_none() && ancestor.return_description.is_some()) |
| 112 | +} |
| 113 | + |
| 114 | +/// Whether [`enrich_constructor_parameters_by_name`] would change any |
| 115 | +/// parameter. |
| 116 | +fn constructor_parameters_would_change( |
| 117 | + existing_params: &[ParameterInfo], |
| 118 | + ancestor_params: &[ParameterInfo], |
| 119 | +) -> bool { |
| 120 | + existing_params.iter().any(|ep| { |
| 121 | + ancestor_params |
| 122 | + .iter() |
| 123 | + .find(|ap| ap.name == ep.name) |
| 124 | + .is_some_and(|ap| parameter_enrichment_would_change(ep, ap)) |
| 125 | + }) |
| 126 | +} |
| 127 | + |
| 128 | +/// Whether [`enrich_parameters_from_ancestor`] would change any |
| 129 | +/// parameter. |
| 130 | +fn positional_parameters_would_change( |
| 131 | + existing_params: &[ParameterInfo], |
| 132 | + ancestor_params: &[ParameterInfo], |
| 133 | +) -> bool { |
| 134 | + existing_params |
| 135 | + .iter() |
| 136 | + .zip(ancestor_params) |
| 137 | + .any(|(ep, ap)| parameter_enrichment_would_change(ep, ap)) |
| 138 | +} |
| 139 | + |
| 140 | +/// Whether [`enrich_single_parameter`] would change `existing_param`. |
| 141 | +/// |
| 142 | +/// Mirrors the apply function's conditions (keep the two in sync). |
| 143 | +fn parameter_enrichment_would_change( |
| 144 | + existing_param: &ParameterInfo, |
| 145 | + ancestor_param: &ParameterInfo, |
| 146 | +) -> bool { |
| 147 | + let child_has_specific_native = existing_param.native_type_hint.as_ref().is_some_and(|nt| { |
| 148 | + !nt.is_object() && !nt.is_mixed() && !nt.is_array_like() && !nt.is_iterable() |
| 149 | + }); |
| 150 | + if !child_has_specific_native |
| 151 | + && lacks_docblock_override(&existing_param.type_hint, &existing_param.native_type_hint) |
| 152 | + && ancestor_has_richer_type(&ancestor_param.type_hint, &ancestor_param.native_type_hint) |
| 153 | + && existing_param.type_hint != ancestor_param.type_hint |
| 154 | + { |
| 155 | + return true; |
| 156 | + } |
| 157 | + existing_param.description.is_none() && ancestor_param.description.is_some() |
| 158 | +} |
| 159 | + |
48 | 160 | /// Enrich a child method with docblock information from an ancestor method. |
49 | 161 | /// |
50 | 162 | /// Propagates return types, parameter types, descriptions, template |
@@ -110,10 +222,17 @@ pub(crate) fn enrich_method_from_ancestor(existing: &mut MethodInfo, ancestor: & |
110 | 222 | // This follows PHPStan's `PhpDocInheritanceResolver`: for |
111 | 223 | // `__construct` the positional parameter name list falls back to |
112 | 224 | // the child's own names, so only same-named parameters inherit. |
| 225 | + // The would-change pre-check keeps the shared parameter list intact |
| 226 | + // (no copy-on-write) when there is nothing to propagate. |
113 | 227 | if existing.name == "__construct" { |
114 | | - enrich_constructor_parameters_by_name(&mut existing.parameters, &ancestor.parameters); |
115 | | - } else { |
116 | | - enrich_parameters_from_ancestor(&mut existing.parameters, &ancestor.parameters); |
| 228 | + if constructor_parameters_would_change(&existing.parameters, &ancestor.parameters) { |
| 229 | + enrich_constructor_parameters_by_name( |
| 230 | + existing.parameters.make_mut(), |
| 231 | + &ancestor.parameters, |
| 232 | + ); |
| 233 | + } |
| 234 | + } else if positional_parameters_would_change(&existing.parameters, &ancestor.parameters) { |
| 235 | + enrich_parameters_from_ancestor(existing.parameters.make_mut(), &ancestor.parameters); |
117 | 236 | } |
118 | 237 |
|
119 | 238 | // ── Descriptions ──────────────────────────────────────────── |
|
0 commit comments