1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
// sphinxcontrib_rust - Sphinx extension for the Rust programming language
// Copyright (C) 2024  Munir Contractor
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! Library for the sphinx-rustdocgen executable.
//!
//! It consists of functions to extract content from the AST and
//! to write the content to an RST or MD file. The crate is tested on itself,
//! so all the documentation in the crate is in RST. The tests for Markdown
//! are done on the dependencies.

// pub(crate) mainly to test re-exports
pub(crate) mod directives;
mod formats;
mod nodes;
mod utils;

use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::{Path, PathBuf};

use serde::Deserialize;

use crate::directives::{CrateDirective, DirectiveVisibility, ExecutableDirective};
use crate::formats::Format;
// pub(crate) mainly to test re-exports
pub(crate) use crate::utils::{check_for_manifest, SourceCodeFile};

#[derive(Clone, Debug, Deserialize)]
pub struct Configuration {
    crate_name: String,
    crate_dir: PathBuf,
    doc_dir: PathBuf,
    #[serde(default)]
    force: bool,
    #[serde(default)]
    format: Format,
    #[serde(default)]
    visibility: DirectiveVisibility,
    strip_src: bool,
}

impl Configuration {
    fn get_canonical_crate_dir(&self) -> PathBuf {
        // Canonicalize, which also checks that it exists.
        let crate_dir = match self.crate_dir.canonicalize() {
            Ok(d) => d,
            Err(e) => panic!("Could not find directory {}", e),
        };
        if !crate_dir.is_dir() {
            panic!("{} is not a directory", crate_dir.to_str().unwrap());
        }
        crate_dir
    }
}

/// Runtime version of the configuration after validation and normalizing
struct RuntimeConfiguration {
    /// The name of the crate in the configuration.
    crate_name: String,
    /// The crate's root directory, the one which contains ``Cargo.toml``.
    crate_dir: PathBuf,
    /// The crate's src/ directory, if one is found and ``strip_src`` is true.
    src_dir: Option<PathBuf>,
    /// The directory under which to write the documents.
    doc_dir: PathBuf,
    /// Whether to rewrite all the documents, even the ones that are unchanged.
    force: bool,
    /// The format of the docstrings.
    format: Format,
    /// Only document items with visibility less than this.
    max_visibility: DirectiveVisibility,
    /// The executables within the crate that will be documented.
    executables: Vec<SourceCodeFile>,
    /// The crate's library to document, if any.
    lib: Option<SourceCodeFile>,
}

impl RuntimeConfiguration {
    /// Write a documentation file for the provided source file path and content
    ///
    /// Args:
    ///     :source_file_path: The path of the source file corresponding to the
    ///         content.
    ///     :content_fn: A function to extract the content for the file.
    fn write_doc_file<F: for<'a> FnOnce(&'a Format, &'a DirectiveVisibility) -> Vec<String>>(
        &self,
        source_file_path: &Path,
        content_fn: F,
    ) {
        let rel_path = source_file_path
            .strip_prefix(self.src_dir.as_ref().unwrap_or(&self.crate_dir))
            .unwrap_or(source_file_path);

        // For mod.rs files, the output file name is the parent directory name.
        // Otherwise, it is same as the file name.
        let mut doc_file = if rel_path.ends_with("mod.rs") {
            rel_path.parent().unwrap().to_owned()
        }
        else {
            rel_path
                .parent()
                .unwrap()
                .join(rel_path.file_stem().unwrap())
        };

        // Add the extension for the file.
        doc_file.set_extension(self.format.extension());

        // Convert to absolute path.
        // Cannot use canonicalize here since it will error.
        let doc_file = self.doc_dir.join(doc_file);

        // Create the directories for the output document.
        create_dir_all(doc_file.parent().unwrap()).unwrap();

        // If file doesn't exist or the module file has been modified since the
        // last modification of the doc file, create/truncate it and rebuild the
        // documentation.
        if self.force
            || !doc_file.exists()
            || doc_file.metadata().unwrap().modified().unwrap()
                < source_file_path.metadata().unwrap().modified().unwrap()
        {
            log::debug!("Writing docs to file {}", doc_file.to_str().unwrap());
            let mut doc_file = File::create(doc_file).unwrap();
            for line in content_fn(&self.format, &self.max_visibility) {
                writeln!(&mut doc_file, "{line}").unwrap();
            }
        }
        else {
            log::debug!("Docs are up to date")
        }
    }
}

