swc_coverage_instrument/visitors/
coverage_visitor.rs

1use swc_core::{
2    common::{comments::Comments, util::take::Take, SourceMapper, DUMMY_SP},
3    ecma::{
4        ast::*,
5        utils::IsDirective,
6        visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith},
7    },
8};
9use tracing::instrument;
10
11use crate::{
12    create_instrumentation_visitor, instrumentation_counter_helper,
13    instrumentation_stmt_counter_helper, instrumentation_visitor, InstrumentOptions,
14};
15
16create_instrumentation_visitor!(CoverageVisitor { file_path: String });
17
18/// Public interface to create a visitor performs transform to inject
19/// coverage instrumentation counter.
20pub fn create_coverage_instrumentation_visitor<C: Clone + Comments, S: SourceMapper>(
21    source_map: std::sync::Arc<S>,
22    comments: C,
23    instrument_options: InstrumentOptions,
24    filename: String,
25) -> CoverageVisitor<C, S> {
26    // create a function name ident for the injected coverage instrumentation counters.
27    crate::create_coverage_fn_ident(&filename);
28
29    let mut cov = crate::SourceCoverage::new(filename.to_string(), instrument_options.report_logic);
30    cov.set_input_source_map(&instrument_options.input_source_map);
31
32    CoverageVisitor::new(
33        source_map,
34        comments.clone(),
35        std::rc::Rc::new(std::cell::RefCell::new(cov)),
36        instrument_options,
37        vec![],
38        None,
39        filename,
40    )
41}
42
43impl<C: Clone + Comments, S: SourceMapper> CoverageVisitor<C, S> {
44    instrumentation_counter_helper!();
45    instrumentation_stmt_counter_helper!();
46
47    /// Not implemented.
48    /// TODO: is this required?
49    fn is_instrumented_already(&self) -> bool {
50        return false;
51    }
52
53    /// Create coverage instrumentation template exprs to be injected into the top of the transformed output.
54    fn get_coverage_templates(&mut self) -> (Stmt, Stmt) {
55        self.cov.borrow_mut().freeze();
56
57        //TODO: option: global coverage variable scope. (optional, default `this`)
58        let coverage_global_scope = "this";
59        //TODO: option: use an evaluated function to find coverageGlobalScope.
60        let coverage_global_scope_func = true;
61
62        let gv_template = if coverage_global_scope_func {
63            // TODO: path.scope.getBinding('Function')
64            let is_function_binding_scope = false;
65
66            if is_function_binding_scope {
67                /*
68                gvTemplate = globalTemplateAlteredFunction({
69                    GLOBAL_COVERAGE_SCOPE: T.stringLiteral(
70                        'return ' + opts.coverageGlobalScope
71                    )
72                });
73                 */
74                unimplemented!("");
75            } else {
76                crate::create_global_stmt_template(coverage_global_scope)
77            }
78        } else {
79            unimplemented!("");
80            /*
81            gvTemplate = globalTemplateVariable({
82                GLOBAL_COVERAGE_SCOPE: opts.coverageGlobalScope
83            });
84            */
85        };
86
87        let coverage_template = crate::create_coverage_fn_decl(
88            &self.instrument_options.coverage_variable,
89            gv_template,
90            &self.cov_fn_ident,
91            &self.file_path,
92            self.cov.borrow().as_ref(),
93            &self.comments,
94            self.instrument_options.debug_initial_coverage_comment,
95        );
96
97        // explicitly call this.varName to ensure coverage is always initialized
98        let call_coverage_template_stmt = Stmt::Expr(ExprStmt {
99            span: DUMMY_SP,
100            expr: Box::new(Expr::Call(CallExpr {
101                callee: Callee::Expr(Box::new(Expr::Ident(self.cov_fn_ident.clone()))),
102                ..CallExpr::dummy()
103            })),
104        });
105
106        (coverage_template, call_coverage_template_stmt)
107    }
108}
109
110impl<C: Clone + Comments, S: SourceMapper> VisitMut for CoverageVisitor<C, S> {
111    instrumentation_visitor!();
112
113    #[instrument(skip_all, fields(node = %self.print_node()))]
114    fn visit_mut_program(&mut self, program: &mut Program) {
115        self.nodes.push(crate::Node::Program);
116        if crate::hint_comments::should_ignore_file(&self.comments, program) {
117            return;
118        }
119
120        if self.is_instrumented_already() {
121            return;
122        }
123
124        program.visit_mut_children_with(self);
125        self.nodes.pop();
126    }
127
128    #[instrument(skip_all, fields(node = %self.print_node()))]
129    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
130        if self.is_instrumented_already() {
131            return;
132        }
133
134        let root_exists = match self.nodes.get(0) {
135            Some(node) => node == &crate::Node::Program,
136            _ => false,
137        };
138
139        // Articulate root by injecting Program node if visut_mut_program is not called.
140        // TODO: Need to figure out why custom_js_pass doesn't hit visit_mut_program
141        // instead of manually injecting node here
142        if !root_exists {
143            let mut new_nodes = vec![crate::Node::Program];
144            new_nodes.extend(self.nodes.drain(..));
145            self.nodes = new_nodes;
146        }
147
148        // TODO: Should module_items need to be added in self.nodes?
149        let mut new_items = vec![];
150        for mut item in items.drain(..) {
151            if let ModuleItem::Stmt(stmt) = &item {
152                // Do not create coverage instrumentation for directives.
153                if stmt.directive_continue() {
154                    new_items.push(item);
155                    continue;
156                }
157            }
158
159            let (old, _ignore_current) = match &mut item {
160                ModuleItem::ModuleDecl(decl) => self.on_enter(decl),
161                ModuleItem::Stmt(stmt) => self.on_enter(stmt),
162            };
163
164            // https://github.com/kwonoj/swc-plugin-coverage-instrument/issues/277
165            // Add statement counter for export const declarations to match istanbul behavior
166            // Istanbul treats export const and export var differently:
167            // - export const: adds statement counter for the export declaration
168            // - export var: only instruments the initializer, no separate export counter
169            if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = &item {
170                if let Decl::Var(var_decl) = &export_decl.decl {
171                    if var_decl.kind == VarDeclKind::Const {
172                        self.mark_prepend_stmt_counter(&export_decl.span);
173                    }
174                }
175            }
176
177            item.visit_mut_children_with(self);
178
179            new_items.extend(self.before.drain(..).map(|v| ModuleItem::Stmt(v)));
180            new_items.push(item);
181            self.on_exit(old);
182        }
183        *items = new_items;
184
185        let (coverage_template, call_coverage_template_stmt) = self.get_coverage_templates();
186
187        // prepend template to the top of the code
188        if items.len() >= 1 {
189            items.insert(1, ModuleItem::Stmt(coverage_template));
190            items.insert(2, ModuleItem::Stmt(call_coverage_template_stmt));
191        } else {
192            items.push(ModuleItem::Stmt(coverage_template));
193            items.push(ModuleItem::Stmt(call_coverage_template_stmt));
194        }
195
196        if !root_exists {
197            self.nodes.pop();
198        }
199    }
200
201    #[instrument(skip_all, fields(node = %self.print_node()))]
202    fn visit_mut_script(&mut self, items: &mut Script) {
203        if self.is_instrumented_already() {
204            return;
205        }
206
207        let mut new_items = vec![];
208        for mut item in items.body.drain(..) {
209            item.visit_mut_children_with(self);
210            new_items.extend(self.before.drain(..));
211            new_items.push(item);
212        }
213        items.body = new_items;
214
215        let (coverage_template, call_coverage_template_stmt) = self.get_coverage_templates();
216
217        // prepend template to the top of the code
218        items.body.insert(0, coverage_template);
219        items.body.insert(1, call_coverage_template_stmt);
220    }
221
222    // ExportDefaultDeclaration: entries(), // ignore processing only
223    #[instrument(skip_all, fields(node = %self.print_node()))]
224    fn visit_mut_export_default_decl(&mut self, export_default_decl: &mut ExportDefaultDecl) {
225        let (old, ignore_current) = self.on_enter(export_default_decl);
226        match ignore_current {
227            Some(crate::hint_comments::IgnoreScope::Next) => {}
228            _ => {
229                // noop
230                export_default_decl.visit_mut_children_with(self);
231            }
232        }
233        self.on_exit(old);
234    }
235
236    // ExportNamedDeclaration: entries(), // ignore processing only
237    #[instrument(skip_all, fields(node = %self.print_node()))]
238    fn visit_mut_export_decl(&mut self, export_named_decl: &mut ExportDecl) {
239        let (old, ignore_current) = self.on_enter(export_named_decl);
240        match ignore_current {
241            Some(crate::hint_comments::IgnoreScope::Next) => {}
242            _ => {
243                // noop
244                export_named_decl.visit_mut_children_with(self);
245            }
246        }
247        self.on_exit(old);
248    }
249
250    // DebuggerStatement: entries(coverStatement),
251    #[instrument(skip_all, fields(node = %self.print_node()))]
252    fn visit_mut_debugger_stmt(&mut self, debugger_stmt: &mut DebuggerStmt) {
253        let (old, ignore_current) = self.on_enter(debugger_stmt);
254        match ignore_current {
255            Some(crate::hint_comments::IgnoreScope::Next) => {}
256            _ => {
257                debugger_stmt.visit_mut_children_with(self);
258            }
259        }
260        self.on_exit(old);
261    }
262}