1use std::fs;
4use std::path::{Path, PathBuf};
5
6use nifty_check::hir::Program;
7use nifty_rt::SourceLocation;
8use nifty_syntax::{Diagnostic, FileId, Span};
9
10use crate::error::CliError;
11use crate::project::{self, ProjectFile};
12
13#[derive(Debug, PartialEq, Eq)]
15pub(crate) struct SourceFile {
16 pub path: PathBuf,
17 pub relative: PathBuf,
19 pub text: String,
20}
21
22impl SourceFile {
23 pub(crate) fn name(&self) -> String {
24 self.path.display().to_string()
25 }
26}
27
28#[derive(Debug, PartialEq, Eq)]
31pub(crate) struct Sources {
32 target: PathBuf,
33 files: Vec<SourceFile>,
34}
35
36impl Sources {
37 pub(crate) fn read(target: &Path) -> Result<Self, CliError> {
38 let files = project::source_files(target)?
39 .into_iter()
40 .map(|ProjectFile { path, relative }| {
41 let text = fs::read_to_string(&path).map_err(|error| {
42 CliError::new(format!("can't read {}: {error}", path.display()))
43 })?;
44 Ok(SourceFile {
45 path,
46 relative,
47 text,
48 })
49 })
50 .collect::<Result<_, CliError>>()?;
51 Ok(Self {
52 target: target.to_path_buf(),
53 files,
54 })
55 }
56
57 pub(crate) fn name(&self) -> String {
59 self.target.display().to_string()
60 }
61
62 pub(crate) fn entry(&self) -> &SourceFile {
63 self.file(FileId::default())
64 }
65
66 pub(crate) fn files(&self) -> &[SourceFile] {
67 &self.files
68 }
69
70 pub(crate) fn file(&self, id: FileId) -> &SourceFile {
72 &self.files[id.index()]
73 }
74
75 pub(crate) fn locate(&self, span: Span) -> SourceLocation {
78 let file = self.file(span.file);
79 let before = &file.text[..span.start.min(file.text.len())];
80 let line_start = before.rfind('\n').map_or(0, |newline| newline + 1);
81 SourceLocation {
82 file: file.relative.display().to_string(),
83 line: count(before.matches('\n').count() + 1),
84 column: count(before[line_start..].chars().count() + 1),
85 }
86 }
87
88 pub(crate) fn compile(&self) -> Result<Program, Vec<Diagnostic>> {
89 let parsed: Vec<_> = self
90 .files
91 .iter()
92 .enumerate()
93 .map(|(index, file)| nifty_syntax::parse(&file.text, FileId::new(index)))
94 .collect();
95 nifty_check::compile(&parsed.iter().collect::<Vec<_>>())
96 }
97}
98
99fn count(number: usize) -> u32 {
101 u32::try_from(number).unwrap_or(u32::MAX)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 const SPLIT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../fixtures/split");
109
110 #[test]
111 fn a_project_split_across_files_compiles_as_one_program() {
112 let sources = Sources::read(Path::new(SPLIT)).expect("the fixture is readable");
113 let relative: Vec<&Path> = sources
114 .files()
115 .iter()
116 .map(|file| file.relative.as_path())
117 .collect();
118 assert_eq!(relative, [Path::new("main.nf"), Path::new("ui/header.nf")]);
119 let program = sources.compile().expect("the fixture checks");
120 assert_eq!(program.views[0].name, "Header");
121 }
122
123 #[test]
124 fn locations_name_the_file_in_its_project_and_count_characters() {
125 let sources = Sources::read(Path::new(SPLIT)).expect("the fixture is readable");
126 let header = &sources.files()[1];
127 let start = header
128 .text
129 .find("view")
130 .expect("the header declares a view");
131 let location = sources.locate(Span::new(FileId::new(1), start, start + 4));
132 let relative = Path::new("ui").join("header.nf");
133 assert_eq!(location.file, relative.display().to_string());
134 let line = header.text[..start].matches('\n').count() + 1;
135 assert_eq!(location.line, u32::try_from(line).expect("a short file"));
136 assert_eq!(location.column, 1);
137 }
138}