Skip to main content

zelkova_compiler/source/
files.rs

1use super::overlay::Overlay;
2use crate::name::Name;
3use crate::{PackageName, PhaseError};
4use codespan_reporting::files::{Error as FilesError, Files, SimpleFile};
5use std::ops::Range;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
9pub struct SourceFileId(usize);
10
11/// One of a package's two source roots, as
12/// [*Source roots*](../../../docs/spec/packages.md#source-roots) fixes them: `src/`
13/// is what the package ships, `tests/` is compiled when this package's own tests are
14/// run. Neither is configurable and there is no third.
15///
16/// Every [`SourceFile`] carries the one it was walked from, because the two roots share
17/// one set of module names: `src/Model.zel` and `tests/Model.zel` are both `Model`, and
18/// a diagnostic about that has to say which file it means.
19#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
20pub enum SourceRoot {
21    Src,
22    Tests,
23}
24
25impl SourceRoot {
26    /// The directory this root is, under the package directory holding `zelkova.toml`.
27    pub fn directory(self) -> &'static str {
28        match self {
29            SourceRoot::Src => "src",
30            SourceRoot::Tests => "tests",
31        }
32    }
33}
34
35impl std::fmt::Display for SourceRoot {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.write_str(self.directory())
38    }
39}
40
41// This should probably implements `Files` directly instead of relying on SimpleFile
42#[derive(Debug)] // TODO Implement Debug manually (don't want to embedded the entire source code)
43pub struct SourceFile {
44    /// The module name the file's path gives it: its path under its root, without the
45    /// `.zel` extension and with `.` for each separator — see
46    /// [`module_name`](Self::module_name).
47    module_name: String,
48    /// Which of the package's two source roots this file was walked from, and its path
49    /// under that root. Together they are [`SourceFile::package_path`], the file's place
50    /// in its package.
51    root: SourceRoot,
52    relative_path: PathBuf,
53    /// `Files` implementation. Its name is what a diagnostic's location line shows: the
54    /// file's path within its package, prefixed with `package:` when the database holds
55    /// a whole build (see [`SourceFile::load`]).
56    file: SimpleFile<String, String>,
57}
58
59impl SourceFile {
60    /// Load a `SourceFile` from the file system.
61    ///
62    /// `root_dir` is the directory the file was walked from and `root` says which of the
63    /// package's two source roots that is; a module's name is its path under its own
64    /// root, so the two roots are walked separately and each file is named relative to
65    /// the one it came from.
66    ///
67    /// `package` is the name of the package `root_dir` belongs to, and is what the
68    /// rendered file name is prefixed with. It is `Some` whenever the file joins a
69    /// database that holds more than one package's files: a path relative to a
70    /// package's own `src/` is not unique across a build — two packages may each hold a
71    /// `Size.zel` — so without it a diagnostic cannot say which package it is about.
72    /// `None` is for a database of exactly one package, where the prefix would be noise.
73    ///
74    /// The text is `overlay`'s when it holds `abs_path`, and the file's otherwise.
75    pub fn load(
76        abs_path: PathBuf,
77        root_dir: &Path,
78        root: SourceRoot,
79        package: Option<&PackageName>,
80        overlay: &Overlay,
81    ) -> Result<SourceFile, SourceFileError> {
82        SourceFile::load_private(&abs_path, root_dir, root, package, overlay)
83            .map_err(|error| SourceFileError { error, abs_path })
84    }
85
86    fn load_private(
87        abs_path: &PathBuf,
88        root_dir: &Path,
89        root: SourceRoot,
90        package: Option<&PackageName>,
91        overlay: &Overlay,
92    ) -> Result<SourceFile, SourceFileErrorType> {
93        let relative_path = abs_path.strip_prefix(root_dir)?.to_path_buf();
94
95        let file_name = relative_path
96            .to_str()
97            .ok_or_else(|| SourceFileErrorType::NonUtf8Module(relative_path.clone()))?
98            .to_owned();
99
100        let module_name = file_name
101            .trim_end_matches(".zel")
102            .replace(std::path::MAIN_SEPARATOR, ".");
103
104        let package_path = format!("{}/{}", root, file_name);
105
106        // `package:path`, the same shape `ModuleName` renders a module in, so the two
107        // halves of a diagnostic — the `[package:Module]` headline and the file its
108        // labels point into — name the package the same way.
109        let rendered_name = match package {
110            Some(package) => format!("{}:{}", package, package_path),
111            None => package_path,
112        };
113
114        let source = match overlay.get(abs_path) {
115            Some(text) => text.to_owned(),
116            None => std::fs::read_to_string(abs_path)?,
117        };
118
119        Ok(SourceFile {
120            module_name,
121            root,
122            relative_path,
123            file: SimpleFile::new(rendered_name, source),
124        })
125    }
126
127    pub fn file(&self) -> &SimpleFile<String, String> {
128        &self.file
129    }
130
131    /// The module name this file's path gives it: `src/Js/Basics.zel` is `Js.Basics`.
132    ///
133    /// A module is otherwise called by the name its header declares, and nothing checks
134    /// the two agree. This is the name a file whose header did not parse is known by.
135    pub fn module_name(&self) -> Name {
136        Name::new(self.module_name.as_str())
137    }
138
139    /// Where this file sits in its package: the source root it was walked from and its
140    /// path under that root, as `src/Model.zel`.
141    ///
142    /// This is the half of the rendered name that belongs to the package rather than to
143    /// the build, and it is what a diagnostic about the package's own layout — two
144    /// modules answering to one name — names each file by.
145    pub fn package_path(&self) -> String {
146        format!("{}/{}", self.root, self.relative_path.display())
147    }
148}
149
150#[derive(Debug)]
151pub struct SourceFileError {
152    error: SourceFileErrorType,
153    abs_path: PathBuf,
154}
155
156impl SourceFileError {
157    /// Build a `SourceFileError` from a `walkdir` failure.
158    ///
159    /// `walkdir::Error::path` is `None` only for the handful of internal errors that
160    /// aren't tied to a specific directory entry (see the crate's `Error::from_io`);
161    /// `root` is the best path to blame in that case, since it is what was being
162    /// walked. Everything else — a root that doesn't exist, a directory the process
163    /// can't read, a symlink loop — carries its own path.
164    pub(super) fn from_walk_error(root: &Path, error: walkdir::Error) -> SourceFileError {
165        let abs_path = error
166            .path()
167            .map(Path::to_path_buf)
168            .unwrap_or_else(|| root.to_path_buf());
169
170        SourceFileError {
171            error: SourceFileErrorType::Walk(error),
172            abs_path,
173        }
174    }
175}
176
177/// Loading is the one phase that has no module and no source text to point into —
178/// it failed before either existed — so its errors name the path instead. It goes
179/// through [`PhaseError`] anyway, so that `as_diagnostic` has exactly one way to
180/// turn a phase error into text.
181impl PhaseError for SourceFileError {
182    fn message(&self) -> String {
183        let detail = match &self.error {
184            SourceFileErrorType::InvalidPathPrefix => {
185                "the module path doesn't start with the package path"
186            }
187            SourceFileErrorType::Io(_) => "an I/O error occured while reading the module",
188            SourceFileErrorType::NonUtf8Module(_) => {
189                // Maybe we should add some details as to where the incorrect characters are ?
190                "module names must be utf-8 encoded"
191            }
192            SourceFileErrorType::Walk(err) => {
193                if err.loop_ancestor().is_some() {
194                    "a symbolic link loop was found while walking the package sources"
195                } else if err.io_error().map(std::io::Error::kind)
196                    == Some(std::io::ErrorKind::NotFound)
197                {
198                    "this path does not exist"
199                } else {
200                    "an I/O error occured while walking the package sources"
201                }
202            }
203        };
204
205        format!("{}: {}", self.abs_path.display(), detail)
206    }
207
208    fn notes(&self) -> Vec<String> {
209        match &self.error {
210            SourceFileErrorType::InvalidPathPrefix => vec![],
211            SourceFileErrorType::Io(err) => vec![format!("detailled error: {:?}", err)],
212            SourceFileErrorType::NonUtf8Module(rel_path) => {
213                // Maybe we should add some details as to where the incorrect characters are ?
214                vec![format!("relative path: {}", rel_path.display())]
215            }
216            SourceFileErrorType::Walk(err) => {
217                let mut notes = vec![];
218                if let Some(ancestor) = err.loop_ancestor() {
219                    notes.push(format!(
220                        "the link back to {} was found here",
221                        ancestor.display()
222                    ));
223                }
224                if let Some(io_err) = err.io_error() {
225                    notes.push(format!("detailled error: {:?}", io_err));
226                }
227                notes
228            }
229        }
230    }
231}
232
233#[derive(Debug)]
234pub enum SourceFileErrorType {
235    InvalidPathPrefix,
236    Io(std::io::Error),
237    NonUtf8Module(PathBuf),
238    /// An entry `walkdir` failed to yield while walking the package root — a root
239    /// that doesn't exist, a directory the process can't read, or a symbolic-link
240    /// loop it detected (see `WalkDir::follow_links` on why the walk can hit one).
241    Walk(walkdir::Error),
242}
243
244impl From<std::path::StripPrefixError> for SourceFileErrorType {
245    fn from(_err: std::path::StripPrefixError) -> Self {
246        SourceFileErrorType::InvalidPathPrefix
247    }
248}
249
250impl From<std::io::Error> for SourceFileErrorType {
251    fn from(err: std::io::Error) -> Self {
252        SourceFileErrorType::Io(err)
253    }
254}
255
256/// A file database that can store multiple source files.
257///
258/// This is taking as is from `codespan_reporting::files::SimpleFiles` with
259/// the added ability to iterate over the files
260///
261/// There are a few TODOs on this structure that may or may not be needed:
262/// - Use a custom `SourceFile` structure instead of `SimpleFile`
263/// - Use a deterministic `FileId` instead of using `usize`
264pub struct SourceFiles {
265    files: Vec<SourceFile>,
266}
267
268impl Default for SourceFiles {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl SourceFiles {
275    /// Create a new files database.
276    pub fn new() -> SourceFiles {
277        SourceFiles { files: Vec::new() }
278    }
279
280    /// Add a file to the database, returning the handle that can be used to
281    /// refer to it again.
282    pub fn add_file(&mut self, file: SourceFile) -> SourceFileId {
283        let file_id = self.files.len();
284        self.files.push(file);
285        SourceFileId(file_id)
286    }
287
288    /// Get the file corresponding to the given id.
289    pub fn get(&self, file_id: SourceFileId) -> Result<&SimpleFile<String, String>, FilesError> {
290        self.files
291            .get(file_id.0)
292            .map(|f| &f.file)
293            .ok_or(FilesError::FileMissing)
294    }
295
296    pub fn iter(&self) -> impl Iterator<Item = (SourceFileId, &'_ SourceFile)> + '_ {
297        self.files
298            .iter()
299            .enumerate()
300            .map(|(i, v)| (SourceFileId(i), v))
301    }
302
303    // Will probably need an accessor module name -> Option<SourceFileId>
304}
305
306impl<'a> Files<'a> for SourceFiles {
307    type FileId = SourceFileId;
308    type Name = String;
309    type Source = &'a String;
310
311    fn name(&self, file_id: Self::FileId) -> Result<String, FilesError> {
312        Ok(self.get(file_id)?.name().clone())
313    }
314
315    fn source(&self, file_id: Self::FileId) -> Result<&String, FilesError> {
316        Ok(self.get(file_id)?.source())
317    }
318
319    fn line_index(&self, file_id: Self::FileId, byte_index: usize) -> Result<usize, FilesError> {
320        self.get(file_id)?.line_index((), byte_index)
321    }
322
323    fn line_range(
324        &self,
325        file_id: Self::FileId,
326        line_index: usize,
327    ) -> Result<Range<usize>, FilesError> {
328        self.get(file_id)?.line_range((), line_index)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334
335    use super::*;
336
337    #[test]
338    fn test_make_source_file() {
339        let root_path = Path::new(
340            "/Users/francoismonniot/Projects/github.com/fmonniot/zelkova-lang/std/core/src",
341        );
342        let abs_path = Path::new("/Users/francoismonniot/Projects/github.com/fmonniot/zelkova-lang/std/core/src/Platform/Cmd.zel");
343
344        let res = SourceFile::load(
345            abs_path.to_path_buf(),
346            root_path,
347            SourceRoot::Src,
348            None,
349            &Overlay::new(),
350        );
351        println!("{:?}", res);
352    }
353}