slint/sixtyfps_compiler/typeloader.rs
Simon Hausmann 3e90a3c827 Improve relative image path resolution
Don't just try to resolve relative paths against the current path,
resolve them against all directories in the include search path.

Together with the include search path directive for tests in tests/cases
this will allow working around the fact that the base directory for the
rust tests is different than for the C++/NodeJS tests.
2021-01-15 16:28:17 +01:00

561 lines
19 KiB
Rust

/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
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::borrow::Cow;
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::diagnostics::{BuildDiagnostics, CompilerDiagnostic, FileDiagnostics, SourceFile};
use crate::object_tree::{self, Document};
use crate::parser::{syntax_nodes, SyntaxKind, SyntaxTokenWithSourceFile};
use crate::typeregister::TypeRegister;
use crate::CompilerConfiguration;
/// 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<PathBuf, Document>,
currently_loading: HashSet<PathBuf>,
}
pub(crate) struct OpenFile {
pub(crate) path: PathBuf,
source_code_future:
core::pin::Pin<Box<dyn std::future::Future<Output = std::io::Result<String>>>>,
}
trait DirectoryAccess<'a> {
fn try_open(&self, file_path: &str) -> Option<OpenFile>;
}
impl<'a> DirectoryAccess<'a> for PathBuf {
fn try_open(&self, file_path: &str) -> Option<OpenFile> {
let candidate = self.join(file_path);
std::fs::File::open(&candidate).ok().map(|mut f| OpenFile {
path: candidate,
source_code_future: Box::pin(async move {
let mut buf = String::new();
f.read_to_string(&mut buf).map(|_| buf)
}),
})
}
}
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: &str) -> Option<OpenFile> {
self.iter().find_map(|virtual_file| {
if virtual_file.path != file_path {
return None;
}
Some(OpenFile {
path: file_path.into(),
source_code_future: Box::pin({
let source = virtual_file.contents.to_owned();
async move { Ok(source) }
}),
})
})
}
}
struct ImportedTypes {
pub type_names: Vec<ImportedName>,
pub import_token: SyntaxTokenWithSourceFile,
pub file: OpenFile,
}
type DependenciesByFile = BTreeMap<PathBuf, ImportedTypes>;
#[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,
) -> impl Iterator<Item = ImportedName> {
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 }
})
}
}
pub struct TypeLoader<'a> {
pub global_type_registry: Rc<RefCell<TypeRegister>>,
pub compiler_config: &'a CompilerConfiguration,
pub builtin_library: Option<&'a VirtualDirectory<'a>>,
all_documents: LoadedDocuments,
}
impl<'a> TypeLoader<'a> {
pub fn new(
global_type_registry: Rc<RefCell<TypeRegister>>,
compiler_config: &'a CompilerConfiguration,
diag: &mut BuildDiagnostics,
) -> Self {
let style = compiler_config
.style
.as_ref()
.map(Cow::from)
.or_else(|| std::env::var("SIXTYFPS_STYLE").map(Cow::from).ok())
.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_internal_error(CompilerDiagnostic {
message: "SIXTYFPS_STYLE not defined, defaulting to 'ugly', see https://github.com/sixtyfpsui/sixtyfps/issues/83 for more info".to_owned(),
span: Default::default(),
level: crate::diagnostics::Level::Warning
}.into());
}
Cow::from("ugly")
});
let builtin_library =
crate::library::widget_library().iter().find(|x| x.0 == style).map(|x| x.1);
Self {
global_type_registry,
compiler_config,
builtin_library,
all_documents: Default::default(),
}
}
pub async fn load_dependencies_recursively(
&mut self,
doc: &syntax_nodes::Document,
mut diagnostics: &mut FileDiagnostics,
build_diagnostics: &mut BuildDiagnostics,
registry_to_populate: &Rc<RefCell<TypeRegister>>,
) {
let dependencies = self.collect_dependencies(&doc, &mut diagnostics).await;
for import in dependencies.into_iter() {
self.load_dependency(import, registry_to_populate, diagnostics, build_diagnostics)
.await;
}
}
pub async fn import_type(
&mut self,
file_to_import: &str,
type_name: &str,
diagnostics: &mut FileDiagnostics,
build_diagnostics: &mut BuildDiagnostics,
) -> Option<crate::langtype::Type> {
let file = match self.import_file(None, file_to_import) {
Some(file) => file,
None => return None,
};
let doc_path = match self
.ensure_document_loaded(
&file.path,
file.source_code_future,
None,
Some(diagnostics),
build_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 {
Some(ty.clone())
} else {
None
}
})
}
async fn ensure_document_loaded<'b>(
&'b mut self,
path: &'b Path,
source_code_future: impl std::future::Future<Output = std::io::Result<String>>,
import_token: Option<SyntaxTokenWithSourceFile>,
importer_diagnostics: Option<&'b mut FileDiagnostics>,
build_diagnostics: &'b mut BuildDiagnostics,
) -> Option<PathBuf> {
let path_canon = path.canonicalize().unwrap_or_else(|_| path.to_owned());
if self.all_documents.docs.get(path_canon.as_path()).is_some() {
return Some(path_canon);
}
if !self.all_documents.currently_loading.insert(path_canon.clone()) {
importer_diagnostics
.unwrap()
.push_error(format!("Recursive import of {}", path.display()), &import_token);
return None;
}
let source_code = match source_code_future.await {
Ok(source) => source,
Err(err) => {
importer_diagnostics.unwrap().push_error(
format!("Error reading requested import {}: {}", path.display(), err),
&import_token,
);
return None;
}
};
self.load_file(
&path_canon,
SourceFile::new(path.to_owned()),
source_code,
build_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: SourceFile,
source_code: String,
build_diagnostics: &mut BuildDiagnostics,
) {
let (dependency_doc, mut dependency_diagnostics) =
crate::parser::parse(source_code, Some(&source_path));
dependency_diagnostics.current_path = source_path;
if dependency_diagnostics.has_error() {
build_diagnostics.add(dependency_diagnostics);
let mut d = Document::default();
d.node = Some(dependency_doc.into());
self.all_documents.docs.insert(path.to_owned(), d);
return;
}
let dependency_doc: syntax_nodes::Document = dependency_doc.into();
let dependency_registry =
Rc::new(RefCell::new(TypeRegister::new(&self.global_type_registry)));
self.load_dependencies_recursively(
&dependency_doc,
&mut dependency_diagnostics,
build_diagnostics,
&dependency_registry,
)
.await;
let doc = crate::object_tree::Document::from_node(
dependency_doc,
&mut dependency_diagnostics,
&dependency_registry,
);
crate::passes::resolving::resolve_expressions(&doc, &self, build_diagnostics);
// Add diagnostics regardless whether they're empty or not. This is used by the syntax_tests to
// also verify that imported files have no errors.
build_diagnostics.add(dependency_diagnostics);
self.all_documents.docs.insert(path.to_owned(), doc);
}
fn load_dependency<'b>(
&'b mut self,
import: ImportedTypes,
registry_to_populate: &'b Rc<RefCell<TypeRegister>>,
importer_diagnostics: &'b mut FileDiagnostics,
build_diagnostics: &'b mut BuildDiagnostics,
) -> core::pin::Pin<Box<dyn std::future::Future<Output = ()> + 'b>> {
Box::pin(async move {
let doc_path = match self
.ensure_document_loaded(
&import.file.path,
import.file.source_code_future,
Some(import.import_token.clone()),
Some(importer_diagnostics),
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 import.type_names {
let imported_type = exports.iter().find_map(|(export_name, ty)| {
if import_name.external_name == *export_name {
Some(ty.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,
import.file.path.display()
),
&import.import_token,
);
continue;
}
};
registry_to_populate
.borrow_mut()
.insert_type_with_name(imported_type, import_name.internal_name);
}
})
}
pub(crate) fn import_file(
&self,
referencing_file: Option<&std::path::Path>,
file_to_import: &str,
) -> Option<OpenFile> {
// 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()
}
}
}))
.find_map(|include_dir| include_dir.try_open(file_to_import))
.or_else(|| self.builtin_library.and_then(|lib| lib.try_open(file_to_import)))
.or_else(|| {
self.compiler_config
.resolve_import_fallback
.as_ref()
.map_or_else(
|| Some(file_to_import.to_owned()),
|resolve_import_callback| {
resolve_import_callback(file_to_import.to_owned())
},
)
.and_then(|resolved_absolute_path| {
self.compiler_config
.open_import_fallback
.as_ref()
.map(|cb| cb(resolved_absolute_path.clone()))
.map(|future| OpenFile {
path: resolved_absolute_path.into(),
source_code_future: future,
})
})
})
}
async fn collect_dependencies(
&mut self,
doc: &syntax_nodes::Document,
doc_diagnostics: &mut FileDiagnostics,
) -> impl Iterator<Item = ImportedTypes> {
let referencing_file = doc.source_file.as_ref().unwrap().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 dependency_entry = if let Some(dependency_file) =
self.import_file(Some(&referencing_file), &path_to_import)
{
match dependencies.entry(dependency_file.path.clone()) {
std::collections::btree_map::Entry::Vacant(vacant_entry) => vacant_entry
.insert(ImportedTypes {
type_names: vec![],
import_token: import_uri,
file: dependency_file,
}),
std::collections::btree_map::Entry::Occupied(existing_entry) => {
existing_entry.into_mut()
}
}
} else {
doc_diagnostics.push_error(
format!(
"Cannot find requested import {} in the include search path",
path_to_import
),
&import_uri,
);
continue;
};
dependency_entry.type_names.extend(ImportedName::extract_imported_names(&import));
}
dependencies.into_iter().map(|(_, value)| value)
}
/// Return a document if it was already loaded
pub fn get_document(&self, path: &Path) -> Option<&object_tree::Document> {
path.canonicalize().map_or_else(
|_| self.all_documents.docs.get(path),
|path| self.all_documents.docs.get(&path),
)
}
}
#[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("ugly".into());
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: 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 test_diags,
&mut build_diagnostics,
&registry,
));
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("ugly".into());
compiler_config.open_import_fallback = Some(Box::new(move |path| {
let ok_ = ok_.clone();
Box::pin(async move {
assert_eq!(path, "../FooBar.60");
assert_eq!(ok_.get(), false);
ok_.set(true);
Ok("export XX := Rectangle {} ".to_owned())
})
}));
let (doc_node, mut test_diags) = crate::parser::parse(
r#"
/* ... */
import { XX } from "../FooBar.60";
X := XX {}
"#
.into(),
Some(&std::path::Path::new("HELLO")),
);
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 test_diags,
&mut build_diagnostics,
&registry,
));
assert_eq!(ok.get(), true);
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("ugly".into());
let mut test_diags = FileDiagnostics::default();
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(
"sixtyfps_widgets.60",
"Button",
&mut test_diags,
&mut build_diagnostics,
));
assert!(!test_diags.has_error());
assert!(!build_diagnostics.has_error());
assert!(maybe_button_type.is_some());
}