// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::rc::Rc; use crate::diagnostics::{BuildDiagnostics, Spanned}; use crate::object_tree::{self, Document}; use crate::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxToken}; use crate::typeregister::TypeRegister; use crate::CompilerConfiguration; use crate::{fileaccess, parser}; /// Storage for a cache of all loaded documents #[derive(Default)] pub struct LoadedDocuments { /// maps from the canonical file name to the object_tree::Document docs: HashMap, currently_loading: HashSet, } pub struct ImportedTypes { pub import_token: SyntaxToken, pub imported_types: syntax_nodes::ImportSpecifier, pub file: String, } #[derive(Debug)] pub struct ImportedName { // name of export to match in the other file pub external_name: String, // name to be used locally pub internal_name: String, } impl ImportedName { pub fn extract_imported_names( import: &syntax_nodes::ImportSpecifier, ) -> Option> { import .ImportIdentifierList() .map(|import_identifiers| import_identifiers.ImportIdentifier().map(Self::from_node)) } pub fn from_node(importident: syntax_nodes::ImportIdentifier) -> Self { let external_name = parser::normalize_identifier(importident.ExternalName().text().to_string().trim()); let internal_name = match importident.InternalName() { Some(name_ident) => parser::normalize_identifier(name_ident.text().to_string().trim()), None => external_name.clone(), }; ImportedName { internal_name, external_name } } } pub struct TypeLoader { pub global_type_registry: Rc>, pub compiler_config: CompilerConfiguration, style: String, all_documents: LoadedDocuments, } impl TypeLoader { pub fn new( global_type_registry: Rc>, compiler_config: CompilerConfiguration, diag: &mut BuildDiagnostics, ) -> Self { let style = compiler_config .style .clone() .or_else(|| std::env::var("SLINT_STYLE").ok()) .or_else(|| { let legacy_fallback = std::env::var("SIXTYFPS_STYLE").ok(); if legacy_fallback.is_some() { diag.push_diagnostic_with_span( "Using `SIXTYFPS_STYLE` environment variable for dynamic backend selection. This is deprecated, use `SLINT_STYLE` instead".to_owned(), Default::default(), crate::diagnostics::DiagnosticLevel::Warning ) } legacy_fallback }) .unwrap_or_else(|| { let is_wasm = cfg!(target_arch = "wasm32") || std::env::var("TARGET").map_or(false, |t| t.starts_with("wasm")); if !is_wasm { diag.push_diagnostic_with_span("SLINT_STYLE not defined, defaulting to 'fluent', see https://github.com/slint-ui/slint/issues/83 for more info".to_owned(), Default::default(), crate::diagnostics::DiagnosticLevel::Warning ); } String::from("fluent") }); let myself = Self { global_type_registry, compiler_config, style: style.clone(), all_documents: Default::default(), }; let known_styles = fileaccess::styles(); if !known_styles.contains(&style.as_ref()) && myself .find_file_in_include_path(None, &format!("{}/std-widgets.slint", style)) .is_none() { diag.push_diagnostic_with_span( format!( "Style {} in not known. Use one of the builtin styles [{}] or make sure your custom style is found in the include directories", &style, known_styles.join(", ") ), Default::default(), crate::diagnostics::DiagnosticLevel::Error, ); } myself } /// Imports of files that don't have the .slint extension are returned. pub async fn load_dependencies_recursively( &mut self, doc: &syntax_nodes::Document, diagnostics: &mut BuildDiagnostics, registry_to_populate: &Rc>, ) -> Vec { let dependencies = self.collect_dependencies(doc, diagnostics).await; let mut foreign_imports = vec![]; for mut import in dependencies { if import.file.ends_with(".60") || import.file.ends_with(".slint") { if let Some(imported_types) = ImportedName::extract_imported_names(&import.imported_types) { self.load_dependency(import, imported_types, registry_to_populate, diagnostics) .await; } else { diagnostics.push_error( "Import names are missing. Please specify which types you would like to import" .into(), &import.import_token, ); } } else { import.file = self .resolve_import_path(Some(&import.import_token.clone().into()), &import.file) .0 .to_string_lossy() .to_string(); foreign_imports.push(import); } } foreign_imports } pub async fn import_type( &mut self, file_to_import: &str, type_name: &str, diagnostics: &mut BuildDiagnostics, ) -> Option { let doc_path = match self.ensure_document_loaded(file_to_import, None, diagnostics).await { Some(doc_path) => doc_path, None => return None, }; let doc = self.all_documents.docs.get(&doc_path).unwrap(); doc.exports().iter().find_map(|(export_name, ty)| { if type_name == export_name.as_str() { Some(ty.clone()) } else { None } }) } /// Append a possibly relative path to a base path. Returns the data if it resolves to a built-in (compiled-in) /// file. pub fn resolve_import_path( &self, import_token: Option<&NodeOrToken>, maybe_relative_path_or_url: &str, ) -> (std::path::PathBuf, Option<&'static [u8]>) { let referencing_file_or_url = import_token.and_then(|tok| tok.source_file().map(|s| s.path())); self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url) .unwrap_or_else(|| { ( referencing_file_or_url .and_then(|base_path_or_url| { let base_path_or_url_str = base_path_or_url.to_string_lossy(); if base_path_or_url_str.contains("://") { url::Url::parse(&base_path_or_url_str).ok().and_then(|base_url| { base_url .join(maybe_relative_path_or_url) .ok() .map(|url| url.to_string().into()) }) } else { base_path_or_url.parent().and_then(|base_dir| { dunce::canonicalize(base_dir.join(maybe_relative_path_or_url)) .ok() }) } }) .unwrap_or_else(|| maybe_relative_path_or_url.into()), None, ) }) } async fn ensure_document_loaded<'b>( &'b mut self, file_to_import: &'b str, import_token: Option, diagnostics: &'b mut BuildDiagnostics, ) -> Option { let (path, is_builtin) = self.resolve_import_path(import_token.as_ref(), file_to_import); let path_canon = dunce::canonicalize(&path).unwrap_or_else(|_| path.to_owned()); if self.all_documents.docs.get(path_canon.as_path()).is_some() { return Some(path_canon); } // Drop &self lifetime attached to is_builtin, in order to mutable borrow self below let builtin = is_builtin.map(|s| s.to_owned()); let is_builtin = builtin.is_some(); if !self.all_documents.currently_loading.insert(path_canon.clone()) { diagnostics .push_error(format!("Recursive import of \"{}\"", path.display()), &import_token); return None; } let source_code_result = if let Some(builtin) = builtin { Ok(String::from_utf8(builtin) .expect("internal error: embedded file is not UTF-8 source code")) } else if let Some(fallback) = &self.compiler_config.open_import_fallback { let result = fallback(path_canon.to_string_lossy().into()).await; result.unwrap_or_else(|| std::fs::read_to_string(&path_canon)) } else { std::fs::read_to_string(&path_canon) }; let source_code = match source_code_result { Ok(source) => source, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { diagnostics.push_error( format!( "Cannot find requested import \"{}\" in the include search path", file_to_import ), &import_token, ); return None; } Err(err) => { diagnostics.push_error( format!("Error reading requested import \"{}\": {}", path.display(), err), &import_token, ); return None; } }; self.load_file(&path_canon, &path, source_code, is_builtin, diagnostics).await; let _ok = self.all_documents.currently_loading.remove(path_canon.as_path()); assert!(_ok); Some(path_canon) } /// Load a file, and its dependency not run the passes. /// /// the path must be the canonical path pub async fn load_file( &mut self, path: &Path, source_path: &Path, source_code: String, is_builtin: bool, diagnostics: &mut BuildDiagnostics, ) { let dependency_doc: syntax_nodes::Document = crate::parser::parse(source_code, Some(source_path), diagnostics).into(); let dependency_registry = Rc::new(RefCell::new(TypeRegister::new(&self.global_type_registry))); dependency_registry.borrow_mut().expose_internal_types = is_builtin; let foreign_imports = self .load_dependencies_recursively(&dependency_doc, diagnostics, &dependency_registry) .await; if diagnostics.has_error() { // If there was error (esp parse error) we don't want to report further error in this document. // because they might be nonsense (TODO: we should check that the parse error were really in this document). // But we still want to create a document to give better error messages in the root document. let mut ignore_diag = BuildDiagnostics::default(); ignore_diag.push_error_with_span( "Dummy error because some of the code asserts there was an error".into(), Default::default(), ); let doc = crate::object_tree::Document::from_node( dependency_doc, foreign_imports, &mut ignore_diag, &dependency_registry, ); self.all_documents.docs.insert(path.to_owned(), doc); return; } let doc = crate::object_tree::Document::from_node( dependency_doc, foreign_imports, diagnostics, &dependency_registry, ); crate::passes::run_import_passes(&doc, self, diagnostics); self.all_documents.docs.insert(path.to_owned(), doc); } fn load_dependency<'b>( &'b mut self, import: ImportedTypes, imported_types: impl Iterator + 'b, registry_to_populate: &'b Rc>, build_diagnostics: &'b mut BuildDiagnostics, ) -> core::pin::Pin + 'b>> { Box::pin(async move { let mut file = import.file.as_str(); if file == "sixtyfps_widgets.60" { file = "std-widgets.slint"; build_diagnostics.push_warning( "\"sixtyfps_widgets.60\" was renamed \"std-widgets.slint\". Use of the old file name is deprecated".into(), &import.import_token, ); } let doc_path = match self .ensure_document_loaded( file, Some(import.import_token.clone().into()), build_diagnostics, ) .await { Some(path) => path, None => return, }; let doc = self.all_documents.docs.get(&doc_path).unwrap(); let exports = doc.exports(); for import_name in imported_types { let imported_type = exports.iter().find_map(|(export_name, ty)| { if import_name.external_name == export_name.as_str() { Some(ty.clone()) } else { None } }); let imported_type = match imported_type { Some(ty) => ty, None => { build_diagnostics.push_error( format!( "No exported type called '{}' found in \"{}\"", import_name.external_name, import.file ), &import.import_token, ); continue; } }; registry_to_populate .borrow_mut() .insert_type_with_name(imported_type, import_name.internal_name); } }) } /// Lookup a filename and try to find the absolute filename based on the include path or /// the current file directory pub fn find_file_in_include_path( &self, referencing_file: Option<&std::path::Path>, file_to_import: &str, ) -> Option<(PathBuf, Option<&'static [u8]>)> { // The directory of the current file is the first in the list of include directories. let maybe_current_directory = referencing_file.and_then(|path| path.parent()).map(|p| p.to_path_buf()); maybe_current_directory .clone() .into_iter() .chain(self.compiler_config.include_paths.iter().map(PathBuf::as_path).map({ |include_path| { if include_path.is_relative() && maybe_current_directory.as_ref().is_some() { maybe_current_directory.as_ref().unwrap().join(include_path) } else { include_path.to_path_buf() } } })) .chain(std::iter::once_with(|| format!("builtin:/{}", self.style).into())) .find_map(|include_dir| { let candidate = include_dir.join(file_to_import); crate::fileaccess::load_file(&candidate) .map(|virtual_file| (candidate, virtual_file.builtin_contents)) }) } async fn collect_dependencies( &mut self, doc: &syntax_nodes::Document, doc_diagnostics: &mut BuildDiagnostics, ) -> impl Iterator { type DependenciesByFile = BTreeMap; let mut dependencies = DependenciesByFile::new(); for import in doc.ImportSpecifier() { let import_uri = match import.child_token(SyntaxKind::StringLiteral) { Some(import_uri) => import_uri, None => { debug_assert!(doc_diagnostics.has_error()); continue; } }; let path_to_import = import_uri.text().to_string(); let path_to_import = path_to_import.trim_matches('\"').to_string(); if path_to_import.is_empty() { doc_diagnostics.push_error("Unexpected empty import url".to_owned(), &import_uri); continue; } dependencies.entry(path_to_import.clone()).or_insert_with(|| ImportedTypes { import_token: import_uri, imported_types: import, file: path_to_import, }); } dependencies.into_iter().map(|(_, value)| value) } /// Return a document if it was already loaded pub fn get_document<'b>(&'b self, path: &Path) -> Option<&'b object_tree::Document> { dunce::canonicalize(path).map_or_else( |_| self.all_documents.docs.get(path), |path| self.all_documents.docs.get(&path), ) } /// Return an iterator over all the loaded file path pub fn all_files<'b>(&'b self) -> impl Iterator + 'b { self.all_documents.docs.keys() } /// Returns an iterator over all the loaded documents pub fn all_documents(&self) -> impl Iterator + '_ { self.all_documents.docs.values() } } #[test] fn test_dependency_loading() { let test_source_path: std::path::PathBuf = [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect(); let mut incdir = test_source_path.clone(); incdir.push("incpath"); let mut compiler_config = CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); compiler_config.include_paths = vec![incdir]; compiler_config.style = Some("fluent".into()); let mut main_test_path = test_source_path; main_test_path.push("dependency_test_main.slint"); let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap(); let doc_node: syntax_nodes::Document = doc_node.into(); let global_registry = TypeRegister::builtin(); let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); let mut build_diagnostics = BuildDiagnostics::default(); let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); spin_on::spin_on(loader.load_dependencies_recursively( &doc_node, &mut build_diagnostics, ®istry, )); assert!(!test_diags.has_error()); assert!(!build_diagnostics.has_error()); } #[test] fn test_load_from_callback_ok() { let ok = Rc::new(core::cell::Cell::new(false)); let ok_ = ok.clone(); let mut compiler_config = CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); compiler_config.style = Some("fluent".into()); compiler_config.open_import_fallback = Some(Rc::new(move |path| { let ok_ = ok_.clone(); Box::pin(async move { assert_eq!(path, "../FooBar.slint"); assert!(!ok_.get()); ok_.set(true); Some(Ok("export XX := Rectangle {} ".to_owned())) }) })); let mut test_diags = crate::diagnostics::BuildDiagnostics::default(); let doc_node = crate::parser::parse( r#" /* ... */ import { XX } from "../FooBar.slint"; X := XX {} "# .into(), Some(std::path::Path::new("HELLO")), &mut test_diags, ); let doc_node: syntax_nodes::Document = doc_node.into(); let global_registry = TypeRegister::builtin(); let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry))); let mut build_diagnostics = BuildDiagnostics::default(); let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); spin_on::spin_on(loader.load_dependencies_recursively( &doc_node, &mut build_diagnostics, ®istry, )); assert!(ok.get()); assert!(!test_diags.has_error()); assert!(!build_diagnostics.has_error()); } #[test] fn test_manual_import() { let mut compiler_config = CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); compiler_config.style = Some("fluent".into()); let global_registry = TypeRegister::builtin(); let mut build_diagnostics = BuildDiagnostics::default(); let mut loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); let maybe_button_type = spin_on::spin_on(loader.import_type("std-widgets.slint", "Button", &mut build_diagnostics)); assert!(!build_diagnostics.has_error()); assert!(maybe_button_type.is_some()); } #[test] fn test_builtin_style() { let test_source_path: std::path::PathBuf = [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect(); let incdir = test_source_path.join("custom_style"); let mut compiler_config = CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); compiler_config.include_paths = vec![incdir]; compiler_config.style = Some("fluent".into()); let global_registry = TypeRegister::builtin(); let mut build_diagnostics = BuildDiagnostics::default(); let _loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); assert!(!build_diagnostics.has_error()); } #[test] fn test_user_style() { let test_source_path: std::path::PathBuf = [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect(); let incdir = test_source_path.join("custom_style"); let mut compiler_config = CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); compiler_config.include_paths = vec![incdir]; compiler_config.style = Some("TestStyle".into()); let global_registry = TypeRegister::builtin(); let mut build_diagnostics = BuildDiagnostics::default(); let _loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); assert!(!build_diagnostics.has_error()); } #[test] fn test_unknown_style() { let test_source_path: std::path::PathBuf = [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect(); let incdir = test_source_path.join("custom_style"); let mut compiler_config = CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter); compiler_config.include_paths = vec![incdir]; compiler_config.style = Some("FooBar".into()); let global_registry = TypeRegister::builtin(); let mut build_diagnostics = BuildDiagnostics::default(); let _loader = TypeLoader::new(global_registry, compiler_config, &mut build_diagnostics); assert!(build_diagnostics.has_error()); let diags = build_diagnostics.to_string_vec(); assert_eq!(diags.len(), 1); assert!(diags[0].starts_with("Style FooBar in not known. Use one of the builtin styles [")); }