Skip to main content

lutra_compiler/project/
mod.rs

1mod analysis;
2
3pub use analysis::{SymbolInfo, TargetMap, TargetSpan};
4
5use std::path;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use indexmap::IndexMap;
10use itertools::Itertools;
11
12use crate::pr;
13
14/// Project, checked.
15#[derive(Debug)]
16pub struct Project {
17    /// Discovered sources
18    pub source: SourceTree,
19
20    /// Resolved definitions
21    pub root_module: pr::ModuleDef,
22
23    /// Resolution ordering of definitions
24    // TODO: make a more efficient "a ordered vec of unordered groups" data structure
25    pub ordering: Vec<Vec<pr::Path>>,
26
27    pub dependencies: Vec<Dependency>,
28
29    /// Index of all resolved identifier references, keyed by source location.
30    /// Built once after name resolution; used for go-to-definition and similar queries.
31    pub target_map: analysis::TargetMap,
32}
33
34#[derive(Debug, Clone)]
35pub struct Dependency {
36    pub name: String,
37
38    pub inner: Arc<Project>,
39}
40
41impl Project {
42    /// Search the resolved module tree recursively for definitions that carry
43    /// the annotation `@<annotation_name>` (bare identifier).
44    ///
45    /// Returns a vec of `(path, def)` pairs where `path` is the fully-qualified
46    /// path of the definition within the module tree.
47    pub fn find_by_annotation(&self, name: &str) -> Vec<pr::Path> {
48        let mut result = Vec::new();
49        find_by_annotation_in(&self.root_module, name, pr::Path::empty(), &mut result);
50        result
51    }
52}
53
54fn find_by_annotation_in(
55    module: &pr::ModuleDef,
56    annotation_name: &str,
57    mut path: pr::Path,
58    result: &mut Vec<pr::Path>,
59) {
60    if has_annotation(&module.annotations, annotation_name) {
61        result.push(path.clone());
62    }
63
64    for (name, def) in &module.defs {
65        path.push(name.clone());
66
67        if has_annotation(&def.annotations, annotation_name) {
68            result.push(path.clone());
69        }
70
71        if let pr::DefKind::Module(inner) = &def.kind {
72            find_by_annotation_in(inner, annotation_name, path.clone(), result);
73        }
74
75        path.pop();
76    }
77}
78
79fn has_annotation(annotations: &[pr::Annotation], annotation_name: &str) -> bool {
80    annotations
81        .iter()
82        .any(|ann| is_named(&ann.expr, annotation_name))
83}
84
85fn is_named(expr: &pr::Expr, name: &str) -> bool {
86    is_ident(expr, name)
87        || matches!(&expr.kind, pr::ExprKind::Call(call) if is_ident(&call.subject, name))
88}
89
90fn is_ident(expr: &pr::Expr, name: &str) -> bool {
91    matches!(&expr.kind, pr::ExprKind::Ident(path) if (path.len() == 1 && path.first() == name))
92}
93
94/// Sources used to resolve the project.
95/// All paths are relative to the project root.
96// We use `SourceTree` to represent both a single file (including a "file" piped
97// from stdin), and a collection of files. (Possibly this could be implemented
98// as a Trait with a Struct for each type, which would use structure over values
99// (i.e. `Option<PathBuf>` below signifies whether it's a project or not). But
100// waiting until it's necessary before splitting it out.)
101#[derive(Debug, Clone)]
102pub struct SourceTree {
103    /// Path to the root of the source tree.
104    /// Can be a directory that contains module.lt or a .lt file.
105    root: path::PathBuf,
106
107    /// Mapping from file paths into into their contents.
108    /// Paths are relative to the root.
109    sources: IndexMap<path::PathBuf, String>,
110}
111
112impl SourceTree {
113    pub fn empty() -> Self {
114        SourceTree {
115            sources: Default::default(),
116            root: path::PathBuf::new(),
117        }
118    }
119
120    pub fn single(path: path::PathBuf, content: String) -> Self {
121        SourceTree {
122            sources: [(path.clone(), content)].into(),
123            root: path::PathBuf::new(),
124        }
125    }
126
127    pub fn new<I>(iter: I, root: path::PathBuf) -> Self
128    where
129        I: IntoIterator<Item = (path::PathBuf, String)>,
130    {
131        SourceTree {
132            sources: IndexMap::from_iter(iter),
133            root,
134        }
135    }
136
137    pub fn is_empty(&self) -> bool {
138        self.sources.len() == 0
139    }
140
141    pub fn insert(&mut self, path: path::PathBuf, content: String) {
142        self.sources.insert(path, content);
143    }
144
145    pub fn replace(&mut self, path: &path::Path, content: String) -> Option<String> {
146        let source = self.sources.get_mut(path)?;
147        Some(std::mem::replace(source, content))
148    }
149
150    pub fn get_ids(&self) -> impl Iterator<Item = u16> {
151        0..self.sources.len() as u16
152    }
153    pub fn get_sources(&self) -> impl Iterator<Item = (&path::PathBuf, &String)> {
154        self.sources.iter()
155    }
156    pub fn get_root(&self) -> &std::path::Path {
157        self.root.as_path()
158    }
159
160    pub fn get_by_id(&self, source_id: u16) -> Option<(&path::Path, &str)> {
161        self.sources
162            .get_index(source_id as usize)
163            .map(|(p, c)| (p.as_path(), c.as_str()))
164    }
165
166    pub fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
167        self.sources
168            .get_full(path)
169            .map(|(i, _, content)| (i as u16, content.as_str()))
170    }
171
172    pub fn get_source_display_paths(&self) -> impl Iterator<Item = &path::Path> {
173        self.sources
174            .keys()
175            .map(|path| self.get_display_path(path).unwrap())
176    }
177
178    /// Converts a "project path" into an absolute path in the file-system.
179    pub fn get_absolute_path(&self, path: impl AsRef<path::Path>) -> path::PathBuf {
180        let path = path.as_ref();
181        if path.as_os_str().is_empty() {
182            self.root.to_path_buf()
183        } else {
184            self.get_project_dir().join(path)
185        }
186    }
187
188    /// Converts an absolute path into a path relative to the root.
189    /// Not that this is not relative to "project dir".
190    pub fn get_relative_path<'a>(
191        &self,
192        absolute_path: &'a path::Path,
193    ) -> Result<&'a path::Path, path::StripPrefixError> {
194        absolute_path.strip_prefix(&self.root)
195    }
196
197    /// Returns project dir: the directory in which the project files reside.
198    /// For example,
199    /// - if root is `/some_path/project/`, then that is also the project dir,
200    /// - if root is `/some_path/my_file.lt`, then project dir is `/some_path/`.
201    pub fn get_project_dir(&self) -> &path::Path {
202        if self.root.extension().is_some_and(|e| e == "lt") {
203            self.root.parent().unwrap()
204        } else {
205            &self.root
206        }
207    }
208
209    /// Converts a path (either absolute or relative to project root) into a
210    /// "display path", which is path relative to "project dir".
211    /// This is equivalent to "relative path", except for single-file projects,
212    /// where root is a file and "project dir" is its parent directory.
213    pub fn get_display_path<'a>(&'a self, path: &'a path::Path) -> Option<&'a path::Path> {
214        if path.is_absolute() {
215            path.strip_prefix(self.get_project_dir()).ok()
216        } else if path.as_os_str().is_empty() {
217            self.root.file_name().map(path::Path::new)
218        } else {
219            Some(path)
220        }
221    }
222}
223
224impl std::fmt::Display for SourceTree {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        let mut r = format!("path: {}\nsources:\n", self.root.to_string_lossy());
227
228        for source in self.sources.keys().sorted() {
229            r += "- ";
230            r += &source.to_string_lossy();
231            r += "\n";
232        }
233
234        f.write_str(&r)
235    }
236}
237
238/// Project source (in [SourceTree]) and a code snippet
239pub struct SourceOverlay<'a> {
240    tree: &'a SourceTree,
241
242    snippet_path: path::PathBuf,
243    snippet: &'a str,
244}
245
246impl<'a> SourceOverlay<'a> {
247    pub fn new(tree: &'a SourceTree, snippet: &'a str, snippet_path: Option<&str>) -> Self {
248        Self {
249            tree,
250            snippet,
251            snippet_path: snippet_path
252                .map(|s| path::PathBuf::from_str(s).unwrap())
253                .unwrap_or_default(),
254        }
255    }
256
257    pub const fn overlay_id() -> u16 {
258        u16::MAX
259    }
260}
261
262pub(crate) trait SourceProvider {
263    fn get_root(&self) -> &path::Path;
264
265    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)>;
266
267    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)>;
268}
269
270impl SourceProvider for SourceTree {
271    fn get_root(&self) -> &path::Path {
272        &self.root
273    }
274    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)> {
275        SourceTree::get_by_id(self, id)
276    }
277
278    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
279        SourceTree::get_by_path(self, path)
280    }
281}
282
283impl<'a> SourceProvider for SourceOverlay<'a> {
284    fn get_root(&self) -> &path::Path {
285        &self.tree.root
286    }
287
288    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)> {
289        if id == SourceOverlay::overlay_id() {
290            Some((self.snippet_path.as_path(), self.snippet))
291        } else {
292            self.tree.get_by_id(id)
293        }
294    }
295
296    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
297        if path == self.snippet_path {
298            Some((SourceOverlay::overlay_id(), self.snippet))
299        } else {
300            self.tree.get_by_path(path)
301        }
302    }
303}