Skip to main content

graphcal_compiler/syntax/
comments.rs

1use crate::syntax::span::{Span, Spanned};
2
3/// The delimiter that starts a comment.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub(crate) enum CommentDelimiter {
6    /// `// ...`
7    Line,
8    /// `/// ...`
9    Doc,
10}
11
12impl CommentDelimiter {
13    #[must_use]
14    pub(crate) const fn lexeme(self) -> &'static str {
15        match self {
16            Self::Line => "//",
17            Self::Doc => "///",
18        }
19    }
20
21    #[must_use]
22    pub(crate) const fn len(self) -> usize {
23        self.lexeme().len()
24    }
25}
26
27/// The text after a comment delimiter, excluding the line ending.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub(crate) struct CommentBody(String);
30
31impl CommentBody {
32    #[must_use]
33    pub(crate) fn new(body: impl Into<String>) -> Self {
34        Self(body.into())
35    }
36
37    #[must_use]
38    fn as_str(&self) -> &str {
39        &self.0
40    }
41}
42
43/// A comment extracted from source text.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Comment {
46    delimiter: CommentDelimiter,
47    body: CommentBody,
48}
49
50impl Comment {
51    #[must_use]
52    pub(crate) const fn new(delimiter: CommentDelimiter, body: CommentBody) -> Self {
53        Self { delimiter, body }
54    }
55
56    /// Reconstruct the source lexeme without the trailing line ending.
57    #[must_use]
58    pub fn lexeme(&self) -> String {
59        format!("{}{}", self.delimiter.lexeme(), self.body.as_str())
60    }
61}
62
63/// A comment paired with its source span.
64pub type SpannedComment = Spanned<Comment>;
65
66/// A blank line represented by the span from the previous line ending through
67/// the line ending that completes the blank line.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct BlankLine {
70    span: Span,
71}
72
73impl BlankLine {
74    #[must_use]
75    pub(crate) const fn new(span: Span) -> Self {
76        Self { span }
77    }
78
79    #[must_use]
80    pub const fn span(self) -> Span {
81        self.span
82    }
83}
84
85/// Metadata extracted from source text for the formatter.
86#[derive(Debug, Clone, PartialEq, Eq, Default)]
87pub struct SourceMetadata {
88    /// All comments in source order.
89    comments: Vec<SpannedComment>,
90    /// Blank-line separators in source order.
91    blank_lines: Vec<BlankLine>,
92}
93
94impl SourceMetadata {
95    #[must_use]
96    pub fn comments(&self) -> &[SpannedComment] {
97        &self.comments
98    }
99
100    #[must_use]
101    pub fn blank_lines(&self) -> &[BlankLine] {
102        &self.blank_lines
103    }
104
105    pub(crate) fn push_comment(&mut self, comment: SpannedComment) {
106        self.comments.push(comment);
107    }
108
109    pub(crate) fn push_blank_line(&mut self, blank_line: BlankLine) {
110        self.blank_lines.push(blank_line);
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn metadata(source: &str) -> SourceMetadata {
119        let mut lexer = crate::syntax::lexer::Lexer::new(source);
120        while lexer.next_token().is_some() {}
121        assert_eq!(lexer.first_error_span(), None);
122        lexer.into_source_metadata()
123    }
124
125    #[test]
126    fn extract_line_comment() {
127        let source = "// hello world\nparam x = 1;";
128        let meta = metadata(source);
129        assert_eq!(meta.comments.len(), 1);
130        assert_eq!(meta.comments[0].value.lexeme(), "// hello world");
131        assert_eq!(meta.comments[0].span.offset(), 0);
132    }
133
134    #[test]
135    fn extract_doc_comment() {
136        let source = "/// doc comment\nparam x = 1;";
137        let meta = metadata(source);
138        assert_eq!(meta.comments.len(), 1);
139        assert_eq!(meta.comments[0].value.lexeme(), "/// doc comment");
140    }
141
142    #[test]
143    fn four_slashes_is_a_line_comment() {
144        let source = "//// not doc\nparam x = 1;";
145        let meta = metadata(source);
146        assert_eq!(meta.comments.len(), 1);
147        assert_eq!(meta.comments[0].value.lexeme(), "//// not doc");
148    }
149
150    #[test]
151    fn extract_inline_comment() {
152        let source = "param x = 1; // inline";
153        let meta = metadata(source);
154        assert_eq!(meta.comments.len(), 1);
155        assert_eq!(meta.comments[0].value.lexeme(), "// inline");
156    }
157
158    #[test]
159    fn no_false_positive_in_string() {
160        let source = r#"import "//not-a-comment.gcl" { x };"#;
161        let meta = metadata(source);
162        assert_eq!(meta.comments.len(), 0);
163    }
164
165    #[test]
166    fn records_lexer_error_for_unrecognized_token() {
167        let source = r#"import "//not-a-comment.gcl"#;
168        let mut lexer = crate::syntax::lexer::Lexer::new(source);
169        while lexer.next_token().is_some() {}
170        assert_eq!(
171            lexer.first_error_span(),
172            Some(Span::new(7, source.len() - 7))
173        );
174    }
175
176    #[test]
177    fn extract_blank_lines() {
178        let source = "param x = 1;\n\nparam y = 2;";
179        let meta = metadata(source);
180        assert_eq!(meta.blank_lines.len(), 1);
181        assert_eq!(meta.blank_lines[0].span(), Span::new(12, 2));
182    }
183
184    #[test]
185    fn extract_blank_lines_with_crlf() {
186        let source = "param x = 1;\r\n\t\r\nparam y = 2;";
187        let meta = metadata(source);
188        assert_eq!(meta.blank_lines.len(), 1);
189        assert_eq!(meta.blank_lines[0].span(), Span::new(12, 5));
190    }
191
192    #[test]
193    fn crlf_comment_excludes_line_ending_from_body() {
194        let source = "// first\r\nparam x = 1;";
195        let meta = metadata(source);
196        assert_eq!(meta.comments[0].value.lexeme(), "// first");
197        assert_eq!(meta.comments[0].span, Span::new(0, 8));
198    }
199
200    #[test]
201    fn multiple_comments() {
202        let source = "// first\n// second\nparam x = 1;";
203        let meta = metadata(source);
204        assert_eq!(meta.comments.len(), 2);
205        assert_eq!(meta.comments[0].value.lexeme(), "// first");
206        assert_eq!(meta.comments[1].value.lexeme(), "// second");
207    }
208}