Skip to main content

ferroui_build_scan/
model_set.rs

1//! The models of several crates read together: the model of a crate and the
2//! models of the crates it is built on (docs/porting/xaml.md, 9.5.1 and
3//! 9.6.3).
4//!
5//! One crate's model names the types of another by the paths its own sources
6//! spell (`::ferroui_base::Ref`, a re-export), the other crate's model by the
7//! module that declares them (`::ferroui_base::type_system::Ref`). The export
8//! tables of the models ([`AssemblyModel::exports`]) make the two one text
9//! ([`ModelSet::canonical`]), so that a type, a handle or the accessor of a
10//! property is found whichever crate names it.
11
12use std::collections::HashMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15
16use crate::model::{AssemblyModel, RegisteredModel, RegistrationModel, TypeModel};
17
18/// How many times a path is followed through export tables (a crate exports a type of
19/// another crate again) and an accessor through the accessors it is an owner or an alias of.
20const MAX_HOPS: usize = 8;
21
22/// The models of a set of crates, with what is looked up across them.
23pub struct ModelSet {
24    models: Vec<AssemblyModel>,
25    /// Every public path of every model, with the path of the declaring module.
26    exports: HashMap<String, String>,
27    /// The types by the text of their Rust type, the first of a text.
28    types: HashMap<String, (usize, usize)>,
29    /// The accessors of registered properties that are functions of another type than the
30    /// one they are listed under, by their path (`Other::name_property`): the model, the
31    /// type and the position among its registered properties.
32    functions: HashMap<String, (usize, usize, usize)>,
33    /// The type aliases of the models, by their path, with the text of their type.
34    aliases: HashMap<String, String>,
35}
36
37impl ModelSet {
38    /// The set of `models`. Of two types with one Rust type the first is the type.
39    pub fn new(models: Vec<AssemblyModel>) -> Self {
40        let mut exports = HashMap::new();
41        for model in &models {
42            for export in &model.exports {
43                exports.entry(export.path.clone()).or_insert_with(|| export.declared.clone());
44            }
45        }
46        let mut set = Self { models, exports, types: HashMap::new(), functions: HashMap::new(), aliases: HashMap::new() };
47        let mut types = HashMap::new();
48        let mut functions = HashMap::new();
49        let mut aliases = HashMap::new();
50        for (model_index, model) in set.models.iter().enumerate() {
51            for (type_index, type_) in model.types.iter().enumerate() {
52                types.entry(set.canonical(&type_.rust_path.text)).or_insert((model_index, type_index));
53                for (position, registered) in type_.registered.iter().enumerate() {
54                    if let Some(function_of) = &registered.function_of {
55                        let path = format!("{}::{}", set.canonical_path(function_of), registered.accessor);
56                        functions.entry(path).or_insert((model_index, type_index, position));
57                    }
58                }
59            }
60            // An alias whose type has a path the scanner did not resolve stays a name.
61            for alias in model.aliases.iter().filter(|alias| alias.target.is_resolved()) {
62                aliases.entry(set.canonical_path(&alias.path)).or_insert_with(|| set.canonical(&alias.target.text));
63            }
64        }
65        set.types = types;
66        set.functions = functions;
67        set.aliases = aliases;
68        set
69    }
70
71    pub fn models(&self) -> &[AssemblyModel] {
72        &self.models
73    }
74
75    pub fn is_empty(&self) -> bool {
76        self.models.is_empty()
77    }
78
79    /// The type at the position `(model, type)` of the set.
80    pub fn type_at(&self, position: (usize, usize)) -> &TypeModel {
81        &self.models[position.0].types[position.1]
82    }
83
84    /// The path `path` (`::ferroui_base::Border::child_property`) with its longest prefix
85    /// that a model exports replaced by the path of the declaring module; `path` itself
86    /// when no model exports a prefix of it.
87    pub fn canonical_path(&self, path: &str) -> String {
88        let mut current = path.to_string();
89        for _ in 0..MAX_HOPS {
90            match self.declared(&current) {
91                Some(next) if next != current => current = next,
92                _ => break,
93            }
94        }
95        current
96    }
97
98    fn declared(&self, path: &str) -> Option<String> {
99        let mut end = path.len();
100        while end > 0 {
101            if let Some(declared) = self.exports.get(&path[..end]) {
102                return Some(format!("{declared}{}", &path[end..]));
103            }
104            end = path[..end].rfind("::")?;
105        }
106        None
107    }
108
109    /// The normalised type text `text` with every absolute path made canonical
110    /// ([`canonical_path`](Self::canonical_path)): the text the model of the declaring
111    /// crate has for the same type.
112    pub fn canonical(&self, text: &str) -> String {
113        if self.exports.is_empty() {
114            return text.to_string();
115        }
116        self.map_paths(text, &|path| self.canonical_path(path))
117    }
118
119    /// The normalised type text `text` as the one text of its Rust type: canonical
120    /// ([`canonical`](Self::canonical)), with every type alias of the models replaced by
121    /// the type it stands for (`PageList` by `FerroList<Ref<Page>>`). Two spellings of one
122    /// type have one expanded text, which is what a handle is looked up by.
123    pub fn expanded(&self, text: &str) -> String {
124        let mut current = self.canonical(text);
125        if self.aliases.is_empty() {
126            return current;
127        }
128        for _ in 0..MAX_HOPS {
129            let next = self.map_paths(&current, &|path| self.aliases.get(path).cloned().unwrap_or_else(|| path.to_string()));
130            if next == current {
131                break;
132            }
133            current = next;
134        }
135        current
136    }
137
138    /// `text` with every absolute path of it (`::a::b::C`) replaced by what `map` gives.
139    fn map_paths(&self, text: &str, map: &dyn Fn(&str) -> String) -> String {
140        let is_word = |character: char| character.is_alphanumeric() || character == '_';
141        let mut result = String::with_capacity(text.len());
142        let mut rest = text;
143        // The last character written: a path starts at `::` only where nothing it could
144        // continue stands before it (`Vec<T>::Item`, `a::B`).
145        let mut before: Option<char> = None;
146        while !rest.is_empty() {
147            let starts = rest.starts_with("::") && !before.is_some_and(|character| is_word(character) || character == '>');
148            if starts {
149                let mut end = 0;
150                loop {
151                    let tail = &rest[end..];
152                    let Some(name) = tail.strip_prefix("::") else { break };
153                    let length: usize = name.chars().take_while(|character| is_word(*character)).map(char::len_utf8).sum();
154                    if length == 0 {
155                        break;
156                    }
157                    end += 2 + length;
158                }
159                if end > 0 {
160                    result.push_str(&map(&rest[..end]));
161                    before = rest[..end].chars().next_back();
162                    rest = &rest[end..];
163                    continue;
164                }
165            }
166            let Some(character) = rest.chars().next() else { break };
167            result.push(character);
168            before = Some(character);
169            rest = &rest[character.len_utf8()..];
170        }
171        result
172    }
173
174    /// The type with the Rust type text `rust_path`, in any spelling of its paths: its
175    /// position in the set.
176    pub fn position_of_rust_type(&self, rust_path: &str) -> Option<(usize, usize)> {
177        if let Some(found) = self.types.get(rust_path) {
178            return Some(*found);
179        }
180        self.types.get(&self.canonical(rust_path)).copied()
181    }
182
183    /// The type with the Rust type text `rust_path` (its path, for a type a crate
184    /// declares), in whichever model has it, with that model.
185    pub fn find_rust_type(&self, rust_path: &str) -> Option<(&AssemblyModel, &TypeModel)> {
186        let (model, type_) = self.position_of_rust_type(rust_path)?;
187        Some((&self.models[model], &self.models[model].types[type_]))
188    }
189
190    /// The first type with the namespace-qualified name `full_name`, with its model.
191    pub fn find_type(&self, full_name: &str) -> Option<(&AssemblyModel, &TypeModel)> {
192        self.models.iter().find_map(|model| model.find_type(full_name).map(|type_| (model, type_)))
193    }
194
195    /// The accessor of a registered property with the absolute path `path`
196    /// (`::ferroui_base::Decorator::child_property`, in any spelling), with its type.
197    ///
198    /// The path names the type the accessor is a function of, which is the type the
199    /// property is listed under unless the model states another
200    /// ([`RegisteredModel::function_of`]): `ThemeVariant::actual_theme_variant_property` is
201    /// found with `StyledElement`, the type that has the property.
202    pub fn find_accessor(&self, path: &str) -> Option<(&TypeModel, &RegisteredModel)> {
203        let path = self.canonical_path(path);
204        if let Some((model, type_, position)) = self.functions.get(&path) {
205            let type_ = self.type_at((*model, *type_));
206            return Some((type_, &type_.registered[*position]));
207        }
208        let (type_path, accessor) = path.rsplit_once("::")?;
209        let type_ = self.type_at(*self.types.get(type_path)?);
210        type_.registered.iter().find(|registered| registered.accessor == accessor && registered.function_of.is_none()).map(|registered| (type_, registered))
211    }
212
213    /// The path generated code names the type by whose function the accessor `registered`
214    /// of the type `listed` is: the type itself, or the one the model states.
215    pub fn accessor_type_path<'a>(&'a self, listed: &'a TypeModel, registered: &'a RegisteredModel) -> &'a str {
216        match &registered.function_of {
217            Some(function_of) => self.find_rust_type(function_of).map_or(function_of.as_str(), |(_, type_)| type_.named_path()),
218            None => listed.named_path(),
219        }
220    }
221
222    /// The declaration of the property `registered` is an accessor of: itself when it
223    /// declares the property, else the accessor its body calls, followed to the
224    /// declaration through the models. Nothing when an accessor on the way is in no model.
225    pub fn declaration_of<'a>(&'a self, registered: &'a RegisteredModel) -> Option<&'a RegisteredModel> {
226        let mut current = registered;
227        for _ in 0..MAX_HOPS {
228            if current.registration == RegistrationModel::Declared {
229                return Some(current);
230            }
231            let source = current.source.as_ref()?.resolved.as_deref()?;
232            current = self.find_accessor(source)?.1;
233        }
234        None
235    }
236
237    /// The name of the property `registered` is an accessor of: its own, else the one of
238    /// its declaration.
239    pub fn name_of<'a>(&'a self, registered: &'a RegisteredModel) -> Option<&'a str> {
240        match &registered.name {
241            Some(name) => Some(name.as_str()),
242            None => self.declaration_of(registered)?.name.as_deref(),
243        }
244    }
245
246    /// The names the module `module` of a crate of the set gives another crate
247    /// (`::ferroui_base`, `::ferroui_base::media`), each with the path of the declaring
248    /// module: what a glob import of the module brings.
249    pub fn exported_names<'a>(&'a self, module: &'a str) -> impl Iterator<Item = (&'a str, &'a str)> + 'a {
250        self.exports.iter().filter_map(move |(path, declared)| {
251            let name = path.strip_prefix(module)?.strip_prefix("::")?;
252            (!name.contains("::")).then_some((name, declared.as_str()))
253        })
254    }
255
256    /// The models of the `.xamlmeta` files `paths` and, transitively, of the files they
257    /// name (relative to the naming file; `DEP_*` metadata exists for direct dependencies
258    /// only), each assembly once, in the order of `paths`, with every file that was read.
259    /// A file of format 1 is a model without types.
260    pub fn read(paths: &[PathBuf]) -> Result<(Vec<AssemblyModel>, Vec<PathBuf>), String> {
261        let mut models: Vec<AssemblyModel> = Vec::new();
262        let mut files = Vec::new();
263        let mut pending: Vec<PathBuf> = paths.iter().rev().cloned().collect();
264        while let Some(path) = pending.pop() {
265            if files.contains(&path) {
266                continue;
267            }
268            let text = fs::read_to_string(&path).map_err(|error| format!("{}: {error}", path.display()))?;
269            let model = AssemblyModel::parse(&text).map_err(|error| format!("{}: {error}", path.display()))?;
270            let directory = path.parent().map(Path::to_path_buf).unwrap_or_default();
271            files.push(path);
272            if models.iter().any(|known| known.name == model.name) {
273                continue;
274            }
275            for dependency in model.dependencies.iter().rev() {
276                pending.push(directory.join(dependency));
277            }
278            models.push(model);
279        }
280        Ok((models, files))
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::model::{AliasModel, CallableModel, ExportModel, RegisteredKind, RustType, TypeKind};
288
289    fn export(path: &str, declared: &str) -> ExportModel {
290        ExportModel { path: path.to_string(), declared: declared.to_string() }
291    }
292
293    fn registered(name: Option<&str>, accessor: &str, registration: RegistrationModel, source: Option<&str>) -> RegisteredModel {
294        RegisteredModel {
295            name: name.map(str::to_string),
296            kind: RegisteredKind::Styled,
297            value_type: RustType::resolved("f64"),
298            owner: None,
299            host: None,
300            accessor: accessor.to_string(),
301            function_of: None,
302            visibility: "pub".to_string(),
303            registration,
304            source: source.map(|path| CallableModel { path: Some(path.to_string()), resolved: Some(path.to_string()), dereferenced: None }),
305            assign_binding: false,
306            inherits: false,
307            read_only: false,
308            added_owners: Vec::new(),
309        }
310    }
311
312    /// `base` declares `Shape` (re-exported at its root, with the property `Fill`) and
313    /// exports `Rc` of another crate again; `controls` declares `Path`, an owner of `Fill`.
314    fn set() -> ModelSet {
315        let mut base = AssemblyModel::new("Base", "base");
316        base.exports = vec![
317            export("::base::Shape", "::base::shapes::shape::Shape"),
318            export("::base::shapes::Shape", "::base::shapes::shape::Shape"),
319            export("::base::Handle", "::other::rc::Rc"),
320            export("::base::media::Brush", "::base::media::Brush"),
321            export("::base::Pen", "::base::media::pen::Pen"),
322            export("::base::Shapes", "::base::shapes::shape::Shapes"),
323        ];
324        let mut shape = TypeModel::new("Shape", TypeKind::Class, RustType::resolved("::base::shapes::shape::Shape"), "base::shapes::shape");
325        shape.namespace = "Base.Shapes".to_string();
326        // `stroke_property` is a function of `Pen`, a type without a declaration of its own.
327        let mut stroke = registered(Some("Stroke"), "stroke_property", RegistrationModel::Declared, None);
328        stroke.function_of = Some("::base::media::pen::Pen".to_string());
329        shape.registered = vec![registered(Some("Fill"), "fill_property", RegistrationModel::Declared, None), stroke];
330        base.types = vec![shape];
331        base.aliases = vec![
332            AliasModel { path: "::base::shapes::shape::Shapes".to_string(), target: RustType::resolved("Vec<::base::Shape>") },
333            AliasModel { path: "::base::shapes::shape::Figures".to_string(), target: RustType::resolved("Option<::base::shapes::shape::Shapes>") },
334            AliasModel { path: "::base::Lost".to_string(), target: RustType { text: "Vec<Missing>".to_string(), unresolved: vec!["Missing".to_string()] } },
335        ];
336
337        let mut other = AssemblyModel::new("Other", "other");
338        other.exports = vec![export("::other::rc::Rc", "::other::alloc::Rc")];
339
340        let mut controls = AssemblyModel::new("Controls", "controls");
341        let mut path = TypeModel::new("Path", TypeKind::Class, RustType::resolved("::controls::path::Path"), "controls::path");
342        path.registered = vec![
343            registered(None, "fill_property", RegistrationModel::AddedOwner, Some("::base::Shape::fill_property")),
344            registered(None, "paint_property", RegistrationModel::Alias, Some("::controls::path::Path::fill_property")),
345            registered(None, "lost_property", RegistrationModel::AddedOwner, Some("::missing::Type::lost_property")),
346            registered(None, "stroke_property", RegistrationModel::AddedOwner, Some("::base::Pen::stroke_property")),
347        ];
348        controls.types = vec![path];
349        ModelSet::new(vec![base, other, controls])
350    }
351
352    /// Not from upstream: a path is canonical by the longest exported prefix, through the
353    /// crates that export it again; a path nothing exports stays as it is.
354    #[test]
355    fn paths_are_made_canonical_through_the_export_tables() {
356        let set = set();
357        assert_eq!(set.canonical_path("::base::Shape"), "::base::shapes::shape::Shape");
358        assert_eq!(set.canonical_path("::base::shapes::Shape::fill_property"), "::base::shapes::shape::Shape::fill_property");
359        assert_eq!(set.canonical_path("::base::Handle"), "::other::alloc::Rc");
360        assert_eq!(set.canonical_path("::base::media::Brush"), "::base::media::Brush");
361        assert_eq!(set.canonical_path("::base::Unknown"), "::base::Unknown");
362        assert_eq!(set.canonical_path("::std::rc::Rc"), "::std::rc::Rc");
363        assert_eq!(
364            set.canonical("Option<::base::Handle<dyn ::base::Shape>>"),
365            "Option<::other::alloc::Rc<dyn ::base::shapes::shape::Shape>>"
366        );
367        assert_eq!(set.canonical("&'static ::base::Shape"), "&'static ::base::shapes::shape::Shape");
368        assert_eq!(set.canonical("(f64, ::base::shapes::Shape)"), "(f64, ::base::shapes::shape::Shape)");
369        // `::` after a name or `>` continues what is before it.
370        assert_eq!(set.canonical("Vec<f64>::base::Shape"), "Vec<f64>::base::Shape");
371        assert_eq!(set.canonical("String"), "String");
372        assert_eq!(ModelSet::new(Vec::new()).canonical("::base::Shape"), "::base::Shape");
373    }
374
375    /// Not from upstream: a type and the accessor of a property are found by any spelling,
376    /// and an owner added in another crate has the name of the declaration.
377    #[test]
378    fn types_and_accessors_are_found_across_models() {
379        let set = set();
380        let found = set.find_rust_type("::base::Shape").map(|(model, type_)| (model.name.as_str(), type_.name.as_str()));
381        assert_eq!(found, Some(("Base", "Shape")));
382        assert_eq!(set.position_of_rust_type("::base::shapes::shape::Shape"), Some((0, 0)));
383        assert_eq!(set.position_of_rust_type("::controls::path::Path"), Some((2, 0)));
384        assert_eq!(set.position_of_rust_type("::base::Missing"), None);
385        assert_eq!(set.find_type("Base.Shapes.Shape").map(|(_, type_)| type_.name.as_str()), Some("Shape"));
386
387        let path = set.type_at((2, 0));
388        assert_eq!(set.name_of(&path.registered[0]), Some("Fill"));
389        assert_eq!(set.name_of(&path.registered[1]), Some("Fill"));
390        assert_eq!(set.name_of(&path.registered[2]), None);
391        assert_eq!(set.declaration_of(&path.registered[1]).map(|declared| declared.accessor.as_str()), Some("fill_property"));
392        assert_eq!(set.find_accessor("::base::Shape::fill_property").map(|(type_, _)| type_.name.as_str()), Some("Shape"));
393        // An accessor that is a function of another type is found by the path of that type,
394        // and not by the path of the type it is listed under.
395        assert_eq!(set.name_of(&path.registered[3]), Some("Stroke"));
396        let stroke = set.find_accessor("::base::Pen::stroke_property").map(|(type_, registered)| (type_.name.as_str(), registered.accessor.as_str()));
397        assert_eq!(stroke, Some(("Shape", "stroke_property")));
398        assert!(set.find_accessor("::base::Shape::stroke_property").is_none());
399        let shape = set.type_at((0, 0));
400        assert_eq!(set.accessor_type_path(shape, &shape.registered[0]), "::base::shapes::shape::Shape");
401        assert_eq!(set.accessor_type_path(shape, &shape.registered[1]), "::base::media::pen::Pen");
402
403        let mut names: Vec<(&str, &str)> = set.exported_names("::base").collect();
404        names.sort();
405        assert_eq!(names, vec![("Handle", "::other::rc::Rc"), ("Pen", "::base::media::pen::Pen"), ("Shape", "::base::shapes::shape::Shape"), ("Shapes", "::base::shapes::shape::Shapes")]);
406    }
407
408    /// Not from upstream: an alias is replaced by its type, in any spelling of its path
409    /// and through other aliases; an alias the scanner did not resolve stays a name.
410    #[test]
411    fn aliases_are_expanded_to_the_type_they_stand_for() {
412        let set = set();
413        const SHAPE: &str = "::base::shapes::shape::Shape";
414        assert_eq!(set.expanded("::base::Shapes"), format!("Vec<{SHAPE}>"));
415        assert_eq!(set.expanded("Option<::base::shapes::shape::Shapes>"), format!("Option<Vec<{SHAPE}>>"));
416        assert_eq!(set.expanded("::base::shapes::shape::Figures"), format!("Option<Vec<{SHAPE}>>"));
417        assert_eq!(set.expanded("::base::Shape"), SHAPE);
418        assert_eq!(set.expanded("::base::Lost"), "::base::Lost");
419        assert_eq!(set.expanded("f64"), "f64");
420        assert_eq!(set.canonical("::base::Shapes"), "::base::shapes::shape::Shapes");
421    }
422}