cairo_lang_doc/
db.rs

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
use cairo_lang_defs::db::DefsGroup;
use cairo_lang_parser::utils::SimpleParserDatabase;
use cairo_lang_syntax::node::db::SyntaxGroup;
use cairo_lang_syntax::node::kind::SyntaxKind;
use cairo_lang_utils::Upcast;
use itertools::Itertools;

use crate::documentable_item::DocumentableItemId;
use crate::markdown::cleanup_doc_markdown;

#[salsa::query_group(DocDatabase)]
pub trait DocGroup: Upcast<dyn DefsGroup> + Upcast<dyn SyntaxGroup> + SyntaxGroup {
    // TODO(mkaput): Add tests.
    // TODO(mkaput): Support #[doc] attribute. This will be a bigger chunk of work because it would
    //   be the best to convert all /// comments to #[doc] attrs before processing items by plugins,
    //   so that plugins would get a nice and clean syntax of documentation to manipulate further.
    /// Gets the documentation above an item definition.
    fn get_item_documentation(&self, item_id: DocumentableItemId) -> Option<String>;

    // TODO(mkaput): Add tests.
    /// Gets the signature of an item (i.e., item without its body).
    fn get_item_signature(&self, item_id: DocumentableItemId) -> String;
}

fn get_item_documentation(db: &dyn DocGroup, item_id: DocumentableItemId) -> Option<String> {
    // Get the text of the item (trivia + definition)
    let doc = item_id.stable_location(db.upcast()).syntax_node(db.upcast()).get_text(db.upcast());

    // Only get the doc comments (start with `///` or `//!`) above the function.
    let doc = doc
        .lines()
        .take_while_ref(|line| {
            !line.trim_start().chars().next().map_or(false, |c| c.is_alphabetic())
        })
        .filter_map(|line| {
            // Remove indentation.
            let dedent = line.trim_start();
            // Check if this is a doc comment.
            for prefix in ["///", "//!"] {
                if let Some(content) = dedent.strip_prefix(prefix) {
                    // TODO(mkaput): The way how removing this indentation is performed is probably
                    //   wrong. The code should probably learn how many spaces are used at the first
                    //   line of comments block, and then remove the same amount of spaces in the
                    //   block, instead of assuming just one space.
                    // Remove inner indentation if one exists.
                    return Some(content.strip_prefix(' ').unwrap_or(content));
                }
            }
            None
        })
        .join("\n");

    // Cleanup the markdown.
    let doc = cleanup_doc_markdown(doc);

    // Nullify empty or just-whitespace documentation strings as they are not useful.
    (!doc.trim().is_empty()).then_some(doc)
}

fn get_item_signature(db: &dyn DocGroup, item_id: DocumentableItemId) -> String {
    let syntax_node = item_id.stable_location(db.upcast()).syntax_node(db.upcast());
    let definition = match syntax_node.green_node(db.upcast()).kind {
        SyntaxKind::ItemConstant
        | SyntaxKind::TraitItemFunction
        | SyntaxKind::ItemTypeAlias
        | SyntaxKind::ItemImplAlias => syntax_node.clone().get_text_without_trivia(db.upcast()),
        SyntaxKind::FunctionWithBody | SyntaxKind::ItemExternFunction => {
            let children = db.get_children(syntax_node);
            children[1..]
                .iter()
                .map_while(|node| {
                    let kind = node.kind(db.upcast());
                    (kind != SyntaxKind::ExprBlock
                        && kind != SyntaxKind::ImplBody
                        && kind != SyntaxKind::TraitBody)
                        .then_some(
                            if kind == SyntaxKind::VisibilityPub
                                || kind == SyntaxKind::TerminalExtern
                            {
                                node.clone().get_text_without_trivia(db.upcast()).trim().to_owned()
                                    + " "
                            } else {
                                node.clone()
                                    .get_text_without_trivia(db.upcast())
                                    .lines()
                                    .map(|line| line.trim())
                                    .collect::<Vec<&str>>()
                                    .join("")
                            },
                        )
                })
                .collect::<Vec<String>>()
                .join("")
        }
        SyntaxKind::ItemEnum | SyntaxKind::ItemExternType | SyntaxKind::ItemStruct => db
            .get_children(syntax_node)
            .iter()
            .skip(1)
            .map(|node| node.clone().get_text(db.upcast()))
            .collect::<Vec<String>>()
            .join(""),
        SyntaxKind::ItemTrait | SyntaxKind::ItemImpl => {
            let children = db.get_children(syntax_node);
            children[1..]
                .iter()
                .enumerate()
                .map_while(|(index, node)| {
                    let kind = node.kind(db.upcast());
                    if kind != SyntaxKind::ImplBody && kind != SyntaxKind::TraitBody {
                        let text = node
                            .clone()
                            .get_text_without_trivia(db.upcast())
                            .lines()
                            .map(|line| line.trim())
                            .collect::<Vec<&str>>()
                            .join("");

                        Some(if index == 0 || kind == SyntaxKind::WrappedGenericParamList {
                            text
                        } else {
                            " ".to_owned() + &text
                        })
                    } else {
                        None
                    }
                })
                .collect::<Vec<String>>()
                .join("")
        }
        _ => "".to_owned(),
    };
    fmt(definition)
}

/// Run Cairo formatter over code with extra post-processing that is specific to signatures.
fn fmt(code: String) -> String {
    let code = cairo_lang_formatter::format_string(&SimpleParserDatabase::default(), code);

    code
        // Trim any whitespace that formatter tends to leave.
        .trim_end()
        // Trim trailing semicolons, that are present in trait/impl functions, constants, etc.
        // and that formatter tends to put in separate line.
        .trim_end_matches("\n;")
        .to_owned()
}