sphinx_rustdocgen/lib.rs
1// sphinxcontrib_rust - Sphinx extension for the Rust programming language
2// Copyright (C) 2024 Munir Contractor
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Library for the sphinx-rustdocgen executable.
18//!
19//! It consists of functions to extract content from the AST and
20//! to write the content to an RST or MD file. The crate is tested on itself,
21//! so all the documentation in the crate is in RST. The tests for Markdown
22//! are done on the dependencies.
23
24// pub(crate) mainly to test re-exports
25pub(crate) mod directives;
26mod formats;
27mod nodes;
28mod utils;
29
30use std::fs::{create_dir_all, File};
31use std::io::Write;
32use std::path::{Path, PathBuf};
33
34use serde::Deserialize;
35
36use crate::directives::{CrateDirective, DirectiveVisibility, ExecutableDirective};
37use crate::formats::Format;
38// pub(crate) mainly to test re-exports
39pub(crate) use crate::utils::{check_for_manifest, SourceCodeFile};
40
41/// Struct to hold the deserialized configuration passed from Python.
42#[derive(Clone, Debug, Deserialize)]
43pub struct Configuration {
44 /// The name of the crate.
45 crate_name: String,
46 /// The directory containing the Cargo.toml file for the crate.
47 crate_dir: PathBuf,
48 /// The directory under which to create the crate's documentation.
49 /// A new directory is created under this directory for the crate.
50 doc_dir: PathBuf,
51 /// Rebuild document for all files, even if it has not changed.
52 #[serde(default)]
53 force: bool,
54 /// The format to use for the output.
55 #[serde(default)]
56 format: Format,
57 /// The required visibility of the items to include.
58 #[serde(default)]
59 visibility: DirectiveVisibility,
60 /// Whether to remove the src/ directory when generating the docs or not.
61 strip_src: bool,
62}
63
64impl Configuration {
65 /// Canonicalize the crate directory and return it.
66 fn get_canonical_crate_dir(&self) -> PathBuf {
67 // Canonicalize, which also checks that it exists.
68 let crate_dir = match self.crate_dir.canonicalize() {
69 Ok(d) => d,
70 Err(e) => panic!("Could not find directory {}", e),
71 };
72 if !crate_dir.is_dir() {
73 panic!("{} is not a directory", crate_dir.to_str().unwrap());
74 }
75 crate_dir
76 }
77}
78
79/// Runtime version of the configuration after validation and normalizing.
80struct RuntimeConfiguration {
81 /// The name of the crate in the configuration.
82 crate_name: String,
83 /// The crate's root directory, the one which contains ``Cargo.toml``.
84 crate_dir: PathBuf,
85 /// The crate's src/ directory, if one is found and ``strip_src`` is true.
86 src_dir: Option<PathBuf>,
87 /// The directory under which to write the documents.
88 doc_dir: PathBuf,
89 /// Whether to rewrite all the documents, even the ones that are unchanged.
90 force: bool,
91 /// The format of the docstrings.
92 format: Format,
93 /// Only document items with visibility less than this.
94 max_visibility: DirectiveVisibility,
95 /// The executables within the crate that will be documented.
96 executables: Vec<SourceCodeFile>,
97 /// The crate's library to document, if any.
98 lib: Option<SourceCodeFile>,
99}
100
101impl RuntimeConfiguration {
102 /// Write a documentation file for the provided source file path and
103 /// content.
104 ///
105 /// Args:
106 /// :source_file_path: The path of the source file corresponding to the
107 /// content.
108 /// :content_fn: A function to extract the content for the file.
109 fn write_doc_file<F: for<'a> FnOnce(&'a Format, &'a DirectiveVisibility) -> Vec<String>>(
110 &self,
111 source_file_path: &Path,
112 content_fn: F,
113 ) {
114 let rel_path = source_file_path
115 .strip_prefix(self.src_dir.as_ref().unwrap_or(&self.crate_dir))
116 .unwrap_or(source_file_path);
117
118 // For mod.rs files, the output file name is the parent directory name.
119 // Otherwise, it is same as the file name.
120 let mut doc_file = if rel_path.ends_with("mod.rs") {
121 rel_path.parent().unwrap().to_owned()
122 }
123 else {
124 rel_path
125 .parent()
126 .unwrap()
127 .join(rel_path.file_stem().unwrap())
128 };
129
130 // Add the extension for the file.
131 doc_file.set_extension(self.format.extension());
132
133 // Convert to absolute path.
134 // Cannot use canonicalize here since it will error.
135 let doc_file = self.doc_dir.join(doc_file);
136
137 // Create the directories for the output document.
138 create_dir_all(doc_file.parent().unwrap()).unwrap();
139
140 // If file doesn't exist or the module file has been modified since the
141 // last modification of the doc file, create/truncate it and rebuild the
142 // documentation.
143 if self.force
144 || !doc_file.exists()
145 || doc_file.metadata().unwrap().modified().unwrap()
146 < source_file_path.metadata().unwrap().modified().unwrap()
147 {
148 log::debug!("Writing docs to file {}", doc_file.to_str().unwrap());
149 let mut doc_file = File::create(doc_file).unwrap();
150 for line in content_fn(&self.format, &self.max_visibility) {
151 writeln!(&mut doc_file, "{line}").unwrap();
152 }
153 }
154 else {
155 log::debug!("Docs are up to date")
156 }
157 }
158}
159
160impl From<Configuration> for RuntimeConfiguration {
161 /// Create a validated and normalized version of the
162 /// :rust:struct:`Configuration`.
163 fn from(config: Configuration) -> Self {
164 // Canonicalize, which also checks that it exists.
165 let crate_dir = config.get_canonical_crate_dir();
166
167 // Check if the crate dir contains Cargo.toml
168 // Also, check parent to provide backwards compatibility for src/ paths.
169 let (crate_dir, manifest) =
170 match check_for_manifest(vec![&crate_dir, crate_dir.parent().unwrap()]) {
171 None => panic!(
172 "Could not find Cargo.toml in {} or its parent directory",
173 crate_dir.to_str().unwrap()
174 ),
175 Some(m) => m,
176 };
177 let executables = manifest.executable_files(&crate_dir);
178 let lib = manifest.lib_file(&crate_dir);
179
180 // The output docs currently strip out the src from any docs. To prevent
181 // things from breaking, that behavior is preserved. It may cause issues
182 // for crates that have a src dir and also files outside of it. However,
183 // that will likely be rare. Hence, the new configuration option.
184 let src_dir = crate_dir.join("src");
185 let src_dir = if src_dir.is_dir() && config.strip_src {
186 Some(src_dir)
187 }
188 else {
189 None
190 };
191
192 // Add the crate's directory under the doc dir and create it.
193 let doc_dir = config.doc_dir.join(&config.crate_name);
194 create_dir_all(&doc_dir).unwrap();
195
196 RuntimeConfiguration {
197 crate_dir,
198 crate_name: config.crate_name,
199 src_dir,
200 doc_dir: doc_dir.canonicalize().unwrap(),
201 force: config.force,
202 format: config.format,
203 max_visibility: config.visibility,
204 executables,
205 lib,
206 }
207 }
208}
209
210// noinspection DuplicatedCode
211/// Traverse the crate and extract the docstrings for the items.
212///
213/// Args:
214/// :config: The configuration for the crate.
215pub fn traverse_crate(config: Configuration) {
216 let runtime: RuntimeConfiguration = config.into();
217
218 log::debug!(
219 "Extracting docs for crate {} from {}",
220 &runtime.crate_name,
221 runtime.crate_dir.to_str().unwrap()
222 );
223 log::debug!(
224 "Generated docs will be stored in {}",
225 runtime.doc_dir.to_str().unwrap()
226 );
227
228 if let Some(file) = &runtime.lib {
229 let mut lib = CrateDirective::new(file);
230 lib.filter_items(&runtime.max_visibility);
231
232 // TODO: Remove the cloning here
233 let mut modules = lib.file_directives.modules.clone();
234 while let Some(module) = modules.pop() {
235 for submodule in &module.file_directives.modules {
236 modules.push(submodule.clone());
237 }
238
239 runtime.write_doc_file(&module.source_code_file.path.clone(), |f, v| {
240 module.text(f, v)
241 });
242 }
243
244 runtime.write_doc_file(&file.path, |f, v| lib.text(f, v));
245 }
246
247 for file in &runtime.executables {
248 let mut exe = ExecutableDirective::new(file);
249 exe.filter_items(&runtime.max_visibility);
250
251 let mut modules = exe.0.file_directives.modules.clone();
252 while let Some(module) = modules.pop() {
253 for submodule in &module.file_directives.modules {
254 modules.push(submodule.clone());
255 }
256
257 runtime.write_doc_file(&module.source_code_file.path.clone(), |f, v| {
258 module.text(f, v)
259 });
260 }
261
262 runtime.write_doc_file(&file.path, |f, v| exe.text(f, v));
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_self() {
272 // Test just extracts the documents for the current crate. This avoids
273 // creating unnecessary test files when the source code itself can be
274 // used.
275 traverse_crate(Configuration {
276 crate_name: String::from("sphinx-rustdocgen"),
277 crate_dir: Path::new(".").to_owned(),
278 doc_dir: Path::new("../docs/crates").to_owned(),
279 format: Format::Rst,
280 visibility: DirectiveVisibility::Pvt,
281 force: true,
282 strip_src: true,
283 })
284 }
285
286 #[test]
287 fn test_markdown() {
288 traverse_crate(Configuration {
289 crate_name: String::from("test_crate"),
290 crate_dir: Path::new("../tests/test_crate").to_owned(),
291 doc_dir: Path::new("../tests/test_crate/docs/crates").to_owned(),
292 format: Format::Md,
293 visibility: DirectiveVisibility::Pvt,
294 force: true,
295 strip_src: true,
296 })
297 }
298}