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