impl From<Configuration> for RuntimeConfiguration {
    fn from(config: Configuration) -> Self {
        // Canonicalize, which also checks that it exists.
        let crate_dir = config.get_canonical_crate_dir();

        // Check if the crate dir contains Cargo.toml
        // Also, check parent to provide backwards compatibility for src/ paths.
        let (crate_dir, manifest) =
            match check_for_manifest(vec![&crate_dir, crate_dir.parent().unwrap()]) {
                None => panic!(
                    "Could not find Cargo.toml in {} or its parent directory",
                    crate_dir.to_str().unwrap()
                ),
                Some(m) => m,
            };
        let executables = manifest.executable_files(&crate_dir);
        let lib = manifest.lib_file(&crate_dir);

        // The output docs currently strip out the src from any docs. To prevent
        // things from breaking, that behavior is preserved. It may cause issues
        // for crates that have a src dir and also files outside of it. However,
        // that will likely be rare. Hence, the new configuration option.
        let src_dir = crate_dir.join("src");
        let src_dir = if src_dir.is_dir() && config.strip_src {
            Some(src_dir)
        }
        else {
            None
        };

        // Add the crate's directory under the doc dir and create it.
        let doc_dir = config.doc_dir.join(&config.crate_name);
        create_dir_all(&doc_dir).unwrap();

        RuntimeConfiguration {
            crate_dir,
            crate_name: config.crate_name,
            src_dir,
            doc_dir: doc_dir.canonicalize().unwrap(),
            force: config.force,
            format: config.format,
            max_visibility: config.visibility,
            executables,
            lib,
        }
    }
}

// noinspection DuplicatedCode
/// Traverse the crate and extract the docstrings for the items.
///
/// Args:
///     :config: The configuration for the crate.
pub fn traverse_crate(config: Configuration) {
    let runtime: RuntimeConfiguration = config.into();

    log::debug!(
        "Extracting docs for crate {} from {}",
        &runtime.crate_name,
        runtime.crate_dir.to_str().unwrap()
    );
    log::debug!(
        "Generated docs will be stored in {}",
        runtime.doc_dir.to_str().unwrap()
    );

    if let Some(file) = &runtime.lib {
        let mut lib = CrateDirective::new(file);
        lib.filter_items(&runtime.max_visibility);

        // TODO: Remove the cloning here
        let mut modules = lib.file_directives.modules.clone();
        while let Some(module) = modules.pop() {
            for submodule in &module.file_directives.modules {
                modules.push(submodule.clone());
            }

            runtime.write_doc_file(&module.source_code_file.path.clone(), |f, v| {
                module.text(f, v)
            });
        }

        runtime.write_doc_file(&file.path, |f, v| lib.text(f, v));
    }

    for file in &runtime.executables {
        let mut exe = ExecutableDirective::new(file);
        exe.filter_items(&runtime.max_visibility);

        let mut modules = exe.0.file_directives.modules.clone();
        while let Some(module) = modules.pop() {
            for submodule in &module.file_directives.modules {
                modules.push(submodule.clone());
            }

            runtime.write_doc_file(&module.source_code_file.path.clone(), |f, v| {
                module.text(f, v)
            });
        }

        runtime.write_doc_file(&file.path, |f, v| exe.text(f, v));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_self() {
        // Test just extracts the documents for the current crate. This avoids
        // creating unnecessary test files when the source code itself can be
        // used.
        traverse_crate(Configuration {
            crate_name: String::from("sphinx-rustdocgen"),
            crate_dir: Path::new(".").to_owned(),
            doc_dir: Path::new("../docs/crates").to_owned(),
            format: Format::Rst,
            visibility: DirectiveVisibility::Pvt,
            force: true,
            strip_src: true,
        })
    }

    #[test]
    fn test_markdown() {
        traverse_crate(Configuration {
            crate_name: String::from("test_crate"),
            crate_dir: Path::new("../tests/test_crate").to_owned(),
            doc_dir: Path::new("../tests/test_crate/docs/crates").to_owned(),
            format: Format::Md,
            visibility: DirectiveVisibility::Pvt,
            force: true,
            strip_src: true,
        })
    }
}