Skip to main content

turbopack_ecmascript/
swc_comments.rs

1use std::{borrow::Cow, cell::RefCell, mem::take, rc::Rc};
2
3use rustc_hash::FxHashMap;
4use swc_core::{
5    base::SwcComments,
6    common::{
7        BytePos,
8        comments::{Comment, CommentKind, Comments, SingleThreadedComments},
9    },
10};
11
12use crate::source_map::extract_source_mapping_url;
13
14/// Converts multi-threaded [`SwcComments`] into [`SingleThreadedComments`] by
15/// copying all entries. Required when APIs (e.g. `swc_ecma_react_compiler`)
16/// only accept the single-threaded variant.
17pub fn swc_comments_to_single_threaded(comments: &SwcComments) -> SingleThreadedComments {
18    let mut leading = FxHashMap::default();
19    for entry in comments.leading.as_ref() {
20        leading.insert(*entry.key(), entry.value().clone());
21    }
22
23    let mut trailing = FxHashMap::default();
24    for entry in comments.trailing.as_ref() {
25        trailing.insert(*entry.key(), entry.value().clone());
26    }
27
28    SingleThreadedComments::from_leading_and_trailing(
29        Rc::new(RefCell::new(leading)),
30        Rc::new(RefCell::new(trailing)),
31    )
32}
33
34/// Immutable version of [SwcComments] which doesn't allow mutation. The `take`
35/// variants are still implemented, but do not mutate the content. They are used
36/// by the SWC Emitter.
37#[derive(Default)]
38pub struct ImmutableComments {
39    pub leading: FxHashMap<BytePos, Vec<Comment>>,
40    pub trailing: FxHashMap<BytePos, Vec<Comment>>,
41}
42
43impl ImmutableComments {
44    pub fn new(comments: SwcComments) -> Self {
45        Self {
46            leading: comments
47                .leading
48                .iter_mut()
49                .filter_map(|mut r| {
50                    let c = take(r.value_mut());
51                    (!c.is_empty()).then_some((*r.key(), c))
52                })
53                .collect(),
54            trailing: comments
55                .trailing
56                .iter_mut()
57                .filter_map(|mut r| {
58                    let c = take(r.value_mut());
59                    (!c.is_empty()).then_some((*r.key(), c))
60                })
61                .collect(),
62        }
63    }
64
65    /// Creates a new ImmutableComments from SwcComments, extracting and removing
66    /// any sourceMappingURL comment. Returns the comments and the extracted URL if found.
67    /// If multiple sourceMappingURL comments exist, the one with the highest position
68    /// (last in the file) is selected per the ECMAScript spec.
69    pub fn new_with_source_mapping_url(comments: SwcComments) -> (Self, Option<String>) {
70        let mut source_mapping_url_by_pos: Vec<(BytePos, String)> = Vec::new();
71
72        let leading: FxHashMap<BytePos, Vec<Comment>> = comments
73            .leading
74            .iter_mut()
75            .filter_map(|mut r| {
76                let pos = *r.key();
77                let mut c = take(r.value_mut());
78                // Extract and remove sourceMappingURL comments
79                c.retain(|comment| {
80                    if let Some(url) = extract_source_mapping_url(comment) {
81                        source_mapping_url_by_pos.push((pos, url.to_string()));
82                        false
83                    } else {
84                        true
85                    }
86                });
87                (!c.is_empty()).then_some((pos, c))
88            })
89            .collect();
90
91        let trailing: FxHashMap<BytePos, Vec<Comment>> = comments
92            .trailing
93            .iter_mut()
94            .filter_map(|mut r| {
95                let pos = *r.key();
96                let mut c = take(r.value_mut());
97                // Extract and remove sourceMappingURL comments
98                c.retain(|comment| {
99                    if let Some(url) = extract_source_mapping_url(comment) {
100                        source_mapping_url_by_pos.push((pos, url.to_string()));
101                        false
102                    } else {
103                        true
104                    }
105                });
106                (!c.is_empty()).then_some((pos, c))
107            })
108            .collect();
109
110        // Select the sourceMappingURL with the highest position (last one in the file)
111        let source_mapping_url = source_mapping_url_by_pos
112            .into_iter()
113            .max_by_key(|&(pos, _)| pos)
114            .map(|(_, url)| url);
115
116        (Self { leading, trailing }, source_mapping_url)
117    }
118
119    pub fn into_consumable(self) -> CowComments<'static> {
120        CowComments::owned(self)
121    }
122
123    pub fn consumable(&self) -> CowComments<'_> {
124        CowComments::borrowed(self)
125    }
126}
127
128impl Comments for ImmutableComments {
129    fn add_leading(
130        &self,
131        _pos: swc_core::common::BytePos,
132        _cmt: swc_core::common::comments::Comment,
133    ) {
134        panic!("Comments are immutable after parsing")
135    }
136
137    fn add_leading_comments(
138        &self,
139        _pos: swc_core::common::BytePos,
140        _comments: Vec<swc_core::common::comments::Comment>,
141    ) {
142        panic!("Comments are immutable after parsing")
143    }
144
145    fn has_leading(&self, pos: swc_core::common::BytePos) -> bool {
146        self.leading.contains_key(&pos)
147    }
148
149    fn move_leading(&self, _from: swc_core::common::BytePos, _to: swc_core::common::BytePos) {
150        panic!("Comments are immutable after parsing")
151    }
152
153    fn take_leading(
154        &self,
155        _pos: swc_core::common::BytePos,
156    ) -> Option<Vec<swc_core::common::comments::Comment>> {
157        panic!(
158            "Comments are immutable after parsing (Use ImmutableComments::consumable() to allow \
159             taking out values)"
160        )
161    }
162
163    fn get_leading(
164        &self,
165        pos: swc_core::common::BytePos,
166    ) -> Option<Vec<swc_core::common::comments::Comment>> {
167        self.leading.get(&pos).map(|v| v.to_owned())
168    }
169
170    fn add_trailing(
171        &self,
172        _pos: swc_core::common::BytePos,
173        _cmt: swc_core::common::comments::Comment,
174    ) {
175        panic!("Comments are immutable after parsing")
176    }
177
178    fn add_trailing_comments(
179        &self,
180        _pos: swc_core::common::BytePos,
181        _comments: Vec<swc_core::common::comments::Comment>,
182    ) {
183        panic!("Comments are immutable after parsing")
184    }
185
186    fn has_trailing(&self, pos: swc_core::common::BytePos) -> bool {
187        self.trailing.contains_key(&pos)
188    }
189
190    fn move_trailing(&self, _from: swc_core::common::BytePos, _to: swc_core::common::BytePos) {
191        panic!("Comments are immutable after parsing")
192    }
193
194    fn take_trailing(
195        &self,
196        _pos: swc_core::common::BytePos,
197    ) -> Option<Vec<swc_core::common::comments::Comment>> {
198        panic!(
199            "Comments are immutable after parsing (Use ImmutableComments::consumable() to allow \
200             taking out values)"
201        )
202    }
203
204    fn get_trailing(
205        &self,
206        pos: swc_core::common::BytePos,
207    ) -> Option<Vec<swc_core::common::comments::Comment>> {
208        self.trailing.get(&pos).map(|v| v.to_owned())
209    }
210
211    fn add_pure_comment(&self, _pos: swc_core::common::BytePos) {
212        panic!("Comments are immutable after parsing")
213    }
214
215    fn has_flag(&self, pos: BytePos, flag: &str) -> bool {
216        self.with_leading(pos, |cmts| {
217            for c in cmts {
218                if c.kind == CommentKind::Block {
219                    for line in c.text.lines() {
220                        // jsdoc
221                        let line = line.trim_start_matches(['*', ' ']);
222                        let line = line.trim();
223
224                        //
225                        if line.len() == (flag.len() + 5)
226                            && (line.starts_with("#__") || line.starts_with("@__"))
227                            && line.ends_with("__")
228                            && flag == &line[3..line.len() - 2]
229                        {
230                            return true;
231                        }
232                    }
233                }
234            }
235
236            false
237        })
238    }
239
240    fn with_leading<F, Ret>(&self, pos: BytePos, f: F) -> Ret
241    where
242        Self: Sized,
243        F: FnOnce(&[Comment]) -> Ret,
244    {
245        let cmts = self.get_leading(pos);
246
247        if let Some(cmts) = &cmts {
248            f(cmts)
249        } else {
250            f(&[])
251        }
252    }
253
254    fn with_trailing<F, Ret>(&self, pos: BytePos, f: F) -> Ret
255    where
256        Self: Sized,
257        F: FnOnce(&[Comment]) -> Ret,
258    {
259        let cmts = self.get_trailing(pos);
260
261        if let Some(cmts) = &cmts {
262            f(cmts)
263        } else {
264            f(&[])
265        }
266    }
267}
268
269pub struct CowComments<'a> {
270    leading: RefCell<FxHashMap<BytePos, Cow<'a, [Comment]>>>,
271    trailing: RefCell<FxHashMap<BytePos, Cow<'a, [Comment]>>>,
272}
273
274impl<'a> CowComments<'a> {
275    fn borrowed(comments: &'a ImmutableComments) -> Self {
276        Self {
277            leading: RefCell::new(
278                comments
279                    .leading
280                    .iter()
281                    .map(|(&key, value)| (key, Cow::Borrowed(&value[..])))
282                    .collect(),
283            ),
284            trailing: RefCell::new(
285                comments
286                    .trailing
287                    .iter()
288                    .map(|(&key, value)| (key, Cow::Borrowed(&value[..])))
289                    .collect(),
290            ),
291        }
292    }
293
294    fn owned(comments: ImmutableComments) -> Self {
295        Self {
296            leading: RefCell::new(
297                comments
298                    .leading
299                    .into_iter()
300                    .map(|(key, value)| (key, Cow::Owned(value)))
301                    .collect(),
302            ),
303            trailing: RefCell::new(
304                comments
305                    .trailing
306                    .into_iter()
307                    .map(|(key, value)| (key, Cow::Owned(value)))
308                    .collect(),
309            ),
310        }
311    }
312}
313
314impl Comments for CowComments<'_> {
315    fn add_leading(
316        &self,
317        _pos: swc_core::common::BytePos,
318        _cmt: swc_core::common::comments::Comment,
319    ) {
320        panic!("Comments are immutable after parsing")
321    }
322
323    fn add_leading_comments(
324        &self,
325        _pos: swc_core::common::BytePos,
326        _comments: Vec<swc_core::common::comments::Comment>,
327    ) {
328        panic!("Comments are immutable after parsing")
329    }
330
331    fn has_leading(&self, pos: swc_core::common::BytePos) -> bool {
332        self.leading.borrow().contains_key(&pos)
333    }
334
335    fn move_leading(&self, _from: swc_core::common::BytePos, _to: swc_core::common::BytePos) {
336        panic!("Comments are immutable after parsing")
337    }
338
339    fn take_leading(
340        &self,
341        pos: swc_core::common::BytePos,
342    ) -> Option<Vec<swc_core::common::comments::Comment>> {
343        self.leading
344            .borrow_mut()
345            .remove(&pos)
346            .map(|v| v.into_owned())
347    }
348
349    fn get_leading(
350        &self,
351        pos: swc_core::common::BytePos,
352    ) -> Option<Vec<swc_core::common::comments::Comment>> {
353        self.leading.borrow().get(&pos).map(|v| (**v).to_vec())
354    }
355
356    fn add_trailing(
357        &self,
358        _pos: swc_core::common::BytePos,
359        _cmt: swc_core::common::comments::Comment,
360    ) {
361        panic!("Comments are immutable after parsing")
362    }
363
364    fn add_trailing_comments(
365        &self,
366        _pos: swc_core::common::BytePos,
367        _comments: Vec<swc_core::common::comments::Comment>,
368    ) {
369        panic!("Comments are immutable after parsing")
370    }
371
372    fn has_trailing(&self, pos: swc_core::common::BytePos) -> bool {
373        self.trailing.borrow().contains_key(&pos)
374    }
375
376    fn move_trailing(&self, _from: swc_core::common::BytePos, _to: swc_core::common::BytePos) {
377        panic!("Comments are immutable after parsing")
378    }
379
380    fn take_trailing(
381        &self,
382        pos: swc_core::common::BytePos,
383    ) -> Option<Vec<swc_core::common::comments::Comment>> {
384        self.trailing
385            .borrow_mut()
386            .remove(&pos)
387            .map(|v| v.into_owned())
388    }
389
390    fn get_trailing(
391        &self,
392        pos: swc_core::common::BytePos,
393    ) -> Option<Vec<swc_core::common::comments::Comment>> {
394        self.trailing.borrow().get(&pos).map(|v| (**v).to_vec())
395    }
396
397    fn add_pure_comment(&self, _pos: swc_core::common::BytePos) {
398        panic!("Comments are immutable after parsing")
399    }
400
401    fn with_leading<F, Ret>(&self, pos: BytePos, f: F) -> Ret
402    where
403        Self: Sized,
404        F: FnOnce(&[Comment]) -> Ret,
405    {
406        let cmts = self.get_leading(pos);
407
408        if let Some(cmts) = &cmts {
409            f(cmts)
410        } else {
411            f(&[])
412        }
413    }
414
415    fn with_trailing<F, Ret>(&self, pos: BytePos, f: F) -> Ret
416    where
417        Self: Sized,
418        F: FnOnce(&[Comment]) -> Ret,
419    {
420        let cmts = self.get_trailing(pos);
421
422        if let Some(cmts) = &cmts {
423            f(cmts)
424        } else {
425            f(&[])
426        }
427    }
428}