| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | |
| 4 | use crate::Cli; |
| 5 | use by_address::ByAddress; |
| 6 | use i_slint_compiler::{ |
| 7 | expression_tree::{Callable, Expression}, |
| 8 | langtype::Type, |
| 9 | lookup::{LookupCtx, LookupObject, LookupResult, LookupResultCallable}, |
| 10 | namedreference::NamedReference, |
| 11 | object_tree::ElementRc, |
| 12 | parser::{SyntaxKind, SyntaxNode}, |
| 13 | }; |
| 14 | use smol_str::{format_smolstr, SmolStr}; |
| 15 | use std::{ |
| 16 | cell::RefCell, |
| 17 | collections::{HashMap, HashSet}, |
| 18 | io::Write, |
| 19 | rc::Rc, |
| 20 | }; |
| 21 | |
| 22 | #[derive (Clone, Default)] |
| 23 | pub struct LookupChangeState { |
| 24 | /// the lookup change pass will map that property reference to another |
| 25 | property_mappings: HashMap<NamedReference, String>, |
| 26 | |
| 27 | /// What needs to be added before the closing brace of the component |
| 28 | extra_component_stuff: Rc<RefCell<Vec<u8>>>, |
| 29 | |
| 30 | /// Replace `self.` with that id |
| 31 | replace_self: Option<SmolStr>, |
| 32 | |
| 33 | /// Elements that should get an id |
| 34 | elements_id: HashMap<ByAddress<ElementRc>, SmolStr>, |
| 35 | |
| 36 | /// the full lookup scope |
| 37 | pub scope: Vec<ElementRc>, |
| 38 | } |
| 39 | |
| 40 | pub(crate) fn fold_node( |
| 41 | node: &SyntaxNode, |
| 42 | file: &mut impl Write, |
| 43 | state: &mut crate::State, |
| 44 | args: &Cli, |
| 45 | ) -> std::io::Result<bool> { |
| 46 | let kind = node.kind(); |
| 47 | if kind == SyntaxKind::QualifiedName |
| 48 | && node.parent().is_some_and(|n| n.kind() == SyntaxKind::Expression) |
| 49 | { |
| 50 | return fully_qualify_property_access(node, file, state); |
| 51 | } else if kind == SyntaxKind::Element |
| 52 | && node.parent().is_some_and(|n| n.kind() == SyntaxKind::Component) |
| 53 | { |
| 54 | return move_properties_to_root(node, state, file, args); |
| 55 | } else if kind == SyntaxKind::Element { |
| 56 | if let Some(new_id) = state |
| 57 | .current_elem |
| 58 | .as_ref() |
| 59 | .and_then(|e| state.lookup_change.elements_id.get(&ByAddress(e.clone()))) |
| 60 | { |
| 61 | write!(file, " {new_id} := " )?; |
| 62 | } |
| 63 | } else if matches!( |
| 64 | kind, |
| 65 | SyntaxKind::Binding | SyntaxKind::TwoWayBinding | SyntaxKind::CallbackConnection |
| 66 | ) && !state.lookup_change.property_mappings.is_empty() |
| 67 | && node.parent().is_some_and(|n| n.kind() == SyntaxKind::Element) |
| 68 | { |
| 69 | if let Some(el) = &state.current_elem { |
| 70 | let prop_name = i_slint_compiler::parser::normalize_identifier( |
| 71 | node.child_token(SyntaxKind::Identifier).unwrap().text(), |
| 72 | ); |
| 73 | let nr = NamedReference::new(el, prop_name); |
| 74 | if let Some(new_name) = state.lookup_change.property_mappings.get(&nr).cloned() { |
| 75 | state.lookup_change.replace_self = Some( |
| 76 | state |
| 77 | .lookup_change |
| 78 | .elements_id |
| 79 | .get(&ByAddress(el.clone())) |
| 80 | .map_or_else(|| el.borrow().id.clone(), |s| s.clone()), |
| 81 | ); |
| 82 | for n in node.children_with_tokens() { |
| 83 | let extra = state.lookup_change.extra_component_stuff.clone(); |
| 84 | if n.kind() == SyntaxKind::Identifier { |
| 85 | write!(&mut *extra.borrow_mut(), " {new_name}" )?; |
| 86 | } else { |
| 87 | crate::visit_node_or_token(n, &mut *extra.borrow_mut(), state, args)?; |
| 88 | } |
| 89 | } |
| 90 | state.lookup_change.replace_self = None; |
| 91 | return Ok(true); |
| 92 | } |
| 93 | } |
| 94 | } else if matches!(kind, SyntaxKind::PropertyDeclaration | SyntaxKind::CallbackDeclaration) { |
| 95 | if let Some(el) = &state.current_elem { |
| 96 | let prop_name = i_slint_compiler::parser::normalize_identifier( |
| 97 | &node.child_node(SyntaxKind::DeclaredIdentifier).unwrap().text().to_string(), |
| 98 | ); |
| 99 | let nr = NamedReference::new(el, prop_name); |
| 100 | if state.lookup_change.property_mappings.contains_key(&nr) { |
| 101 | return Ok(true); |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | Ok(false) |
| 106 | } |
| 107 | |
| 108 | /// Make sure that a qualified name is fully qualified with `self.`. |
| 109 | /// Also rename the property in `state.lookup_change.property_mappings` |
| 110 | fn fully_qualify_property_access( |
| 111 | node: &SyntaxNode, |
| 112 | file: &mut impl Write, |
| 113 | state: &mut crate::State, |
| 114 | ) -> std::io::Result<bool> { |
| 115 | let mut it = node |
| 116 | .children_with_tokens() |
| 117 | .filter(|n| n.kind() == SyntaxKind::Identifier) |
| 118 | .filter_map(|n| n.into_token()); |
| 119 | let first = match it.next() { |
| 120 | Some(first) => first, |
| 121 | None => return Ok(false), |
| 122 | }; |
| 123 | let first_str = i_slint_compiler::parser::normalize_identifier(first.text()); |
| 124 | with_lookup_ctx(state, |ctx| -> std::io::Result<bool> { |
| 125 | ctx.current_token = Some(first.clone().into()); |
| 126 | let global_lookup = i_slint_compiler::lookup::global_lookup(); |
| 127 | match global_lookup.lookup(ctx, &first_str) { |
| 128 | Some( |
| 129 | LookupResult::Expression { expression: Expression::PropertyReference(nr), .. } |
| 130 | | LookupResult::Callable(LookupResultCallable::Callable( |
| 131 | Callable::Callback(nr) | Callable::Function(nr), |
| 132 | )), |
| 133 | ) => { |
| 134 | if let Some(new_name) = state.lookup_change.property_mappings.get(&nr) { |
| 135 | write!(file, "root. {new_name} " )?; |
| 136 | Ok(true) |
| 137 | } else { |
| 138 | let element = nr.element(); |
| 139 | if state |
| 140 | .current_component |
| 141 | .as_ref() |
| 142 | .is_some_and(|c| Rc::ptr_eq(&element, &c.root_element)) |
| 143 | { |
| 144 | write!(file, "root." )?; |
| 145 | } else if state |
| 146 | .lookup_change |
| 147 | .scope |
| 148 | .last() |
| 149 | .is_some_and(|e| Rc::ptr_eq(&element, e)) |
| 150 | { |
| 151 | if let Some(replace_self) = &state.lookup_change.replace_self { |
| 152 | write!(file, " {replace_self}." )?; |
| 153 | } else { |
| 154 | write!(file, "self." )?; |
| 155 | } |
| 156 | } |
| 157 | Ok(false) |
| 158 | } |
| 159 | } |
| 160 | Some(LookupResult::Expression { |
| 161 | expression: Expression::ElementReference(el), .. |
| 162 | }) => { |
| 163 | let second = match it.next() { |
| 164 | Some(second) => second, |
| 165 | None => return Ok(false), |
| 166 | }; |
| 167 | let prop_name = i_slint_compiler::parser::normalize_identifier(second.text()); |
| 168 | let nr = NamedReference::new(&el.upgrade().unwrap(), prop_name); |
| 169 | if let Some(new_name) = state.lookup_change.property_mappings.get(&nr) { |
| 170 | write!(file, "root. {new_name} " )?; |
| 171 | Ok(true) |
| 172 | } else if let Some(replace_self) = &state.lookup_change.replace_self { |
| 173 | if first_str == "self" || first_str == "parent" { |
| 174 | if first_str == "self" { |
| 175 | write!(file, " {replace_self}. {second} " )?; |
| 176 | } else { |
| 177 | let replace_parent = state |
| 178 | .lookup_change |
| 179 | .elements_id |
| 180 | .get(&ByAddress(nr.element())) |
| 181 | .map_or_else(|| nr.element().borrow().id.clone(), |s| s.clone()); |
| 182 | write!(file, " {replace_parent}. {second} " )?; |
| 183 | } |
| 184 | for t in it { |
| 185 | write!(file, ". {} " , t.text())?; |
| 186 | } |
| 187 | Ok(true) |
| 188 | } else { |
| 189 | Ok(false) |
| 190 | } |
| 191 | } else { |
| 192 | Ok(false) |
| 193 | } |
| 194 | } |
| 195 | _ => Ok(false), |
| 196 | } |
| 197 | }) |
| 198 | .unwrap_or(Ok(false)) |
| 199 | } |
| 200 | |
| 201 | /// Move the properties from state.lookup_change.property_mappings to the root |
| 202 | fn move_properties_to_root( |
| 203 | node: &SyntaxNode, |
| 204 | state: &mut crate::State, |
| 205 | file: &mut impl Write, |
| 206 | args: &Cli, |
| 207 | ) -> std::io::Result<bool> { |
| 208 | if state.lookup_change.property_mappings.is_empty() { |
| 209 | return Ok(false); |
| 210 | } |
| 211 | let mut seen_brace = false; |
| 212 | for c in node.children_with_tokens() { |
| 213 | let k = c.kind(); |
| 214 | if k == SyntaxKind::LBrace { |
| 215 | seen_brace = true; |
| 216 | } else if seen_brace && k != SyntaxKind::Whitespace { |
| 217 | let property_mappings = state.lookup_change.property_mappings.clone(); |
| 218 | for (nr, prop) in property_mappings.iter() { |
| 219 | let decl = |
| 220 | nr.element().borrow().property_declarations.get(nr.name()).unwrap().clone(); |
| 221 | let n2: SyntaxNode = decl.node.clone().unwrap(); |
| 222 | let old_current_element = |
| 223 | std::mem::replace(&mut state.current_elem, Some(nr.element())); |
| 224 | state.lookup_change.replace_self = Some( |
| 225 | state |
| 226 | .lookup_change |
| 227 | .elements_id |
| 228 | .get(&ByAddress(nr.element())) |
| 229 | .map_or_else(|| nr.element().borrow().id.clone(), |s| s.clone()), |
| 230 | ); |
| 231 | for c2 in n2.children_with_tokens() { |
| 232 | if c2.kind() == SyntaxKind::DeclaredIdentifier { |
| 233 | write!(file, " {prop} " )?; |
| 234 | } else { |
| 235 | crate::visit_node_or_token(c2, file, state, args)?; |
| 236 | } |
| 237 | } |
| 238 | state.lookup_change.replace_self = None; |
| 239 | write!(file, " \n " )?; |
| 240 | state.current_elem = old_current_element; |
| 241 | } |
| 242 | seen_brace = false; |
| 243 | } |
| 244 | |
| 245 | if k == SyntaxKind::RBrace { |
| 246 | file.write_all(&*std::mem::take( |
| 247 | &mut *state.lookup_change.extra_component_stuff.borrow_mut(), |
| 248 | ))?; |
| 249 | } |
| 250 | crate::visit_node_or_token(c, file, state, args)?; |
| 251 | } |
| 252 | |
| 253 | Ok(true) |
| 254 | } |
| 255 | |
| 256 | pub(crate) fn with_lookup_ctx<R>( |
| 257 | state: &crate::State, |
| 258 | f: impl FnOnce(&mut LookupCtx) -> R, |
| 259 | ) -> Option<R> { |
| 260 | let mut build_diagnostics = Default::default(); |
| 261 | let tr: &TypeRegister = &state.current_doc.as_ref()?.local_registry; |
| 262 | let mut lookup_context: LookupCtx<'_> = LookupCtx::empty_context(type_register:tr, &mut build_diagnostics); |
| 263 | |
| 264 | let ty: Type = state |
| 265 | .current_elem |
| 266 | .as_ref() |
| 267 | .zip(state.property_name.as_ref()) |
| 268 | .map_or(default:Type::Invalid, |(e: &Rc>, n: &SmolStr)| e.borrow().lookup_property(name:n).property_type); |
| 269 | |
| 270 | lookup_context.property_name = state.property_name.as_ref().map(SmolStr::as_str); |
| 271 | lookup_context.property_type = ty; |
| 272 | lookup_context.component_scope = &state.lookup_change.scope; |
| 273 | Some(f(&mut lookup_context)) |
| 274 | } |
| 275 | |
| 276 | pub(crate) fn collect_movable_properties(state: &mut crate::State) { |
| 277 | pub fn collect_movable_properties_recursive(vec: &mut Vec<NamedReference>, elem: &ElementRc) { |
| 278 | for c in &elem.borrow().children { |
| 279 | if c.borrow().repeated.is_some() { |
| 280 | continue; |
| 281 | } |
| 282 | vec.extend( |
| 283 | c.borrow() |
| 284 | .property_declarations |
| 285 | .iter() |
| 286 | .map(|(name, _)| NamedReference::new(c, name.clone())), |
| 287 | ); |
| 288 | collect_movable_properties_recursive(vec, c); |
| 289 | } |
| 290 | } |
| 291 | if let Some(c) = &state.current_component { |
| 292 | let mut props_to_move = Vec::new(); |
| 293 | collect_movable_properties_recursive(&mut props_to_move, &c.root_element); |
| 294 | let mut seen = HashSet::new(); |
| 295 | state.lookup_change.property_mappings = props_to_move |
| 296 | .into_iter() |
| 297 | .map(|nr| { |
| 298 | let element = nr.element(); |
| 299 | ensure_element_has_id(&element, &mut state.lookup_change.elements_id); |
| 300 | if let Some(parent) = i_slint_compiler::object_tree::find_parent_element(&element) { |
| 301 | ensure_element_has_id(&parent, &mut state.lookup_change.elements_id); |
| 302 | } |
| 303 | let mut name = format!("_ {}_ {}" , element.borrow().id, nr.name()); |
| 304 | while !seen.insert(name.clone()) |
| 305 | || c.root_element.borrow().lookup_property(&name).is_valid() |
| 306 | { |
| 307 | name = format!("_ {name}" ); |
| 308 | } |
| 309 | (nr, name) |
| 310 | }) |
| 311 | .collect() |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | fn ensure_element_has_id( |
| 316 | element: &ElementRc, |
| 317 | elements_id: &mut HashMap<ByAddress<ElementRc>, SmolStr>, |
| 318 | ) { |
| 319 | static ID_GEN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); |
| 320 | if element.borrow().id.is_empty() { |
| 321 | elements_id.entry(ByAddress(element.clone())).or_insert_with(|| { |
| 322 | format_smolstr!("_ {}" , ID_GEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) |
| 323 | }); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | pub(crate) fn enter_element(state: &mut crate::State) { |
| 328 | if state.lookup_change.scope.last().is_some_and(|e: &Rc>| e.borrow().base_type.to_string() == "Path" ) |
| 329 | { |
| 330 | // Path's sub-elements have strange lookup rules: They are considering self as the Path |
| 331 | state.lookup_change.replace_self = Some("parent" .into()); |
| 332 | } else { |
| 333 | state.lookup_change.scope.extend(iter:state.current_elem.iter().cloned()) |
| 334 | } |
| 335 | } |
| 336 | |