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#[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 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}