1use std::borrow::Cow;
5use std::cell::RefCell;
6use std::collections::{BTreeMap, HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use std::rc::Rc;
9
10use crate::diagnostics::{BuildDiagnostics, Spanned};
11use crate::object_tree::{self, Document};
12use crate::parser;
13use crate::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxToken};
14use crate::typeregister::TypeRegister;
15use crate::CompilerConfiguration;
16
17#[derive(Default)]
19pub struct LoadedDocuments {
20 docs: HashMap<PathBuf, Document>,
22 currently_loading: HashSet<PathBuf>,
23}
24
25pub struct ImportedTypes {
26 pub import_token: SyntaxToken,
27 pub imported_types: syntax_nodes::ImportSpecifier,
28 pub file: String,
29}
30
31#[derive(Debug)]
32pub struct ImportedName {
33 pub external_name: String,
35 pub internal_name: String,
37}
38
39impl ImportedName {
40 pub fn extract_imported_names(
41 import: &syntax_nodes::ImportSpecifier,
42 ) -> Option<impl Iterator<Item = ImportedName>> {
43 import
44 .ImportIdentifierList()
45 .map(|import_identifiers| import_identifiers.ImportIdentifier().map(Self::from_node))
46 }
47
48 pub fn from_node(importident: syntax_nodes::ImportIdentifier) -> Self {
49 let external_name =
50 parser::normalize_identifier(importident.ExternalName().text().to_string().trim());
51
52 let internal_name = match importident.InternalName() {
53 Some(name_ident) => parser::normalize_identifier(name_ident.text().to_string().trim()),
54 None => external_name.clone(),
55 };
56
57 ImportedName { internal_name, external_name }
58 }
59}
60
61pub struct TypeLoader<'a> {
62 pub global_type_registry: Rc<RefCell<TypeRegister>>,
63 pub compiler_config: &'a CompilerConfiguration,
64 style: Cow<'a, str>,
65 all_documents: LoadedDocuments,
66}
67
68impl<'a> TypeLoader<'a> {
69 pub fn new(
70 global_type_registry: Rc<RefCell<TypeRegister>>,
71 compiler_config: &'a CompilerConfiguration,
72 diag: &mut BuildDiagnostics,
73 ) -> Self {
74 let style = compiler_config
75 .style
76 .as_ref()
77 .map(Cow::from)
78 .or_else(|| std::env::var("SIXTYFPS_STYLE").map(Cow::from).ok())
79 .unwrap_or_else(|| {
80 let is_wasm = cfg!(target_arch = "wasm32")
81 || std::env::var("TARGET").map_or(false, |t| t.starts_with("wasm"));
82 if !is_wasm {
83 diag.push_diagnostic_with_span("SIXTYFPS_STYLE not defined, defaulting to 'fluent', see https://github.com/sixtyfpsui/sixtyfps/issues/83 for more info".to_owned(),
84 Default::default(),
85 crate::diagnostics::DiagnosticLevel::Warning
86 );
87 }
88 Cow::from("fluent")
89 });
90
91 Self { global_type_registry, compiler_config, style, all_documents: Default::default() }
92 }
93
94 pub async fn load_dependencies_recursively(
96 &mut self,
97 doc: &syntax_nodes::Document,
98 diagnostics: &mut BuildDiagnostics,
99 registry_to_populate: &Rc<RefCell<TypeRegister>>,
100 ) -> Vec<ImportedTypes> {
101 let dependencies = self.collect_dependencies(doc, diagnostics).await;
102 let mut foreign_imports = vec![];
103 for mut import in dependencies {
104 if import.file.ends_with(".60") {
105 if let Some(imported_types) =
106 ImportedName::extract_imported_names(&import.imported_types)
107 {
108 self.load_dependency(import, imported_types, registry_to_populate, diagnostics)
109 .await;
110 } else {
111 diagnostics.push_error(
112 "Import names are missing. Please specify which types you would like to import"
113 .into(),
114 &import.import_token,
115 );
116 }
117 } else {
118 import.file = self
119 .resolve_import_path(Some(&import.import_token.clone().into()), &import.file)
120 .0
121 .to_string_lossy()
122 .to_string();
123 foreign_imports.push(import);
124 }
125 }
126 foreign_imports
127 }
128
129 pub async fn import_type(
130 &mut self,
131 file_to_import: &str,
132 type_name: &str,
133 diagnostics: &mut BuildDiagnostics,
134 ) -> Option<crate::langtype::Type> {
135 let doc_path = match self.ensure_document_loaded(file_to_import, None, diagnostics).await {
136 Some(doc_path) => doc_path,
137 None => return None,
138 };
139
140 let doc = self.all_documents.docs.get(&doc_path).unwrap();
141
142 doc.exports().iter().find_map(|(export_name, ty)| {
143 if type_name == export_name.as_str() {
144 Some(ty.clone())
145 } else {
146 None
147 }
148 })
149 }
150
151 pub fn resolve_import_path(
154 &self,
155 import_token: Option<&NodeOrToken>,
156 maybe_relative_path_or_url: &str,
157 ) -> (std::path::PathBuf, Option<&'static [u8]>) {
158 let referencing_file_or_url =
159 import_token.and_then(|tok| tok.source_file().map(|s| s.path()));
160
161 self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url)
162 .unwrap_or_else(|| {
163 (
164 referencing_file_or_url
165 .and_then(|base_path_or_url| {
166 let base_path_or_url_str = base_path_or_url.to_string_lossy();
167 if base_path_or_url_str.contains("://") {
168 url::Url::parse(&base_path_or_url_str).ok().and_then(|base_url| {
169 base_url
170 .join(maybe_relative_path_or_url)
171 .ok()
172 .map(|url| url.to_string().into())
173 })
174 } else {
175 base_path_or_url.parent().and_then(|base_dir| {
176 dunce::canonicalize(base_dir.join(maybe_relative_path_or_url))
177 .ok()
178 })
179 }
180 })
181 .unwrap_or_else(|| maybe_relative_path_or_url.into()),
182 None,
183 )
184 })
185 }
186
187 async fn ensure_document_loaded<'b>(
188 &'b mut self,
189 file_to_import: &'b str,
190 import_token: Option<NodeOrToken>,
191 diagnostics: &'b mut BuildDiagnostics,
192 ) -> Option<PathBuf> {
193 let (path, is_builtin) = self.resolve_import_path(import_token.as_ref(), file_to_import);
194
195 let path_canon = dunce::canonicalize(&path).unwrap_or_else(|_| path.to_owned());
196
197 if self.all_documents.docs.get(path_canon.as_path()).is_some() {
198 return Some(path_canon);
199 }
200
201 let builtin = is_builtin.map(|s| s.to_owned());
203 let is_builtin = builtin.is_some();
204
205 if !self.all_documents.currently_loading.insert(path_canon.clone()) {
206 diagnostics
207 .push_error(format!("Recursive import of \"{}\"", path.display()), &import_token);
208 return None;
209 }
210
211 let source_code_result = if let Some(builtin) = builtin {
212 Ok(String::from_utf8(builtin)
213 .expect("internal error: embedded file is not UTF-8 source code"))
214 } else if let Some(fallback) = &self.compiler_config.open_import_fallback {
215 let result = fallback(path_canon.to_string_lossy().into()).await;
216 result.unwrap_or_else(|| std::fs::read_to_string(&path_canon))
217 } else {
218 std::fs::read_to_string(&path_canon)
219 };
220
221 let source_code = match source_code_result {
222 Ok(source) => source,
223 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
224 diagnostics.push_error(
225 format!(
226 "Cannot find requested import \"{}\" in the include search path",
227 file_to_import
228 ),
229 &import_token,
230 );
231 return None;
232 }
233 Err(err) => {
234 diagnostics.push_error(
235 format!("Error reading requested import \"{}\": {}", path.display(), err),
236 &import_token,
237 );
238 return None;
239 }
240 };
241
242 self.load_file(&path_canon, &path, source_code, is_builtin, diagnostics).await;
243 let _ok = self.all_documents.currently_loading.remove(path_canon.as_path());
244 assert!(_ok);
245 Some(path_canon)
246 }
247
248 pub async fn load_file(
252 &mut self,
253 path: &Path,
254 source_path: &Path,
255 source_code: String,
256 is_builtin: bool,
257 diagnostics: &mut BuildDiagnostics,
258 ) {
259 let dependency_doc: syntax_nodes::Document =
260 crate::parser::parse(source_code, Some(source_path), diagnostics).into();
261
262 let dependency_registry =
263 Rc::new(RefCell::new(TypeRegister::new(&self.global_type_registry)));
264 dependency_registry.borrow_mut().expose_internal_types = is_builtin;
265 let foreign_imports = self
266 .load_dependencies_recursively(&dependency_doc, diagnostics, &dependency_registry)
267 .await;
268
269 if diagnostics.has_error() {
270 let mut ignore_diag = BuildDiagnostics::default();
274 ignore_diag.push_error_with_span(
275 "Dummy error because some of the code asserts there was an error".into(),
276 Default::default(),
277 );
278 let doc = crate::object_tree::Document::from_node(
279 dependency_doc,
280 foreign_imports,
281 &mut ignore_diag,
282 &dependency_registry,
283 );
284 self.all_documents.docs.insert(path.to_owned(), doc);
285 return;
286 }
287 let doc = crate::object_tree::Document::from_node(
288 dependency_doc,
289 foreign_imports,
290 diagnostics,
291 &dependency_registry,
292 );
293 crate::passes::run_import_passes(&doc, self, diagnostics);
294
295 self.all_documents.docs.insert(path.to_owned(), doc);
296 }
297
298 fn load_dependency<'b>(
299 &'b mut self,
300 import: ImportedTypes,
301 imported_types: impl Iterator<Item = ImportedName> + 'b,
302 registry_to_populate: &'b Rc<RefCell<TypeRegister>>,
303 build_diagnostics: &'b mut BuildDiagnostics,
304 ) -> core::pin::Pin<Box<dyn std::future::Future<Output = ()> + 'b>> {
305 Box::pin(async move {
306 let doc_path = match self
307 .ensure_document_loaded(
308 &import.file,
309 Some(import.import_token.clone().into()),
310 build_diagnostics,
311 )
312 .await
313 {
314 Some(path) => path,
315 None => return,
316 };
317
318 let doc = self.all_documents.docs.get(&doc_path).unwrap();
319 let exports = doc.exports();
320
321 for import_name in imported_types {
322 let imported_type = exports.iter().find_map(|(export_name, ty)| {
323 if import_name.external_name == export_name.as_str() {
324 Some(ty.clone())
325 } else {
326 None
327 }
328 });
329
330 let imported_type = match imported_type {
331 Some(ty) => ty,
332 None => {
333 build_diagnostics.push_error(
334 format!(
335 "No exported type called '{}' found in \"{}\"",
336 import_name.external_name, import.file
337 ),
338 &import.import_token,
339 );
340 continue;
341 }
342 };
343
344 registry_to_populate
345 .borrow_mut()
346 .insert_type_with_name(imported_type, import_name.internal_name);
347 }
348 })
349 }
350
351 pub fn find_file_in_include_path(
354 &self,
355 referencing_file: Option<&std::path::Path>,
356 file_to_import: &str,
357 ) -> Option<(PathBuf, Option<&'static [u8]>)> {
358 let maybe_current_directory =
360 referencing_file.and_then(|path| path.parent()).map(|p| p.to_path_buf());
361 maybe_current_directory
362 .clone()
363 .into_iter()
364 .chain(self.compiler_config.include_paths.iter().map(PathBuf::as_path).map({
365 |include_path| {
366 if include_path.is_relative() && maybe_current_directory.as_ref().is_some() {
367 maybe_current_directory.as_ref().unwrap().join(include_path)
368 } else {
369 include_path.to_path_buf()
370 }
371 }
372 }))
373 .chain(std::iter::once_with(|| format!("builtin:/{}", self.style).into()))
374 .find_map(|include_dir| {
375 let candidate = include_dir.join(file_to_import);
376 crate::fileaccess::load_file(&candidate)
377 .map(|virtual_file| (candidate, virtual_file.builtin_contents))
378 })
379 }
380
381 async fn collect_dependencies(
382 &mut self,
383 doc: &syntax_nodes::Document,
384 doc_diagnostics: &mut BuildDiagnostics,
385 ) -> impl Iterator<Item = ImportedTypes> {
386 type DependenciesByFile = BTreeMap<String, ImportedTypes>;
387 let mut dependencies = DependenciesByFile::new();
388
389 for import in doc.ImportSpecifier() {
390 let import_uri = match import.child_token(SyntaxKind::StringLiteral) {
391 Some(import_uri) => import_uri,
392 None => {
393 debug_assert!(doc_diagnostics.has_error());
394 continue;
395 }
396 };
397 let path_to_import = import_uri.text().to_string();
398 let path_to_import = path_to_import.trim_matches('\"').to_string();
399 if path_to_import.is_empty() {
400 doc_diagnostics.push_error("Unexpected empty import url".to_owned(), &import_uri);
401 continue;
402 }
403
404 dependencies.entry(path_to_import.clone()).or_insert_with(|| ImportedTypes {
405 import_token: import_uri,
406 imported_types: import,
407 file: path_to_import,
408 });
409 }
410
411 dependencies.into_iter().map(|(_, value)| value)
412 }
413
414 pub fn get_document<'b>(&'b self, path: &Path) -> Option<&'b object_tree::Document> {
416 dunce::canonicalize(path).map_or_else(
417 |_| self.all_documents.docs.get(path),
418 |path| self.all_documents.docs.get(&path),
419 )
420 }
421
422 pub fn all_files<'b>(&'b self) -> impl Iterator<Item = &PathBuf> + 'b {
424 self.all_documents.docs.keys()
425 }
426
427 pub fn all_documents(&self) -> impl Iterator<Item = &object_tree::Document> + '_ {
429 self.all_documents.docs.values()
430 }
431}
432
433#[test]
434fn test_dependency_loading() {
435 let test_source_path: std::path::PathBuf =
436 [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
437
438 let mut incdir = test_source_path.clone();
439 incdir.push("incpath");
440
441 let mut compiler_config =
442 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
443 compiler_config.include_paths = vec![incdir];
444 compiler_config.style = Some("fluent".into());
445
446 let mut main_test_path = test_source_path;
447 main_test_path.push("dependency_test_main.60");
448
449 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
450 let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap();
451
452 let doc_node: syntax_nodes::Document = doc_node.into();
453
454 let global_registry = TypeRegister::builtin();
455
456 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
457
458 let mut build_diagnostics = BuildDiagnostics::default();
459
460 let mut loader = TypeLoader::new(global_registry, &compiler_config, &mut build_diagnostics);
461
462 spin_on::spin_on(loader.load_dependencies_recursively(
463 &doc_node,
464 &mut build_diagnostics,
465 ®istry,
466 ));
467
468 assert!(!test_diags.has_error());
469 assert!(!build_diagnostics.has_error());
470}
471
472#[test]
473fn test_load_from_callback_ok() {
474 let ok = Rc::new(core::cell::Cell::new(false));
475 let ok_ = ok.clone();
476
477 let mut compiler_config =
478 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
479 compiler_config.style = Some("fluent".into());
480 compiler_config.open_import_fallback = Some(Rc::new(move |path| {
481 let ok_ = ok_.clone();
482 Box::pin(async move {
483 assert_eq!(path, "../FooBar.60");
484 assert!(!ok_.get());
485 ok_.set(true);
486 Some(Ok("export XX := Rectangle {} ".to_owned()))
487 })
488 }));
489
490 let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
491 let doc_node = crate::parser::parse(
492 r#"
493/* ... */
494import { XX } from "../FooBar.60";
495X := XX {}
496"#
497 .into(),
498 Some(std::path::Path::new("HELLO")),
499 &mut test_diags,
500 );
501
502 let doc_node: syntax_nodes::Document = doc_node.into();
503 let global_registry = TypeRegister::builtin();
504 let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
505 let mut build_diagnostics = BuildDiagnostics::default();
506 let mut loader = TypeLoader::new(global_registry, &compiler_config, &mut build_diagnostics);
507 spin_on::spin_on(loader.load_dependencies_recursively(
508 &doc_node,
509 &mut build_diagnostics,
510 ®istry,
511 ));
512 assert!(ok.get());
513 assert!(!test_diags.has_error());
514 assert!(!build_diagnostics.has_error());
515}
516
517#[test]
518fn test_manual_import() {
519 let mut compiler_config =
520 CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
521 compiler_config.style = Some("fluent".into());
522 let global_registry = TypeRegister::builtin();
523 let mut build_diagnostics = BuildDiagnostics::default();
524 let mut loader = TypeLoader::new(global_registry, &compiler_config, &mut build_diagnostics);
525
526 let maybe_button_type = spin_on::spin_on(loader.import_type(
527 "sixtyfps_widgets.60",
528 "Button",
529 &mut build_diagnostics,
530 ));
531
532 assert!(!build_diagnostics.has_error());
533 assert!(maybe_button_type.is_some());
534}