swc_estree_compat/babelify/
module.rs

1use serde::{Deserialize, Serialize};
2use swc_common::{comments::Comment, Span};
3use swc_ecma_ast::{Module, ModuleItem, Program, Script};
4use swc_ecma_visit::{Visit, VisitWith};
5use swc_estree_ast::{
6    flavor::Flavor, BaseNode, File, InterpreterDirective, LineCol, Loc, ModuleDeclaration,
7    Program as BabelProgram, SrcType, Statement,
8};
9use swc_node_comments::SwcComments;
10
11use crate::babelify::{Babelify, Context};
12
13impl Babelify for Program {
14    type Output = File;
15
16    fn babelify(self, ctx: &Context) -> Self::Output {
17        let comments = extract_all_comments(&self, ctx);
18        let program = match self {
19            Program::Module(module) => module.babelify(ctx),
20            Program::Script(script) => script.babelify(ctx),
21            // TODO: reenable once experimental_metadata breaking change is merged
22            // _ => unreachable!(),
23        };
24
25        File {
26            base: BaseNode {
27                leading_comments: Default::default(),
28                inner_comments: Default::default(),
29                trailing_comments: Default::default(),
30                start: program.base.start,
31                end: program.base.end,
32                loc: program.base.loc,
33                range: if matches!(Flavor::current(), Flavor::Acorn { .. }) {
34                    match (program.base.start, program.base.end) {
35                        (Some(start), Some(end)) => Some([start, end]),
36                        _ => None,
37                    }
38                } else {
39                    None
40                },
41            },
42            program,
43            comments: Some(ctx.convert_comments(comments)),
44            tokens: Default::default(),
45        }
46    }
47}
48
49impl Babelify for Module {
50    type Output = BabelProgram;
51
52    fn babelify(self, ctx: &Context) -> Self::Output {
53        let span = if has_comment_first_line(self.span, ctx) {
54            self.span.with_lo(ctx.fm.start_pos)
55        } else {
56            self.span
57        };
58        BabelProgram {
59            base: base_with_trailing_newline(span, ctx),
60            source_type: SrcType::Module,
61            body: self
62                .body
63                .into_iter()
64                .map(|stmt| stmt.babelify(ctx).into())
65                .collect(),
66            interpreter: self.shebang.map(|s| InterpreterDirective {
67                base: ctx.base(extract_shebang_span(span, ctx)),
68                value: s,
69            }),
70            directives: Default::default(),
71            source_file: Default::default(),
72            comments: Default::default(),
73        }
74    }
75}
76
77impl Babelify for Script {
78    type Output = BabelProgram;
79
80    fn babelify(self, ctx: &Context) -> Self::Output {
81        let span = if has_comment_first_line(self.span, ctx) {
82            self.span.with_lo(ctx.fm.start_pos)
83        } else {
84            self.span
85        };
86        BabelProgram {
87            base: base_with_trailing_newline(span, ctx),
88            source_type: SrcType::Script,
89            body: self.body.babelify(ctx),
90            interpreter: self.shebang.map(|s| InterpreterDirective {
91                base: ctx.base(extract_shebang_span(span, ctx)),
92                value: s,
93            }),
94            directives: Default::default(),
95            source_file: Default::default(),
96            comments: Default::default(),
97        }
98    }
99}
100
101/// Babel adds a trailing newline to the end of files when parsing, while swc
102/// truncates trailing whitespace. In order to get the converted base node to
103/// locations to match babel, we imitate the trailing newline for Script and
104/// Module nodes.
105fn base_with_trailing_newline(span: Span, ctx: &Context) -> BaseNode {
106    let mut base = ctx.base(span);
107
108    base.end = base.end.map(|num| num + 1);
109    base.loc = base.loc.map(|loc| Loc {
110        end: LineCol {
111            line: loc.end.line + 1,
112            column: 0,
113        },
114        ..loc
115    });
116    base.range = base.range.map(|range| [range[0], range[1] + 1]);
117
118    base
119}
120
121/// Should return true if the first line in parsed file is a comment.
122/// Required because babel and swc have slightly different handlings for first
123/// line comments. Swc ignores them and starts the program on the next line
124/// down, while babel includes them in the file start/end.
125fn has_comment_first_line(sp: Span, ctx: &Context) -> bool {
126    if let Some(comments) = ctx.comments.leading.get(&sp.hi) {
127        !comments
128            .first()
129            .map(|c| c.span.lo == ctx.fm.start_pos)
130            .unwrap_or(false)
131    } else {
132        true
133    }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub enum ModuleItemOutput {
138    ModuleDecl(ModuleDeclaration),
139    Stmt(Statement),
140}
141
142impl Babelify for ModuleItem {
143    type Output = ModuleItemOutput;
144
145    fn parallel(cnt: usize) -> bool {
146        cnt >= 32
147    }
148
149    fn babelify(self, ctx: &Context) -> Self::Output {
150        match self {
151            ModuleItem::ModuleDecl(d) => ModuleItemOutput::ModuleDecl(d.babelify(ctx).into()),
152            ModuleItem::Stmt(s) => ModuleItemOutput::Stmt(s.babelify(ctx)),
153        }
154    }
155}
156
157impl From<ModuleItemOutput> for Statement {
158    fn from(m: ModuleItemOutput) -> Self {
159        match m {
160            ModuleItemOutput::Stmt(stmt) => stmt,
161            ModuleItemOutput::ModuleDecl(decl) => match decl {
162                ModuleDeclaration::ExportAll(e) => Statement::ExportAllDecl(e),
163                ModuleDeclaration::ExportDefault(e) => Statement::ExportDefaultDecl(e),
164                ModuleDeclaration::ExportNamed(e) => Statement::ExportNamedDecl(e),
165                ModuleDeclaration::Import(i) => Statement::ImportDecl(i),
166            },
167        }
168    }
169}
170
171fn extract_shebang_span(span: Span, ctx: &Context) -> Span {
172    ctx.cm.span_take_while(span, |ch| *ch != '\n')
173}
174
175fn extract_all_comments(program: &Program, ctx: &Context) -> Vec<Comment> {
176    let mut collector = CommentCollector {
177        comments: ctx.comments.clone(),
178        collected: Vec::new(),
179    };
180    program.visit_with(&mut collector);
181    collector.collected
182}
183
184struct CommentCollector {
185    comments: SwcComments,
186    collected: Vec<Comment>,
187}
188
189impl Visit for CommentCollector {
190    fn visit_span(&mut self, sp: &Span) {
191        let mut span_comments: Vec<Comment> = Vec::new();
192        // Comments must be deduped since it's possible for a single comment to show up
193        // multiple times since they are not removed from the comments map.
194        // For example, this happens when the first line in a file is a comment.
195        if let Some(comments) = self.comments.leading.get(&sp.lo) {
196            for comment in comments.iter() {
197                if !self.collected.contains(comment) {
198                    span_comments.push(comment.clone());
199                }
200            }
201        }
202
203        if let Some(comments) = self.comments.trailing.get(&sp.hi) {
204            for comment in comments.iter() {
205                if !self.collected.contains(comment) {
206                    span_comments.push(comment.clone());
207                }
208            }
209        }
210        self.collected.append(&mut span_comments);
211    }
212}