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}