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
// Copyright (c) ZeroC, Inc.

//! This module pulls in the parsing code generated by LALRPOP and contains private helper functions used by it.
//!
//! While many of these functions could be written directly into the parser rules, we implement them here instead, to
//! keep the rules focused on grammar instead of implementation details, making the grammar easier to read and modify.

use crate::grammar::{DocComment, Message, MessageComponent, Overview};
use crate::slice_file::{Location, Span};
use lalrpop_util::lalrpop_mod;

// Place the code generated by LALRPOP into a submodule named 'lalrpop'.
lalrpop_mod!(
    #[allow(unused, clippy::all)] // LALRPOP generates stuff we don't use, and isn't worth linting.
    pub lalrpop,
    "/parsers/comments/grammar.rs"
);

// Helper macro for storing parsed tags inside the correct field of a doc comment,
// and extending the doc comment's span to the end of the new tag.
macro_rules! append_tag_to_comment {
    ($comment:ident, $field:ident, $block:expr) => {{
        $comment.span.end = $block.span.end;
        $comment.$field.push($block);
        $comment
    }};
}

use append_tag_to_comment; // To let LALRPOP use the macro.

// Grammar Rule Functions

/// Creates a new doc comment with the specified overview and everything else empty.
fn create_doc_comment(overview: Option<Overview>, start: Location, file: &str) -> DocComment {
    // We subtract 3 from the start of the comment to account for the leading "///" that is always present.
    // This span is automatically extended as more constructs are parsed.
    let mut span = Span::new(start, start, file);
    span.start.col -= 3;

    // If an overview is present, extend the comment's span to include the overview.
    if let Some(overview_field) = &overview {
        span.end = overview_field.span.end;
    }

    DocComment {
        overview,
        params: Vec::new(),
        returns: Vec::new(),
        throws: Vec::new(),
        see: Vec::new(),
        span,
    }
}

/// Creates a string representing a Slice identifier that can be relatively or globally scoped.
fn get_scoped_identifier_string<'a>(first: &'a str, mut others: Vec<&'a str>, is_globally_scoped: bool) -> String {
    others.insert(0, first);
    if is_globally_scoped {
        others.insert(0, ""); // Gives a leading "::" when we `join`.
    }
    others.join("::")
}

/// Removes any leading whitespace from the inline part of the message, then combines it with any following lines.
fn construct_section_message(inline_message: Option<Message>, message_lines: Option<Message>) -> Message {
    let mut message_lines = message_lines.unwrap_or_default();

    if let Some(mut message) = inline_message {
        // Remove any leading whitespace from the inline portion of the message.
        if let Some(MessageComponent::Text(text)) = message.first_mut() {
            *text = text.trim_start().to_owned();
        }

        // Add a newline to the end of the inline message.
        message.push(MessageComponent::Text("\n".to_owned()));

        // Combine the 2 messages together, with the inline portion at the front.
        message.append(&mut message_lines);
        message_lines = message;
    }

    message_lines
}

/// Removes any common leading whitespace from the provided lines and returns the result.
/// Each element in the vector represents one line of the message.
/// `None` means the line existed but was empty, `Some(message)` means the line had a message.
fn sanitize_message_lines(lines: Vec<Option<Message>>) -> Message {
    // First compute the amount of leading whitespace that is common to every line.
    let mut common_leading_whitespace = usize::MAX;
    for line in &lines {
        // We only check lines that have a message on them (eg: they're non-empty).
        if let Some(message) = &line {
            // To check the start of the line, we check the first message component.
            // It's safe to unwrap because we know the line was non-empty in this block.
            match message.first().unwrap() {
                MessageComponent::Text(text) => {
                    // Determine how many whitespace characters are at the beginning of this line,
                    // then take the minimum of this and the amount of whitespace on all the other lines so far.
                    let whitespace_index = text.find(|c: char| !c.is_whitespace()).unwrap_or_default();
                    common_leading_whitespace = std::cmp::min(whitespace_index, common_leading_whitespace);
                }
                MessageComponent::Link(_) => {
                    // If a line starts with a link, the common leading whitespace must be 0.
                    // We set this, then exit the loop, since the loop won't find less than 0 whitespace characters.
                    common_leading_whitespace = 0;
                    break;
                }
            }
        }
    }

    // Now that we know the common leading whitespace, we iterate through the lines again and remove the whitespace.
    lines
        .into_iter()
        .flat_map(|line| match line {
            // If the message had text, we remove the common leading whitespace and append a newline at the end.
            Some(mut message) => {
                if let MessageComponent::Text(text) = message.first_mut().unwrap() {
                    text.replace_range(..common_leading_whitespace, "");
                }
                message.push(MessageComponent::Text("\n".to_owned()));
                message
            }

            // If the line was empty, we create a new message that only contains a newline character.
            None => vec![MessageComponent::Text("\n".to_owned())],
        })
        .collect()
}