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}