sphinx_rustdocgen/directives/
module_directive.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//! Implementation of the ``rust:module`` directive
18
19use std::cmp::max;
20use std::collections::BTreeMap;
21use std::path::{Path, PathBuf};
22
23use syn::{ItemMod, Meta};
24
25use crate::directives::directive_options::{DirectiveOption, DirectiveVisibility, IndexEntryType};
26use crate::directives::{
27    extract_doc_from_attrs,
28    order_items,
29    Directive,
30    FileDirectives,
31    ImplDirective,
32    UseDirective,
33};
34use crate::formats::{Format, MdContent, MdDirective, RstContent, RstDirective};
35use crate::utils::SourceCodeFile;
36
37/// Struct to hold data for a module's documentation.
38#[derive(Clone, Debug)]
39pub struct ModuleDirective {
40    /// The full path to the module.
41    pub(crate) name: String,
42    /// The options for the module directive.
43    pub(crate) options: Vec<DirectiveOption>,
44    /// The docstring for the module.
45    pub(crate) content: Vec<String>,
46    /// The identifier of the module (i.e. the final portion of name).
47    pub(crate) ident: String,
48    /// The source code file for the module.
49    pub(crate) source_code_file: SourceCodeFile,
50    /// The directives within the crate's document file.
51    pub(crate) file_directives: FileDirectives,
52}
53
54/// Check if the module is a test module or not.
55#[inline]
56fn has_test_token(tokens: &str) -> bool {
57    tokens.split(',').any(|t| t.trim() == "test")
58}
59
60/// Find the file for the module under the provided directory.
61///
62/// Args:
63///     :module_ident: The identifier of the module.
64///     :directory: The directory under which to find the module's file.
65///
66/// Returns:
67///     A None value if neither ``<directory>/<module>.rs`` nor
68///     ``<directory>/<module>/mod.rs`` exist, otherwise a Some value
69///     for the path of the file.
70fn find_file_under_dir(module_ident: &str, directory: &Path) -> Option<PathBuf> {
71    // Check for <module>.rs in the directory.
72    let mut mod_file = directory.join(format!("{module_ident}.rs"));
73    if mod_file.is_file() {
74        return Some(mod_file);
75    }
76
77    // Check <module>/mod.rs in the directory.
78    mod_file = directory.join(module_ident).join("mod.rs");
79    if mod_file.is_file() {
80        return Some(mod_file);
81    }
82
83    None
84}
85
86/// Create a file path for the module's source code and documentation.
87///
88/// If the module is defined in its own file, the function will try to find the
89/// file and return its path. Otherwise, it will return a hypothetical path as
90/// if the module were defined in a ``mod.rs`` file in its own directory. This
91/// is mainly to prevent conflicts with existing files of the same name.
92fn get_module_file(module_ident: &str, parent_file: &SourceCodeFile) -> PathBuf {
93    if parent_file.path.is_dir() {
94        // Parent path is a directory. Check it or create a pseudo-path.
95        find_file_under_dir(module_ident, &parent_file.path)
96            .unwrap_or(parent_file.path.join(module_ident).join("mod.rs"))
97    }
98    else if parent_file.path.ends_with("mod.rs") {
99        // Parent module is in a mod.rs file. Check the same directory for the module.
100        let parent_dir = parent_file.path.parent().unwrap();
101        find_file_under_dir(module_ident, parent_dir)
102            .unwrap_or(parent_dir.join(module_ident).join("mod.rs"))
103    }
104    else {
105        // Parent is a file. Check under a directory with the same name as the parent,
106        // in the directory where the parent file exists.
107        let parent_dir = parent_file
108            .path
109            .parent()
110            .unwrap()
111            .join(parent_file.path.file_stem().unwrap());
112        find_file_under_dir(module_ident, &parent_dir)
113            .unwrap_or(parent_dir.join(module_ident).join("mod.rs"))
114    }
115}
116
117/// Determines if the module is a test module or not.
118fn is_test_module(item_mod: &ItemMod) -> bool {
119    // XXX: Find a better way to do this.
120    for attr in &item_mod.attrs {
121        if let Meta::List(meta) = &attr.meta {
122            if meta.path.segments.len() == 1
123                && meta.path.segments.first().unwrap().ident == "cfg"
124                && has_test_token(&meta.tokens.to_string())
125            {
126                return true;
127            }
128        }
129    }
130    false
131}
132
133impl ModuleDirective {
134    const DIRECTIVE_NAME: &'static str = "module";
135
136    /// Create a :rust:struct:`ModuleDirective` from the item, if the module is
137    /// not a test module.
138    ///
139    /// If the module's items are in a different file, the function will try to
140    /// find the appropriate file and parse it for the items. This is done
141    /// recursively for any submodules within the items.
142    ///
143    /// Args:
144    ///     :parent_path: The path of the module's parent module or the crate
145    ///         name.
146    ///     :item: The ``ItemMod`` parsed out by ``syn``.
147    ///
148    /// Returns:
149    ///     A ``Some`` value if the module is not a test module, otherwise
150    ///     ``None``.
151    pub(crate) fn from_item(parent_file: &SourceCodeFile, item: &ItemMod) -> Option<Self> {
152        if is_test_module(item) {
153            return None;
154        }
155
156        // Get the path for the module's file. If the module is defined inline,
157        // create a new pseudo-path that'll be used to determine the output file.
158        let source_code_file = SourceCodeFile {
159            path: get_module_file(&item.ident.to_string(), parent_file),
160            item: format!("{}::{}", &parent_file.item, item.ident),
161        };
162
163        // Get the items or parse them from the file
164        let mod_items = item.content.as_ref().map(|(_, items)| items);
165        let file_directives = FileDirectives::from_ast_items(
166            mod_items.unwrap_or(&source_code_file.ast().items),
167            &source_code_file,
168        );
169        let mut mod_attrs = item.attrs.clone();
170        mod_attrs.extend(source_code_file.ast().attrs);
171
172        Some(ModuleDirective {
173            name: source_code_file.item.clone(),
174            options: vec![
175                DirectiveOption::Index(IndexEntryType::Normal),
176                DirectiveOption::Vis(DirectiveVisibility::from(&item.vis)),
177            ],
178            content: extract_doc_from_attrs(&mod_attrs),
179            ident: item.ident.to_string(),
180            source_code_file,
181            file_directives,
182        })
183    }
184
185    /// Generate the text for the module's documentation file.
186    pub(crate) fn text(self, format: &Format, max_visibility: &DirectiveVisibility) -> Vec<String> {
187        let mut text = format.make_title(&format.make_inline_code(format!("mod {}", self.ident)));
188        text.extend(format.format_directive(self, max_visibility));
189        text
190    }
191
192    /// Filter out items that will not be documented due to visibility
193    /// restrictions.
194    ///
195    /// The function recurses through all submodules and identifies items that
196    /// will not be documented due to the configured visibility. As part of the
197    /// filtering, it will also identify any re-exports that should be inlined.
198    ///
199    /// Returns:
200    ///     A vec of directives that will not be documented.
201    // noinspection DuplicatedCode
202    pub(crate) fn filter_items(&mut self, max_visibility: &DirectiveVisibility) -> Vec<Directive> {
203        let mut excluded_items = vec![];
204        self.file_directives.modules.retain_mut(|module| {
205            // Get any items that the submodules won't document.
206            excluded_items.extend(module.filter_items(max_visibility));
207            // Keep any submodule that meets the visibility criteria.
208            module.directive_visibility() <= max_visibility
209        });
210
211        // Avoids issue with mutable and immutable references at the same time.
212        let directive_visibility = *self.directive_visibility();
213
214        // Gather all re-exports that might be documented.
215        let mut reexports = vec![];
216        for use_ in &mut self.file_directives.uses {
217            if use_.reexport.is_some() && use_.directive_visibility() <= max_visibility {
218                reexports.push(use_)
219            }
220        }
221
222        if &directive_visibility > max_visibility {
223            // The module won't be documented. Check for any items that should
224            // be documented.
225            while let Some(item) = self.file_directives.items.pop() {
226                if item.directive_visibility() <= max_visibility {
227                    excluded_items.push(item);
228                }
229            }
230            // Check if any of the excluded items are reexported by this module.
231            // Possible when a pub item is in a pvt module, reexported as
232            // pub(crate) by a pub(crate) module, and then reexported again as
233            // pub by the crate. In this case, we need to change the parent of
234            // the excluded item before returning it.
235            'item_loop: for item in excluded_items.iter_mut() {
236                for reexport in &reexports {
237                    if reexport.contains(item.name()) {
238                        item.change_parent(&self.name);
239                        continue 'item_loop;
240                    }
241                }
242            }
243            return excluded_items;
244        }
245
246        // If the module will be documented, claim ownership of any reexports
247        // from the excluded items and return the rest of them.
248        let mut not_documented = vec![];
249        let mut inlined = BTreeMap::new();
250        'item_loop: for mut item in excluded_items {
251            for reexport in &mut reexports {
252                if reexport.contains(item.name()) {
253                    if !matches!(item, Directive::Impl(_)) {
254                        let (k, v) = reexport.inline(item.name()).unwrap();
255                        inlined.insert(k, v);
256                    }
257                    item.change_parent(&self.name);
258                    item.add_content(reexport.content.clone());
259                    self.file_directives.items.push(item);
260                    continue 'item_loop;
261                }
262            }
263            not_documented.push(item);
264        }
265        self.file_directives
266            .uses
267            .push(UseDirective::for_use_paths(inlined));
268
269        not_documented
270    }
271
272    /// Return the visibility of this directive.
273    pub(crate) fn directive_visibility(&self) -> &DirectiveVisibility {
274        if let DirectiveOption::Vis(v) = &self.options[1] {
275            return v;
276        }
277        unreachable!("Module: order of options changed")
278    }
279
280    /// Change the parent module of the module.
281    pub(crate) fn change_parent(&mut self, new_parent: &str) {
282        self.name = format!("{new_parent}::{}", self.ident);
283        for item in &mut self.file_directives.items {
284            item.change_parent(&self.name);
285        }
286    }
287
288    /// Collect all impl directives from self and any sub-modules.
289    pub(crate) fn collect_impls(&mut self) -> Vec<ImplDirective> {
290        let mut impls = vec![];
291        impls.append(&mut self.file_directives.impls);
292
293        for module in &mut self.file_directives.modules {
294            impls.extend(module.collect_impls())
295        }
296
297        impls
298    }
299
300    /// Attach impl directives to the appropriate struct, enum or trait
301    /// directive.
302    ///
303    /// Args:
304    ///     :impls: The impl directives to claim from.
305    ///
306    /// Returns:
307    ///     A vec of impl directives that were not claimed.
308    pub(crate) fn claim_impls(&mut self, impls: Vec<ImplDirective>) -> Vec<ImplDirective> {
309        self.file_directives.claim_impls(&self.name, impls)
310    }
311}
312
313impl RstDirective for ModuleDirective {
314    // noinspection DuplicatedCode
315    fn get_rst_text(self, level: usize, max_visibility: &DirectiveVisibility) -> Vec<String> {
316        // Do not filter for visibility here. Modules are always documented.
317        let content_indent = Self::make_content_indent(level);
318
319        // Create the directive for the module.
320        let mut text =
321            Self::make_rst_header(Self::DIRECTIVE_NAME, &self.name, &self.options, level);
322        text.extend(self.content.get_rst_text(&content_indent));
323
324        text.extend(Self::make_rst_toctree(
325            &content_indent,
326            "Modules",
327            Some(1),
328            self.file_directives
329                .modules
330                .iter()
331                .map(|m| format!("{}/{}", &self.ident, m.ident)),
332        ));
333
334        let mut reexports = vec![];
335        for use_ in self.file_directives.uses {
336            if use_.reexport.is_some() && use_.directive_visibility() <= max_visibility {
337                for path in use_.paths.values() {
338                    reexports.push((path.clone(), use_.content.clone()));
339                }
340            }
341            text.extend(use_.get_rst_text(level + 1, max_visibility));
342        }
343        text.extend(Self::make_rst_list(
344            &content_indent,
345            "Re-exports",
346            &reexports,
347        ));
348
349        for (name, item) in order_items(self.file_directives.items) {
350            text.extend(Self::make_rst_section(name, level, item, max_visibility));
351        }
352
353        text
354    }
355}
356
357impl MdDirective for ModuleDirective {
358    // noinspection DuplicatedCode
359    fn get_md_text(self, fence_size: usize, max_visibility: &DirectiveVisibility) -> Vec<String> {
360        // Do not filter for visibility here. Modules are always documented.
361        let fence = Self::make_fence(max(fence_size, 4));
362
363        // Create the directive for the module.
364        let mut text =
365            Self::make_md_header(Self::DIRECTIVE_NAME, &self.name, &self.options, &fence);
366        text.extend(self.content.get_md_text());
367
368        text.extend(Self::make_md_toctree(
369            3,
370            "Modules",
371            Some(1),
372            self.file_directives
373                .modules
374                .iter()
375                .map(|m| format!("{}/{}", &self.ident, m.ident)),
376        ));
377
378        let mut reexports = vec![];
379        for use_ in self.file_directives.uses {
380            if use_.reexport.is_some() && use_.directive_visibility() <= max_visibility {
381                for path in use_.paths.values() {
382                    reexports.push((path.clone(), use_.content.clone()));
383                }
384            }
385            text.extend(use_.get_md_text(3, max_visibility));
386        }
387        text.extend(Self::make_md_list(3, "Re-exports", &reexports));
388
389        for (name, item) in order_items(self.file_directives.items) {
390            text.extend(Self::make_md_section(
391                name,
392                fence_size,
393                item,
394                max_visibility,
395            ));
396        }
397        text.push(fence);
398
399        text
400    }
401
402    fn fence_size(&self) -> usize {
403        Self::calc_fence_size(&self.file_directives.items)
404    }
405}