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