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