1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3
4use std::cell::RefCell;
5use std::collections::{HashMap, HashSet};
6use std::path::{Path, PathBuf};
7use std::rc::Rc;
8
9use crate::diagnostics::{BuildDiagnostics, SourceFileVersion, Spanned};
10use crate::object_tree::{self, Document, ExportedName, Exports};
11use crate::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxToken};
12use crate::typeregister::TypeRegister;
13use crate::CompilerConfiguration;
14use crate::{fileaccess, parser};
15use core::future::Future;
16use itertools::Itertools;
17
18/// Storage for a cache of all loaded documents
19#[derive(Default)]
20struct LoadedDocuments {
21 /// maps from the canonical file name to the object_tree::Document
22 docs: HashMap<PathBuf, Document>,
23 /// The .slint files that are currently being loaded, potentially asynchronously.
24 /// When a task start loading a file, it will add an empty vector to this map, and
25 /// the same task will remove the entry from the map when finished, and awake all
26 /// wakers.
27 currently_loading: HashMap<PathBuf, Vec<std::task::Waker>>,
28}
29
30pub enum ImportKind {
31 ImportList(syntax_nodes::ImportSpecifier),
32 ModuleReexport(syntax_nodes::ExportModule), // re-export all types, as per export * from "foo".
33}
34
35pub struct ImportedTypes {
36 pub import_uri_token: SyntaxToken,
37 pub import_kind: ImportKind,
38 pub file: String,
39}
40
41#[derive(Debug)]
42pub struct ImportedName {
43 // name of export to match in the other file
44 pub external_name: String,
45 // name to be used locally
46 pub internal_name: String,
47}
48
49impl ImportedName {
50 pub fn extract_imported_names(
51 import: &syntax_nodes::ImportSpecifier,
52 ) -> impl Iterator<Item = ImportedName> + '_ {
53 import.ImportIdentifierList().into_iter().flat_map(|import_identifiers: ImportIdentifierList| {
54 import_identifiers.ImportIdentifier().map(Self::from_node)
55 })
56 }
57
58 pub fn from_node(importident: syntax_nodes::ImportIdentifier) -> Self {
59 let external_name: String =
60 parser::normalize_identifier(ident:importident.ExternalName().text().to_string().trim());
61
62 let internal_name: String = match importident.InternalName() {
63 Some(name_ident: InternalName) => parser::normalize_identifier(ident:name_ident.text().to_string().trim()),
64 None => external_name.clone(),
65 };
66
67 ImportedName { internal_name, external_name }
68 }
69}
70
71pub struct TypeLoader {
72 pub global_type_registry: Rc<RefCell<TypeRegister>>,
73 pub compiler_config: CompilerConfiguration,
74 style: String,
75 all_documents: LoadedDocuments,
76}
77
78struct BorrowedTypeLoader<'a> {
79 tl: &'a mut TypeLoader,
80 diag: &'a mut BuildDiagnostics,
81}
82
83impl TypeLoader {
84 pub fn new(
85 global_type_registry: Rc<RefCell<TypeRegister>>,
86 compiler_config: CompilerConfiguration,
87 diag: &mut BuildDiagnostics,
88 ) -> Self {
89 let mut style = compiler_config
90 .style
91 .clone()
92 .or_else(|| std::env::var("SLINT_STYLE").ok())
93 .unwrap_or_else(|| "native".into());
94
95 if style == "native" {
96 style = get_native_style(&mut diag.all_loaded_files);
97 }
98
99 let myself = Self {
100 global_type_registry,
101 compiler_config,
102 style: style.clone(),
103 all_documents: Default::default(),
104 };
105
106 let mut known_styles = fileaccess::styles();
107 known_styles.push("native");
108 if !known_styles.contains(&style.as_ref())
109 && myself
110 .find_file_in_include_path(None, &format!("{}/std-widgets.slint", style))
111 .is_none()
112 {
113 diag.push_diagnostic_with_span(
114 format!(
115 "Style {} in not known. Use one of the builtin styles [{}] or make sure your custom style is found in the include directories",
116 &style,
117 known_styles.join(", ")
118 ),
119 Default::default(),
120 crate::diagnostics::DiagnosticLevel::Error,
121 );
122 }
123
124 myself
125 }
126
127 /// Imports of files that don't have the .slint extension are returned.
128 pub async fn load_dependencies_recursively<'a>(
129 &'a mut self,
130 doc: &'a syntax_nodes::Document,
131 diag: &'a mut BuildDiagnostics,
132 registry_to_populate: &'a Rc<RefCell<TypeRegister>>,
133 ) -> (Vec<ImportedTypes>, Exports) {
134 let state = RefCell::new(BorrowedTypeLoader { tl: self, diag });
135 Self::load_dependencies_recursively_impl(
136 &state,
137 doc,
138 registry_to_populate,
139 &Default::default(),
140 )
141 .await
142 }
143
144 fn load_dependencies_recursively_impl<'a: 'b, 'b>(
145 state: &'a RefCell<BorrowedTypeLoader<'a>>,
146 doc: &'b syntax_nodes::Document,
147 registry_to_populate: &'b Rc<RefCell<TypeRegister>>,
148 import_stack: &'b HashSet<PathBuf>,
149 ) -> core::pin::Pin<Box<dyn std::future::Future<Output = (Vec<ImportedTypes>, Exports)> + 'b>>
150 {
151 let mut foreign_imports = vec![];
152 let mut dependencies = Self::collect_dependencies(state, doc)
153 .filter_map(|mut import| {
154 let resolved_import = if let Some((path, _)) = state.borrow().tl.resolve_import_path(Some(&import.import_uri_token.clone().into()), &import.file) {
155 path.to_string_lossy().to_string()
156 } else {
157 import.file.clone()
158 };
159 if resolved_import.ends_with(".slint") || resolved_import.ends_with(".60") || import.file.starts_with('@') {
160 Some(Box::pin(async move {
161 let file = import.file.as_str();
162 let doc_path = Self::ensure_document_loaded(
163 state,
164 file,
165 Some(import.import_uri_token.clone().into()),
166 import_stack.clone()
167 )
168 .await?;
169 let mut state = state.borrow_mut();
170 let state = &mut *state;
171 let doc = state.tl.all_documents.docs.get(&doc_path).unwrap();
172 match &import.import_kind {
173 ImportKind::ImportList(imported_types) => {
174 let mut imported_types =
175 ImportedName::extract_imported_names(imported_types).peekable();
176 if imported_types.peek().is_some() {
177 Self::register_imported_types(doc, &import, imported_types, registry_to_populate, state.diag);
178 } else {
179 state.diag.push_error("Import names are missing. Please specify which types you would like to import".into(), &import.import_uri_token.parent());
180 }
181 None
182 }
183 ImportKind::ModuleReexport(export_module_syntax_node) => {
184 let mut exports = Exports::default();
185 exports.add_reexports(
186 doc.exports.iter().map(|(exported_name, compo_or_type)| {
187 (
188 ExportedName {
189 name: exported_name.name.clone(),
190 name_ident: (**export_module_syntax_node).clone(),
191 },
192 compo_or_type.clone(),
193 )
194 }),
195 state.diag,
196 );
197 Some((exports, export_module_syntax_node.clone()))
198 }
199 }
200 }))
201 } else {
202 import.file = resolved_import;
203 foreign_imports.push(import);
204 None
205 }
206 })
207 .collect::<Vec<_>>();
208
209 Box::pin(async move {
210 let mut reexports = None;
211 std::future::poll_fn(|cx| {
212 dependencies.retain_mut(|fut| {
213 let core::task::Poll::Ready(export) = fut.as_mut().poll(cx) else {
214 return true;
215 };
216 let Some((exports, node)) = export else { return false };
217 if reexports.is_none() {
218 reexports = Some(exports);
219 } else {
220 state.borrow_mut().diag.push_error(
221 "re-exporting modules is only allowed once per file".into(),
222 &node,
223 );
224 };
225 false
226 });
227 if dependencies.is_empty() {
228 core::task::Poll::Ready(())
229 } else {
230 core::task::Poll::Pending
231 }
232 })
233 .await;
234 (foreign_imports, reexports.unwrap_or_default())
235 })
236 }
237
238 pub async fn import_component(
239 &mut self,
240 file_to_import: &str,
241 type_name: &str,
242 diag: &mut BuildDiagnostics,
243 ) -> Option<Rc<object_tree::Component>> {
244 let state = RefCell::new(BorrowedTypeLoader { tl: self, diag });
245 let doc_path =
246 match Self::ensure_document_loaded(&state, file_to_import, None, Default::default())
247 .await
248 {
249 Some(doc_path) => doc_path,
250 None => return None,
251 };
252
253 let doc = self.all_documents.docs.get(&doc_path).unwrap();
254
255 doc.exports.find(type_name).and_then(|compo_or_type| compo_or_type.left())
256 }
257
258 /// Append a possibly relative path to a base path. Returns the data if it resolves to a built-in (compiled-in)
259 /// file.
260 pub fn resolve_import_path(
261 &self,
262 import_token: Option<&NodeOrToken>,
263 maybe_relative_path_or_url: &str,
264 ) -> Option<(PathBuf, Option<&'static [u8]>)> {
265 if let Some(maybe_library_import) = maybe_relative_path_or_url.strip_prefix('@') {
266 self.find_file_in_library_path(maybe_library_import)
267 } else {
268 let referencing_file_or_url =
269 import_token.and_then(|tok| tok.source_file().map(|s| s.path()));
270 self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url)
271 .or_else(|| {
272 referencing_file_or_url
273 .and_then(|base_path_or_url| {
274 crate::pathutils::join(
275 &crate::pathutils::dirname(base_path_or_url),
276 &PathBuf::from(maybe_relative_path_or_url),
277 )
278 })
279 .filter(|p| p.exists())
280 .map(|p| (p, None))
281 })
282 }
283 }
284
285 async fn ensure_document_loaded<'a: 'b, 'b>(
286 state: &'a RefCell<BorrowedTypeLoader<'a>>,
287 file_to_import: &'b str,
288 import_token: Option<NodeOrToken>,
289 mut import_stack: HashSet<PathBuf>,
290 ) -> Option<PathBuf> {
291 let mut borrowed_state = state.borrow_mut();
292
293 let (path_canon, builtin) = match borrowed_state
294 .tl
295 .resolve_import_path(import_token.as_ref(), file_to_import)
296 {
297 Some(x) => x,
298 None => {
299 let import_path = crate::pathutils::clean_path(Path::new(file_to_import));
300 if import_path.exists() {
301 if import_token.as_ref().and_then(|x| x.source_file()).is_some() {
302 borrowed_state.diag.push_warning(
303 format!(
304 "Loading \"{file_to_import}\" relative to the work directory is deprecated. Files should be imported relative to their import location",
305 ),
306 &import_token,
307 );
308 }
309 (import_path, None)
310 } else {
311 // We will load using the `open_import_fallback`
312 // Simplify the path to remove the ".."
313 let base_path = import_token
314 .as_ref()
315 .and_then(|tok| tok.source_file().map(|s| s.path()))
316 .map_or(PathBuf::new(), |p| p.into());
317 let path = crate::pathutils::join(
318 &crate::pathutils::dirname(&base_path),
319 Path::new(file_to_import),
320 )?;
321 (path, None)
322 }
323 }
324 };
325
326 if !import_stack.insert(path_canon.clone()) {
327 borrowed_state.diag.push_error(
328 format!("Recursive import of \"{}\"", path_canon.display()),
329 &import_token,
330 );
331 return None;
332 }
333 drop(borrowed_state);
334
335 let is_loaded = core::future::poll_fn(|cx| {
336 let mut state = state.borrow_mut();
337 let all_documents = &mut state.tl.all_documents;
338 match all_documents.currently_loading.entry(path_canon.clone()) {
339 std::collections::hash_map::Entry::Occupied(mut e) => {
340 let waker = cx.waker();
341 if !e.get().iter().any(|w| w.will_wake(waker)) {
342 e.get_mut().push(cx.waker().clone());
343 }
344 core::task::Poll::Pending
345 }
346 std::collections::hash_map::Entry::Vacant(v) => {
347 if all_documents.docs.contains_key(path_canon.as_path()) {
348 core::task::Poll::Ready(true)
349 } else {
350 v.insert(Default::default());
351 core::task::Poll::Ready(false)
352 }
353 }
354 }
355 })
356 .await;
357 if is_loaded {
358 return Some(path_canon);
359 }
360
361 let source_code_result = if let Some(builtin) = builtin {
362 Ok(String::from(
363 core::str::from_utf8(builtin)
364 .expect("internal error: embedded file is not UTF-8 source code"),
365 ))
366 } else if let Some(fallback) = {
367 let fallback = state.borrow().tl.compiler_config.open_import_fallback.clone();
368 fallback
369 } {
370 let result = fallback(path_canon.to_string_lossy().into()).await;
371 result.unwrap_or_else(|| std::fs::read_to_string(&path_canon))
372 } else {
373 std::fs::read_to_string(&path_canon)
374 };
375
376 let ok = match source_code_result {
377 Ok(source) => {
378 Self::load_file_impl(
379 state,
380 &path_canon,
381 None,
382 &path_canon,
383 source,
384 builtin.is_some(),
385 &import_stack,
386 )
387 .await;
388
389 true
390 }
391 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
392 state.borrow_mut().diag.push_error(
393 if file_to_import.starts_with('@') {
394 format!(
395 "Cannot find requested import \"{file_to_import}\" in the library search path",
396 )
397 } else {
398 format!(
399 "Cannot find requested import \"{file_to_import}\" in the include search path",
400 )
401 },
402 &import_token,
403 );
404 false
405 }
406 Err(err) => {
407 state.borrow_mut().diag.push_error(
408 format!("Error reading requested import \"{}\": {}", path_canon.display(), err),
409 &import_token,
410 );
411 false
412 }
413 };
414
415 let wakers = state
416 .borrow_mut()
417 .tl
418 .all_documents
419 .currently_loading
420 .remove(path_canon.as_path())
421 .unwrap();
422 for x in wakers {
423 x.wake();
424 }
425
426 ok.then_some(path_canon)
427 }
428
429 /// Load a file, and its dependency not run the passes.
430 ///
431 /// the path must be the canonical path
432 pub async fn load_file(
433 &mut self,
434 path: &Path,
435 version: SourceFileVersion,
436 source_path: &Path,
437 source_code: String,
438 is_builtin: bool,
439 diag: &mut BuildDiagnostics,
440 ) {
441 let state = RefCell::new(BorrowedTypeLoader { tl: self, diag });
442 Self::load_file_impl(
443 &state,
444 path,
445 version,
446 source_path,
447 source_code,
448 is_builtin,
449 &Default::default(),
450 )
451 .await;
452 }
453
454 /// Load a file, and its dependency not run the passes.
455 ///
456 /// the path must be the canonical path
457 pub async fn load_root_file(
458 &mut self,
459 path: &Path,
460 version: SourceFileVersion,
461 source_path: &Path,
462 source_code: String,
463 diag: &mut BuildDiagnostics,
464 ) -> PathBuf {
465 let path = crate::pathutils::clean_path(path);
466 let state = RefCell::new(BorrowedTypeLoader { tl: self, diag });
467 let (path, doc) = Self::load_file_no_pass(
468 &state,
469 &path,
470 version,
471 source_path,
472 source_code,
473 false,
474 &Default::default(),
475 )
476 .await;
477
478 let mut state = state.borrow_mut();
479 let state = &mut *state;
480 if !state.diag.has_error() {
481 crate::passes::run_passes(&doc, state.tl, state.diag).await;
482 }
483 state.tl.all_documents.docs.insert(path.clone(), doc);
484 path
485 }
486
487 async fn load_file_impl<'a>(
488 state: &'a RefCell<BorrowedTypeLoader<'a>>,
489 path: &Path,
490 version: SourceFileVersion,
491 source_path: &Path,
492 source_code: String,
493 is_builtin: bool,
494 import_stack: &HashSet<PathBuf>,
495 ) {
496 let (path, doc) = Self::load_file_no_pass(
497 state,
498 path,
499 version,
500 source_path,
501 source_code,
502 is_builtin,
503 import_stack,
504 )
505 .await;
506
507 let mut state = state.borrow_mut();
508 let state = &mut *state;
509 if !state.diag.has_error() {
510 crate::passes::run_import_passes(&doc, state.tl, state.diag);
511 }
512 state.tl.all_documents.docs.insert(path, doc);
513 }
514
515 async fn load_file_no_pass<'a>(
516 state: &'a RefCell<BorrowedTypeLoader<'a>>,
517 path: &Path,
518 version: SourceFileVersion,
519 source_path: &Path,
520 source_code: String,
521 is_builtin: bool,
522 import_stack: &HashSet<PathBuf>,
523 ) -> (PathBuf, Document) {
524 let dependency_doc: syntax_nodes::Document =
525 crate::parser::parse(source_code, Some(source_path), version, state.borrow_mut().diag)
526 .into();
527
528 let dependency_registry =
529 Rc::new(RefCell::new(TypeRegister::new(&state.borrow().tl.global_type_registry)));
530 dependency_registry.borrow_mut().expose_internal_types = is_builtin;
531 let (foreign_imports, reexports) = Self::load_dependencies_recursively_impl(
532 state,
533 &dependency_doc,
534 &dependency_registry,
535 import_stack,
536 )
537 .await;
538
539 if state.borrow().diag.has_error() {
540 // If there was error (esp parse error) we don't want to report further error in this document.
541 // because they might be nonsense (TODO: we should check that the parse error were really in this document).
542 // But we still want to create a document to give better error messages in the root document.
543 let mut ignore_diag = BuildDiagnostics::default();
544 ignore_diag.push_error_with_span(
545 "Dummy error because some of the code asserts there was an error".into(),
546 Default::default(),
547 );
548 let doc = crate::object_tree::Document::from_node(
549 dependency_doc,
550 foreign_imports,
551 reexports,
552 &mut ignore_diag,
553 &dependency_registry,
554 );
555 return (path.to_owned(), doc);
556 }
557 let mut state = state.borrow_mut();
558 let state = &mut *state;
559 let doc = crate::object_tree::Document::from_node(
560 dependency_doc,
561 foreign_imports,
562 reexports,
563 state.diag,
564 &dependency_registry,
565 );
566 (path.to_owned(), doc)
567 }
568
569 fn register_imported_types(
570 doc: &Document,
571 import: &ImportedTypes,
572 imported_types: impl Iterator<Item = ImportedName>,
573 registry_to_populate: &Rc<RefCell<TypeRegister>>,
574 build_diagnostics: &mut BuildDiagnostics,
575 ) {
576 for import_name in imported_types {
577 let imported_type = doc.exports.find(&import_name.external_name);
578
579 let imported_type = match imported_type {
580 Some(ty) => ty,
581 None => {
582 build_diagnostics.push_error(
583 format!(
584 "No exported type called '{}' found in \"{}\"",
585 import_name.external_name, import.file
586 ),
587 &import.import_uri_token,
588 );
589 continue;
590 }
591 };
592
593 match imported_type {
594 itertools::Either::Left(c) => {
595 registry_to_populate.borrow_mut().add_with_name(import_name.internal_name, c)
596 }
597 itertools::Either::Right(ty) => registry_to_populate
598 .borrow_mut()
599 .insert_type_with_name(ty, import_name.internal_name),
600 }
601 }
602 }
603
604 /// Lookup a library and filename and try to find the absolute filename based on the library path
605 fn find_file_in_library_path(
606 &self,
607 maybe_library_import: &str,
608 ) -> Option<(PathBuf, Option<&'static [u8]>)> {
609 let (library, file) = maybe_library_import
610 .splitn(2, '/')
611 .collect_tuple()
612 .map(|(library, path)| (library, Some(path)))
613 .unwrap_or((maybe_library_import, None));
614 self.compiler_config.library_paths.get(library).and_then(|library_path| {
615 let path = match file {
616 // "@library/file.slint" -> "/path/to/library/" + "file.slint"
617 Some(file) => library_path.join(file),
618 // "@library" -> "/path/to/library/lib.slint"
619 None => library_path.clone(),
620 };
621 crate::fileaccess::load_file(path.as_path())
622 .map(|virtual_file| (virtual_file.canon_path, virtual_file.builtin_contents))
623 })
624 }
625
626 /// Lookup a filename and try to find the absolute filename based on the include path or
627 /// the current file directory
628 pub fn find_file_in_include_path(
629 &self,
630 referencing_file: Option<&Path>,
631 file_to_import: &str,
632 ) -> Option<(PathBuf, Option<&'static [u8]>)> {
633 // The directory of the current file is the first in the list of include directories.
634 referencing_file
635 .map(base_directory)
636 .into_iter()
637 .chain(self.compiler_config.include_paths.iter().map(PathBuf::as_path).map(
638 |include_path| {
639 let base = referencing_file.map(Path::to_path_buf).unwrap_or_default();
640 crate::pathutils::join(&crate::pathutils::dirname(&base), include_path)
641 .unwrap_or_else(|| include_path.to_path_buf())
642 },
643 ))
644 .chain(
645 (file_to_import == "std-widgets.slint"
646 || referencing_file.map_or(false, |x| x.starts_with("builtin:/")))
647 .then(|| format!("builtin:/{}", self.style).into()),
648 )
649 .find_map(|include_dir| {
650 let candidate = crate::pathutils::join(&include_dir, Path::new(file_to_import))?;
651 crate::fileaccess::load_file(&candidate)
652 .map(|virtual_file| (virtual_file.canon_path, virtual_file.builtin_contents))
653 })
654 }
655
656 fn collect_dependencies<'a: 'b, 'b>(
657 state: &'a RefCell<BorrowedTypeLoader<'a>>,
658 doc: &'b syntax_nodes::Document,
659 ) -> impl Iterator<Item = ImportedTypes> + 'a {
660 doc.ImportSpecifier()
661 .map(|import| {
662 let maybe_import_uri = import.child_token(SyntaxKind::StringLiteral);
663 (maybe_import_uri, ImportKind::ImportList(import))
664 })
665 .chain(
666 // process `export * from "foo"`
667 doc.ExportsList().flat_map(|exports| exports.ExportModule()).map(|reexport| {
668 let maybe_import_uri = reexport.child_token(SyntaxKind::StringLiteral);
669 (maybe_import_uri, ImportKind::ModuleReexport(reexport))
670 }),
671 )
672 .filter_map(|(maybe_import_uri, type_specifier)| {
673 let import_uri = match maybe_import_uri {
674 Some(import_uri) => import_uri,
675 None => {
676 debug_assert!(state.borrow().diag.has_error());
677 return None;
678 }
679 };
680 let path_to_import = import_uri.text().to_string();
681 let path_to_import = path_to_import.trim_matches('\"').to_string();
682
683 if path_to_import.is_empty() {
684 state
685 .borrow_mut()
686 .diag
687 .push_error("Unexpected empty import url".to_owned(), &import_uri);
688 return None;
689 }
690
691 Some(ImportedTypes {
692 import_uri_token: import_uri,
693 import_kind: type_specifier,
694 file: path_to_import,
695 })
696 })
697 }
698
699 /// Return a document if it was already loaded
700 pub fn get_document<'b>(&'b self, path: &Path) -> Option<&'b object_tree::Document> {
701 let path = crate::pathutils::clean_path(path);
702 self.all_documents.docs.get(&path)
703 }
704
705 /// Return an iterator over all the loaded file path
706 pub fn all_files(&self) -> impl Iterator<Item = &PathBuf> {
707 self.all_documents.docs.keys()
708 }
709
710 /// Returns an iterator over all the loaded documents
711 pub fn all_documents(&self) -> impl Iterator<Item = &object_tree::Document> + '_ {
712 self.all_documents.docs.values()
713 }
714
715 /// Returns an iterator over all the loaded documents
716 pub fn all_file_documents(
717 &self,
718 ) -> impl Iterator<Item = (&PathBuf, &object_tree::Document)> + '_ {
719 self.all_documents.docs.iter()
720 }
721}
722
723fn get_native_style(all_loaded_files: &mut Vec<PathBuf>) -> String {
724 // Try to get the value written by the i-slint-backend-selector's build script
725
726 // It is in the target/xxx/build directory
727 let target_path = std::env::var_os("OUT_DIR")
728 .and_then(|path| {
729 // Same logic as in i-slint-backend-selector's build script to get the path
730 crate::pathutils::join(Path::new(&path), Path::new("../../SLINT_DEFAULT_STYLE.txt"))
731 })
732 .or_else(|| {
733 // When we are called from a slint!, OUT_DIR is only defined when the crate having the macro has a build.rs script.
734 // As a fallback, try to parse the rustc arguments
735 // https://stackoverflow.com/questions/60264534/getting-the-target-folder-from-inside-a-rust-proc-macro
736 let mut args = std::env::args();
737 let mut out_dir = None;
738 while let Some(arg) = args.next() {
739 if arg == "--out-dir" {
740 out_dir = args.next();
741 break;
742 }
743 }
744 out_dir.and_then(|od| {
745 crate::pathutils::join(
746 Path::new(&od),
747 Path::new("../build/SLINT_DEFAULT_STYLE.txt"),
748 )
749 })
750 });
751 if let Some(style) = target_path.and_then(|target_path| {
752 all_loaded_files.push(target_path.clone());
753 std::fs::read_to_string(target_path).map(|style| style.trim().into()).ok()
754 }) {
755 return style;
756 }
757 i_slint_common::get_native_style(false, &std::env::var("TARGET").unwrap_or_default()).into()
758}
759
760/// return the base directory from which imports are loaded
761///
762/// For a .slint file, this is the parent directory.
763/// For a .rs file, this is relative to the CARGO_MANIFEST_DIR
764///
765/// Note: this function is only called for .rs path as part of the LSP or viewer.
766/// Because from a proc_macro, we don't actually know the path of the current file, and this
767/// is why we must be relative to CARGO_MANIFEST_DIR.
768pub fn base_directory(referencing_file: &Path) -> PathBuf {
769 if referencing_file.extension().map_or(false, |e| e == "rs") {
770 // For .rs file, this is a rust macro, and rust macro locates the file relative to the CARGO_MANIFEST_DIR which is the directory that has a Cargo.toml file.
771 let mut candidate = referencing_file;
772 loop {
773 candidate =
774 if let Some(c) = candidate.parent() { c } else { break referencing_file.parent() };
775
776 if candidate.join("Cargo.toml").exists() {
777 break Some(candidate);
778 }
779 }
780 } else {
781 referencing_file.parent()
782 }
783 .map_or_else(Default::default, |p: &Path| p.to_path_buf())
784}
785
786#[test]
787fn test_dependency_loading() {
788 let test_source_path: PathBuf =
789 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
790
791 let mut incdir = test_source_path.clone();
792 incdir.push("incpath");
793
794 let mut compiler_config =
795 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
796 compiler_config.include_paths = vec![incdir];
797 compiler_config.library_paths =
798 HashMap::from([("library".into(), test_source_path.join("library").join("lib.slint"))]);
799 compiler_config.style = Some("fluent".into());
800
801 let mut main_test_path = test_source_path;
802 main_test_path.push("dependency_test_main.slint");
803
804 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
805 let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap();
806
807 let doc_node: syntax_nodes::Document = doc_node.into();
808
809 let global_registry = TypeRegister::builtin();
810
811 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
812
813 let mut build_diagnostics = BuildDiagnostics::default();
814
815 let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
816
817 let (foreign_imports, _) = spin_on::spin_on(loader.load_dependencies_recursively(
818 &doc_node,
819 &mut build_diagnostics,
820 &registry,
821 ));
822
823 assert!(!test_diags.has_error());
824 assert!(!build_diagnostics.has_error());
825 assert!(foreign_imports.is_empty());
826}
827
828#[test]
829fn test_dependency_loading_from_rust() {
830 let test_source_path: PathBuf =
831 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
832
833 let mut incdir = test_source_path.clone();
834 incdir.push("incpath");
835
836 let mut compiler_config =
837 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
838 compiler_config.include_paths = vec![incdir];
839 compiler_config.library_paths =
840 HashMap::from([("library".into(), test_source_path.join("library").join("lib.slint"))]);
841 compiler_config.style = Some("fluent".into());
842
843 let mut main_test_path = test_source_path;
844 main_test_path.push("some_rust_file.rs");
845
846 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
847 let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap();
848
849 let doc_node: syntax_nodes::Document = doc_node.into();
850
851 let global_registry = TypeRegister::builtin();
852
853 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
854
855 let mut build_diagnostics = BuildDiagnostics::default();
856
857 let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
858
859 let (foreign_imports, _) = spin_on::spin_on(loader.load_dependencies_recursively(
860 &doc_node,
861 &mut build_diagnostics,
862 &registry,
863 ));
864
865 assert!(!test_diags.has_error());
866 assert!(test_diags.is_empty()); // also no warnings
867 assert!(!build_diagnostics.has_error());
868 assert!(build_diagnostics.is_empty()); // also no warnings
869 assert!(foreign_imports.is_empty());
870}
871
872#[test]
873fn test_load_from_callback_ok() {
874 let ok = Rc::new(core::cell::Cell::new(false));
875 let ok_ = ok.clone();
876
877 let mut compiler_config =
878 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
879 compiler_config.style = Some("fluent".into());
880 compiler_config.open_import_fallback = Some(Rc::new(move |path| {
881 let ok_ = ok_.clone();
882 Box::pin(async move {
883 assert_eq!(path.replace('\\', "/"), "../FooBar.slint");
884 assert!(!ok_.get());
885 ok_.set(true);
886 Some(Ok("export XX := Rectangle {} ".to_owned()))
887 })
888 }));
889
890 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
891 let doc_node = crate::parser::parse(
892 r#"
893/* ... */
894import { XX } from "../Ab/.././FooBar.slint";
895X := XX {}
896"#
897 .into(),
898 Some(std::path::Path::new("HELLO")),
899 None,
900 &mut test_diags,
901 );
902
903 let doc_node: syntax_nodes::Document = doc_node.into();
904 let global_registry = TypeRegister::builtin();
905 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
906 let mut build_diagnostics = BuildDiagnostics::default();
907 let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
908 spin_on::spin_on(loader.load_dependencies_recursively(
909 &doc_node,
910 &mut build_diagnostics,
911 &registry,
912 ));
913 assert!(ok.get());
914 assert!(!test_diags.has_error());
915 assert!(!build_diagnostics.has_error());
916}
917
918#[test]
919fn test_load_error_twice() {
920 let mut compiler_config =
921 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
922 compiler_config.style = Some("fluent".into());
923 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
924
925 let doc_node = crate::parser::parse(
926 r#"
927/* ... */
928import { XX } from "error.slint";
929component Foo { XX {} }
930"#
931 .into(),
932 Some(std::path::Path::new("HELLO")),
933 None,
934 &mut test_diags,
935 );
936
937 let doc_node: syntax_nodes::Document = doc_node.into();
938 let global_registry = TypeRegister::builtin();
939 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
940 let mut build_diagnostics = BuildDiagnostics::default();
941 let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
942 spin_on::spin_on(loader.load_dependencies_recursively(
943 &doc_node,
944 &mut build_diagnostics,
945 &registry,
946 ));
947 assert!(!test_diags.has_error());
948 assert!(build_diagnostics.has_error());
949 let diags = build_diagnostics.to_string_vec();
950 assert_eq!(
951 diags,
952 &["HELLO:3: Cannot find requested import \"error.slint\" in the include search path"]
953 );
954 // Try loading another time with the same registry
955 let mut build_diagnostics = BuildDiagnostics::default();
956 spin_on::spin_on(loader.load_dependencies_recursively(
957 &doc_node,
958 &mut build_diagnostics,
959 &registry,
960 ));
961 assert!(build_diagnostics.has_error());
962 let diags = build_diagnostics.to_string_vec();
963 assert_eq!(
964 diags,
965 &["HELLO:3: Cannot find requested import \"error.slint\" in the include search path"]
966 );
967}
968
969#[test]
970fn test_manual_import() {
971 let mut compiler_config: CompilerConfiguration =
972 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
973 compiler_config.style = Some("fluent".into());
974 let global_registry: Rc> = TypeRegister::builtin();
975 let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default();
976 let mut loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics);
977
978 let maybe_button_type: Option> = spin_on::spin_on(future:loader.import_component(
979 file_to_import:"std-widgets.slint",
980 type_name:"Button",
981 &mut build_diagnostics,
982 ));
983
984 assert!(!build_diagnostics.has_error());
985 assert!(maybe_button_type.is_some());
986}
987
988#[test]
989fn test_builtin_style() {
990 let test_source_path: PathBuf =
991 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
992
993 let incdir: PathBuf = test_source_path.join(path:"custom_style");
994
995 let mut compiler_config: CompilerConfiguration =
996 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
997 compiler_config.include_paths = vec![incdir];
998 compiler_config.style = Some("fluent".into());
999
1000 let global_registry: Rc> = TypeRegister::builtin();
1001 let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default();
1002 let _loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics);
1003
1004 assert!(!build_diagnostics.has_error());
1005}
1006
1007#[test]
1008fn test_user_style() {
1009 let test_source_path: PathBuf =
1010 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
1011
1012 let incdir: PathBuf = test_source_path.join(path:"custom_style");
1013
1014 let mut compiler_config: CompilerConfiguration =
1015 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
1016 compiler_config.include_paths = vec![incdir];
1017 compiler_config.style = Some("TestStyle".into());
1018
1019 let global_registry: Rc> = TypeRegister::builtin();
1020 let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default();
1021 let _loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics);
1022
1023 assert!(!build_diagnostics.has_error());
1024}
1025
1026#[test]
1027fn test_unknown_style() {
1028 let test_source_path: PathBuf =
1029 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
1030
1031 let incdir: PathBuf = test_source_path.join(path:"custom_style");
1032
1033 let mut compiler_config: CompilerConfiguration =
1034 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
1035 compiler_config.include_paths = vec![incdir];
1036 compiler_config.style = Some("FooBar".into());
1037
1038 let global_registry: Rc> = TypeRegister::builtin();
1039 let mut build_diagnostics: BuildDiagnostics = BuildDiagnostics::default();
1040 let _loader: TypeLoader = TypeLoader::new(global_type_registry:global_registry, compiler_config, &mut build_diagnostics);
1041
1042 assert!(build_diagnostics.has_error());
1043 let diags: Vec = build_diagnostics.to_string_vec();
1044 assert_eq!(diags.len(), 1);
1045 assert!(diags[0].starts_with("Style FooBar in not known. Use one of the builtin styles ["));
1046}
1047
1048#[test]
1049fn test_library_import() {
1050 let test_source_path: PathBuf =
1051 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader", "library"].iter().collect();
1052
1053 let library_paths = HashMap::from([
1054 ("libdir".into(), test_source_path.clone()),
1055 ("libfile.slint".into(), test_source_path.join("lib.slint")),
1056 ]);
1057
1058 let mut compiler_config =
1059 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
1060 compiler_config.library_paths = library_paths;
1061 compiler_config.style = Some("fluent".into());
1062 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
1063
1064 let doc_node = crate::parser::parse(
1065 r#"
1066/* ... */
1067import { LibraryType } from "@libfile.slint";
1068import { LibraryHelperType } from "@libdir/library_helper_type.slint";
1069"#
1070 .into(),
1071 Some(std::path::Path::new("HELLO")),
1072 None,
1073 &mut test_diags,
1074 );
1075
1076 let doc_node: syntax_nodes::Document = doc_node.into();
1077 let global_registry = TypeRegister::builtin();
1078 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
1079 let mut build_diagnostics = BuildDiagnostics::default();
1080 let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
1081 spin_on::spin_on(loader.load_dependencies_recursively(
1082 &doc_node,
1083 &mut build_diagnostics,
1084 &registry,
1085 ));
1086 assert!(!test_diags.has_error());
1087 assert!(!build_diagnostics.has_error());
1088}
1089
1090#[test]
1091fn test_library_import_errors() {
1092 let test_source_path: PathBuf =
1093 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader", "library"].iter().collect();
1094
1095 let library_paths = HashMap::from([
1096 ("libdir".into(), test_source_path.clone()),
1097 ("libfile.slint".into(), test_source_path.join("lib.slint")),
1098 ]);
1099
1100 let mut compiler_config =
1101 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
1102 compiler_config.library_paths = library_paths;
1103 compiler_config.style = Some("fluent".into());
1104 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
1105
1106 let doc_node = crate::parser::parse(
1107 r#"
1108/* ... */
1109import { A } from "@libdir";
1110import { B } from "@libdir/unknown.slint";
1111import { C } from "@libfile.slint/unknown.slint";
1112import { D } from "@unknown";
1113import { E } from "@unknown/lib.slint";
1114"#
1115 .into(),
1116 Some(std::path::Path::new("HELLO")),
1117 None,
1118 &mut test_diags,
1119 );
1120
1121 let doc_node: syntax_nodes::Document = doc_node.into();
1122 let global_registry = TypeRegister::builtin();
1123 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
1124 let mut build_diagnostics = BuildDiagnostics::default();
1125 let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics);
1126 spin_on::spin_on(loader.load_dependencies_recursively(
1127 &doc_node,
1128 &mut build_diagnostics,
1129 &registry,
1130 ));
1131 assert!(!test_diags.has_error());
1132 assert!(build_diagnostics.has_error());
1133 let diags = build_diagnostics.to_string_vec();
1134 assert_eq!(diags.len(), 5);
1135 assert!(diags[0].starts_with(&format!(
1136 "HELLO:3: Error reading requested import \"{}\": ",
1137 test_source_path.to_string_lossy()
1138 )));
1139 assert_eq!(&diags[1], "HELLO:4: Cannot find requested import \"@libdir/unknown.slint\" in the library search path");
1140 assert_eq!(&diags[2], "HELLO:5: Cannot find requested import \"@libfile.slint/unknown.slint\" in the library search path");
1141 assert_eq!(
1142 &diags[3],
1143 "HELLO:6: Cannot find requested import \"@unknown\" in the library search path"
1144 );
1145 assert_eq!(
1146 &diags[4],
1147 "HELLO:7: Cannot find requested import \"@unknown/lib.slint\" in the library search path"
1148 );
1149}
1150