libmoonwave/
source_file.rs

1use crate::{
2    diagnostic::Diagnostics, doc_comment::DocComment, doc_entry::DocEntry, error::Error, tags::Tag,
3};
4use full_moon::{
5    self,
6    ast::{LastStmt, Stmt},
7    node::Node,
8    tokenizer::{Token, TokenReference, TokenType},
9    visitors::Visitor,
10};
11
12#[derive(Debug)]
13pub struct SourceFile {
14    doc_comments: Vec<DocComment>,
15    file_id: usize,
16}
17
18impl<'a> SourceFile {
19    pub fn from_str(source: &'a str, file_id: usize, relative_path: String) -> Result<Self, Error> {
20        let ast = full_moon::parse(source).map_err(|e| {
21            Error::FullMoonError(
22                e.iter()
23                    .map(|e| (relative_path.clone(), e.to_owned()))
24                    .collect::<Vec<(String, full_moon::Error)>>(),
25            )
26        })?;
27
28        struct Collector<'b> {
29            buffer: Vec<(Token, Option<Stmt>)>,
30            last_line: usize,
31            file_id: usize,
32            relative_path: &'b str,
33            doc_comments: Vec<DocComment>,
34        }
35
36        impl<'b> Collector<'b> {
37            fn new(file_id: usize, relative_path: &'b str) -> Self {
38                Self {
39                    buffer: Vec::new(),
40                    file_id,
41                    last_line: 0,
42                    relative_path,
43                    doc_comments: Vec::new(),
44                }
45            }
46
47            fn scan(&mut self, token: Token, stmt: Option<Stmt>) {
48                match token.token_type() {
49                    TokenType::MultiLineComment { blocks: 1, comment } => {
50                        self.last_line = token.end_position().line();
51                        self.clear();
52
53                        self.doc_comments.push(DocComment::new(
54                            comment.to_string(),
55                            token.start_position().bytes() + "--[=[".len(),
56                            token.end_position().line() + 1,
57                            self.file_id,
58                            self.relative_path.to_owned(),
59                            stmt,
60                        ));
61                    }
62                    TokenType::SingleLineComment { comment } => {
63                        self.last_line = token.start_position().line();
64
65                        if let Some(comment) = comment.strip_prefix('-') {
66                            if comment.trim().chars().all(|char| char == '-') {
67                                // Comment is all -------
68                                return;
69                            }
70
71                            if comment.len() > 1 {
72                                if let Some(first_non_whitespace) =
73                                    comment.find(|char: char| !char.is_whitespace())
74                                {
75                                    // Compatibility: Drop lines like `---@module <path>` used
76                                    // for Roblox LSP comments (#39)
77                                    let tag_body = &comment[first_non_whitespace..];
78
79                                    if tag_body.starts_with("@module") {
80                                        return;
81                                    }
82                                }
83                            }
84
85                            self.buffer.push((token, stmt));
86                        } else if let Some(doc_comment) = self.flush() {
87                            self.doc_comments.push(doc_comment);
88                        }
89                    }
90                    TokenType::Whitespace { .. } => {
91                        let line = token.start_position().line();
92                        let is_consecutive_newline = line > self.last_line;
93
94                        self.last_line = line;
95
96                        if is_consecutive_newline {
97                            if let Some(doc_comment) = self.flush() {
98                                self.doc_comments.push(doc_comment);
99                            }
100                        }
101                    }
102                    _ => {}
103                }
104            }
105
106            fn clear(&mut self) {
107                self.buffer.clear();
108            }
109
110            fn flush(&mut self) -> Option<DocComment> {
111                if self.buffer.is_empty() {
112                    return None;
113                }
114
115                let comment = self
116                    .buffer
117                    .iter()
118                    .map(|(token, _)| match token.token_type() {
119                        TokenType::SingleLineComment { comment } => {
120                            format!("--{}", comment)
121                        }
122                        _ => unreachable!(),
123                    })
124                    .collect::<Vec<_>>()
125                    .join("\n");
126
127                let doc_comment = Some(DocComment::new(
128                    comment,
129                    self.buffer.first().unwrap().0.start_position().bytes(),
130                    self.buffer.last().unwrap().0.end_position().line() + 1,
131                    self.file_id,
132                    self.relative_path.to_owned(),
133                    self.buffer.last().unwrap().1.as_ref().cloned(),
134                ));
135
136                self.clear();
137
138                doc_comment
139            }
140
141            fn finish(mut self) -> Vec<DocComment> {
142                if let Some(doc_comment) = self.flush() {
143                    self.doc_comments.push(doc_comment);
144                }
145
146                self.doc_comments
147            }
148        }
149
150        impl Visitor for Collector<'_> {
151            fn visit_stmt(&mut self, stmt: &Stmt) {
152                let surrounding_trivia = stmt.surrounding_trivia().0;
153                for trivia in surrounding_trivia {
154                    self.scan(trivia.clone(), Some(stmt.clone()));
155                }
156            }
157
158            fn visit_last_stmt(&mut self, stmt: &LastStmt) {
159                let stmt = stmt.clone();
160                let surrounding_trivia = stmt.surrounding_trivia().0;
161                for trivia in surrounding_trivia {
162                    self.scan(trivia.clone(), None);
163                }
164            }
165
166            fn visit_eof(&mut self, stmt: &TokenReference) {
167                let surrounding_trivia = stmt.surrounding_trivia().0;
168                for trivia in surrounding_trivia {
169                    self.scan(trivia.clone(), None);
170                }
171            }
172        }
173
174        let mut collector = Collector::new(file_id, &relative_path);
175
176        collector.visit_ast(&ast);
177
178        let doc_comments = collector.finish();
179
180        Ok(Self {
181            doc_comments,
182            file_id,
183        })
184    }
185
186    pub fn parse(&'a self) -> Result<(Vec<DocEntry>, Vec<Tag>), Error> {
187        let (doc_entries, errors): (Vec<_>, Vec<_>) = self
188            .doc_comments
189            .iter()
190            .map(DocEntry::parse)
191            .partition(Result::is_ok);
192
193        let (doc_entries, tags): (Vec<_>, Vec<_>) =
194            doc_entries.into_iter().map(Result::unwrap).unzip();
195
196        let tags: Vec<Tag> = tags.into_iter().flatten().collect();
197
198        let errors: Diagnostics = errors
199            .into_iter()
200            .map(Result::unwrap_err)
201            .flat_map(Diagnostics::into_iter)
202            .collect::<Vec<_>>()
203            .into();
204
205        if errors.is_empty() {
206            Ok((doc_entries, tags))
207        } else {
208            Err(Error::ParseErrors(errors))
209        }
210    }
211}