naga_oil/compose/
comment_strip_iter.rs

1use std::{borrow::Cow, str::Lines};
2
3use regex::Regex;
4use std::sync::LazyLock;
5
6// outside of blocks and quotes, change state on //, /* or "
7static RE_NONE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(//|/\*|\")"#).unwrap());
8// in blocks, change on /* and */
9static RE_BLOCK: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\*|\*/)").unwrap());
10// in quotes, change only on "
11static RE_QUOTE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\""#).unwrap());
12
13#[derive(PartialEq, Eq)]
14enum CommentState {
15    None,
16    Block(usize),
17    Quote,
18}
19
20pub struct CommentReplaceIter<'a> {
21    lines: &'a mut Lines<'a>,
22    state: CommentState,
23}
24
25impl<'a> Iterator for CommentReplaceIter<'a> {
26    type Item = Cow<'a, str>;
27
28    fn next(&mut self) -> Option<Self::Item> {
29        let line_in = self.lines.next()?;
30
31        // fast path
32        if self.state == CommentState::None && !RE_NONE.is_match(line_in) {
33            return Some(Cow::Borrowed(line_in));
34        }
35
36        let mut output = String::new();
37        let mut section_start = 0;
38
39        loop {
40            let marker = match self.state {
41                CommentState::None => &RE_NONE,
42                CommentState::Block(_) => &RE_BLOCK,
43                CommentState::Quote => &RE_QUOTE,
44            }
45            .find(&line_in[section_start..]);
46
47            let section_end = marker
48                .map(|m| section_start + m.start())
49                .unwrap_or(line_in.len());
50
51            if let CommentState::Block(_) = self.state {
52                output.extend(std::iter::repeat_n(' ', section_end - section_start));
53            } else {
54                output.push_str(&line_in[section_start..section_end]);
55            }
56
57            match marker {
58                None => return Some(Cow::Owned(output)),
59                Some(marker) => {
60                    match marker.as_str() {
61                        // only possible in None state
62                        "//" => {
63                            output.extend(std::iter::repeat_n(
64                                ' ',
65                                line_in.len() - marker.start() - section_start,
66                            ));
67                            return Some(Cow::Owned(output));
68                        }
69                        // only possible in None or Block state
70                        "/*" => {
71                            self.state = match self.state {
72                                CommentState::None => CommentState::Block(1),
73                                CommentState::Block(n) => CommentState::Block(n + 1),
74                                _ => unreachable!(),
75                            };
76                            output.push_str("  ");
77                        }
78                        // only possible in Block state
79                        "*/" => {
80                            self.state = match self.state {
81                                CommentState::Block(1) => CommentState::None,
82                                CommentState::Block(n) => CommentState::Block(n - 1),
83                                _ => unreachable!(),
84                            };
85                            output.push_str("  ");
86                        }
87                        // only possible in None or Quote state
88                        "\"" => {
89                            self.state = match self.state {
90                                CommentState::None => CommentState::Quote,
91                                CommentState::Quote => CommentState::None,
92                                _ => unreachable!(),
93                            };
94                            output.push('"');
95                        }
96                        _ => unreachable!(),
97                    }
98                    section_start += marker.end();
99                }
100            }
101        }
102    }
103}
104
105pub trait CommentReplaceExt<'a> {
106    /// replace WGSL and GLSL comments with whitespace characters
107    fn replace_comments(&'a mut self) -> CommentReplaceIter<'a>;
108}
109
110impl<'a> CommentReplaceExt<'a> for Lines<'a> {
111    fn replace_comments(&'a mut self) -> CommentReplaceIter<'a> {
112        CommentReplaceIter {
113            lines: self,
114            state: CommentState::None,
115        }
116    }
117}
118
119#[test]
120fn comment_test() {
121    const INPUT: &str = r"
122not commented
123// line commented
124not commented
125/* block commented on a line */
126not commented
127// line comment with a /* block comment unterminated
128not commented
129/* block comment
130   spanning lines */
131not commented
132/* block comment
133   spanning lines and with // line comments
134   even with a // line commented terminator */
135not commented
136";
137
138    assert_eq!(
139        INPUT
140            .lines()
141            .replace_comments()
142            .zip(INPUT.lines())
143            .find(|(line, original)| {
144                (line != "not commented" && !line.chars().all(|c| c == ' '))
145                    || line.len() != original.len()
146            }),
147        None
148    );
149
150    const PARTIAL_TESTS: [(&str, &str); 11] = [
151        (
152            "1.0 /* block comment with a partial line comment on the end *// 2.0",
153            "1.0                                                           / 2.0",
154        ),
155        (
156            "1.0 /* block comment with a partial block comment on the end */* 2.0",
157            "1.0                                                            * 2.0",
158        ),
159        (
160            "1.0 /* block comment 1 *//* block comment 2 */ * 2.0",
161            "1.0                                            * 2.0",
162        ),
163        (
164            "1.0 /* block comment with real line comment after */// line comment",
165            "1.0                                                                ",
166        ),
167        ("*/", "*/"),
168        (
169            r#"#import "embedded://file.wgsl""#,
170            r#"#import "embedded://file.wgsl""#,
171        ),
172        (
173            r#"// #import "embedded://file.wgsl""#,
174            r#"                                 "#,
175        ),
176        (
177            r#"/* #import "embedded://file.wgsl" */"#,
178            r#"                                    "#,
179        ),
180        (
181            r#"/* #import "embedded:*/file.wgsl" */"#,
182            r#"                       file.wgsl" */"#,
183        ),
184        (
185            r#"#import "embedded://file.wgsl" // comment"#,
186            r#"#import "embedded://file.wgsl"           "#,
187        ),
188        (
189            r#"#import "embedded:/* */ /* /**/* / / /// * / //*/*/ / */*file.wgsl""#,
190            r#"#import "embedded:/* */ /* /**/* / / /// * / //*/*/ / */*file.wgsl""#,
191        ),
192    ];
193
194    for &(input, expected) in PARTIAL_TESTS.iter() {
195        let mut nasty_processed = input.lines();
196        let nasty_processed = nasty_processed.replace_comments().next().unwrap();
197        assert_eq!(&nasty_processed, expected);
198    }
199}
200
201#[test]
202fn multiline_comment_test() {
203    let test_cases = [
204        (
205            // Basic test
206            r"/*
207hoho
208*/",
209            r"  
210    
211  ",
212        ),
213        (
214            // Testing the commenting-out of multiline comments
215            r"///*
216hehe
217//*/",
218            r"    
219hehe
220    ",
221        ),
222        (
223            // Testing the commenting-out of single-line comments
224            r"/* // */ code goes here /*
225Still a comment // */
226/* dummy */",
227            r"         code goes here   
228                     
229           ",
230        ),
231        (
232            // A comment with a nested multiline comment
233            // Notice how the "//" inside the multiline comment doesn't take effect
234            r"/*
235//*
236*/commented
237*/not commented",
238            r"  
239   
240           
241  not commented",
242        ),
243    ];
244
245    for &(input, expected) in test_cases.iter() {
246        for (output_line, expected_line) in input.lines().replace_comments().zip(expected.lines()) {
247            assert_eq!(output_line.as_ref(), expected_line);
248        }
249    }
250}
251
252#[test]
253fn test_comment_becomes_spaces() {
254    let test_cases = [("let a/**/b =3u;", "let a    b =3u;")];
255    for &(input, expected) in test_cases.iter() {
256        for (output_line, expected_line) in input.lines().replace_comments().zip(expected.lines()) {
257            assert_eq!(output_line.as_ref(), expected_line);
258        }
259    }
260}