Skip to main content

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