libmoonwave/
lib.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    io,
4    path::{self, Path, PathBuf},
5};
6
7use fs_err as fs;
8
9use anyhow::bail;
10use codespan_reporting::{
11    diagnostic::Diagnostic as CodeSpanDiagnostic,
12    files::SimpleFiles,
13    term::{
14        self,
15        termcolor::{ColorChoice, StandardStream},
16    },
17};
18
19use diagnostic::{Diagnostic, Diagnostics};
20use doc_comment::DocComment;
21use doc_entry::{ClassDocEntry, DocEntry, FunctionDocEntry, PropertyDocEntry, TypeDocEntry};
22use pathdiff::diff_paths;
23use serde::Serialize;
24
25use tags::{validate_global_tags, Tag};
26use walkdir::{self, WalkDir};
27
28mod cli;
29mod diagnostic;
30mod doc_comment;
31mod doc_entry;
32pub mod error;
33pub mod realm;
34mod serde_util;
35pub mod source_file;
36mod span;
37mod tags;
38
39pub use cli::*;
40
41use error::Error;
42use source_file::SourceFile;
43
44/// The class struct that is used in the main output, which owns its members
45#[derive(Debug, Serialize)]
46struct OutputClass<'a> {
47    functions: Vec<FunctionDocEntry<'a>>,
48    properties: Vec<PropertyDocEntry<'a>>,
49    types: Vec<TypeDocEntry<'a>>,
50
51    #[serde(flatten)]
52    class: ClassDocEntry<'a>,
53}
54
55type CodespanFilesPaths = (PathBuf, usize);
56
57pub fn generate_docs_from_path(input_path: &Path, base_path: &Path) -> anyhow::Result<()> {
58    let (codespan_files, files) = find_files(input_path)?;
59
60    let mut errors: Vec<Error> = Vec::new();
61    let mut source_files: Vec<SourceFile> = Vec::new();
62
63    for (file_path, file_id) in files {
64        let source = codespan_files.get(file_id).unwrap().source();
65
66        let human_path = match diff_paths(&file_path, base_path) {
67            Some(relative_path) => relative_path,
68            None => file_path,
69        };
70
71        let human_path = human_path
72            .to_string_lossy()
73            .to_string()
74            .replace(path::MAIN_SEPARATOR, "/");
75
76        match SourceFile::from_str(source, file_id, human_path) {
77            Ok(source_file) => source_files.push(source_file),
78            Err(error) => errors.push(error),
79        }
80    }
81
82    let (results, source_file_errors): (Vec<_>, Vec<_>) = source_files
83        .iter()
84        .map(SourceFile::parse)
85        .partition(Result::is_ok);
86
87    errors.extend(source_file_errors.into_iter().map(Result::unwrap_err));
88
89    let (entries, tags): (Vec<_>, Vec<_>) = results.into_iter().map(Result::unwrap).unzip();
90    let entries: Vec<DocEntry> = entries.into_iter().flatten().collect();
91    let tags: Vec<Tag> = tags.into_iter().flatten().collect();
92
93    let diagnostics = validate_global_tags(&tags);
94    if !diagnostics.is_empty() {
95        errors.push(Error::ParseErrors(Diagnostics::from(diagnostics)));
96    }
97
98    match into_classes(entries) {
99        Ok(classes) => {
100            if errors.is_empty() {
101                println!("{}", serde_json::to_string_pretty(&classes)?);
102            }
103        }
104        Err(diagnostics) => errors.push(Error::ParseErrors(diagnostics)),
105    }
106
107    if !errors.is_empty() {
108        let count_errors = errors.len();
109
110        report_errors(errors, &codespan_files);
111
112        if count_errors == 1 {
113            bail!("aborting due to diagnostic error");
114        } else {
115            bail!("aborting due to {} diagnostic errors", count_errors);
116        }
117    }
118
119    Ok(())
120}
121
122fn into_classes<'a>(entries: Vec<DocEntry<'a>>) -> Result<Vec<OutputClass<'a>>, Diagnostics> {
123    let mut map: BTreeMap<String, OutputClass<'a>> = BTreeMap::new();
124
125    let (classes, entries): (Vec<_>, Vec<_>) = entries
126        .into_iter()
127        .partition(|entry| matches!(*entry, DocEntry::Class(_)));
128
129    let mut alias_map: HashMap<String, String> = HashMap::new();
130
131    for entry in classes {
132        if let DocEntry::Class(class) = entry {
133            let (functions, properties, types) = Default::default();
134
135            let class_name = class.name.to_owned();
136            let __index = class.__index.to_owned();
137
138            map.insert(
139                class_name.clone(),
140                OutputClass {
141                    class,
142                    functions,
143                    properties,
144                    types,
145                },
146            );
147
148            alias_map.insert(
149                format!("{}.{}", class_name.clone(), __index),
150                class_name.clone(),
151            );
152            alias_map.insert(class_name.clone(), class_name);
153        }
154    }
155
156    let mut diagnostics: Vec<Diagnostic> = Vec::new();
157
158    let mut emit_diagnostic = |source: &DocComment, within: &str| {
159        diagnostics.push(source.diagnostic(format!(
160            "This entry's parent class \"{}\" is missing a doc entry",
161            within
162        )));
163    };
164
165    for entry in entries {
166        match entry {
167            DocEntry::Function(entry) => match alias_map.get(&entry.within) {
168                Some(class_name) => map.get_mut(class_name).unwrap().functions.push(entry),
169                None => emit_diagnostic(entry.source, &entry.within),
170            },
171            DocEntry::Property(entry) => match alias_map.get(&entry.within) {
172                Some(class_name) => map.get_mut(class_name).unwrap().properties.push(entry),
173                None => emit_diagnostic(entry.source, &entry.within),
174            },
175            DocEntry::Type(entry) => match alias_map.get(&entry.within) {
176                Some(class_name) => map.get_mut(class_name).unwrap().types.push(entry),
177                None => emit_diagnostic(entry.source, &entry.within),
178            },
179            _ => unreachable!(),
180        };
181    }
182
183    if diagnostics.is_empty() {
184        Ok(map.into_iter().map(|(_, value)| value).collect())
185    } else {
186        Err(Diagnostics::from(diagnostics))
187    }
188}
189
190fn find_files(
191    path: &Path,
192) -> Result<(SimpleFiles<String, String>, Vec<CodespanFilesPaths>), io::Error> {
193    let mut codespan_files = SimpleFiles::new();
194    let mut files: Vec<CodespanFilesPaths> = Vec::new();
195
196    let walker = WalkDir::new(path).follow_links(true).into_iter();
197    for entry in walker
198        .filter_map(|e| e.ok())
199        .filter(|e| e.file_type().is_file())
200        .filter(|e| {
201            matches!(
202                e.path().extension().and_then(|s| s.to_str()),
203                Some("lua") | Some("luau")
204            )
205        })
206    {
207        let path = entry.path();
208        let contents = fs::read_to_string(path)?;
209
210        let file_id = codespan_files.add(
211            // We need the separator to consistently be forward slashes for snapshot
212            // consistency across platforms
213            path.to_string_lossy().replace(path::MAIN_SEPARATOR, "/"),
214            contents,
215        );
216
217        files.push((path.to_path_buf(), file_id));
218    }
219
220    Ok((codespan_files, files))
221}
222
223fn report_errors(errors: Vec<Error>, codespan_files: &SimpleFiles<String, String>) {
224    let writer = StandardStream::stderr(ColorChoice::Auto);
225    let config = codespan_reporting::term::Config {
226        end_context_lines: usize::MAX,
227        ..Default::default()
228    };
229
230    for error in errors {
231        match error {
232            Error::ParseErrors(diagnostics) => {
233                for diagnostic in diagnostics.into_iter() {
234                    term::emit(
235                        &mut writer.lock(),
236                        &config,
237                        codespan_files,
238                        &CodeSpanDiagnostic::from(diagnostic),
239                    )
240                    .unwrap()
241                }
242            }
243            Error::FullMoonError(errors) => {
244                let text = errors
245                    .iter()
246                    .map(|(s, e)| format!("Full-Moon: {}\n    in {}", e, s))
247                    .collect::<Vec<String>>()
248                    .join("\n");
249
250                eprintln!("{}", text)
251            }
252        }
253    }
254}