use crate::sema::ast;
use itertools::Itertools;
use normalize_path::NormalizePath;
use solang_parser::pt::Loc;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Default)]
pub struct FileResolver {
import_paths: Vec<(Option<OsString>, PathBuf)>,
cached_paths: HashMap<PathBuf, usize>,
files: Vec<ResolvedFile>,
}
#[derive(Clone, Debug)]
pub struct ResolvedFile {
pub path: OsString,
pub full_path: PathBuf,
pub import_no: Option<usize>,
pub contents: Arc<str>,
}
impl FileResolver {
pub fn add_import_path(&mut self, path: &Path) {
assert!(!self.import_paths.contains(&(None, path.to_path_buf())));
self.import_paths.push((None, path.to_path_buf()));
}
pub fn add_import_map(&mut self, map: OsString, path: PathBuf) {
let map = Some(map);
if let Some((_, e)) = self.import_paths.iter_mut().find(|(k, _)| *k == map) {
*e = path;
} else {
self.import_paths.push((map, path));
}
}
pub fn get_import_path(&self, import_no: usize) -> Option<&(Option<OsString>, PathBuf)> {
self.import_paths.get(import_no)
}
pub fn get_import_paths(&self) -> &[(Option<OsString>, PathBuf)] {
self.import_paths.as_slice()
}
pub fn get_import_map(&self, map: &OsString) -> Option<&PathBuf> {
self.import_paths
.iter()
.find(|(m, _)| m.as_ref() == Some(map))
.map(|(_, pb)| pb)
}
pub fn set_file_contents(&mut self, path: &str, contents: String) {
let pos = self.files.len();
let pathbuf = PathBuf::from(path);
self.files.push(ResolvedFile {
path: path.into(),
full_path: pathbuf.clone(),
contents: Arc::from(contents),
import_no: None,
});
self.cached_paths.insert(pathbuf, pos);
}
pub fn get_contents_of_file_no(&self, file_no: usize) -> Option<Arc<str>> {
self.files.get(file_no).map(|f| f.contents.clone())
}
pub fn get_file_contents_and_number(&self, file: &Path) -> (Arc<str>, usize) {
let file_no = self.cached_paths[file];
(self.files[file_no].contents.clone(), file_no)
}
fn try_file(
&mut self,
filename: &OsStr,
path: &Path,
import_no: Option<usize>,
) -> Result<Option<ResolvedFile>, String> {
let cache_path = path.normalize();
if let Some(cache) = self.cached_paths.get(&cache_path) {
let mut file = self.files[*cache].clone();
file.import_no = import_no;
return Ok(Some(file));
}
if let Ok(full_path) = path.canonicalize() {
let file = self.load_file(filename, &full_path, import_no)?;
return Ok(Some(file.clone()));
}
Ok(None)
}
fn load_file(
&mut self,
filename: &OsStr,
path: &Path,
import_no: Option<usize>,
) -> Result<&ResolvedFile, String> {
let path_filename = PathBuf::from(filename);
if let Some(cache) = self.cached_paths.get(&path_filename) {
if self.files[*cache].import_no == import_no {
return Ok(&self.files[*cache]);
}
}
let mut f = match File::open(path) {
Err(err_info) => {
return Err(format!(
"cannot open file '{}': {}",
path.display(),
err_info
));
}
Ok(file) => file,
};
let mut contents = String::new();
if let Err(e) = f.read_to_string(&mut contents) {
return Err(format!("failed to read file '{}': {}", path.display(), e));
}
let pos = self.files.len();
self.files.push(ResolvedFile {
path: filename.into(),
full_path: path.to_path_buf(),
import_no,
contents: Arc::from(contents),
});
self.cached_paths.insert(path.to_path_buf(), pos);
Ok(&self.files[pos])
}
pub fn resolve_file(
&mut self,
parent: Option<&ResolvedFile>,
filename: &OsStr,
) -> Result<ResolvedFile, String> {
let path_filename = PathBuf::from(filename);
let mut result: Vec<ResolvedFile> = vec![];
if path_filename.starts_with("./") || path_filename.starts_with("../") {
if let Some(ResolvedFile {
import_no,
full_path,
..
}) = parent
{
let curdir = PathBuf::from(".");
let base = full_path.parent().unwrap_or(&curdir);
let path = base.join(&path_filename);
if let Some(file) = self.try_file(filename, &path, *import_no)? {
return Ok(file);
}
}
return Err(format!("file not found '{}'", path_filename.display()));
}
if parent.is_none() {
if let Some(file) = self.try_file(filename, &path_filename, None)? {
return Ok(file);
} else if path_filename.is_absolute() {
return Err(format!("file not found '{}'", path_filename.display()));
}
}
let mut remapped = path_filename.clone();
for import_map_no in 0..self.import_paths.len() {
if let (Some(mapping), target) = &self.import_paths[import_map_no].clone() {
if let Ok(relpath) = path_filename.strip_prefix(mapping) {
remapped = target.join(relpath);
}
}
}
let path = remapped;
for import_no in 0..self.import_paths.len() {
if let (None, import_path) = &self.import_paths[import_no] {
let path = import_path.join(&path);
if let Some(file) = self.try_file(filename, &path, Some(import_no))? {
result.push(file);
}
}
}
if !self.import_paths.iter().any(|(m, _)| m.is_none()) {
if let Some(file) = self.try_file(filename, &path, None)? {
result.push(file);
}
}
match result.len() {
0 => Err(format!("file not found '{}'", path_filename.display())),
1 => Ok(result.pop().unwrap()),
_ => Err(format!(
"found multiple files matching '{}': {}",
path_filename.display(),
result
.iter()
.map(|f| format!("'{}'", f.full_path.display()))
.join(", ")
)),
}
}
pub fn get_line_and_offset_from_loc(
&self,
file: &ast::File,
loc: &Loc,
) -> (String, usize, usize, usize) {
let (start, end) = if let Loc::File(_, start, end) = loc {
(start, end)
} else {
unreachable!();
};
let cache_no = file.cache_no.unwrap();
let (begin_line, mut begin_column) = file.offset_to_line_column(*start);
let (end_line, mut end_column) = file.offset_to_line_column(*end);
let mut full_line = self.files[cache_no]
.contents
.lines()
.nth(begin_line)
.unwrap()
.to_owned();
if begin_line != end_line {
for i in begin_line + 1..=end_line {
let line = self.files[cache_no].contents.lines().nth(i).unwrap();
if i == end_line {
end_column += full_line.len();
}
full_line.push_str(line);
}
}
let old_size = full_line.len();
full_line = full_line.trim_start().parse().unwrap();
let size = end_column - begin_column;
begin_column -= old_size - full_line.len();
(full_line, begin_line, begin_column, size)
}
}