graphcal_compiler/syntax/
comments.rs1use crate::syntax::span::{Span, Spanned};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub(crate) enum CommentDelimiter {
6 Line,
8 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#[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#[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 #[must_use]
58 pub fn lexeme(&self) -> String {
59 format!("{}{}", self.delimiter.lexeme(), self.body.as_str())
60 }
61}
62
63pub type SpannedComment = Spanned<Comment>;
65
66#[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#[derive(Debug, Clone, PartialEq, Eq, Default)]
87pub struct SourceMetadata {
88 comments: Vec<SpannedComment>,
90 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}