/* LICENSE BEGIN This file is part of the SixtyFPS Project -- https://sixtyfps.io Copyright (c) 2020 Olivier Goffart Copyright (c) 2020 Simon Hausmann SPDX-License-Identifier: GPL-3.0-only This file is also available under commercial licensing terms. Please contact info@sixtyfps.io for more information. LICENSE END */ use std::cell::RefCell; use std::collections::BTreeMap; use std::io::Read; use std::path::PathBuf; use std::rc::Rc; use crate::diagnostics::{BuildDiagnostics, FileDiagnostics, SourceFile}; use crate::parser::{ syntax_nodes::{Document, ImportSpecifier}, SyntaxKind, SyntaxTokenWithSourceFile, }; use crate::typeregister::TypeRegister; use crate::CompilerConfiguration; pub struct OpenFile<'a> { pub path: PathBuf, pub file: Box, } pub trait DirectoryAccess<'a> { fn try_open(&self, file_path: String) -> Option>; } impl<'a> DirectoryAccess<'a> for PathBuf { fn try_open(&self, file_path: String) -> Option> { let mut candidate = (*self).clone(); candidate.push(file_path); std::fs::File::open(&candidate) .ok() .map(|f| OpenFile { path: candidate, file: Box::new(f) as Box }) } } pub struct VirtualFile<'a> { pub path: &'a str, pub contents: &'a str, } pub type VirtualDirectory<'a> = [&'a VirtualFile<'a>]; impl<'a> DirectoryAccess<'a> for &'a VirtualDirectory<'a> { fn try_open(&self, file_path: String) -> Option> { self.iter().find_map(|virtual_file| { if virtual_file.path != file_path { return None; } Some(OpenFile { path: file_path.clone().into(), file: Box::new(std::io::Cursor::new(virtual_file.contents.as_bytes())), }) }) } } pub fn load_dependencies_recursively<'a>( doc: &Document, mut diagnostics: &mut FileDiagnostics, registry: &Rc>, compiler_config: &CompilerConfiguration, builtin_library: Option<&'a VirtualDirectory<'a>>, build_diagnostics: &mut BuildDiagnostics, ) { let dependencies = collect_dependencies(&doc, &mut diagnostics, compiler_config, builtin_library); for (dependency_path, imported_types) in dependencies { load_dependency( dependency_path, imported_types, ®istry, diagnostics, compiler_config, builtin_library, build_diagnostics, ); } } fn load_dependency<'a>( path: PathBuf, imported_types: ImportedTypes, registry_to_populate: &Rc>, importer_diagnostics: &mut FileDiagnostics, compiler_config: &CompilerConfiguration, builtin_library: Option<&'a VirtualDirectory<'a>>, build_diagnostics: &mut BuildDiagnostics, ) { let (dependency_doc, mut dependency_diagnostics) = crate::parser::parse(imported_types.source_code, Some(&path)); dependency_diagnostics.current_path = SourceFile::new(path); let dependency_doc: Document = dependency_doc.into(); let dependency_registry = Rc::new(RefCell::new(TypeRegister::new(®istry_to_populate))); load_dependencies_recursively( &dependency_doc, &mut dependency_diagnostics, &dependency_registry, compiler_config, builtin_library, build_diagnostics, ); let doc = crate::object_tree::Document::from_node( dependency_doc, &mut dependency_diagnostics, &dependency_registry, ); let exports = doc.exports(); for import_name in imported_types.type_names { let imported_type = exports.iter().find_map(|(export_name, component)| { if import_name.external_name == *export_name { Some(component.clone()) } else { None } }); let imported_type = match imported_type { Some(ty) => ty, None => { importer_diagnostics.push_error( format!( "No exported type called {} found in {}", import_name.external_name, dependency_diagnostics.current_path.to_string_lossy() ), &imported_types.import_token, ); continue; } }; registry_to_populate.borrow_mut().add_with_name(import_name.internal_name, imported_type); } build_diagnostics.add(dependency_diagnostics); } 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: &ImportSpecifier) -> impl Iterator { import.ImportIdentifierList().ImportIdentifier().map(|importident| { let external_name = importident.ExternalName().text().to_string().trim().to_string(); let internal_name = match importident.InternalName() { Some(name_ident) => name_ident.text().to_string().trim().to_string(), None => external_name.clone(), }; ImportedName { internal_name, external_name } }) } } struct ImportedTypes { pub type_names: Vec, pub import_token: SyntaxTokenWithSourceFile, pub source_code: String, } type DependenciesByFile = BTreeMap; fn collect_dependencies<'a>( doc: &Document, doc_diagnostics: &mut FileDiagnostics, compiler_config: &CompilerConfiguration, builtin_library: Option<&'a VirtualDirectory<'a>>, ) -> DependenciesByFile { // The directory of the current file is the first in the list of include directories. let mut current_directory = doc.source_file.as_ref().unwrap().clone().as_ref().clone(); current_directory.pop(); let open_file_from_include_paths = |file_path: String| { [current_directory.clone()] .iter() .chain(compiler_config.include_paths.iter()) .map(|include_path| { if include_path.is_relative() { let mut abs_path = current_directory.clone(); abs_path.push(include_path); abs_path } else { include_path.clone() } }) .find_map(|include_dir| include_dir.try_open(file_path.clone())) .or_else(|| builtin_library.and_then(|lib| lib.try_open(file_path.clone()))) }; let mut dependencies = DependenciesByFile::new(); for import in doc.ImportSpecifier() { let import_uri = import .child_token(SyntaxKind::StringLiteral) .expect("Internal error: missing import uri literal, this is a parsing/grammar bug"); 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; } let import_path = path_to_import.to_string(); if let Some(mut dependency_file) = open_file_from_include_paths(import_path) { let dependency_entry = match dependencies.entry(dependency_file.path.clone()) { std::collections::btree_map::Entry::Vacant(vacant_entry) => { let mut source_code = String::new(); if dependency_file.file.read_to_string(&mut source_code).is_err() { doc_diagnostics.push_error( format!( "Error reading requested import {}", dependency_file.path.to_string_lossy() ), &import_uri, ); break; } vacant_entry.insert(ImportedTypes { type_names: vec![], import_token: import_uri, source_code, }) } std::collections::btree_map::Entry::Occupied(existing_entry) => { existing_entry.into_mut() } }; dependency_entry.type_names.extend(ImportedName::extract_imported_names(&import)); } else { doc_diagnostics.push_error( format!( "Cannot find requested import {} in the include search path", path_to_import ), &import_uri, ); } } dependencies } #[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 compiler_config = CompilerConfiguration { include_paths: &[incdir], ..Default::default() }; let mut main_test_path = test_source_path.clone(); main_test_path.push("dependency_test_main.60"); let (doc_node, mut test_diags) = crate::parser::parse_file(main_test_path.clone()).unwrap(); let doc_node: Document = doc_node.into(); let registry = Rc::new(RefCell::new(TypeRegister::new(&TypeRegister::builtin()))); let mut build_diagnostics = BuildDiagnostics::default(); load_dependencies_recursively( &doc_node, &mut test_diags, ®istry, &compiler_config, None, &mut build_diagnostics, ); assert!(!test_diags.has_error()); assert!(!build_diagnostics.has_error()); }