├── spec ├── README.md ├── swc-coverage-custom-transform │ ├── build.rs │ ├── README.md │ ├── package.json │ ├── Cargo.toml │ └── src │ │ ├── util.rs │ │ └── lib.rs ├── constants.ts ├── util │ ├── constants.ts │ ├── guards.ts │ ├── read-coverage.ts │ └── verifier.ts ├── swc-coverage-instrument-wasm │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── fixtures │ ├── ignore.yaml │ ├── jsx.yaml │ ├── meta-property.yaml │ ├── issue-277.yaml │ ├── object-spread.yaml │ ├── issue-233.yaml │ ├── with.yaml │ ├── es6-modules.yaml │ ├── yield.yaml │ ├── object-method.yaml │ ├── tenary.yaml │ ├── classes.yaml │ ├── debug-test.yaml │ ├── for-of.yaml │ ├── input-source-map.yaml │ ├── for-in.yaml │ ├── default-args.yaml │ ├── issue-258.yaml │ ├── statement.yaml │ ├── class-properties.yaml │ ├── do-while.yaml │ ├── try.yaml │ ├── while.yaml │ ├── strict.yaml.skipped │ ├── expressions.yaml │ ├── arrow-fn.yaml │ ├── for.yaml │ ├── truthy.yaml │ ├── statement-hints.yaml │ ├── functions.yaml │ ├── switch.yaml │ └── if-hints.yaml ├── e2e │ ├── fixtures │ │ ├── should-be-excluded.test.js │ │ └── should-be-included.js │ └── e2e.spec.ts ├── fixture.spec.ts └── plugin.spec.ts ├── .husky ├── .gitignore └── pre-commit ├── rust-toolchain ├── packages ├── swc-coverage-instrument │ ├── src │ │ ├── constants │ │ │ ├── mod.rs │ │ │ └── idents.rs │ │ ├── options │ │ │ ├── mod.rs │ │ │ └── instrument_options.rs │ │ ├── utils │ │ │ ├── mod.rs │ │ │ ├── lookup_range.rs │ │ │ ├── node.rs │ │ │ └── hint_comments.rs │ │ ├── instrument │ │ │ ├── mod.rs │ │ │ ├── create_increase_counter_expr.rs │ │ │ └── create_increase_true_expr.rs │ │ ├── visitors │ │ │ ├── mod.rs │ │ │ ├── stmt_like_visitor.rs │ │ │ ├── finders.rs │ │ │ ├── switch_case_visitor.rs │ │ │ ├── logical_expr_visitor.rs │ │ │ └── coverage_visitor.rs │ │ ├── macros │ │ │ ├── mod.rs │ │ │ ├── visit_mut_for_like.rs │ │ │ ├── instrumentation_stmt_counter_helper.rs │ │ │ └── create_instrumentation_visitor.rs │ │ ├── coverage_template │ │ │ ├── mod.rs │ │ │ ├── create_assignment_stmt.rs │ │ │ ├── create_global_stmt_template.rs │ │ │ └── create_coverage_fn_decl.rs │ │ └── lib.rs │ ├── Cargo.toml │ ├── build.rs │ └── README.md ├── istanbul-oxide │ ├── README.md │ ├── src │ │ ├── coverage.rs │ │ ├── lib.rs │ │ ├── percent.rs │ │ ├── source_map.rs │ │ ├── range.rs │ │ ├── types.rs │ │ ├── coverage_map.rs │ │ └── coverage_summary.rs │ └── Cargo.toml └── swc-plugin-coverage │ ├── Cargo.toml │ └── src │ └── lib.rs ├── .cargo └── config.toml ├── .swcrc ├── tsconfig.json ├── .taplo.toml ├── .mocharc.json ├── .gitignore ├── .github └── workflows │ ├── bump-swc-core.yml │ └── ci_main.yml ├── LICENSE ├── Cargo.toml ├── package.json └── README.md /spec/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2025-05-06 2 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/constants/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod idents; 2 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/options/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod instrument_options; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_arch = "wasm32")'] 2 | rustflags = ["--cfg=swc_ast_unknown"] 3 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hint_comments; 2 | pub mod lookup_range; 3 | pub mod node; 4 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "target": "es2019", 4 | "parser": { 5 | "syntax": "typescript" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /spec/swc-coverage-custom-transform/build.rs: -------------------------------------------------------------------------------- 1 | extern crate napi_build; 2 | 3 | fn main() { 4 | napi_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/instrument/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_increase_counter_expr; 2 | pub mod create_increase_true_expr; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "ES2022", 5 | "strict": true 6 | } 7 | } -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | # https://taplo.tamasfe.dev/configuration/formatter-options.html 2 | [formatting] 3 | align_entries = true 4 | indent_tables = true 5 | reorder_keys = true 6 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/README.md: -------------------------------------------------------------------------------- 1 | # istanbul-oxide 2 | 3 | This is a port of `FileCoverage` and relative structs from Javascript to rust for istanbuljs-compatible coverage struct. 4 | -------------------------------------------------------------------------------- /spec/constants.ts: -------------------------------------------------------------------------------- 1 | const COVERAGE_MAGIC_KEY = "_coverageSchema"; 2 | const COVERAGE_MAGIC_VALUE = "7101652470475984838"; 3 | 4 | export { COVERAGE_MAGIC_KEY, COVERAGE_MAGIC_VALUE }; 5 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/visitors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod coverage_visitor; 2 | pub mod finders; 3 | pub mod logical_expr_visitor; 4 | pub mod stmt_like_visitor; 5 | pub mod switch_case_visitor; 6 | -------------------------------------------------------------------------------- /spec/swc-coverage-custom-transform/README.md: -------------------------------------------------------------------------------- 1 | ### SWC-Coverage-instrument-wasm 2 | 3 | A custom swc binary includes coverage intrument transform. This is for the internal integration testing only. 4 | -------------------------------------------------------------------------------- /spec/util/constants.ts: -------------------------------------------------------------------------------- 1 | const COVERAGE_MAGIC_KEY = "_coverageSchema"; 2 | const COVERAGE_MAGIC_VALUE = "11020577277169172593"; 3 | 4 | export { 5 | COVERAGE_MAGIC_KEY, 6 | COVERAGE_MAGIC_VALUE 7 | } -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["@swc-node/register"], 3 | "reporter": "dot", 4 | "extensions": ["ts", "js"], 5 | "recursive": true, 6 | "enable-source-maps": true, 7 | "spec": "./spec/**/*.spec.*" 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | node_modules/ 3 | .swc/ 4 | *.log 5 | *.node 6 | index.d.ts 7 | index.js 8 | spec/swc-coverage-custom-transform/target/ 9 | a.js 10 | swc-plugin-coverage-*.tgz 11 | /tmp 12 | trace-*.json 13 | -------------------------------------------------------------------------------- /spec/swc-coverage-instrument-wasm/README.md: -------------------------------------------------------------------------------- 1 | ### SWC-Coverage-instrument-wasm 2 | 3 | Internal packages provides wasm-compiled bindings for `SourceCoverage`. This _can_ be used in js, but not meant to be published at this moment. 4 | -------------------------------------------------------------------------------- /spec/fixtures/ignore.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ignore file comment 3 | code: | 4 | /* istanbul ignore file */ 5 | output = true === true ? "works" : "doesn't work" 6 | opts: 7 | noCoverage: true 8 | tests: 9 | - name: file is ignored 10 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/macros/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod create_instrumentation_visitor; 2 | pub(crate) mod instrumentation_counter_helper; 3 | pub(crate) mod instrumentation_stmt_counter_helper; 4 | pub(crate) mod instrumentation_visitor; 5 | pub(crate) mod visit_mut_for_like; 6 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/coverage_template/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions to create an AST for instrumentation wrapper object injection. 2 | 3 | pub(crate) mod create_assignment_stmt; 4 | pub(crate) mod create_coverage_data_object; 5 | pub(crate) mod create_coverage_fn_decl; 6 | pub(crate) mod create_global_stmt_template; 7 | -------------------------------------------------------------------------------- /spec/fixtures/jsx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: handles experimental babylon features 3 | code: | 4 | var profile =
5 | 6 |

{[user.firstName, user.lastName].join(' ')}

7 |
; 8 | opts: 9 | generateOnly: true 10 | instrumentOpts: 11 | parserPlugins: 12 | - "jsx" 13 | tests: 14 | - name: jsx syntax 15 | -------------------------------------------------------------------------------- /spec/fixtures/meta-property.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: meta properties 3 | code: | 4 | "use strict"; 5 | class FooClass { 6 | constructor() { 7 | if (new.target === FooClass) { 8 | throw new Error('Cannot instantiate directly.'); 9 | } 10 | } 11 | } 12 | opts: 13 | generateOnly: true 14 | tests: 15 | - name: does not throw 16 | args: __notest__ 17 | -------------------------------------------------------------------------------- /spec/fixtures/issue-277.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: export const statements coverage (issue 277) 3 | code: | 4 | export const sayHi = () => 'hi'; 5 | 6 | export const sayHii = () => 'hii'; 7 | instrumentOpts: 8 | esModules: true 9 | tests: 10 | - name: export const coverage 11 | lines: {'1': 1, '3': 1} 12 | statements: {'0': 1, '1': 0, '2': 1, '3': 0} 13 | functions: {'0': 0, '1': 0} 14 | -------------------------------------------------------------------------------- /spec/fixtures/object-spread.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ES6 Object Spread 3 | guard: isObjectSpreadAvailable 4 | code: | 5 | const a = {foo: args[0]} 6 | const b = {...a} 7 | output = b.foo 8 | tests: 9 | - name: is instrumented 10 | args: [10] 11 | out: 10 12 | lines: { '1': 1, '2': 1, '3': 1 } 13 | statements: {'0': 1, '1': 1, '2': 1 } 14 | functions: {} 15 | branches: {} 16 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/coverage.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, PartialEq)] 2 | pub struct Coverage { 3 | covered: u32, 4 | total: u32, 5 | coverage: f32, 6 | } 7 | 8 | impl Coverage { 9 | pub fn new(covered: u32, total: u32, coverage: f32) -> Coverage { 10 | Coverage { 11 | covered, 12 | total, 13 | coverage, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod coverage; 2 | mod coverage_map; 3 | mod coverage_summary; 4 | mod file_coverage; 5 | mod percent; 6 | mod range; 7 | mod source_map; 8 | pub mod types; 9 | 10 | pub use coverage_map::CoverageMap; 11 | use coverage_summary::*; 12 | pub use file_coverage::FileCoverage; 13 | use percent::*; 14 | pub use range::*; 15 | pub use source_map::SourceMap; 16 | pub use types::*; 17 | -------------------------------------------------------------------------------- /spec/fixtures/issue-233.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: issue 233 3 | code: | 4 | let i = 0; 5 | 6 | const t = true && { 7 | renderFn: () => { 8 | i++ 9 | }, 10 | } 11 | 12 | t.renderFn(); 13 | tests: 14 | - name: covers right bin expr fn 15 | lines: {'1': 1, '3': 1, '5': 1, '9': 1} 16 | branches: {'0': [1, 1]} 17 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 18 | functions: {'0': 1} 19 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["OJ Kwon "] 3 | description = "Istanbul compatible coverage data struct" 4 | edition = "2021" 5 | license = "MIT" 6 | name = "istanbul-oxide" 7 | repository = "https://github.com/kwonoj/swc-coverage-instrument" 8 | version = "0.0.32" 9 | 10 | [dependencies] 11 | indexmap = { workspace = true, features = ["serde"] } 12 | serde = { workspace = true, features = ["derive"] } 13 | -------------------------------------------------------------------------------- /spec/swc-coverage-instrument-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "swc-coverage-instrument-wasm" 4 | version = "0.1.0" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | getrandom = { workspace = true, features = ["js"] } 11 | serde = { workspace = true, features = ["derive"] } 12 | serde-wasm-bindgen = { workspace = true } 13 | wasm-bindgen = { workspace = true, features = ["serde-serialize"] } 14 | 15 | swc-coverage-instrument = { workspace = true } 16 | -------------------------------------------------------------------------------- /spec/fixtures/with.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: with statement - no blocks 3 | code: | 4 | with (Math) output = abs(args[0]); 5 | tests: 6 | - args: [-1] 7 | out: 1 8 | lines: {'1': 1} 9 | statements: {'0': 1, '1': 1} 10 | instrumentOpts: 11 | esModules: false 12 | 13 | --- 14 | name: with statement with block 15 | code: | 16 | with (Math) { output = abs(args[0]); } 17 | tests: 18 | - args: [-1] 19 | out: 1 20 | lines: {'1': 1} 21 | statements: {'0': 1, '1': 1} 22 | instrumentOpts: 23 | esModules: false 24 | -------------------------------------------------------------------------------- /spec/fixtures/es6-modules.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: covers export statements 3 | guard: isExportAvailable 4 | code: | 5 | export var a =2, b=3; 6 | output = a + b; 7 | instrumentOpts: 8 | esModules: true 9 | opts: 10 | generateOnly: true 11 | tests: 12 | - name: export 13 | 14 | --- 15 | name: covers import statements 16 | guard: isImportAvailable 17 | code: | 18 | import util from "util"; 19 | output = util.format(args[0], args[1]); 20 | instrumentOpts: 21 | esModules: true 22 | opts: 23 | generateOnly: true 24 | tests: 25 | - name: import 26 | 27 | -------------------------------------------------------------------------------- /spec/fixtures/yield.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple yield 3 | guard: isYieldAvailable 4 | code: | 5 | function *yielder() { 6 | yield 1; 7 | yield 2; 8 | yield 3; 9 | } 10 | var x = 0, y = yielder(); 11 | for (var i = 0; i < 2; i += 1 ) { 12 | x += y.next().value; 13 | } 14 | output = x; 15 | tests: 16 | - name: coverage as expected 17 | args: [] 18 | out: 3 19 | lines: { '2': 1, '3': 1, '4': 0, '6': 1, '7': 1, '8': 2, '10': 1} 20 | functions: {'0': 1} 21 | statements: {'0': 1, '1':1, '2': 0, '3': 1, '4': 1, '5': 1, '6': 1, '7': 2, '8': 1} 22 | -------------------------------------------------------------------------------- /spec/fixtures/object-method.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: object method 3 | code: | 4 | const foo = {test() { return 'value' }} 5 | output = foo.test() 6 | tests: 7 | - name: object method is instrumented 8 | out: 'value' 9 | lines: {'1': 1, '2': 1} 10 | functions: {'0': 1} 11 | statements: {'0': 1, '1': 1, '2': 1} 12 | --- 13 | name: object getter 14 | code: | 15 | const foo = {get test() { return 'value' }} 16 | output = foo.test; 17 | tests: 18 | - name: object getter is instrumented 19 | out: 'value' 20 | lines: {'1': 1, '2': 1} 21 | functions: {'0': 1} 22 | statements: {'0': 1, '1': 1, '2': 1} 23 | -------------------------------------------------------------------------------- /spec/e2e/fixtures/should-be-excluded.test.js: -------------------------------------------------------------------------------- 1 | // This file should be excluded from coverage instrumentation 2 | // because it matches the **/*.test.* pattern 3 | 4 | function calculateSum(a, b) { 5 | return a + b; 6 | } 7 | 8 | function calculateProduct(x, y) { 9 | return x * y; 10 | } 11 | 12 | const testHelper = { 13 | setup: function () { 14 | console.log("Setting up test"); 15 | }, 16 | 17 | teardown: function () { 18 | console.log("Tearing down test"); 19 | }, 20 | }; 21 | 22 | if (typeof module !== "undefined") { 23 | module.exports = { 24 | calculateSum, 25 | calculateProduct, 26 | testHelper, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /spec/fixtures/tenary.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ternary operator 3 | code: | 4 | var x = args[0]; 5 | var y = args[1]; 6 | function foo() { 7 | return x > y ? x : y 8 | } 9 | output = foo(); 10 | tests: 11 | - name: covers then path 12 | args: [20, 10] 13 | out: 20 14 | lines: {'1': 1, '2': 1, '4': 1, '6': 1} 15 | branches: {'0': [1, 0]} 16 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 17 | functions: {'0': 1} 18 | - name: covers else path 19 | args: [10, 20] 20 | out: 20 21 | lines: { '1': 1, '2': 1, '4': 1, '6': 1 } 22 | branches: {'0': [0, 1]} 23 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 24 | functions: {'0': 1} -------------------------------------------------------------------------------- /spec/swc-coverage-custom-transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swc-coverage-custom-transform", 3 | "version": "0.1.0", 4 | "description": "Custom SWC transform for coverage instrumentation testing", 5 | "main": "./index.js", 6 | "napi": { 7 | "binaryName": "swc", 8 | "targets": [ 9 | "x86_64-unknown-linux-musl", 10 | "x86_64-unknown-freebsd", 11 | "i686-pc-windows-msvc", 12 | "aarch64-unknown-linux-gnu", 13 | "armv7-unknown-linux-gnueabihf", 14 | "aarch64-apple-darwin", 15 | "aarch64-linux-android", 16 | "aarch64-unknown-linux-musl", 17 | "aarch64-pc-windows-msvc", 18 | "armv7-linux-androideabi" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/percent.rs: -------------------------------------------------------------------------------- 1 | pub fn percent(covered: u32, total: u32) -> f32 { 2 | if total > 0 { 3 | let tmp: f64 = ((1000 * 100 * covered as u64) / total as u64) as f64; 4 | return (tmp as f32 / 10 as f32).floor() / 100 as f32; 5 | } else { 6 | return 100.0; 7 | } 8 | } 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use crate::percent; 13 | 14 | #[test] 15 | fn calculate_percentage_covered_and_total() { 16 | let p = percent(1, 1); 17 | assert_eq!(p as i32, 100); 18 | } 19 | 20 | #[test] 21 | fn calculate_percentage_with_precision() { 22 | let p = percent(999998, 999999); 23 | assert_eq!(p < 100 as f32, true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/visitors/stmt_like_visitor.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | common::{comments::Comments, util::take::Take, SourceMapper}, 3 | ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith}, 4 | }; 5 | 6 | use crate::{ 7 | create_instrumentation_visitor, instrumentation_counter_helper, 8 | instrumentation_stmt_counter_helper, instrumentation_visitor, 9 | }; 10 | 11 | create_instrumentation_visitor!(StmtVisitor {}); 12 | 13 | impl StmtVisitor { 14 | instrumentation_counter_helper!(); 15 | instrumentation_stmt_counter_helper!(); 16 | } 17 | 18 | impl VisitMut for StmtVisitor { 19 | instrumentation_visitor!(); 20 | } 21 | -------------------------------------------------------------------------------- /spec/fixtures/classes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: class declaration assignment name (top-level) 3 | guard: isInferredClassNameAvailable 4 | code: | 5 | const foo = class {} 6 | var bar = class {} 7 | output = foo.name + ' ' + bar.name; 8 | tests: 9 | - name: properly sets function name 10 | out: 'foo bar' 11 | lines: {'1': 1, '2': 1, '3': 1} 12 | functions: {} 13 | statements: {'0': 1, '1': 1, '2': 1} 14 | --- 15 | name: bug https://github.com/istanbuljs/nyc/issues/685 16 | guard: isClassAvailable 17 | code: | 18 | class MyClass extends (Object||Object) {} 19 | output = MyClass.name 20 | tests: 21 | - name: properly instruments code 22 | out: 'MyClass' 23 | lines: {'2': 1} 24 | functions: {} 25 | statements: {'0': 1} 26 | branches: {'0': [1, 0]} 27 | -------------------------------------------------------------------------------- /spec/fixtures/debug-test.yaml: -------------------------------------------------------------------------------- 1 | # For easier single test fixture debugging, `npm run test:debug` runs this test only. 2 | # Place any test want to run. 3 | --- 4 | name: ternary operator 5 | code: | 6 | var x = args[0]; 7 | var y = args[1]; 8 | function foo() { 9 | return x > y ? x : y 10 | } 11 | output = foo(); 12 | tests: 13 | - name: covers then path 14 | args: [20, 10] 15 | out: 20 16 | lines: {'1': 1, '2': 1, '4': 1, '6': 1} 17 | branches: {'0': [1, 0]} 18 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 19 | functions: {'0': 1} 20 | - name: covers else path 21 | args: [10, 20] 22 | out: 20 23 | lines: { '1': 1, '2': 1, '4': 1, '6': 1 } 24 | branches: {'0': [0, 1]} 25 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 26 | functions: {'0': 1} -------------------------------------------------------------------------------- /spec/fixtures/for-of.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple for-of 3 | guard: isForOfAvailable 4 | code: | 5 | function *x() { yield 1; yield 2; }; 6 | var k; 7 | output = 0; 8 | for (k of x()) { 9 | output += k; 10 | } 11 | tests: 12 | - args: [] 13 | out: 3 14 | lines: {'1': 1, '3': 1, '4': 1, '5': 2} 15 | functions: {'0': 1} 16 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 2} 17 | --- 18 | 19 | name: for-of with loop initializer 20 | guard: isForOfAvailable 21 | code: | 22 | function *x() { yield 1; yield 2; }; 23 | output = 0; 24 | for (var k of x()) { 25 | output += k; 26 | } 27 | tests: 28 | - args: [] 29 | out: 3 30 | lines: {'1': 1, '2': 1, '3': 1, '4': 2} 31 | functions: {'0': 1} 32 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 2} 33 | -------------------------------------------------------------------------------- /spec/fixtures/input-source-map.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: defined input source map 3 | code: | 4 | output = "test" 5 | inputSourceMap: { file: "test.js", mappings: "", names: [], sourceRoot: undefined, sources: [ "test.js" ], sourcesContent: [ 'output = "test"' ], version: 3 } 6 | tests: 7 | - name: sets the input source map 8 | args: [] 9 | out: "test" 10 | lines: { '1': 1 } 11 | statements: { '0': 1 } 12 | inputSourceMap: { file: "test.js", mappings: "", names: [], sourceRoot: undefined, sources: [ "test.js" ], sourcesContent: [ 'output = "test"' ], version: 3 } 13 | --- 14 | name: without input source map 15 | code: | 16 | output = "test" 17 | tests: 18 | - name: is not set on the coverage object 19 | args: [] 20 | out: "test" 21 | lines: { '1': 1 } 22 | statements: { '0': 1 } 23 | -------------------------------------------------------------------------------- /packages/swc-plugin-coverage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["OJ Kwon "] 3 | description = "Istanbul compatible coverage instrumentation plugin for SWC" 4 | edition = "2021" 5 | license = "MIT" 6 | name = "swc-plugin-coverage" 7 | repository = "https://github.com/kwonoj/swc-coverage-instrument" 8 | version = "0.0.32" 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | serde_json = { workspace = true } 15 | swc-coverage-instrument = { workspace = true } 16 | swc_core = { workspace = true, features = ["ecma_plugin_transform"] } 17 | tracing = { workspace = true } 18 | tracing-subscriber = { workspace = true, features = ["fmt"] } 19 | typed-path = { workspace = true } 20 | wax = { workspace = true } 21 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/coverage_template/create_assignment_stmt.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | common::{util::take::Take, DUMMY_SP}, 3 | ecma::ast::*, 4 | }; 5 | 6 | /// Create an assignment stmt AST for `var $var_decl_ident = $value;` 7 | pub fn create_assignment_stmt(var_decl_ident: &Ident, value: Expr) -> Stmt { 8 | Stmt::Decl(Decl::Var(Box::new(VarDecl { 9 | kind: VarDeclKind::Var, 10 | decls: vec![VarDeclarator { 11 | span: DUMMY_SP, 12 | name: Pat::Assign(AssignPat { 13 | span: DUMMY_SP, 14 | left: Box::new(Pat::Ident(BindingIdent::from(var_decl_ident.clone()))), 15 | right: Box::new(value), 16 | }), 17 | init: None, 18 | definite: false, 19 | }], 20 | ..VarDecl::dummy() 21 | }))) 22 | } 23 | -------------------------------------------------------------------------------- /spec/fixtures/for-in.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple for-in 3 | code: | 4 | var x = { a: args[0], b: args[1] }, k; 5 | output = 0; 6 | for (k in x) { 7 | if (x.hasOwnProperty(k) && x[k]) { 8 | output += x[k]; 9 | } 10 | } 11 | tests: 12 | - args: [10, 0] 13 | out: 10 14 | lines: {'1': 1, '2': 1, '3': 1, '4': 2, '5': 1} 15 | branches: {'0': [1, 1], '1': [2, 2]} 16 | statements: {'0': 1, '1': 1, '2': 1, '3': 2, '4': 1} 17 | 18 | --- 19 | name: for-in with loop initializer 20 | code: | 21 | var x = { a: args[0], b: args[1] }; 22 | output = 0; 23 | for (var k in x) { 24 | if (x.hasOwnProperty(k) && x[k]) { 25 | output += x[k]; 26 | } 27 | } 28 | tests: 29 | - args: [10, 0] 30 | out: 10 31 | lines: {'1': 1, '2': 1, '3': 1, '4': 2, '5': 1} 32 | branches: {'0': [1, 1], '1': [2, 2]} 33 | statements: {'0': 1, '1': 1, '2': 1, '3': 2, '4': 1} 34 | -------------------------------------------------------------------------------- /spec/fixtures/default-args.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ES6 default arguments 3 | guard: isDefaultArgsAvailable 4 | code: | 5 | function add(a = 1, b = 2, c = 3, d = 4) { 6 | return a + b + c + d; 7 | } 8 | output = add(args[0], args[1], args[2], args[3]) 9 | tests: 10 | - name: everything specified 11 | args: [10, 20, 30, 40] 12 | out: 100 13 | lines: { '2': 1, 4: 1} 14 | statements: {'0': 1, '1': 1 } 15 | functions: {'0': 1} 16 | branches: { '0': [0], '1': [0], '2': [0], '3': [0] } 17 | 18 | - name: 2 of 4 specified 19 | args: [3, 4 ] 20 | out: 14 21 | lines: { '2': 1, 4: 1} 22 | statements: {'0': 1, '1': 1 } 23 | functions: {'0': 1} 24 | branches: { '0': [0], '1': [0], '2': [1], '3': [1] } 25 | 26 | - name: nothing specified 27 | args: [] 28 | out: 10 29 | lines: { '2': 1, 4: 1} 30 | statements: {'0': 1, '1': 1 } 31 | functions: {'0': 1} 32 | branches: { '0': [1], '1': [1], '2': [1], '3': [1] } 33 | -------------------------------------------------------------------------------- /.github/workflows/bump-swc-core.yml: -------------------------------------------------------------------------------- 1 | name: Bump up swc_core 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | - cron: '0 6 * * *' 7 | 8 | jobs: 9 | upgrade-swc-core: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | submodules: true 16 | 17 | - uses: actions/cache@v3 18 | with: 19 | path: | 20 | ~/.cargo/bin/ 21 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 22 | 23 | - name: Install Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | override: true 28 | 29 | - uses: Swatinem/rust-cache@v2 30 | with: 31 | shared-key: "gha-cargo-upgrade" 32 | cache-on-failure: true 33 | 34 | - name: Run cargo upgrade 35 | uses: kwonoj/gha-cargo-upgrade@latest 36 | with: 37 | token: ${{ secrets.GHA_UPGRADE_TOKEN }} 38 | packages: "swc_core" 39 | incompatible: true 40 | -------------------------------------------------------------------------------- /spec/fixtures/issue-258.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: issue 258 - class property arrow function coverage 3 | code: | 4 | class Testing { 5 | method() { 6 | return 'Hello, World! ' + this.propertyFunction(); 7 | } 8 | 9 | propertyFunction = () => { 10 | return 'Instance'; 11 | }; 12 | } 13 | 14 | const instance = new Testing(); 15 | output = args === 1 ? instance.method() : 'not called'; 16 | tests: 17 | - name: covers arrow function body when called 18 | args: 1 19 | out: 'Hello, World! Instance' 20 | lines: {'3': 1, '6': 1, '7': 1, '11': 1, '12': 1} 21 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 22 | branches: {'0': [1, 0]} 23 | functions: {'0': 1, '1': 1} 24 | - name: does not cover arrow function body when not called 25 | args: 2 26 | out: 'not called' 27 | lines: {'3': 0, '6': 1, '7': 0, '11': 1, '12': 1} 28 | statements: {'0': 0, '1': 1, '2': 0, '3': 1, '4': 1} 29 | branches: {'0': [0, 1]} 30 | functions: {'0': 0, '1': 0} -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["OJ Kwon "] 3 | description = "Istanbul compatible coverage instrumentation visitor for SWC" 4 | edition = "2021" 5 | license = "MIT" 6 | name = "swc-coverage-instrument" 7 | repository = "https://github.com/kwonoj/swc-coverage-instrument" 8 | version = "0.0.32" 9 | 10 | [dependencies] 11 | istanbul-oxide = { workspace = true } 12 | once_cell = { workspace = true } 13 | regex = "1.8.1" 14 | serde = { workspace = true, features = ["derive"] } 15 | serde_json = { workspace = true } 16 | swc_atoms = { workspace = true } 17 | 18 | swc_core = { workspace = true, features = [ 19 | "common", 20 | "ecma_quote", 21 | "ecma_visit", 22 | "ecma_utils", 23 | "ecma_ast", 24 | ] } 25 | tracing = "0.1.37" 26 | 27 | [dev-dependencies] 28 | pretty_assertions = "1.3.0" 29 | 30 | [lints.rust] 31 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(swc_ast_unknown)'] } 32 | -------------------------------------------------------------------------------- /spec/swc-coverage-custom-transform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "swc-coverage-custom-transform" 4 | version = "0.1.0" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [build-dependencies] 10 | napi-build = "2.2.4" 11 | 12 | [dependencies] 13 | anyhow = "1.0.70" 14 | backtrace = "0.3.67" 15 | napi = { version = "3.4.0", default-features = false, features = [ 16 | "napi3", 17 | "serde-json", 18 | ] } 19 | napi-derive = { version = "3.3.0", default-features = false, features = [ 20 | "type-def", 21 | ] } 22 | serde = { version = "1.0.203", features = ["derive"] } 23 | serde_json = { version = "1.0.120", features = ["unbounded_depth"] } 24 | swc-coverage-instrument = { version = "0.0.32", path = "../../packages/swc-coverage-instrument" } 25 | swc_core = { version = "50.0.0", features = [ 26 | "common_concurrent", 27 | "ecma_transforms", 28 | "ecma_ast", 29 | "allocator_node", 30 | "ecma_visit", 31 | "base_node", 32 | ] } 33 | swc_error_reporters = { version = "20.0.0" } 34 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/source_map.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 4 | #[serde(rename_all = "camelCase", default)] 5 | pub struct SourceMap { 6 | pub version: u32, 7 | #[serde(default, skip_serializing_if = "Option::is_none")] 8 | pub file: Option, 9 | #[serde(default, skip_serializing_if = "Option::is_none")] 10 | pub source_root: Option, 11 | pub sources: Vec, 12 | #[serde(default, skip_serializing_if = "Option::is_none")] 13 | pub sources_content: Option>>, 14 | pub names: Vec, 15 | pub mappings: String, 16 | } 17 | 18 | impl Default for SourceMap { 19 | fn default() -> Self { 20 | SourceMap { 21 | version: 3, 22 | file: Default::default(), 23 | source_root: Default::default(), 24 | sources: Default::default(), 25 | sources_content: Default::default(), 26 | names: Default::default(), 27 | mappings: "".to_string(), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/utils/lookup_range.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use istanbul_oxide::Range; 4 | 5 | use swc_core::common::{SourceMapper, Span}; 6 | 7 | pub fn get_range_from_span(source_map: &Arc, span: &Span) -> Range { 8 | // https://github.com/swc-project/swc/issues/5535 9 | // There are some node types SWC passes transformed instead of original, 10 | // which are not able to locate original locations. 11 | // This'll makes to create less-accurate coverage for those types (i.e enums) 12 | // while waiting upstream decision instead of hard panic. 13 | if span.hi.is_dummy() || span.lo.is_dummy() { 14 | return Default::default(); 15 | } 16 | 17 | let span_hi_loc = source_map.lookup_char_pos(span.hi); 18 | let span_lo_loc = source_map.lookup_char_pos(span.lo); 19 | 20 | Range::new( 21 | span_lo_loc.line as u32, 22 | // TODO: swc_plugin::source_map::Pos to use to_u32() instead 23 | span_lo_loc.col.0 as u32, 24 | span_hi_loc.line as u32, 25 | span_hi_loc.col.0 as u32, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/range.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] 4 | pub struct Location { 5 | pub line: u32, 6 | pub column: u32, 7 | } 8 | impl Location { 9 | pub fn default() -> Location { 10 | Location { line: 0, column: 0 } 11 | } 12 | } 13 | 14 | #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] 15 | pub struct Range { 16 | pub start: Location, 17 | pub end: Location, 18 | } 19 | 20 | impl Range { 21 | pub fn default() -> Range { 22 | Range { 23 | start: Default::default(), 24 | end: Default::default(), 25 | } 26 | } 27 | pub fn new(start_line: u32, start_column: u32, end_line: u32, end_column: u32) -> Range { 28 | Range { 29 | start: Location { 30 | line: start_line, 31 | column: start_column, 32 | }, 33 | end: Location { 34 | line: end_line, 35 | column: end_column, 36 | }, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/e2e/fixtures/should-be-included.js: -------------------------------------------------------------------------------- 1 | // This file should be included in coverage instrumentation 2 | // because it does NOT match any exclusion patterns 3 | 4 | function add(a, b) { 5 | if (typeof a !== "number" || typeof b !== "number") { 6 | throw new Error("Both arguments must be numbers"); 7 | } 8 | return a + b; 9 | } 10 | 11 | function multiply(x, y) { 12 | if (x === 0 || y === 0) { 13 | return 0; 14 | } 15 | return x * y; 16 | } 17 | 18 | function divide(dividend, divisor) { 19 | if (divisor === 0) { 20 | throw new Error("Cannot divide by zero"); 21 | } 22 | return dividend / divisor; 23 | } 24 | 25 | const mathUtils = { 26 | isEven: function (num) { 27 | return num % 2 === 0; 28 | }, 29 | 30 | isOdd: function (num) { 31 | return num % 2 !== 0; 32 | }, 33 | 34 | factorial: function (n) { 35 | if (n < 0) return undefined; 36 | if (n === 0) return 1; 37 | return n * this.factorial(n - 1); 38 | }, 39 | }; 40 | 41 | if (typeof module !== "undefined") { 42 | module.exports = { 43 | add, 44 | multiply, 45 | divide, 46 | mathUtils, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OJ Kwon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/utils/node.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | #[derive(Copy, Debug, Clone, PartialEq)] 4 | pub enum Node { 5 | Program, 6 | ModuleDecl, 7 | Stmt, 8 | Stmts, 9 | Expr, 10 | VarDeclarator, 11 | ExprStmt, 12 | ModuleItems, 13 | ArrowExpr, 14 | SetterProp, 15 | GetterProp, 16 | MethodProp, 17 | BinExpr, 18 | LogicalExpr, 19 | CondExpr, 20 | LabeledStmt, 21 | FnExpr, 22 | FnDecl, 23 | WithStmt, 24 | SwitchCase, 25 | SwitchStmt, 26 | DoWhileStmt, 27 | WhileStmt, 28 | ForOfStmt, 29 | ForInStmt, 30 | ForStmt, 31 | IfStmt, 32 | VarDecl, 33 | TryStmt, 34 | ThrowStmt, 35 | ReturnStmt, 36 | DebuggerStmt, 37 | ContinueStmt, 38 | BreakStmt, 39 | PrivateProp, 40 | ClassProp, 41 | ClassDecl, 42 | ClassMethod, 43 | ExportDecl, 44 | ExportDefaultDecl, 45 | BlockStmt, 46 | AssignPat, 47 | TaggedTpl, 48 | } 49 | 50 | impl Display for Node { 51 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 52 | write!(f, "{:#?}", self) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | exclude = ["spec/swc-coverage-custom-transform"] 3 | members = [ 4 | "packages/swc-plugin-coverage", 5 | "packages/istanbul-oxide", 6 | "packages/swc-coverage-instrument", 7 | "spec/swc-coverage-instrument-wasm", 8 | ] 9 | resolver = "2" 10 | 11 | [profile.release] 12 | #lto = true 13 | 14 | [workspace.dependencies] 15 | istanbul-oxide = { path = "./packages/istanbul-oxide", version = "0.0.32" } 16 | swc-coverage-instrument = { path = "./packages/swc-coverage-instrument" } 17 | 18 | getrandom = { version = "0.2.15" } 19 | indexmap = { version = "2.2.6" } 20 | once_cell = { version = "1.19.0" } 21 | serde = { version = "1.0.203" } 22 | serde-wasm-bindgen = { version = "0.6.5" } 23 | serde_json = { version = "1.0.120" } 24 | swc_atoms = { version = "9.0.0" } 25 | swc_core = { version = "50.0.0" } 26 | tracing = { version = "0.1.37" } 27 | tracing-subscriber = { version = "0.3.17" } 28 | typed-path = { version = "0.11.0" } 29 | wasm-bindgen = { version = "0.2.92" } 30 | wax = { version = "0.6.0" } 31 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Include prebuilt constant values with build script 2 | include!(concat!(env!("OUT_DIR"), "/constants.rs")); 3 | mod constants; 4 | mod source_coverage; 5 | 6 | mod instrument; 7 | use instrument::create_increase_counter_expr::create_increase_counter_expr; 8 | use instrument::create_increase_true_expr::create_increase_true_expr; 9 | 10 | mod coverage_template; 11 | use coverage_template::create_assignment_stmt::create_assignment_stmt; 12 | use coverage_template::create_coverage_data_object::create_coverage_data_object; 13 | use coverage_template::create_coverage_fn_decl::*; 14 | use coverage_template::create_global_stmt_template::create_global_stmt_template; 15 | use source_coverage::SourceCoverage; 16 | 17 | #[macro_use] 18 | mod macros; 19 | 20 | mod visitors; 21 | pub use visitors::coverage_visitor::{create_coverage_instrumentation_visitor, CoverageVisitor}; 22 | mod options; 23 | pub use options::instrument_options::*; 24 | 25 | mod utils; 26 | use utils::hint_comments; 27 | use utils::lookup_range; 28 | pub use utils::node::Node; 29 | 30 | // Reexports 31 | pub use istanbul_oxide::types::*; 32 | pub use istanbul_oxide::FileCoverage; 33 | pub use istanbul_oxide::Range; 34 | pub use istanbul_oxide::SourceMap; 35 | -------------------------------------------------------------------------------- /spec/fixtures/statement.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: var decl no initializer 3 | code: | 4 | var x; 5 | output = 10 6 | tests: 7 | - name: ignores variable decl 8 | args: [] 9 | out: 10 10 | lines: {'2': 1} 11 | branches: {} 12 | statements: {'0': 1} 13 | --- 14 | name: simple statement 15 | code: | 16 | var x = args[0] > 5 ? args[0] : "undef"; 17 | output = x; 18 | tests: 19 | - name: covers line and one branch 20 | args: [10] 21 | out: 10 22 | lines: {'1': 1, '2': 1} 23 | branches: {'0': [1, 0]} 24 | statements: {'0': 1, '1': 1} 25 | 26 | - name: covers line and other branch 27 | args: [1] 28 | out: undef 29 | lines: {'1': 1, '2': 1} 30 | branches: {'0': [0, 1]} 31 | statements: {'0': 1, '1': 1} 32 | --- 33 | name: shebang code 34 | code: | 35 | #!/usr/bin/env node 36 | var x = args[0] > 5 ? args[0] : "undef"; 37 | output = x; 38 | opts: 39 | generateOnly: true 40 | 41 | # NOTE: SWC does not support mainline return syntax 42 | # --- 43 | # name: mainline return 44 | # instrumentOpts: 45 | # autoWrap: true 46 | # code: | 47 | # return 10; 48 | # tests: 49 | # - name: coverage for mainline return 50 | # args: [] 51 | # out: 10 52 | # lines: {'1': 1 } 53 | # statements: { '0': 1 } 54 | -------------------------------------------------------------------------------- /spec/fixtures/class-properties.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: class property declaration 3 | guard: isClassPropAvailable 4 | code: | 5 | class Foo { 6 | bar = 1; 7 | uninitialized; 8 | } 9 | output = args === 1 ? new Foo().bar : args 10 | tests: 11 | - name: covered 12 | args: 1 13 | out: 1 14 | lines: {'2': 1, '5': 1} 15 | statements: {'0': 1, '1': 1} 16 | branches: {'0': [1, 0]} 17 | functions: {} 18 | - name: not covered 19 | args: 2 20 | out: 2 21 | lines: {'2': 0, '5': 1} 22 | statements: {'0': 0, '1': 1} 23 | branches: {'0': [0, 1]} 24 | functions: {} 25 | --- 26 | name: class private property declaration 27 | guard: isClassPrivatePropAvailable 28 | code: | 29 | class Foo { 30 | #bar = 1; 31 | get bar() { return this.#bar; } 32 | #uninitialized; 33 | } 34 | output = args === 1 ? new Foo().bar : args 35 | tests: 36 | - name: covered 37 | args: 1 38 | out: 1 39 | lines: {'2': 1, '3': 1, '6': 1} 40 | statements: {'0': 1, '1': 1, '2': 1} 41 | branches: {'0': [1, 0]} 42 | functions: {'0': 1} 43 | - name: not covered 44 | args: 2 45 | out: 2 46 | lines: {'2': 0, '3': 0, '6': 1} 47 | statements: {'0': 0, '1': 0, '2': 1} 48 | branches: {'0': [0, 1]} 49 | functions: {'0': 0} 50 | -------------------------------------------------------------------------------- /spec/fixtures/do-while.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple do-while 3 | code: | 4 | var x = args[0], i=0; 5 | do { i++; } while (i < x); 6 | output = i; 7 | tests: 8 | - name: correct line coverage 9 | args: [10] 10 | out: 10 11 | lines: {'1': 1, '2': 10, '3': 1} 12 | statements: {'0': 1, '1': 1, '2': 1, '3': 10, '4': 1} 13 | 14 | - name: single entry into while 15 | args: [-1] 16 | out: 1 17 | lines: {'1': 1, '2': 1, '3': 1} 18 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 19 | 20 | --- 21 | name: block do-while on separate line 22 | code: | 23 | var x = args[0], i=0; 24 | do { 25 | i++; 26 | } while (i < x); 27 | output = i; 28 | tests: 29 | - name: correct line coverage 30 | args: [10] 31 | out: 10 32 | lines: {'1': 1, '2': 1, '3': 10, '5': 1} 33 | statements: {'0': 1, '1':1, '2': 1, '3': 10, '4': 1} 34 | 35 | - name: single entry into while 36 | args: [-1] 37 | out: 1 38 | lines: {'1': 1, '2': 1, '3': 1, '5': 1} 39 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 40 | 41 | --- 42 | name: ignore inside do-while 43 | code: | 44 | do { 45 | /* istanbul ignore next */ 46 | output = 20; 47 | } while (false); 48 | output = 10 49 | tests: 50 | - args: [] 51 | out: 10 52 | lines: {'1': 1, '5': 1} 53 | statements: {'0': 1, '1': 1 } 54 | -------------------------------------------------------------------------------- /spec/fixtures/try.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple try/catch 3 | code: | 4 | try { 5 | if (args[0] === "X") { throw "foo"; } 6 | output = args[0]; 7 | } catch (ex) { 8 | output="Y"; 9 | } finally { 10 | output += 1; 11 | } 12 | tests: 13 | - name: happy path 14 | args: [1] 15 | out: 2 16 | lines: {'1': 1, '2': 1, '3': 1, '5': 0, '7': 1} 17 | branches: {'0': [0, 1]} 18 | statements: {'0': 1, '1': 1, '2': 0, '3': 1, '4': 0, '5': 1} 19 | 20 | - name: sad path 21 | args: [X] 22 | out: Y1 23 | lines: {'1': 1, '2': 1, '3': 0, '5': 1, '7': 1} 24 | branches: {'0': [1, 0]} 25 | statements: {'0': 1, '1': 1, '2': 1, '3': 0, '4': 1, '5': 1} 26 | --- 27 | name: optional catch binding 28 | guard: isOptionalCatchBindingAvailable 29 | code: | 30 | try { 31 | if (args[0] === "X") { throw "foo"; } 32 | output = args[0]; 33 | } catch { 34 | output="Y"; 35 | } finally { 36 | output += 1; 37 | } 38 | tests: 39 | - name: happy path 40 | args: [1] 41 | out: 2 42 | lines: {'1': 1, '2': 1, '3': 1, '5': 0, '7': 1} 43 | branches: {'0': [0, 1]} 44 | statements: {'0': 1, '1': 1, '2': 0, '3': 1, '4': 0, '5': 1} 45 | 46 | - name: sad path 47 | args: [X] 48 | out: Y1 49 | lines: {'1': 1, '2': 1, '3': 0, '5': 1, '7': 1} 50 | branches: {'0': [1, 0]} 51 | statements: {'0': 1, '1': 1, '2': 1, '3': 0, '4': 1, '5': 1} 52 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/macros/visit_mut_for_like.rs: -------------------------------------------------------------------------------- 1 | /// A macro creates body for the for-variant visitors (for, for-of, for-in) which 2 | /// shares same logic. This also works for other loops like while, do-while. 3 | #[macro_export] 4 | macro_rules! visit_mut_for_like { 5 | ($self: ident, $for_like_stmt: ident) => { 6 | let (old, ignore_current) = $self.on_enter($for_like_stmt); 7 | 8 | match ignore_current { 9 | Some(crate::hint_comments::IgnoreScope::Next) => {} 10 | _ => { 11 | // cover_statement's is_stmt prepend logic for individual child stmt visitor 12 | $self.mark_prepend_stmt_counter(&$for_like_stmt.span); 13 | 14 | let body = *$for_like_stmt.body.take(); 15 | // if for stmt body is not block, wrap it before insert statement counter 16 | let body = if let Stmt::Block(body) = body { 17 | body 18 | } else { 19 | let stmts = vec![body]; 20 | BlockStmt { 21 | span: swc_core::common::DUMMY_SP, 22 | stmts, 23 | ..Default::default() 24 | } 25 | }; 26 | 27 | $for_like_stmt.body = Box::new(Stmt::Block(body)); 28 | // Iterate children for inner stmt's counter insertion 29 | $for_like_stmt.visit_mut_children_with($self); 30 | } 31 | } 32 | 33 | $self.on_exit(old); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::hash_map::DefaultHasher, 3 | hash::{Hash, Hasher}, 4 | }; 5 | 6 | /// Create compile-time constant values for the coverage schema hash & coverage lib version hash (magic-value) 7 | fn main() { 8 | let magic_key = "_coverageSchema"; 9 | let mut hasher = DefaultHasher::new(); 10 | let name = std::env::var("CARGO_PKG_NAME").unwrap(); 11 | // Use major as schema version, changing schema means major breaking anyway. 12 | let version = std::env::var("CARGO_PKG_VERSION_MAJOR").unwrap(); 13 | format!("{}@{}", name, version).hash(&mut hasher); 14 | let magic_value = hasher.finish().to_string(); 15 | 16 | let out_dir = std::env::var_os("OUT_DIR").unwrap(); 17 | let path = std::path::Path::new(&out_dir).join("constants.rs"); 18 | 19 | std::fs::write( 20 | &path, 21 | format!( 22 | r#"pub static COVERAGE_MAGIC_KEY: &'static str = "{}"; 23 | pub static COVERAGE_MAGIC_VALUE: &'static str = "{}";"#, 24 | magic_key, magic_value 25 | ), 26 | ) 27 | .unwrap(); 28 | 29 | let out_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap(); 30 | let path = std::path::PathBuf::from(&out_dir) 31 | .join("../../spec/util/") 32 | .join("constants.ts"); 33 | 34 | let _ = std::fs::write( 35 | &path, 36 | format!( 37 | r#"const COVERAGE_MAGIC_KEY = "{}"; 38 | const COVERAGE_MAGIC_VALUE = "{}"; 39 | 40 | export {{ 41 | COVERAGE_MAGIC_KEY, 42 | COVERAGE_MAGIC_VALUE 43 | }}"#, 44 | magic_key, magic_value 45 | ), 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/ci_main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: ['opened', 'reopened', 'synchronize'] 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | name: Run test 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.cargo/bin/ 24 | ~/.cargo/registry/index/ 25 | ~/.cargo/registry/cache/ 26 | ~/.cargo/git/db/ 27 | target/ 28 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: "18" 32 | cache: "npm" 33 | 34 | - uses: actions-rust-lang/setup-rust-toolchain@v1 35 | with: 36 | components: llvm-tools-preview 37 | - name: Install cargo-llvm-cov 38 | uses: taiki-e/install-action@cargo-llvm-cov 39 | - name: install 40 | run: | 41 | npm ci 42 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 43 | rustup target add wasm32-wasip1 44 | 45 | - name: test 46 | run: npm test 47 | - name: build 48 | run: | 49 | cargo check 50 | - name: Generate code coverage 51 | if: matrix.os == 'ubuntu-latest' 52 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 53 | - uses: codecov/codecov-action@v2 54 | with: 55 | token: ${{ secrets.CODECOV_TOKEN }} 56 | files: lcov.info 57 | verbose: true 58 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/coverage_template/create_global_stmt_template.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | common::{util::take::Take, DUMMY_SP}, 3 | ecma::{ast::*, utils::quote_ident}, 4 | }; 5 | 6 | use crate::constants::idents::IDENT_GLOBAL; 7 | 8 | use super::create_assignment_stmt::create_assignment_stmt; 9 | 10 | /// Creates an assignment statement for the global scope lookup function 11 | /// `var global = new Function("return $global_coverage_scope")();` 12 | pub fn create_global_stmt_template(coverage_global_scope: &str) -> Stmt { 13 | // Note: we don't support function template based on scoped binding 14 | // like https://github.com/istanbuljs/istanbuljs/blob/c7693d4608979ab73ebb310e0a1647e2c51f31b6/packages/istanbul-lib-instrument/src/visitor.js#L793= 15 | // due to scope checking is tricky. 16 | let fn_ctor = quote_ident!(Default::default(), "((function(){}).constructor)"); 17 | 18 | let expr = Expr::New(NewExpr { 19 | callee: Box::new(Expr::Ident(fn_ctor)), 20 | args: Some(vec![ExprOrSpread { 21 | spread: None, 22 | expr: Box::new(Expr::Lit(Lit::Str(Str { 23 | value: format!("return {}", coverage_global_scope).into(), 24 | ..Str::dummy() 25 | }))), 26 | }]), 27 | ..NewExpr::dummy() 28 | }); 29 | 30 | create_assignment_stmt( 31 | &IDENT_GLOBAL, 32 | Expr::Call(CallExpr { 33 | callee: Callee::Expr(Box::new(Expr::Paren(ParenExpr { 34 | span: DUMMY_SP, 35 | expr: Box::new(expr), 36 | }))), 37 | ..CallExpr::dummy() 38 | }), 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/macros/instrumentation_stmt_counter_helper.rs: -------------------------------------------------------------------------------- 1 | /// Create a fn inserts stmt counter for each stmt 2 | #[macro_export] 3 | macro_rules! instrumentation_stmt_counter_helper { 4 | () => { 5 | /// Visit individual statements with stmt_visitor and update. 6 | #[tracing::instrument(skip_all, fields(node = %self.print_node()))] 7 | fn insert_stmts_counter(&mut self, stmts: &mut Vec) { 8 | let mut new_stmts = vec![]; 9 | 10 | for mut stmt in stmts.drain(..) { 11 | if !self.is_injected_counter_stmt(&stmt) { 12 | let (old, ignore_current) = self.on_enter(&mut stmt); 13 | 14 | match ignore_current { 15 | Some(crate::hint_comments::IgnoreScope::Next) => {} 16 | _ => { 17 | let mut visitor = crate::visitors::stmt_like_visitor::StmtVisitor::new( 18 | self.source_map.clone(), 19 | self.comments.clone(), 20 | self.cov.clone(), 21 | self.instrument_options.clone(), 22 | self.nodes.clone(), 23 | ignore_current, 24 | ); 25 | stmt.visit_mut_children_with(&mut visitor); 26 | 27 | new_stmts.extend(visitor.before.drain(..)); 28 | } 29 | } 30 | self.on_exit(old); 31 | } 32 | 33 | new_stmts.push(stmt); 34 | } 35 | 36 | *stmts = new_stmts; 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/options/instrument_options.rs: -------------------------------------------------------------------------------- 1 | use istanbul_oxide::SourceMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase", default)] 6 | pub struct InstrumentLogOptions { 7 | pub level: Option, 8 | pub enable_trace: bool, 9 | } 10 | 11 | impl Default for InstrumentLogOptions { 12 | fn default() -> Self { 13 | InstrumentLogOptions { 14 | level: None, 15 | enable_trace: false, 16 | } 17 | } 18 | } 19 | 20 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 21 | #[serde(rename_all = "camelCase", default)] 22 | pub struct InstrumentOptions { 23 | pub coverage_variable: String, 24 | pub compact: bool, 25 | pub report_logic: bool, 26 | pub ignore_class_methods: Vec, 27 | pub input_source_map: Option, 28 | pub instrument_log: InstrumentLogOptions, 29 | pub debug_initial_coverage_comment: bool, 30 | // Allow to specify which files should be excluded from instrumentation. 31 | // This option accepts an array of wax(https://crates.io/crates/wax)-compatible glob patterns 32 | // and will match against the filename provided by swc's core. 33 | pub unstable_exclude: Option>, 34 | } 35 | 36 | impl Default for InstrumentOptions { 37 | fn default() -> Self { 38 | InstrumentOptions { 39 | coverage_variable: "__coverage__".to_string(), 40 | compact: false, 41 | report_logic: false, 42 | ignore_class_methods: Default::default(), 43 | input_source_map: Default::default(), 44 | instrument_log: Default::default(), 45 | debug_initial_coverage_comment: false, 46 | unstable_exclude: Default::default(), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/instrument/create_increase_counter_expr.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{common::DUMMY_SP, ecma::ast::*}; 2 | 3 | /// Creates a expr like `cov_17709493053001988098().s[0]++;` 4 | /// idx indicates index of vec-based counters (i.e branches). 5 | /// If it exists, creates a expr with idx like 6 | /// 1cov_17709493053001988098().b[0][idx]++;` instead. 7 | pub fn create_increase_counter_expr( 8 | type_ident: &Ident, 9 | id: u32, 10 | var_name: &Ident, 11 | idx: Option, 12 | ) -> Expr { 13 | let call = CallExpr { 14 | span: DUMMY_SP, 15 | callee: Callee::Expr(Box::new(Expr::Ident(var_name.clone()))), 16 | args: vec![], 17 | type_args: None, 18 | ..Default::default() 19 | }; 20 | 21 | let c = MemberExpr { 22 | span: DUMMY_SP, 23 | obj: Box::new(Expr::Call(call)), 24 | prop: MemberProp::Ident(type_ident.clone().into()), 25 | }; 26 | 27 | let expr = MemberExpr { 28 | span: DUMMY_SP, 29 | obj: Box::new(Expr::Member(c)), 30 | prop: MemberProp::Computed(ComputedPropName { 31 | span: DUMMY_SP, 32 | expr: Box::new(Expr::Lit(Lit::Num(Number { 33 | span: DUMMY_SP, 34 | value: id as f64, 35 | raw: None, 36 | }))), 37 | }), 38 | }; 39 | 40 | let expr = if let Some(idx) = idx { 41 | MemberExpr { 42 | span: DUMMY_SP, 43 | obj: Box::new(Expr::Member(expr)), 44 | prop: MemberProp::Computed(ComputedPropName { 45 | span: DUMMY_SP, 46 | expr: Box::new(Expr::Lit(Lit::Num(Number { 47 | span: DUMMY_SP, 48 | value: idx as f64, 49 | raw: None, 50 | }))), 51 | }), 52 | } 53 | } else { 54 | expr 55 | }; 56 | 57 | Expr::Update(UpdateExpr { 58 | span: DUMMY_SP, 59 | op: UpdateOp::PlusPlus, 60 | prefix: false, 61 | arg: Box::new(Expr::Member(expr)), 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /spec/fixtures/while.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple while 3 | code: | 4 | var x = args[0], i=0; 5 | while (i < x) i++; 6 | output = i; 7 | tests: 8 | - name: covers loop once 9 | args: [1] 10 | out: 1 11 | lines: {'1': 1, '2': 1, '3': 1} 12 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 13 | 14 | - name: covers loop multiple times 15 | args: [10] 16 | out: 10 17 | lines: {'1': 1, '2': 10, '3': 1} 18 | statements: {'0': 1, '1': 1, '2': 1, '3': 10, '4': 1} 19 | 20 | --- 21 | name: simple while - statement on new line 22 | code: | 23 | var x = args[0], i=0; 24 | while (i < x) 25 | i++; 26 | output = i; 27 | tests: 28 | - name: enters loop 29 | args: [10] 30 | out: 10 31 | lines: {'1': 1, '2': 1, '3': 10, '4': 1} 32 | statements: {'0': 1, '1': 1, '2': 1, '3': 10, '4': 1} 33 | 34 | - name: does not enter loop 35 | args: [-1] 36 | out: 0 37 | lines: {'1': 1, '2': 1, '3': 0, '4': 1} 38 | statements: {'0': 1, '1': 1, '2': 1, '3': 0, '4': 1} 39 | 40 | --- 41 | name: simple while - statement in block 42 | code: | 43 | var x = args[0], i=0; 44 | while (i < x) { i++; } 45 | output = i; 46 | tests: 47 | - name: enters loop 48 | args: [10] 49 | out: 10 50 | lines: {'1': 1, '2': 10, '3': 1} 51 | statements: {'0': 1, '1': 1, '2': 1, '3': 10, '4': 1} 52 | 53 | --- 54 | name: labeled nested while 55 | code: | 56 | var x = args[0], i=0, j=0, output = 0; 57 | outer: 58 | while (i++ < x) { 59 | j =0; 60 | while (j++ < i) { 61 | output++; 62 | if (j === 2) continue outer; 63 | } 64 | } 65 | tests: 66 | - name: line/branch coverage when all branches exercised 67 | args: [10] 68 | out: 19 69 | lines: {'1': 1, '2': 1, '3': 1, '4': 10, '5': 10, '6': 19, '7': 19} 70 | branches: {'0': [9, 10]} 71 | statements: {'0': 1, '1':1, '2':1, '3':1, '4': 1, '5': 1, '6': 10, '7': 10, '8': 19, '9': 19, '10': 9} 72 | 73 | - name: line/branch coverage when nothing exercised 74 | args: [-1] 75 | out: 0 76 | lines: {'1': 1, '2': 1, '3': 1, '4': 0, '5': 0, '6': 0, '7': 0} 77 | branches: {'0': [0, 0]} 78 | statements: {'0': 1, '1':1, '2':1, '3':1, '4': 1, '5': 1, '6': 0, '7': 0, '8': 0, '9': 0, '10': 0} 79 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/README.md: -------------------------------------------------------------------------------- 1 | # SWC-coverage-instrument 2 | 3 | `swc-coverage-instrument` is a set of packages to support [istanbuljs](https://github.com/istanbuljs/istanbuljs) compatible coverage instrumentation in [SWC](https://github.com/swc-project/swc)'s transform passes. Instrumentation transform can be performed either via SWC's wasm-based plugin, or using custom passes in rust side transform chains. 4 | 5 | ## What does compatible exactly means? 6 | 7 | This instrumentation will generate a data struct mimics istanbuljs's `FileCoverage` [object] (https://github.com/istanbuljs/istanbuljs/blob/c7693d4608979ab73ebb310e0a1647e2c51f31b6/packages/istanbul-lib-coverage/lib/file-coverage.js#L97=) conforms fixture test suite from istanbuljs itself. 8 | 9 | However, this doesn't mean instrumentation supports exact same [interfaces](https://github.com/istanbuljs/istanbuljs/blob/c7693d4608979ab73ebb310e0a1647e2c51f31b6/packages/istanbul-lib-instrument/src/source-coverage.js#L37=) surrounding coverage object as well as supporting exact same options. There are some fundamental differences between runtime, and ast visitor architecture between different compilers does not allow identical behavior. This package will try `best attempt` as possible. 10 | 11 | **NOTE: Package can have breaking changes without major semver bump** 12 | 13 | While stablzing its interfaces, this package does not gaurantee semver compliant breaking changes yet. Please refer changelogs if you're encountering unexpected breaking behavior across versions. 14 | 15 | # Usage 16 | 17 | ## Using custom transform pass in rust 18 | 19 | There is a single interface exposed to create a visitor for the transform, which you can pass into `before_custom_pass`. 20 | 21 | ``` 22 | let visitor = swc_coverage_instrument::create_coverage_instrumentation_visitor( 23 | source_map: std::sync::Arc, 24 | comments: C, 25 | instrument_options: InstrumentOptions, 26 | filename: String, 27 | ); 28 | 29 | let fold = as_folder(visitor); 30 | ``` 31 | 32 | `InstrumentationOptions` is a subset of istanbul's instrumentation options. Refer [istanbul's option](https://github.com/istanbuljs/istanbuljs/blob/master/packages/istanbul-lib-instrument/src/instrumenter.js#L16-L27=) for the same configuration flags. 33 | 34 | For the logging, this package does not init any subscriber by itself. Caller should setup proper `tracing-subscriber` as needed. 35 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/visitors/finders.rs: -------------------------------------------------------------------------------- 1 | use swc_core::ecma::{ 2 | ast::*, 3 | visit::{Visit, VisitWith}, 4 | }; 5 | 6 | /// A visitor to check if counter need to be `hoisted` for certain types of nodes. 7 | #[derive(Debug)] 8 | pub struct HoistingFinder(pub bool); 9 | 10 | impl HoistingFinder { 11 | pub fn new() -> HoistingFinder { 12 | HoistingFinder(false) 13 | } 14 | } 15 | 16 | impl Visit for HoistingFinder { 17 | fn visit_fn_expr(&mut self, _fn_expr: &FnExpr) { 18 | self.0 = true; 19 | } 20 | 21 | fn visit_arrow_expr(&mut self, _arrow_expr: &ArrowExpr) { 22 | self.0 = true; 23 | } 24 | 25 | fn visit_class_expr(&mut self, _class_expr: &ClassExpr) { 26 | self.0 = true; 27 | } 28 | } 29 | 30 | /// Check if nodes have block statements. 31 | #[derive(Debug)] 32 | pub struct BlockStmtFinder(pub bool); 33 | 34 | impl BlockStmtFinder { 35 | pub fn new() -> BlockStmtFinder { 36 | BlockStmtFinder(false) 37 | } 38 | } 39 | 40 | impl Visit for BlockStmtFinder { 41 | fn visit_block_stmt(&mut self, _block: &BlockStmt) { 42 | self.0 = true; 43 | } 44 | } 45 | 46 | #[derive(Debug)] 47 | pub struct StmtFinder(pub bool); 48 | 49 | impl StmtFinder { 50 | pub fn new() -> StmtFinder { 51 | StmtFinder(false) 52 | } 53 | } 54 | 55 | impl Visit for StmtFinder { 56 | fn visit_stmt(&mut self, _block: &Stmt) { 57 | self.0 = true; 58 | } 59 | } 60 | 61 | // Check a node have expressions. 62 | #[derive(Debug)] 63 | pub struct ExprFinder(pub bool); 64 | 65 | impl ExprFinder { 66 | pub fn new() -> ExprFinder { 67 | ExprFinder(false) 68 | } 69 | } 70 | 71 | impl Visit for ExprFinder { 72 | fn visit_expr(&mut self, _block: &Expr) { 73 | self.0 = true; 74 | } 75 | } 76 | 77 | /// Traverse down given nodes to check if it's leaf of the logical expr, 78 | /// or have inner logical expr to recurse. 79 | #[derive(Debug)] 80 | pub struct LogicalExprLeafFinder(pub bool); 81 | 82 | impl Visit for LogicalExprLeafFinder { 83 | fn visit_bin_expr(&mut self, bin_expr: &BinExpr) { 84 | match &bin_expr.op { 85 | BinaryOp::LogicalOr | BinaryOp::LogicalAnd | BinaryOp::NullishCoalescing => { 86 | self.0 = true; 87 | // short curcuit, we know it's not leaf 88 | return; 89 | } 90 | _ => {} 91 | } 92 | 93 | bin_expr.visit_children_with(self); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /spec/fixtures/strict.yaml.skipped: -------------------------------------------------------------------------------- 1 | # TODO: this test is skipped in upstream via guard isObjectFreezeAvailable, 2 | # look like using transpiler (swc-node) enforces strict modes for the guard fn. 3 | --- 4 | name: function expr using strict 5 | guard: isObjectFreezeAvailable 6 | code: | 7 | (function () { 8 | "use strict"; 9 | var x = Object.freeze({ foo: 1 }); 10 | try { 11 | x.foo = 2; 12 | output = "fail"; 13 | } catch (ex) { 14 | output = "pass"; 15 | } 16 | }()); 17 | tests: 18 | - name: covers one statement less 19 | args: [] 20 | out: pass 21 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 0, '5': 1} 22 | lines: {'1': 1, '3': 1, '4': 1, '5': 1, '6': 0, '8': 1} 23 | functions: {'0': 1} 24 | 25 | --- 26 | name: function decl using strict 27 | guard: isObjectFreezeAvailable 28 | code: | 29 | function foo() { 30 | "use strict"; 31 | var x = Object.freeze({ foo: 1 }); 32 | try { 33 | x.foo = 2; 34 | output = "fail"; 35 | } catch (ex) { 36 | output = "pass"; 37 | } 38 | } 39 | foo(); 40 | tests: 41 | - name: covers one statement less 42 | args: [] 43 | out: pass 44 | statements: {'0': 1, '1': 1, '2': 1, '3': 0, '4': 1, '5': 1 } 45 | lines: { '3': 1, '4': 1, '5': 1, '6': 0, '8': 1, '11': 1} 46 | functions: {'0': 1} 47 | 48 | --- 49 | name: function decl that looks like strict but is not 50 | guard: isObjectFreezeAvailable 51 | code: | 52 | function foo() { 53 | 1; 54 | "use strict"; 55 | var x = Object.freeze({ foo: 1 }); 56 | try { 57 | x.foo = 2; 58 | output = "fail"; 59 | } catch (ex) { 60 | output = "pass"; 61 | } 62 | } 63 | foo(); 64 | tests: 65 | - name: covers all statements as usual 66 | args: [] 67 | out: fail 68 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '6': 0, '7': 1 } 69 | lines: { '2': 1, '3': 1, '4': 1, '5': 1, '6': 1, '7': 1, '9': 0, '12': 1} 70 | functions: {'0': 1} 71 | 72 | --- 73 | name: file-level strict declaration 74 | guard: isObjectFreezeAvailable 75 | code: | 76 | "use strict"; 77 | var x = Object.freeze({ foo: 1 }); 78 | try { 79 | x.foo = 2; 80 | output = "fail"; 81 | } catch (ex) { 82 | output = "pass"; 83 | } 84 | tests: 85 | - args: [] 86 | out: fail 87 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 0} 88 | lines: {'2': 1, '3': 1, '4': 1, '5': 1, '7': 0} 89 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/visitors/switch_case_visitor.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | common::{comments::Comments, util::take::Take, SourceMapper, DUMMY_SP}, 3 | ecma::{ 4 | ast::*, 5 | visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith}, 6 | }, 7 | }; 8 | use tracing::instrument; 9 | 10 | use crate::{ 11 | constants::idents::IDENT_B, create_instrumentation_visitor, instrumentation_counter_helper, 12 | instrumentation_stmt_counter_helper, instrumentation_visitor, 13 | }; 14 | 15 | create_instrumentation_visitor!(SwitchCaseVisitor { branch: u32 }); 16 | 17 | /// A visitor to traverse down given logical expr's value (left / right) with existing branch idx. 18 | /// This is required to preserve branch id to recursively traverse logical expr's inner child. 19 | impl SwitchCaseVisitor { 20 | instrumentation_counter_helper!(); 21 | instrumentation_stmt_counter_helper!(); 22 | } 23 | 24 | impl VisitMut for SwitchCaseVisitor { 25 | instrumentation_visitor!(); 26 | 27 | // SwitchCase: entries(coverSwitchCase), 28 | #[instrument(skip_all, fields(node = %self.print_node()))] 29 | fn visit_mut_switch_case(&mut self, switch_case: &mut SwitchCase) { 30 | let (old, ignore_current) = self.on_enter(switch_case); 31 | match ignore_current { 32 | Some(crate::hint_comments::IgnoreScope::Next) => {} 33 | _ => { 34 | // TODO: conslidate brach expr creation, i.e ifstmt 35 | let range = 36 | crate::lookup_range::get_range_from_span(&self.source_map, &switch_case.span); 37 | let idx = self.cov.borrow_mut().add_branch_path(self.branch, &range); 38 | let expr = crate::create_increase_counter_expr( 39 | &IDENT_B, 40 | self.branch, 41 | &self.cov_fn_ident, 42 | Some(idx), 43 | ); 44 | 45 | switch_case.visit_mut_children_with(self); 46 | 47 | let expr = Stmt::Expr(ExprStmt { 48 | span: DUMMY_SP, 49 | expr: Box::new(expr), 50 | }); 51 | 52 | let mut new_stmts = vec![expr]; 53 | new_stmts.extend(switch_case.cons.drain(..)); 54 | 55 | switch_case.cons = new_stmts; 56 | } 57 | } 58 | self.on_exit(old); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /spec/fixtures/expressions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple expression 3 | code: | 4 | var x = args[0] > 0 && args[0] < 5; 5 | output = x; 6 | tests: 7 | - name: covers line and one branch 8 | args: [-1] 9 | out: false 10 | lines: {'1': 1, '2': 1} 11 | branches: {'0': [1, 0]} 12 | statements: {'0': 1, '1': 1} 13 | 14 | - name: covers line, both branches, returns false 15 | args: [10] 16 | out: false 17 | lines: {'1': 1, '2': 1} 18 | branches: {'0': [1, 1]} 19 | statements: {'0': 1, '1': 1} 20 | 21 | - name: covers line, both branches, returns true 22 | args: [3] 23 | out: true 24 | lines: {'1': 1, '2': 1} 25 | branches: {'0': [1, 1]} 26 | statements: {'0': 1, '1': 1} 27 | 28 | --- 29 | name: complex expression 30 | code: | 31 | var x = args[0] > 0 && (args[0] < 5 || args[0] > 10); 32 | output = x; 33 | tests: 34 | - name: covers line and one branch 35 | args: [-1] 36 | out: false 37 | lines: {'1': 1, '2': 1} 38 | branches: {'0': [1, 0, 0]} 39 | statements: {'0': 1, '1': 1} 40 | 41 | - name: covers line, both branches, returns false 42 | args: [9] 43 | out: false 44 | lines: {'1': 1, '2': 1} 45 | branches: {'0': [1, 1, 1]} 46 | statements: {'0': 1, '1': 1} 47 | 48 | - name: covers line, both branches, returns true 49 | args: [3] 50 | out: true 51 | lines: {'1': 1, '2': 1} 52 | branches: {'0': [1, 1, 0]} 53 | statements: {'0': 1, '1': 1} 54 | 55 | --- 56 | name: array expression with empty positions 57 | code: | 58 | var x = [args[0], , args[1], ]; 59 | output = x.indexOf(args[1]) === x.length - 1 && x[0] !== x[1]; 60 | tests: 61 | - name: covers correctly without error 62 | args: [1,5] 63 | out: true 64 | lines: {'1': 1, '2': 1} 65 | branches: { '0': [1, 1] } 66 | statements: {'0': 1, '1': 1} 67 | 68 | --- 69 | name: or with object expression (bug track) 70 | code: | 71 | var x = args[0] ? { foo: 1 } : { foo: 2 }; 72 | output = x.foo; 73 | tests: 74 | - name: covers all branches correctly 75 | args: [ false ] 76 | out: 2 77 | lines: {'1': 1, '2': 1} 78 | branches: { '0': [0, 1] } 79 | statements: {'0': 1, '1': 1} 80 | 81 | --- 82 | name: or with object expression (part 2) 83 | code: | 84 | var x = args[0] || { foo: 2 }; 85 | output = x.foo; 86 | tests: 87 | - name: covers all branches correctly 88 | args: [ false ] 89 | out: 2 90 | lines: {'1': 1, '2': 1} 91 | branches: { '0': [1, 1] } 92 | statements: {'0': 1, '1': 1} 93 | -------------------------------------------------------------------------------- /spec/swc-coverage-instrument-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unexpected_cfgs)] 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use swc_coverage_instrument::FileCoverage; 6 | use swc_coverage_instrument::COVERAGE_MAGIC_KEY; 7 | use swc_coverage_instrument::COVERAGE_MAGIC_VALUE; 8 | use wasm_bindgen::prelude::*; 9 | 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct CoverageMagicValue { 13 | key: String, 14 | value: String, 15 | } 16 | 17 | #[wasm_bindgen(js_name = "getCoverageMagicConstants")] 18 | pub fn get_coverage_magic_constants() -> JsValue { 19 | serde_wasm_bindgen::to_value(&CoverageMagicValue { 20 | key: COVERAGE_MAGIC_KEY.to_string(), 21 | value: COVERAGE_MAGIC_VALUE.to_string(), 22 | }) 23 | .unwrap() 24 | } 25 | 26 | /// Wraps FileCoverage for the wasm-bindgen to allow to use coverage struct in JS context. 27 | #[wasm_bindgen] 28 | pub struct FileCoverageInterop { 29 | inner: FileCoverage, 30 | } 31 | 32 | #[wasm_bindgen] 33 | impl FileCoverageInterop { 34 | #[wasm_bindgen(constructor)] 35 | pub fn new(val: &JsValue) -> FileCoverageInterop { 36 | #[allow(deprecated)] 37 | let inner: FileCoverage = val.into_serde().unwrap(); 38 | 39 | FileCoverageInterop { inner } 40 | } 41 | 42 | #[wasm_bindgen(js_name = "getLineCoverage")] 43 | pub fn get_line_coverage(&self) -> JsValue { 44 | serde_wasm_bindgen::to_value(&self.inner.get_line_coverage()).unwrap() 45 | } 46 | 47 | #[wasm_bindgen(js_name = "f")] 48 | pub fn get_f(&self) -> JsValue { 49 | serde_wasm_bindgen::to_value(&self.inner.f).unwrap() 50 | } 51 | 52 | #[wasm_bindgen(js_name = "b")] 53 | pub fn get_b(&self) -> JsValue { 54 | serde_wasm_bindgen::to_value(&self.inner.b).unwrap() 55 | } 56 | 57 | #[wasm_bindgen(js_name = "bT")] 58 | pub fn get_b_t(&self) -> JsValue { 59 | serde_wasm_bindgen::to_value(&self.inner.b_t).unwrap() 60 | } 61 | 62 | #[wasm_bindgen(js_name = "s")] 63 | pub fn get_s(&self) -> JsValue { 64 | serde_wasm_bindgen::to_value(&self.inner.s).unwrap() 65 | } 66 | 67 | #[wasm_bindgen(js_name = "inputSourceMap")] 68 | pub fn get_source_map(&self) -> JsValue { 69 | if let Some(source_map) = &self.inner.input_source_map { 70 | serde_wasm_bindgen::to_value(source_map).unwrap_or(JsValue::undefined()) 71 | } else { 72 | JsValue::undefined() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/types.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{coverage::Coverage, Range}; 5 | 6 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 7 | pub struct Function { 8 | pub name: String, 9 | pub decl: Range, 10 | pub loc: Range, 11 | pub line: u32, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 15 | #[serde(rename_all = "kebab-case")] 16 | pub enum BranchType { 17 | BinaryExpr, 18 | DefaultArg, 19 | If, 20 | Switch, 21 | CondExpr, 22 | } 23 | 24 | impl ToString for BranchType { 25 | fn to_string(&self) -> String { 26 | match self { 27 | BranchType::BinaryExpr => "binary-expr".to_string(), 28 | BranchType::DefaultArg => "default-arg".to_string(), 29 | BranchType::If => "if".to_string(), 30 | BranchType::Switch => "switch".to_string(), 31 | BranchType::CondExpr => "cond-expr".to_string(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 37 | pub struct Branch { 38 | pub loc: Option, 39 | #[serde(rename = "type")] 40 | pub branch_type: BranchType, 41 | pub locations: Vec, 42 | pub line: Option, 43 | } 44 | 45 | impl Branch { 46 | pub fn from_line(branch_type: BranchType, line: u32, locations: Vec) -> Branch { 47 | Branch { 48 | loc: None, 49 | branch_type, 50 | locations, 51 | line: Some(line), 52 | } 53 | } 54 | pub fn from_loc(branch_type: BranchType, loc: Range, locations: Vec) -> Branch { 55 | Branch { 56 | loc: Some(loc), 57 | branch_type, 58 | locations, 59 | line: None, 60 | } 61 | } 62 | } 63 | 64 | /// Map to line number to hit count. 65 | pub type LineHitMap = IndexMap; 66 | pub type StatementMap = IndexMap; 67 | pub type FunctionMap = IndexMap; 68 | pub type BranchMap = IndexMap; 69 | pub type BranchHitMap = IndexMap>; 70 | pub type BranchCoverageMap = IndexMap; 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use crate::BranchType; 75 | 76 | #[test] 77 | fn branch_type_should_return_kebab_string() { 78 | assert_eq!(&BranchType::BinaryExpr.to_string(), "binary-expr"); 79 | assert_eq!(&BranchType::DefaultArg.to_string(), "default-arg"); 80 | assert_eq!(&BranchType::If.to_string(), "if"); 81 | assert_eq!(&BranchType::Switch.to_string(), "switch"); 82 | assert_eq!(&BranchType::CondExpr.to_string(), "cond-expr"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /spec/util/guards.ts: -------------------------------------------------------------------------------- 1 | function tryThis(str, feature, generateOnly) { 2 | if (!generateOnly) { 3 | try { 4 | eval(str); 5 | } catch (ex) { 6 | console.error( 7 | "ES6 feature [" + feature + "] is not available in this environment" 8 | ); 9 | return false; 10 | } 11 | } 12 | return true; 13 | } 14 | 15 | function isYieldAvailable() { 16 | return tryThis("function *foo() { yield 1; }", "yield"); 17 | } 18 | 19 | function isClassPropAvailable() { 20 | return tryThis("class Foo { a = 1; }", "class property"); 21 | } 22 | 23 | function isClassPrivatePropAvailable() { 24 | return tryThis("class Foo { #a = 1; }", "class private property"); 25 | } 26 | 27 | function isForOfAvailable() { 28 | return tryThis( 29 | "function *foo() { yield 1; }\n" + "for (var k of foo()) {}", 30 | "for-of" 31 | ); 32 | } 33 | 34 | function isArrowFnAvailable() { 35 | return tryThis("[1 ,2, 3].map(x => x * x)", "arrow function"); 36 | } 37 | 38 | function isObjectSpreadAvailable() { 39 | return tryThis("const a = {...{b: 33}}", "object-spread"); 40 | } 41 | 42 | function isObjectFreezeAvailable() { 43 | if (!Object.freeze) { 44 | return false; 45 | } 46 | const foo = Object.freeze({}); 47 | try { 48 | foo.bar = 1; 49 | return false; 50 | } catch (ex) { 51 | return true; 52 | } 53 | } 54 | 55 | function isOptionalCatchBindingAvailable() { 56 | return tryThis("try {} catch {}"); 57 | } 58 | 59 | function isImportAvailable() { 60 | return tryThis('import fs from "fs"', "import", true); 61 | } 62 | 63 | function isExportAvailable() { 64 | return tryThis("export default function foo() {}", "export", true); 65 | } 66 | 67 | function isDefaultArgsAvailable() { 68 | return tryThis("function foo(a=1) { return a + 1; }", "default args"); 69 | } 70 | 71 | function isInferredFunctionNameAvailable() { 72 | return tryThis( 73 | 'const foo = function () {}; require("assert").equal(foo.name, "foo")' 74 | ); 75 | } 76 | 77 | function isInferredClassNameAvailable() { 78 | return tryThis( 79 | 'const foo = class {}; require("assert").equal(foo.name, "foo")' 80 | ); 81 | } 82 | 83 | function isClassAvailable() { 84 | return tryThis("new Function('args', '{class Foo extends (Bar) {}}')"); 85 | } 86 | 87 | export { 88 | isClassAvailable, 89 | isInferredClassNameAvailable, 90 | isInferredFunctionNameAvailable, 91 | isDefaultArgsAvailable, 92 | isExportAvailable, 93 | isImportAvailable, 94 | isOptionalCatchBindingAvailable, 95 | isObjectFreezeAvailable, 96 | isYieldAvailable, 97 | isClassPropAvailable, 98 | isClassPrivatePropAvailable, 99 | isForOfAvailable, 100 | isArrowFnAvailable, 101 | isObjectSpreadAvailable, 102 | }; 103 | -------------------------------------------------------------------------------- /spec/swc-coverage-custom-transform/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::type_name, 3 | panic::{catch_unwind, AssertUnwindSafe}, 4 | }; 5 | 6 | use anyhow::{anyhow, Context, Error}; 7 | use napi::Status; 8 | use serde::de::DeserializeOwned; 9 | use swc_core::{ 10 | base::{try_with_handler, HandlerOpts}, 11 | common::{errors::Handler, sync::Lrc, SourceMap, GLOBALS}, 12 | }; 13 | 14 | pub fn try_with(cm: Lrc, skip_filename: bool, op: F) -> Result 15 | where 16 | F: FnOnce(&Handler) -> Result, 17 | { 18 | GLOBALS 19 | .set(&Default::default(), || { 20 | try_with_handler( 21 | cm, 22 | HandlerOpts { 23 | skip_filename, 24 | ..Default::default() 25 | }, 26 | |handler| { 27 | // 28 | let result = catch_unwind(AssertUnwindSafe(|| op(handler))); 29 | 30 | let p = match result { 31 | Ok(v) => return v, 32 | Err(v) => v, 33 | }; 34 | 35 | if let Some(s) = p.downcast_ref::() { 36 | Err(anyhow!("failed to handle: {}", s)) 37 | } else if let Some(s) = p.downcast_ref::<&str>() { 38 | Err(anyhow!("failed to handle: {}", s)) 39 | } else { 40 | Err(anyhow!("failed to handle with unknown panic message")) 41 | } 42 | }, 43 | ) 44 | }) 45 | .map_err(|e| e.to_pretty_error()) 46 | } 47 | 48 | pub trait MapErr: Into> { 49 | fn convert_err(self) -> napi::Result { 50 | self.into() 51 | .map_err(|err| napi::Error::new(Status::GenericFailure, format!("{:?}", err))) 52 | } 53 | } 54 | 55 | impl MapErr for Result {} 56 | 57 | pub(crate) fn get_deserialized(buffer: B) -> napi::Result 58 | where 59 | T: DeserializeOwned, 60 | B: AsRef<[u8]>, 61 | { 62 | let mut deserializer = serde_json::Deserializer::from_slice(buffer.as_ref()); 63 | deserializer.disable_recursion_limit(); 64 | 65 | let v = T::deserialize(&mut deserializer) 66 | .with_context(|| { 67 | format!( 68 | "Failed to deserialize buffer as {}\nJSON: {}", 69 | type_name::(), 70 | String::from_utf8_lossy(buffer.as_ref()) 71 | ) 72 | }) 73 | .convert_err()?; 74 | 75 | Ok(v) 76 | } 77 | 78 | pub(crate) fn deserialize_json(json: &str) -> Result 79 | where 80 | T: DeserializeOwned, 81 | { 82 | let mut deserializer = serde_json::Deserializer::from_str(json); 83 | deserializer.disable_recursion_limit(); 84 | 85 | T::deserialize(&mut deserializer) 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swc-plugin-coverage-instrument", 3 | "version": "0.0.32", 4 | "description": "SWC coverage instrumentation plugin", 5 | "main": "./target/wasm32-wasip1/release/swc_plugin_coverage.wasm", 6 | "napi": { 7 | "binaryName": "swc", 8 | "targets": [ 9 | "x86_64-unknown-linux-musl", 10 | "x86_64-unknown-freebsd", 11 | "i686-pc-windows-msvc", 12 | "aarch64-unknown-linux-gnu", 13 | "armv7-unknown-linux-gnueabihf", 14 | "aarch64-apple-darwin", 15 | "aarch64-linux-android", 16 | "aarch64-unknown-linux-musl", 17 | "aarch64-pc-windows-msvc", 18 | "armv7-linux-androideabi" 19 | ] 20 | }, 21 | "files": [ 22 | "package.json", 23 | "README.md", 24 | "LICENSE", 25 | "target/wasm32-wasip1/release/swc_plugin_coverage.wasm" 26 | ], 27 | "scripts": { 28 | "prepublishOnly": "npm-run-all test && npm run build:plugin -- --release", 29 | "build:all": "npm-run-all build:customtransform build:instrument build:plugin", 30 | "build:customtransform": "napi build --platform --cwd ./spec/swc-coverage-custom-transform", 31 | "build:instrument": "wasm-pack build spec/swc-coverage-instrument-wasm --target nodejs", 32 | "build:plugin": "cargo build -p swc-plugin-coverage --target wasm32-wasip1", 33 | "test:plugin": "npm-run-all build:all && mocha", 34 | "test:customtransform": "npm-run-all build:all && cross-env SWC_TRANSFORM_CUSTOM=1 mocha", 35 | "test": "npm-run-all test:plugin test:customtransform", 36 | "test:debug": "npm-run-all build:all && cross-env FILTER=\"debug-test\" DEBUG=1 mocha", 37 | "prepare": "husky install" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/kwonoj/swc-coverage-instrument.git" 42 | }, 43 | "keywords": [ 44 | "SWC", 45 | "plugin", 46 | "istanbul", 47 | "coverage" 48 | ], 49 | "author": "OJ Kwon ", 50 | "license": "MIT", 51 | "bugs": { 52 | "url": "https://github.com/kwonoj/swc-coverage-instrument/issues" 53 | }, 54 | "homepage": "https://github.com/kwonoj/swc-coverage-instrument#readme", 55 | "devDependencies": { 56 | "@napi-rs/cli": "^3.4.1", 57 | "@swc-node/register": "^1.11.1", 58 | "@swc/core": "^1.15.0", 59 | "@taplo/cli": "^0.7.0", 60 | "@types/chai": "^4.3.3", 61 | "@types/js-yaml": "^4.0.5", 62 | "@types/lodash.clone": "^4.5.6", 63 | "@types/mocha": "^10.0.7", 64 | "@types/node": "^20.14.9", 65 | "chai": "^4.3.6", 66 | "cross-env": "^7.0.3", 67 | "husky": "^9.0.11", 68 | "js-yaml": "^4.1.0", 69 | "lint-staged": "^15.2.7", 70 | "lodash.clone": "^4.5.0", 71 | "mocha": "^10.6.0", 72 | "npm-run-all": "^4.1.5", 73 | "prettier": "^3.3.2", 74 | "typescript": "^5.5.3", 75 | "wasm-pack": "^0.13.0" 76 | }, 77 | "lint-staged": { 78 | "*.{js,ts,css,md}": "prettier --write", 79 | "*.toml": [ 80 | "taplo format" 81 | ], 82 | "*.rs": [ 83 | "cargo fmt --" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/visitors/logical_expr_visitor.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | common::{comments::Comments, util::take::Take, SourceMapper}, 3 | ecma::visit::{VisitMut, VisitMutWith, VisitWith}, 4 | }; 5 | use tracing::instrument; 6 | 7 | use crate::{create_instrumentation_visitor, instrumentation_branch_wrap_counter_helper}; 8 | 9 | create_instrumentation_visitor!(LogicalExprVisitor { branch: u32 }); 10 | 11 | /// A visitor to traverse down given logical expr's value (left / right) with existing branch idx. 12 | /// This is required to preserve branch id to recursively traverse logical expr's inner child. 13 | impl LogicalExprVisitor { 14 | instrumentation_branch_wrap_counter_helper!(); 15 | } 16 | 17 | impl VisitMut for LogicalExprVisitor { 18 | fn visit_mut_expr(&mut self, expr: &mut Expr) { 19 | let (old, _ignore_current) = self.on_enter(expr); 20 | expr.visit_mut_children_with(self); 21 | self.on_exit(old); 22 | } 23 | 24 | // TODO: common logic between coveragevisitor::visit_mut_bin_expr 25 | #[instrument(skip_all, fields(node = %self.print_node()))] 26 | fn visit_mut_bin_expr(&mut self, bin_expr: &mut BinExpr) { 27 | // We don't use self.on_enter() here since Node::LogicalExpr is a dialect of BinExpr 28 | // which we can't pass directly via on_enter() macro 29 | let old = self.should_ignore; 30 | let ignore_current = match old { 31 | Some(crate::hint_comments::IgnoreScope::Next) => old, 32 | _ => { 33 | self.should_ignore = 34 | crate::hint_comments::should_ignore(&self.comments, Some(&bin_expr.span)); 35 | self.should_ignore 36 | } 37 | }; 38 | 39 | match ignore_current { 40 | Some(crate::hint_comments::IgnoreScope::Next) => { 41 | self.nodes.push(crate::Node::BinExpr); 42 | bin_expr.visit_mut_children_with(self); 43 | self.on_exit(old); 44 | } 45 | _ => { 46 | match &bin_expr.op { 47 | BinaryOp::LogicalOr | BinaryOp::LogicalAnd | BinaryOp::NullishCoalescing => { 48 | self.nodes.push(crate::Node::LogicalExpr); 49 | 50 | // Iterate over each expr, wrap it with branch counter. 51 | // This does not create new branch counter - should use parent's index instead. 52 | self.wrap_bin_expr_with_branch_counter(self.branch, &mut *bin_expr.left); 53 | self.wrap_bin_expr_with_branch_counter(self.branch, &mut *bin_expr.right); 54 | } 55 | _ => { 56 | // iterate as normal for non loigical expr 57 | self.nodes.push(crate::Node::BinExpr); 58 | bin_expr.visit_mut_children_with(self); 59 | self.on_exit(old); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spec/fixtures/arrow-fn.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: es6 arrow function expression 3 | guard: isArrowFnAvailable 4 | code: | 5 | var input = args 6 | output = input.map(x => x * x) 7 | tests: 8 | - name: function called 9 | args: [1, 2, 3, 4] 10 | out: [1, 4, 9, 16] 11 | lines: {'1': 1, '2': 4} 12 | statements: {'0': 1, '1': 1, '2': 4} 13 | functions: {'0': 4} 14 | 15 | - name: function not called 16 | args: [] 17 | out: [] 18 | lines: {'1': 1, '2': 1} 19 | statements: {'0': 1, '1': 1, '2': 0} 20 | functions: {'0': 0} 21 | 22 | --- 23 | name: es6 arrow function expression - multiple args 24 | guard: isArrowFnAvailable 25 | code: | 26 | var input = args 27 | output = input.reduce((memo, item) => memo + item, 0) 28 | tests: 29 | - name: function called 30 | args: [1, 2, 3, 4] 31 | out: 10 32 | lines: {'1': 1, '2': 4} 33 | statements: {'0': 1, '1': 1, '2': 4} 34 | functions: {'0': 4} 35 | 36 | - name: function not called 37 | args: [] 38 | out: 0 39 | lines: {'1': 1, '2': 1} 40 | statements: {'0': 1, '1': 1, '2': 0} 41 | functions: {'0': 0} 42 | 43 | --- 44 | name: es6 arrow function block 45 | guard: isArrowFnAvailable 46 | code: | 47 | var input = args 48 | output = input.map(x => { return x * x; }) 49 | tests: 50 | - name: function called 51 | args: [1, 2, 3, 4] 52 | out: [1, 4, 9, 16] 53 | lines: {'1': 1, '2': 4} 54 | statements: {'0': 1, '1': 1, '2': 4} 55 | functions: {'0': 4} 56 | 57 | - name: function not called 58 | args: [] 59 | out: [] 60 | lines: {'1': 1, '2': 1} 61 | statements: {'0': 1, '1': 1, '2': 0} 62 | functions: {'0': 0} 63 | 64 | --- 65 | name: es6 arrow function block - multiple args 66 | guard: isArrowFnAvailable 67 | code: | 68 | var input = args 69 | output = input.reduce((memo, item) => { return memo + item }, 0) 70 | tests: 71 | - name: function called 72 | args: [1, 2, 3, 4] 73 | out: 10 74 | lines: {'1': 1, '2': 4} 75 | statements: {'0': 1, '1': 1, '2': 4} 76 | functions: {'0': 4} 77 | 78 | - name: function not called 79 | args: [] 80 | out: 0 81 | lines: {'1': 1, '2': 1} 82 | statements: {'0': 1, '1': 1, '2': 0} 83 | functions: {'0': 0} 84 | 85 | --- 86 | name: complex arrow fn 87 | guard: isArrowFnAvailable 88 | code: | 89 | const arrow = a => ( 90 | b => ( 91 | c => ( 92 | d => a + b + c + d 93 | ) 94 | ) 95 | ) 96 | output = arrow(args[0])(args[1])(args[2])(args[3]) 97 | tests: 98 | - name: call nested arrow fn 99 | args: [1, 2, 3, 4] 100 | out: 10 101 | lines: { '1': 1, '2': 1, '3': 1, '4': 1, '8': 1} 102 | functions: { '0': 1, '1': 1, '2': 1, '3': 1 } 103 | statements: { '0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5':1 } 104 | 105 | --- 106 | name: arrow function name assignment 107 | guard: isInferredFunctionNameAvailable 108 | code: | 109 | const foo = (bar) => {} 110 | output = foo.name; 111 | tests: 112 | - name: properly sets function name 113 | out: 'foo' 114 | lines: {'1': 1, '2': 1} 115 | functions: {'0': 0} 116 | statements: {'0': 1, '1': 1} 117 | -------------------------------------------------------------------------------- /spec/fixture.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs"; 3 | import * as yaml from "js-yaml"; 4 | import { create, instrumentSync } from "./util/verifier"; 5 | import * as guards from "./util/guards"; 6 | import { assert } from "chai"; 7 | import { getCoverageMagicConstants } from "./swc-coverage-instrument-wasm/pkg/swc_coverage_instrument_wasm"; 8 | 9 | // dummy: initiate wasm compilation before any test runs 10 | getCoverageMagicConstants(); 11 | instrumentSync(`console.log('boo')`, "anon"); 12 | 13 | const clone: typeof import("lodash.clone") = require("lodash.clone"); 14 | 15 | const dir = path.resolve(__dirname, "fixtures"); 16 | const files = fs.readdirSync(dir).filter((f) => { 17 | let match = true; 18 | if (process.env.FILTER) { 19 | match = new RegExp(`.*${process.env.FILTER}.*`).test(f); 20 | } 21 | return f.match(/\.yaml$/) && match; 22 | }); 23 | 24 | function loadDocs() { 25 | const docs = []; 26 | files.forEach((f) => { 27 | const filePath = path.resolve(dir, f); 28 | const contents = fs.readFileSync(filePath, "utf8"); 29 | try { 30 | yaml.loadAll(contents, (obj) => { 31 | obj.file = f; 32 | docs.push(obj); 33 | }); 34 | } catch (ex) { 35 | docs.push({ 36 | file: f, 37 | name: "loaderr", 38 | err: "Unable to load file [" + f + "]\n" + ex.message + "\n" + ex.stack, 39 | }); 40 | } 41 | }); 42 | return docs; 43 | } 44 | 45 | function generateTests(docs) { 46 | docs.forEach((doc) => { 47 | const guard = doc.guard; 48 | let skip = false; 49 | let skipText = ""; 50 | 51 | if (guard && guards[guard]) { 52 | if (!guards[guard]()) { 53 | skip = true; 54 | skipText = "[SKIP] "; 55 | } 56 | } 57 | 58 | describe(skipText + doc.file + "/" + (doc.name || "suite"), () => { 59 | if (doc.err) { 60 | it("has errors", () => { 61 | console.error(doc.err); 62 | assert.ok(false, doc.err); 63 | }); 64 | } else { 65 | (doc.tests || []).forEach((t) => { 66 | const fn = async function () { 67 | const genOnly = (doc.opts || {}).generateOnly; 68 | const noCoverage = (doc.opts || {}).noCoverage; 69 | const opts = doc.opts || {}; 70 | opts.filename = path.resolve(__dirname, doc.file); 71 | opts.transformOptions = { 72 | isModule: doc?.instrumentOpts?.esModules, 73 | }; 74 | const v = create( 75 | doc.code, 76 | opts, 77 | doc.instrumentOpts, 78 | doc.inputSourceMap 79 | ); 80 | const test = clone(t); 81 | const args = test.args; 82 | const out = test.out; 83 | delete test.args; 84 | delete test.out; 85 | if (!genOnly && !noCoverage) { 86 | await v.verify(args, out, test); 87 | } 88 | if (noCoverage) { 89 | assert.equal(v.code, v.generatedCode); 90 | } 91 | }; 92 | if (skip) { 93 | it.skip(t.name || "default test", fn); 94 | } else { 95 | it(t.name || "default test", fn); 96 | } 97 | }); 98 | } 99 | }); 100 | }); 101 | } 102 | 103 | generateTests(loadDocs()); 104 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/utils/hint_comments.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use regex::Regex as Regexp; 3 | use swc_core::{ 4 | common::{ 5 | comments::{Comment, Comments}, 6 | Span, 7 | }, 8 | ecma::ast::*, 9 | }; 10 | 11 | /// pattern for istanbul to ignore the whole file 12 | /// This is not fully identical to original file comments 13 | /// https://github.com/istanbuljs/istanbuljs/blob/6f45283feo31faaa066375528f6b68e3a9927b2d5/packages/istanbul-lib-instrument/src/visitor.js#L10= 14 | /// as regex package doesn't support lookaround 15 | static COMMENT_FILE_REGEX: Lazy = 16 | Lazy::new(|| Regexp::new(r"^\s*istanbul\s+ignore\s+(file)(\W|$)").unwrap()); 17 | 18 | /// pattern for istanbul to ignore a section 19 | pub static COMMENT_RE: Lazy = 20 | Lazy::new(|| Regexp::new(r"^\s*istanbul\s+ignore\s+(if|else|next)(\W|$)").unwrap()); 21 | 22 | pub fn should_ignore_file(comments: &C, program: &Program) -> bool { 23 | let pos = match &program { 24 | Program::Module(module) => module.span, 25 | Program::Script(script) => script.span, 26 | #[cfg(swc_ast_unknown)] 27 | _ => panic!("unknown node"), 28 | }; 29 | 30 | let validate_comments = |comments: &Option>| { 31 | if let Some(comments) = comments { 32 | comments 33 | .iter() 34 | .any(|comment| COMMENT_FILE_REGEX.is_match(&comment.text)) 35 | } else { 36 | false 37 | } 38 | }; 39 | 40 | vec![ 41 | comments.get_leading(pos.lo), 42 | comments.get_leading(pos.hi), 43 | comments.get_trailing(pos.lo), 44 | comments.get_trailing(pos.hi), 45 | ] 46 | .iter() 47 | .any(|c| validate_comments(c)) 48 | } 49 | 50 | pub fn lookup_hint_comments( 51 | comments: &C, 52 | span: Option<&Span>, 53 | ) -> Option { 54 | if let Some(span) = span { 55 | let h = comments.get_leading(span.hi); 56 | let l = comments.get_leading(span.lo); 57 | 58 | if let Some(h) = h { 59 | let h_value = h.iter().find_map(|c| { 60 | COMMENT_RE 61 | .captures(&c.text) 62 | .map(|captures| captures.get(1).map(|c| c.as_str().trim().to_string())) 63 | .flatten() 64 | }); 65 | 66 | if let Some(h_value) = h_value { 67 | return Some(h_value); 68 | } 69 | } 70 | 71 | if let Some(l) = l { 72 | let l_value = l.iter().find_map(|c| { 73 | COMMENT_RE 74 | .captures(&c.text) 75 | .map(|captures| captures.get(1).map(|c| c.as_str().trim().to_string())) 76 | .flatten() 77 | }); 78 | 79 | return l_value; 80 | } 81 | } 82 | 83 | return None; 84 | } 85 | 86 | #[derive(Debug, Copy, Clone, PartialEq)] 87 | pub enum IgnoreScope { 88 | Next, 89 | If, 90 | Else, 91 | } 92 | 93 | pub fn should_ignore( 94 | comments: &C, 95 | span: Option<&Span>, 96 | ) -> Option { 97 | let comments = lookup_hint_comments(comments, span); 98 | 99 | if let Some(comments) = comments.as_deref() { 100 | match comments { 101 | "next" => Some(IgnoreScope::Next), 102 | "if" => Some(IgnoreScope::If), 103 | "else" => Some(IgnoreScope::Else), 104 | _ => None, 105 | } 106 | } else { 107 | None 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /spec/e2e/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { instrumentSync } from "../util/verifier"; 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | 6 | // Custom transform test doesn't use plugin's exclude option 7 | const tryDescribe = process.env.SWC_TRANSFORM_CUSTOM ? describe.skip : describe; 8 | 9 | tryDescribe("e2e", () => { 10 | it("issue-274", () => { 11 | const testFiles = [ 12 | { 13 | path: path.resolve(__dirname, "fixtures", "should-be-excluded.test.js"), 14 | shouldBeExcluded: true, 15 | description: "Test file with .test. extension should be excluded", 16 | }, 17 | { 18 | path: path.resolve(__dirname, "fixtures", "should-be-included.js"), 19 | shouldBeExcluded: false, 20 | description: "Regular file should be included", 21 | }, 22 | ]; 23 | 24 | const exclusionPattern = [ 25 | "**/node_modules/**", 26 | "**/dist/**", 27 | "**/test/**", 28 | "**/__tests__/**", 29 | "**/__mocks__/**", 30 | "**/*.{test,spec}.[jt]s", 31 | "**/*.{test,spec}.[c|m][jt]s", 32 | "**/*.{test,spec}.[jt]sx", 33 | "**/*.{test,spec}.[c|m][jt]sx", 34 | ]; 35 | 36 | testFiles.forEach((testFile) => { 37 | // Read the actual file content 38 | const fileContent = fs.readFileSync(testFile.path, "utf8"); 39 | 40 | // Transform the file using SWC with exclusion patterns 41 | const output = instrumentSync(fileContent, testFile.path, undefined, { 42 | unstableExclude: exclusionPattern, 43 | }); 44 | 45 | if (testFile.shouldBeExcluded) { 46 | // File should be excluded - no coverage instrumentation 47 | assert.notInclude( 48 | output.code, 49 | "__coverage__", 50 | `${testFile.description}: File should not contain coverage variables`, 51 | ); 52 | assert.notInclude( 53 | output.code, 54 | "cov_", 55 | `${testFile.description}: File should not contain coverage function calls`, 56 | ); 57 | } else { 58 | // File should be included - should have coverage instrumentation 59 | assert.include( 60 | output.code, 61 | "__coverage__", 62 | `${testFile.description}: File should be instrumented with coverage`, 63 | ); 64 | assert.include( 65 | output.code, 66 | "cov_", 67 | `${testFile.description}: File should contain coverage function calls`, 68 | ); 69 | } 70 | }); 71 | 72 | // Test cross-platform path normalization with different path formats 73 | const testCode = fs.readFileSync(testFiles[0].path, "utf8"); 74 | const pathVariations = [ 75 | "project/test/file.test.js", // Unix-style path 76 | "project\\test\\file.test.js", // Windows-style path 77 | "C:\\Users\\project\\test\\file.test.js", // Windows absolute path 78 | "/home/user/project/test/file.test.js", // Unix absolute path 79 | ]; 80 | 81 | pathVariations.forEach((testPath) => { 82 | const output = instrumentSync(testCode, testPath, undefined, { 83 | unstableExclude: ["**/*.test.*"], 84 | }); 85 | 86 | // All should be excluded since they all match the *.test.* pattern 87 | assert.notInclude( 88 | output.code, 89 | "__coverage__", 90 | `Path ${testPath} should be excluded regardless of separator style`, 91 | ); 92 | assert.notInclude( 93 | output.code, 94 | "cov_", 95 | `Path ${testPath} should not contain coverage calls`, 96 | ); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /spec/fixtures/for.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple for 3 | code: | 4 | var x = args[0], i, j = -10; 5 | for (i =0; i < x; i++) j = i; 6 | output = j; 7 | tests: 8 | - name: covers loop exactly once 9 | args: [10] 10 | out: 9 11 | lines: {'1': 1, '2': 10, '3': 1} 12 | statements: {'0': 1, '1':1, '2': 1, '3': 10, '4': 1} 13 | --- 14 | 15 | name: for with loop initializer 16 | code: | 17 | var x = args[0], j = -10; 18 | for (var i =0; i < x; i++) j = i; 19 | output = j; 20 | tests: 21 | - name: covers loop exactly once 22 | args: [10] 23 | out: 9 24 | lines: {'1': 1, '2': 10, '3': 1} 25 | statements: {'0': 1, '1':1, '2': 1, '3': 1, '4': 10, '5': 1} 26 | --- 27 | 28 | name: simple for, no initializer 29 | code: | 30 | var x = args[0], j = -10, i=0; 31 | for (; i < x; i++) j = i; 32 | output = j; 33 | tests: 34 | - args: [10] 35 | out: 9 36 | lines: {'1': 1, '2': 10, '3': 1} 37 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 10, '5': 1} 38 | --- 39 | 40 | name: simple for, statement on different line 41 | code: | 42 | var x = args[0], i, j = -10; 43 | for (i =0; i < x; i++) 44 | j = i; 45 | output = j; 46 | tests: 47 | - name: covers loop once 48 | args: [10] 49 | out: 9 50 | lines: {'1': 1, '2': 1, '3': 10, '4': 1} 51 | statements: {'0': 1, '2': 1, '1':1, '3': 10, '4': 1} 52 | 53 | - name: does not cover loop 54 | args: [-1] 55 | out: -10 56 | lines: {'1': 1, '2': 1, '3': 0, '4': 1} 57 | statements: {'0': 1, '1': 1, '2': 1, '3':0, '4': 1} 58 | 59 | --- 60 | name: for with block body 61 | code: | 62 | var x = args[0], i, j = -10; 63 | for (i =0; i < x; i++) { j = i; } 64 | output = j; 65 | tests: 66 | - name: covers loop exactly once 67 | args: [10] 68 | out: 9 69 | lines: {'1': 1, '2': 10, '3': 1} 70 | statements: {'0': 1, '1': 1, '2':1, '3': 10, '4': 1} 71 | 72 | - name: does not cover loop 73 | args: [-1] 74 | out: -10 75 | lines: {'1': 1, '2': 1, '3': 1} 76 | statements: {'0': 1, '1': 1, '2':1, '3': 0, '4': 1} 77 | 78 | --- 79 | name: labeled for 80 | code: | 81 | var x = args[0], i, j = -10; 82 | outer:for (i =0; i < x; i++) { j = i; } 83 | output = j; 84 | tests: 85 | - args: [10] 86 | out: 9 87 | lines: {'1': 1, '2': 10, '3': 1} 88 | statements: {'0': 1, '1':1, '2': 1, '3': 1, '4': 10, '5': 1} 89 | 90 | --- 91 | name: nested labeled for 92 | code: | 93 | var x = args[0], i, j, k = 0; 94 | outer:for (i = 0; i < x; i++) 95 | for (j=0; j < i ; j++) { 96 | if (j === 2) continue outer; 97 | k++; 98 | } 99 | output = k; 100 | tests: 101 | - args: [10] 102 | out: 17 103 | lines: {'1': 1, '2': 1, '3': 10, '4': 24, '5': 17, '7': 1} 104 | branches: {'0': [7, 17]} 105 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 10, '5': 24, '6': 7, '7': 17, '8': 1} 106 | 107 | --- 108 | name: nested labeled for (label on different line) 109 | code: | 110 | var x = args[0], i, j, k = 0; 111 | outer: 112 | for (i = 0; i < x; i++) 113 | for (j=0; j < i ; j++) { 114 | if (j === 2) continue outer; 115 | k++; 116 | } 117 | output = k; 118 | tests: 119 | - args: [10] 120 | out: 17 121 | lines: {'1': 1, '2': 1, '3': 1, '4': 10, '5': 24, '6': 17, '8': 1} 122 | branches: {'0': [7, 17]} 123 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 10, '5': 24, '6': 7, '7': 17, '8': 1} 124 | 125 | --- 126 | name: function in initializer 127 | code: | 128 | for (var x = function(){ return 100; }, y = true; y; y = false){ output = x(); } 129 | tests: 130 | - out: 100 131 | lines: {'1': 1} 132 | functions: {'0': 1} 133 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1 } 134 | -------------------------------------------------------------------------------- /spec/swc-coverage-custom-transform/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "2048"] 2 | #![allow(dead_code)] 3 | 4 | mod util; 5 | 6 | #[macro_use] 7 | extern crate napi_derive; 8 | 9 | //extern crate swc_core; 10 | 11 | use std::sync::Arc; 12 | 13 | use crate::util::MapErr; 14 | use swc_core::{ 15 | base::{config::Options, Compiler, TransformOutput}, 16 | common::{ 17 | comments::{Comments, SingleThreadedComments}, 18 | errors::SourceMapper, 19 | sync::Lazy, 20 | FileName, FilePathMapping, SourceMap, 21 | }, 22 | ecma::{ 23 | ast::{noop_pass, Pass}, 24 | visit::visit_mut_pass, 25 | }, 26 | }; 27 | use swc_coverage_instrument::{create_coverage_instrumentation_visitor, InstrumentOptions}; 28 | 29 | use std::path::Path; 30 | 31 | use napi::bindgen_prelude::Buffer; 32 | 33 | use crate::util::{get_deserialized, try_with}; 34 | 35 | static COMPILER: Lazy> = Lazy::new(|| { 36 | let cm = Arc::new(SourceMap::new(FilePathMapping::empty())); 37 | 38 | Arc::new(Compiler::new(cm)) 39 | }); 40 | 41 | // Module initialization is handled automatically in napi 3.x 42 | // #[napi::module_init] 43 | // fn init() { 44 | // if cfg!(debug_assertions) || env::var("SWC_DEBUG").unwrap_or_default() == "1" { 45 | // set_hook(Box::new(|panic_info| { 46 | // let backtrace = Backtrace::new(); 47 | // println!("Panic: {:?}\nBacktrace: {:?}", panic_info, backtrace); 48 | // })); 49 | // } 50 | // } 51 | 52 | fn get_compiler() -> Arc { 53 | COMPILER.clone() 54 | } 55 | 56 | #[napi(js_name = "Compiler")] 57 | pub struct JsCompiler { 58 | _compiler: Arc, 59 | } 60 | 61 | #[napi] 62 | impl JsCompiler { 63 | #[napi(constructor)] 64 | #[allow(clippy::new_without_default)] 65 | pub fn new() -> Self { 66 | Self { 67 | _compiler: COMPILER.clone(), 68 | } 69 | } 70 | } 71 | 72 | pub type ArcCompiler = Arc; 73 | 74 | #[napi] 75 | pub fn transform_sync( 76 | s: String, 77 | _is_module: bool, 78 | opts: Buffer, 79 | instrument_opts: Buffer, 80 | ) -> napi::Result { 81 | let c = get_compiler(); 82 | 83 | let mut options: Options = get_deserialized(&opts)?; 84 | let instrument_option: InstrumentOptions = get_deserialized(&instrument_opts)?; 85 | 86 | if !options.filename.is_empty() { 87 | options.config.adjust(Path::new(&options.filename)); 88 | } 89 | 90 | try_with( 91 | c.cm.clone(), 92 | !options.config.error.filename.into_bool(), 93 | |handler| { 94 | c.run(|| { 95 | let filename = if options.filename.is_empty() { 96 | FileName::Anon 97 | } else { 98 | FileName::Real(options.filename.clone().into()) 99 | }; 100 | 101 | let comments = SingleThreadedComments::default(); 102 | 103 | let fm = c.cm.new_source_file(filename.clone().into(), s); 104 | c.process_js_with_custom_pass( 105 | fm, 106 | None, 107 | handler, 108 | &options, 109 | comments.clone(), 110 | |_program| { 111 | coverage_instrument( 112 | c.cm.clone(), 113 | comments.clone(), 114 | instrument_option, 115 | filename.to_string(), 116 | ) 117 | }, 118 | |_| noop_pass(), 119 | ) 120 | }) 121 | }, 122 | ) 123 | .convert_err() 124 | } 125 | 126 | fn coverage_instrument<'a, C: Comments + 'a + std::clone::Clone, S: 'a + SourceMapper>( 127 | source_map: Arc, 128 | comments: C, 129 | instrument_options: InstrumentOptions, 130 | filename: String, 131 | ) -> impl Pass + 'a { 132 | let visitor = 133 | create_coverage_instrumentation_visitor(source_map, comments, instrument_options, filename); 134 | 135 | visit_mut_pass(visitor) 136 | } 137 | -------------------------------------------------------------------------------- /spec/fixtures/truthy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple expression 3 | code: | 4 | var x = args[0] > 0 && args[0] < 5; 5 | output = x; 6 | instrumentOpts: 7 | reportLogic: true 8 | tests: 9 | - name: covers line and one branch 10 | args: [-1] 11 | out: false 12 | lines: {'1': 1, '2': 1} 13 | branches: {'0': [1, 0]} 14 | branchesTrue: {'0': [0, 0]} 15 | statements: {'0': 1, '1': 1} 16 | 17 | - name: covers line, both branches, returns false 18 | args: [10] 19 | out: false 20 | lines: {'1': 1, '2': 1} 21 | branches: {'0': [1, 1]} 22 | branchesTrue: {'0': [1, 0]} 23 | statements: {'0': 1, '1': 1} 24 | 25 | --- 26 | name: complex expression 27 | code: | 28 | var x = args[0] > 0 && (args[0] < 5 || args[0] > 10); 29 | output = x; 30 | instrumentOpts: 31 | reportLogic: true 32 | tests: 33 | - name: covers line and one branch 34 | args: [-1] 35 | out: false 36 | lines: {'1': 1, '2': 1} 37 | branches: {'0': [1, 0, 0]} 38 | branchesTrue: {'0': [0, 0, 0]} 39 | statements: {'0': 1, '1': 1} 40 | 41 | - name: covers line, both branches, returns false 42 | args: [9] 43 | out: false 44 | lines: {'1': 1, '2': 1} 45 | branches: {'0': [1, 1, 1]} 46 | branchesTrue: {'0': [1, 0, 0]} 47 | statements: {'0': 1, '1': 1} 48 | 49 | --- 50 | name: or with object expression (bug track) 51 | code: | 52 | var x = args[0] ? { foo: 1 } : { foo: 2 }; 53 | output = x.foo; 54 | instrumentOpts: 55 | reportLogic: true 56 | tests: 57 | - name: covers all branches correctly 58 | args: [ false ] 59 | out: 2 60 | lines: {'1': 1, '2': 1} 61 | branches: { '0': [0, 1] } 62 | branchesTrue: {} 63 | statements: {'0': 1, '1': 1} 64 | 65 | --- 66 | name: or with object expression (part 2) 67 | code: | 68 | var x = args[0] || { foo: 2 }; 69 | output = x.foo; 70 | instrumentOpts: 71 | reportLogic: true 72 | tests: 73 | - name: covers all branches correctly 74 | args: [ false ] 75 | out: 2 76 | lines: {'1': 1, '2': 1} 77 | branches: { '0': [1, 1] } 78 | branchesTrue: {'0': [0, 1]} 79 | statements: {'0': 1, '1': 1} 80 | 81 | --- 82 | name: function evaluation 83 | code: | 84 | let f = function() { return 42; }; 85 | var x = args || f(); 86 | output = x; 87 | instrumentOpts: 88 | reportLogic: true 89 | tests: 90 | - name: covers truthy function evaluation 91 | args: false 92 | out: 42 93 | lines: {'1': 1, '2': 1, '3': 1} 94 | functions: { '0': 1 } 95 | branches: { '0': [1, 1] } 96 | branchesTrue: {'0': [0, 1]} 97 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 98 | 99 | --- 100 | name: async 101 | code: | 102 | var x = args[0] || await args[1]; 103 | output = x.foo; 104 | opts: 105 | isAsync: true 106 | instrumentOpts: 107 | reportLogic: true 108 | tests: 109 | - name: covers all branches correctly 110 | args: [ false, { foo: 2 } ] 111 | out: 2 112 | lines: {'1': 1, '2': 1} 113 | branches: { '0': [1, 1] } 114 | branchesTrue: {'0': [0, 1]} 115 | statements: {'0': 1, '1': 1} 116 | 117 | --- 118 | name: mutate and evaluate at the same time 119 | code: | 120 | var x = args[0]++ || args[1]; 121 | output = args[0]; 122 | instrumentOpts: 123 | reportLogic: true 124 | tests: 125 | - name: ends up with a single increment and not two 126 | args: [ 0, { foo: 2 } ] 127 | out: 1 128 | lines: {'1': 1, '2': 1} 129 | branches: { '0': [1, 1] } 130 | branchesTrue: {'0': [0, 1]} 131 | statements: {'0': 1, '1': 1} 132 | 133 | --- 134 | name: empty object 135 | code: | 136 | var x = args[0] || { }; 137 | output = true; 138 | instrumentOpts: 139 | reportLogic: true 140 | tests: 141 | - name: covers all branches correctly 142 | args: [ false ] 143 | out: true 144 | lines: {'1': 1, '2': 1} 145 | branches: { '0': [1, 1] } 146 | branchesTrue: {'0': [0, 0]} 147 | statements: {'0': 1, '1': 1} 148 | 149 | --- 150 | name: empty array 151 | code: | 152 | var x = args[0] || []; 153 | output = true; 154 | instrumentOpts: 155 | reportLogic: true 156 | tests: 157 | - name: covers all branches correctly 158 | args: [ false ] 159 | out: true 160 | lines: {'1': 1, '2': 1} 161 | branches: { '0': [1, 1] } 162 | branchesTrue: {'0': [0, 0]} 163 | statements: {'0': 1, '1': 1} 164 | -------------------------------------------------------------------------------- /spec/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { getCoverageMagicConstants } from "./swc-coverage-instrument-wasm/pkg/swc_coverage_instrument_wasm"; 3 | import { instrumentSync } from "./util/verifier"; 4 | 5 | // dummy: initiate wasm compilation before any test runs 6 | getCoverageMagicConstants(); 7 | instrumentSync(`console.log('boo')`, "anon"); 8 | 9 | const tryDescribe = process.env.SWC_TRANSFORM_CUSTOM ? describe.skip : describe; 10 | 11 | tryDescribe("Plugin options", () => { 12 | it("should able to exclude", () => { 13 | const code = `console.log('hello');`; 14 | 15 | const output = instrumentSync( 16 | code, 17 | "somepath/file/excluded.js", 18 | undefined, 19 | { 20 | unstableExclude: ["somepath/**/excluded.*"], 21 | }, 22 | ); 23 | 24 | assert.equal( 25 | output.code, 26 | `"use strict"; 27 | ${code} 28 | `, 29 | ); 30 | }); 31 | 32 | it("should normalize paths", () => { 33 | const code = `console.log('hello world');`; 34 | 35 | const output = instrumentSync( 36 | code, 37 | "C:\\Users\\project\\test\\index.test.ts", 38 | undefined, 39 | { 40 | unstableExclude: ["**/test/**"], 41 | }, 42 | ); 43 | 44 | assert.equal( 45 | output.code, 46 | `"use strict"; 47 | ${code} 48 | `, 49 | ); 50 | }); 51 | 52 | it("should preserve emotion styled component labels with template literals", () => { 53 | // This reproduces the issue from GitHub #247 54 | // Input: code AFTER emotion processing (as shown in the GitHub issue) 55 | const code = `export var TabsList = /*#__PURE__*/ styled(TabsListCore, { 56 | target: "ebt2y835", 57 | label: "TabsList" 58 | })("margin:0 auto;width:fit-content;");`; 59 | 60 | const output = instrumentSync(code, "test-emotion.js"); 61 | 62 | // Expected output: should preserve label like v0.0.20 did (from GitHub issue) 63 | // The key difference: label should remain "TabsList", not become "" 64 | const expectedOutput = `"use strict"; 65 | Object.defineProperty(exports, "__esModule", { 66 | value: true 67 | }); 68 | Object.defineProperty(exports, "TabsList", { 69 | enumerable: true, 70 | get: function() { 71 | return TabsList; 72 | } 73 | }); 74 | var TabsList = (cov_14220330533750098279().s[0]++, /*#__PURE__*/ styled(TabsListCore, { 75 | target: "ebt2y835", 76 | label: "TabsList" 77 | })("margin:0 auto;width:fit-content;")); /*__coverage_data_json_comment__::{"all":false,"path":"test-emotion.js","statementMap":{"0":{"start":{"line":1,"column":36},"end":{"line":4,"column":38}}},"fnMap":{},"branchMap":{},"s":{"0":0},"f":{},"b":{}}*/ 78 | function cov_14220330533750098279() { 79 | var path = "test-emotion.js"; 80 | var hash = "15339889637910252771"; 81 | var global = new ((function(){}).constructor)("return this")(); 82 | var gcv = "__coverage__"; 83 | var coverageData = { 84 | all: false, 85 | path: "test-emotion.js", 86 | statementMap: { 87 | "0": { 88 | start: { 89 | line: 1, 90 | column: 36 91 | }, 92 | end: { 93 | line: 4, 94 | column: 38 95 | } 96 | } 97 | }, 98 | fnMap: {}, 99 | branchMap: {}, 100 | s: { 101 | "0": 0 102 | }, 103 | f: {}, 104 | b: {}, 105 | _coverageSchema: "11020577277169172593", 106 | hash: "15339889637910252771" 107 | }; 108 | var coverage = global[gcv] || (global[gcv] = {}); 109 | if (!coverage[path] || coverage[path].$hash !== hash) { 110 | coverage[path] = coverageData; 111 | } 112 | var actualCoverage = coverage[path]; 113 | { 114 | cov_14220330533750098279 = function() { 115 | return actualCoverage; 116 | }; 117 | } 118 | return actualCoverage; 119 | } 120 | cov_14220330533750098279();`; 121 | 122 | // Compare whole output.code to the raw output as requested 123 | // This ensures emotion labels are preserved without explicitly asserting them 124 | assert.equal( 125 | output.code.trim(), 126 | expectedOutput.trim(), 127 | "Instrumented code should preserve emotion styled component label property", 128 | ); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /spec/util/read-coverage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Declaration, 3 | FunctionDeclaration, 4 | Module, 5 | parseSync, 6 | Property, 7 | SpreadElement, 8 | VariableDeclaration, 9 | } from "@swc/core"; 10 | import { Visitor } from "@swc/core/Visitor"; 11 | import { getCoverageMagicConstants } from "../swc-coverage-instrument-wasm/pkg/swc_coverage_instrument_wasm"; 12 | 13 | const { key: COVERAGE_MAGIC_KEY, value: COVERAGE_MAGIC_VALUE } = 14 | getCoverageMagicConstants(); 15 | 16 | function getAst(code: any, options: any): Module { 17 | if (typeof code === "object" && typeof code.type === "string") { 18 | // Assume code is already a babel ast. 19 | return code; 20 | } 21 | 22 | if (typeof code !== "string") { 23 | throw new Error("Code must be a string"); 24 | } 25 | 26 | return parseSync(code, { 27 | syntax: "ecmascript", 28 | script: true, 29 | isModule: options?.isModule, 30 | }); 31 | } 32 | 33 | class CoverageReadVisitor extends Visitor { 34 | private current: FunctionDeclaration | null = null; 35 | private coverageScope: FunctionDeclaration | null = null; 36 | public getCoverageScope(): FunctionDeclaration | null { 37 | return this.coverageScope; 38 | } 39 | 40 | public visitFunctionDeclaration(n: FunctionDeclaration): Declaration { 41 | this.current = n; 42 | super.visitFunctionDeclaration(n); 43 | this.current = null; 44 | return n; 45 | } 46 | 47 | public visitObjectProperty( 48 | n: Property | SpreadElement 49 | ): Property | SpreadElement { 50 | if (n.type !== "KeyValueProperty") { 51 | return n; 52 | } 53 | 54 | if (n.key.type === "Identifier" && n.key.value === COVERAGE_MAGIC_KEY) { 55 | if ( 56 | n.value.type === "StringLiteral" && 57 | n.value.value === COVERAGE_MAGIC_VALUE 58 | ) { 59 | this.coverageScope = this.current; 60 | } 61 | } 62 | return n; 63 | } 64 | } 65 | 66 | export function readInitialCoverage(code: any, options?: any) { 67 | const ast = getAst(code, options); 68 | 69 | let visitor = new CoverageReadVisitor(); 70 | visitor.visitProgram(ast); 71 | 72 | let covScope = visitor.getCoverageScope(); 73 | 74 | if (!covScope) { 75 | return null; 76 | } 77 | 78 | const result = {}; 79 | 80 | const declarations = covScope.body.stmts 81 | .map( 82 | (stmt) => 83 | (stmt.type === "VariableDeclaration" 84 | ? stmt 85 | : null) as any as VariableDeclaration 86 | ) 87 | .filter(Boolean); 88 | 89 | for (const key of ["path", "hash", "gcv", "coverageData"]) { 90 | const binding = declarations.reduce((acc, value) => { 91 | if (!acc) { 92 | acc = value.declarations.find( 93 | (decl) => decl.id.type === "Identifier" && decl.id.value === key 94 | ); 95 | } 96 | 97 | return acc; 98 | }, null as any); 99 | 100 | if (!binding) { 101 | return null; 102 | } 103 | 104 | const valuePath = binding.init; 105 | 106 | function setPropertiesRecursive( 107 | obj: Record, 108 | binding: any, 109 | resultKey: string 110 | ) { 111 | if (binding?.value !== null && binding?.value !== undefined) { 112 | obj[resultKey] = binding?.value; 113 | } else if (binding?.properties) { 114 | obj[resultKey] = {}; 115 | binding?.properties.forEach((p) => { 116 | setPropertiesRecursive(obj[resultKey], p.value, p.key.value); 117 | }); 118 | } else if (binding?.elements) { 119 | if (binding?.elements.length === 0) { 120 | obj[resultKey] = []; 121 | } 122 | binding?.elements.forEach((elem, idx) => { 123 | if (!Array.isArray(obj[resultKey])) { 124 | obj[resultKey] = []; 125 | } 126 | 127 | if (elem?.expression?.properties) { 128 | setPropertiesRecursive(obj[resultKey], elem?.expression, idx); 129 | } else if ( 130 | elem?.expression?.value !== null && 131 | elem?.expression?.value !== undefined 132 | ) { 133 | obj[resultKey].push(elem.expression?.value); 134 | } 135 | }); 136 | } 137 | } 138 | 139 | setPropertiesRecursive(result, valuePath, key); 140 | } 141 | 142 | delete result.coverageData[COVERAGE_MAGIC_KEY]; 143 | delete result.coverageData.hash; 144 | 145 | return result; 146 | } 147 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/constants/idents.rs: -------------------------------------------------------------------------------- 1 | //! Static ident declarations being used across template 2 | use once_cell::sync::Lazy; 3 | use swc_core::{common::util::take::Take, ecma::ast::Ident}; 4 | 5 | pub static IDENT_ALL: Lazy = Lazy::new(|| Ident { 6 | sym: "all".into(), 7 | ..Ident::dummy() 8 | }); 9 | 10 | pub static IDENT_HASH: Lazy = Lazy::new(|| Ident { 11 | sym: "hash".into(), 12 | ..Ident::dummy() 13 | }); 14 | 15 | pub static IDENT_PATH: Lazy = Lazy::new(|| Ident { 16 | sym: "path".into(), 17 | ..Ident::dummy() 18 | }); 19 | 20 | pub static IDENT_GCV: Lazy = Lazy::new(|| Ident { 21 | sym: "gcv".into(), 22 | ..Ident::dummy() 23 | }); 24 | 25 | pub static IDENT_COVERAGE_DATA: Lazy = Lazy::new(|| Ident { 26 | sym: "coverageData".into(), 27 | ..Ident::dummy() 28 | }); 29 | 30 | pub static IDENT_GLOBAL: Lazy = Lazy::new(|| Ident { 31 | sym: "global".into(), 32 | ..Ident::dummy() 33 | }); 34 | 35 | pub static IDENT_START: Lazy = Lazy::new(|| Ident { 36 | sym: "start".into(), 37 | ..Ident::dummy() 38 | }); 39 | 40 | pub static IDENT_END: Lazy = Lazy::new(|| Ident { 41 | sym: "end".into(), 42 | ..Ident::dummy() 43 | }); 44 | 45 | pub static IDENT_LINE: Lazy = Lazy::new(|| Ident { 46 | sym: "line".into(), 47 | ..Ident::dummy() 48 | }); 49 | 50 | pub static IDENT_COLUMN: Lazy = Lazy::new(|| Ident { 51 | sym: "column".into(), 52 | ..Ident::dummy() 53 | }); 54 | 55 | pub static IDENT_STATEMENT_MAP: Lazy = Lazy::new(|| Ident { 56 | sym: "statementMap".into(), 57 | ..Ident::dummy() 58 | }); 59 | 60 | pub static IDENT_FN_MAP: Lazy = Lazy::new(|| Ident { 61 | sym: "fnMap".into(), 62 | ..Ident::dummy() 63 | }); 64 | 65 | pub static IDENT_BRANCH_MAP: Lazy = Lazy::new(|| Ident { 66 | sym: "branchMap".into(), 67 | ..Ident::dummy() 68 | }); 69 | 70 | pub static IDENT_S: Lazy = Lazy::new(|| Ident { 71 | sym: "s".into(), 72 | ..Ident::dummy() 73 | }); 74 | 75 | pub static IDENT_F: Lazy = Lazy::new(|| Ident { 76 | sym: "f".into(), 77 | ..Ident::dummy() 78 | }); 79 | 80 | pub static IDENT_B: Lazy = Lazy::new(|| Ident { 81 | sym: "b".into(), 82 | ..Ident::dummy() 83 | }); 84 | 85 | pub static IDENT_BT: Lazy = Lazy::new(|| Ident { 86 | sym: "bT".into(), 87 | ..Ident::dummy() 88 | }); 89 | 90 | pub static IDENT_COVERAGE_MAGIC_KEY: Lazy = Lazy::new(|| Ident { 91 | sym: crate::COVERAGE_MAGIC_KEY.into(), 92 | ..Ident::dummy() 93 | }); 94 | 95 | pub static IDENT_NAME: Lazy = Lazy::new(|| Ident { 96 | sym: "name".into(), 97 | ..Ident::dummy() 98 | }); 99 | 100 | pub static IDENT_DECL: Lazy = Lazy::new(|| Ident { 101 | sym: "decl".into(), 102 | ..Ident::dummy() 103 | }); 104 | 105 | pub static IDENT_LOC: Lazy = Lazy::new(|| Ident { 106 | sym: "loc".into(), 107 | ..Ident::dummy() 108 | }); 109 | 110 | pub static IDENT_TYPE: Lazy = Lazy::new(|| Ident { 111 | sym: "type".into(), 112 | ..Ident::dummy() 113 | }); 114 | 115 | pub static IDENT_LOCATIONS: Lazy = Lazy::new(|| Ident { 116 | sym: "locations".into(), 117 | ..Ident::dummy() 118 | }); 119 | 120 | pub static IDENT_INPUT_SOURCE_MAP: Lazy = Lazy::new(|| Ident { 121 | sym: "inputSourceMap".into(), 122 | ..Ident::dummy() 123 | }); 124 | 125 | pub static IDENT_VERSION: Lazy = Lazy::new(|| Ident { 126 | sym: "version".into(), 127 | ..Ident::dummy() 128 | }); 129 | 130 | pub static IDENT_FILE: Lazy = Lazy::new(|| Ident { 131 | sym: "file".into(), 132 | ..Ident::dummy() 133 | }); 134 | 135 | pub static IDENT_SOURCE_ROOT: Lazy = Lazy::new(|| Ident { 136 | sym: "sourceRoot".into(), 137 | ..Ident::dummy() 138 | }); 139 | 140 | pub static IDENT_SOURCES: Lazy = Lazy::new(|| Ident { 141 | sym: "sources".into(), 142 | ..Ident::dummy() 143 | }); 144 | 145 | pub static IDENT_SOURCES_CONTENT: Lazy = Lazy::new(|| Ident { 146 | sym: "sourcesContent".into(), 147 | ..Ident::dummy() 148 | }); 149 | 150 | pub static IDENT_NAMES: Lazy = Lazy::new(|| Ident { 151 | sym: "names".into(), 152 | ..Ident::dummy() 153 | }); 154 | 155 | pub static IDENT_MAPPINGS: Lazy = Lazy::new(|| Ident { 156 | sym: "mappings".into(), 157 | ..Ident::dummy() 158 | }); 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SWC-coverage-instrument 2 | 3 | `swc-coverage-instrument` is a set of packages to support [istanbuljs](https://github.com/istanbuljs/istanbuljs) compatible coverage instrumentation in [SWC](https://github.com/swc-project/swc)'s transform passes. Instrumentation transform can be performed either via SWC's wasm-based plugin, or using custom passes in rust side transform chains. 4 | 5 | ## What does compatible exactly means? 6 | 7 | This instrumentation will generate a data struct mimics istanbuljs's `FileCoverage` [object](https://github.com/istanbuljs/istanbuljs/blob/c7693d4608979ab73ebb310e0a1647e2c51f31b6/packages/istanbul-lib-coverage/lib/file-coverage.js#L97=) conforms fixture test suite from istanbuljs itself. 8 | 9 | However, this doesn't mean instrumentation supports exact same [interfaces](https://github.com/istanbuljs/istanbuljs/blob/c7693d4608979ab73ebb310e0a1647e2c51f31b6/packages/istanbul-lib-instrument/src/source-coverage.js#L37=) surrounding coverage object as well as supporting exact same options. There are some fundamental differences between runtime, and ast visitor architecture between different compilers does not allow identical behavior. This package will try `best attempt` as possible. 10 | 11 | **NOTE: Package can have breaking changes without major semver bump** 12 | 13 | Given SWC's plugin interface itself is under experimental stage does not gaurantee semver-based major bump yet, this package also does not gaurantee semver compliant breaking changes yet. Please refer changelogs if you're encountering unexpected breaking behavior across versions. 14 | 15 | # Usage 16 | 17 | ## Using SWC's wasm-based experimental plugin 18 | 19 | First, install package via npm: 20 | 21 | ``` 22 | npm install --save-dev swc-plugin-coverage-instrument 23 | ``` 24 | 25 | Then add plugin into swc's configuration: 26 | 27 | ``` 28 | const pluginOptions: InstrumentationOptions = {...} 29 | 30 | jsc: { 31 | ... 32 | experimental: { 33 | plugins: [ 34 | ["swc-plugin-coverage-instrument", pluginOptions] 35 | ] 36 | } 37 | } 38 | ``` 39 | 40 | `InstrumentationOptions` is a subset of istanbul's instrumentation options. Refer [istanbul's option](https://github.com/istanbuljs/istanbuljs/blob/master/packages/istanbul-lib-instrument/src/instrumenter.js#L16-L27=) for the same configuration flags. However there are few exceptions or differences, referencing [InstrumentOptions](https://github.com/kwonoj/swc-plugin-coverage-instrument/blob/4689fc9d281e11c875edd2376e8d92819472b9fe/packages/swc-coverage-instrument/src/options/instrument_options.rs#L22-L33) will list all possible options. 41 | 42 | ``` 43 | interface InstrumentationOptions { 44 | coverageVariable?: String, 45 | compact?: bool, 46 | reportLogic?: bool, 47 | ignoreClassMethods?: Array, 48 | inputSourceMap?: object, 49 | instrumentLog: { 50 | // Currently there aren't logs other than spans. 51 | // Enabling >= info can display span traces. 52 | level: 'trace' | 'warn' | 'error' | 'info' 53 | // Emits spans along with any logs 54 | // Only effective if level sets higher than info. 55 | enableTrace: bool 56 | }, 57 | unstableExclude?: Array 58 | } 59 | ``` 60 | 61 | ## Using custom transform pass in rust 62 | 63 | There is a single interface exposed to create a visitor for the transform, which you can pass into `before_custom_pass`. 64 | 65 | ``` 66 | let visitor = swc_coverage_instrument::create_coverage_instrumentation_visitor( 67 | source_map: std::sync::Arc, 68 | comments: C, 69 | instrument_options: InstrumentOptions, 70 | filename: String, 71 | ); 72 | 73 | let fold = as_folder(visitor); 74 | ``` 75 | 76 | # Building / Testing 77 | 78 | This package runs istanbuljs' fixture tests against SWC with its wasm plugin & custom transform both. `spec` contains set of the fixtures & unit test to run it, as well as supplimental packages to interop between instrumentation visitor to node.js runtime. `swc-coverage-instrument-wasm` exposes `FileCoverageInterop` allows to consume `FileCoverage` struct inside of js, and `swc-coverage-custom-transform` is an example implementation to run `before_custom_pass` with `swc-coverage-instrument` visitor. 79 | 80 | Few npm scripts are supported for wrapping those setups. 81 | 82 | - `build:all`: Build all relative packages as debug build. 83 | - `test`: Runs unit test for wasm plugin & custom transform. 84 | - `test:debug`: Runs unit test, but only for `debug-test.yaml` fixture. This is mainly for local dev debugging for individual test fixture behavior. 85 | -------------------------------------------------------------------------------- /spec/fixtures/statement-hints.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ignore before simple statement with branches 3 | code: | 4 | /* istanbul ignore next */ 5 | var x = args[0] > 5 ? args[0] : "undef"; 6 | output = x; 7 | tests: 8 | - name: coverage correct with skip meta 9 | args: [10] 10 | out: 10 11 | lines: {'3': 1} 12 | branches: {} 13 | statements: {'0': 1} 14 | 15 | --- 16 | name: ignore before function declaration 17 | code: | 18 | /* istanbul ignore next */ 19 | function foo(x) { return x; } 20 | output = args[0]; 21 | tests: 22 | - args: [10] 23 | out: 10 24 | lines: { '3': 1 } 25 | functions: {} 26 | statements: { '0': 1 } 27 | 28 | --- 29 | name: ignore before func expressions 30 | code: | 31 | /* istanbul ignore next */ 32 | (function () { output = args[0]; })(); 33 | tests: 34 | - args: [10] 35 | out: 10 36 | lines: {} 37 | functions: {} 38 | statements: {} 39 | 40 | --- 41 | name: ignore before switch 42 | code: | 43 | /* istanbul ignore next */ 44 | switch (args[0]) { 45 | case "1": output = 2; break; 46 | default: output = 1; 47 | } 48 | tests: 49 | - args: ['1'] 50 | out: 2 51 | lines: {} 52 | branches: {} 53 | statements: {} 54 | 55 | --- 56 | name: ignore before case 57 | code: | 58 | switch (args[0]) { 59 | /* istanbul ignore next */ 60 | case "1": output = 2; break; 61 | default: output = 1; 62 | } 63 | tests: 64 | - args: ['2'] 65 | out: 1 66 | lines: {'1': 1, '4': 1} 67 | branches: {'0': [1]} 68 | statements: {'0': 1, '1': 1} 69 | 70 | --- 71 | name: ignore before ternary statement 72 | code: | 73 | /* istanbul ignore next */ 74 | output = args[0] === 1 ? 1: 0; 75 | tests: 76 | - args: [2] 77 | out: 0 78 | lines: {} 79 | branches: {} 80 | statements: {} 81 | 82 | --- 83 | name: ignore before ternary condition 84 | code: | 85 | output = args[0] === 1 ? /* istanbul ignore next */ 1 : 0; 86 | tests: 87 | - args: [2] 88 | out: 0 89 | lines: {'1': 1} 90 | branches: {'0': [1]} 91 | statements: {'0': 1} 92 | 93 | --- 94 | name: ignore in logical expression 95 | code: | 96 | if (args[0] === 1 || /* istanbul ignore next */ args[0] === 2 ) { 97 | output = args[0] + 10; 98 | } else { 99 | output = 20; 100 | } 101 | tests: 102 | - args: [1] 103 | out: 11 104 | lines: {'1': 1, '2': 1, '4': 0} 105 | branches: {'0': [1, 0], '1': [1]} 106 | statements: {'0': 1, '1': 1, '2': 0} 107 | 108 | --- 109 | name: ignore in complex logical expression 110 | code: > 111 | if (args[0] === 1 || (/* istanbul ignore next */ args[0] === 2 || args[0] === 3)) { 112 | output = args[0] + 10; 113 | } else { 114 | output = 20; 115 | } 116 | tests: 117 | - args: [1] 118 | out: 11 119 | lines: {'1': 1, '2': 1, '4': 0} 120 | branches: {'0': [1, 0], '1': [1]} 121 | statements: {'0': 1, '1': 1, '2': 0} 122 | 123 | --- 124 | name: ignore in logical expression with implied operator precedence 125 | code: > 126 | if (args[0] === 1 || /* istanbul ignore next */args[0] === 2 && args[1] === 2) { 127 | output = args[0] + 10; 128 | } else { 129 | output = 20; 130 | } 131 | tests: 132 | - args: [1, 1] 133 | out: 11 134 | lines: {'1': 1, '2': 1, '4': 0} 135 | branches: {'0': [1, 0], '1': [1]} 136 | statements: {'0': 1, '1': 1, '2': 0} 137 | 138 | --- 139 | name: ignore before logical expression 140 | code: > 141 | if (/* istanbul ignore next */ args[0] === 1 || args[0] === 2 && args[1] === 2) { 142 | output = args[0] + 10; 143 | } else { 144 | output = 20; 145 | } 146 | tests: 147 | - args: [1, 1] 148 | out: 11 149 | lines: {'1': 1, '2': 1, '4': 0} 150 | branches: {'0': [1, 0] } 151 | statements: {'0': 1, '1': 1, '2': 0} 152 | --- 153 | name: ignore class methods 154 | guard: isClassAvailable 155 | code: | 156 | class TestClass { 157 | dummy(i) {return i;} 158 | nonIgnored(i) {return i;} 159 | } 160 | var testClass = new TestClass(); 161 | testClass.nonIgnored(); 162 | output = testClass.dummy(args[0]); 163 | instrumentOpts: 164 | ignoreClassMethods: ['dummy'] 165 | tests: 166 | - name: ignores only specified es6 methods 167 | args: [10] 168 | out: 10 169 | lines: {'3': 1, '5': 1, '6': 1, '7': 1} 170 | functions: {'0': 1} 171 | branches: {} 172 | statements: {'0': 1, '1': 1, '2': 1, '3': 1} 173 | --- 174 | name: ignore class methods 175 | code: | 176 | function TestClass() {} 177 | TestClass.prototype.testMethod = function testMethod(i) { 178 | return i; 179 | }; 180 | TestClass.prototype.goodMethod = function goodMethod(i) {return i;}; 181 | var testClass = new TestClass(); 182 | testClass.goodMethod(); 183 | output = testClass.testMethod(args[0]); 184 | instrumentOpts: 185 | ignoreClassMethods: ['testMethod'] 186 | tests: 187 | - name: ignores only specified es5 methods 188 | args: [10] 189 | out: 10 190 | lines: {'2': 1, '5': 1, '6': 1, '7': 1, '8': 1} 191 | functions: {'0': 1, '1': 1} 192 | branches: {} 193 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1} 194 | 195 | -------------------------------------------------------------------------------- /spec/fixtures/functions.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: simple function 3 | code: | 4 | var x = args[0]; 5 | function foo() { 6 | return 42; 7 | } 8 | output = x < 5 ? foo() : 15; 9 | tests: 10 | - name: covers line and function 11 | args: [2] 12 | out: 42 13 | lines: {'1': 1, '3': 1, '5': 1} 14 | branches: {'0': [1, 0]} 15 | functions: {'0': 1} 16 | statements: {'0': 1, '1': 1, '2': 1 } 17 | 18 | - name: does not cover function 19 | args: [10] 20 | out: 15 21 | lines: {'1': 1, '3': 0, '5': 1} 22 | branches: {'0': [0, 1]} 23 | functions: {'0': 0} 24 | statements: {'0': 1, '1': 0, '2': 1 } 25 | 26 | --- 27 | name: anonymous function 28 | code: | 29 | var x = args[0]; 30 | output = x < 5 ? (function() { return 42; }()) : 15; 31 | tests: 32 | - name: covers line and function 33 | args: [2] 34 | out: 42 35 | lines: {'1': 1, '2': 1} 36 | branches: {'0': [1, 0]} 37 | functions: {'0': 1} 38 | statements: {'0': 1, '1': 1, '2': 1 } 39 | 40 | - name: does not cover function 41 | args: [10] 42 | out: 15 43 | lines: {'1': 1, '2': 1} 44 | branches: {'0': [0, 1]} 45 | functions: {'0': 0} 46 | statements: {'0': 1, '1': 1, '2': 0} 47 | 48 | --- 49 | name: anonymous function newline 50 | code: | 51 | var x = args[0]; 52 | output = x < 5 ? 53 | (function meaningOfLife() { 54 | return 42; 55 | }()) 56 | : 15; 57 | tests: 58 | - name: covers line and function 59 | args: [2] 60 | out: 42 61 | lines: {'1': 1, '2': 1, '4': 1} 62 | branches: {'0': [1, 0]} 63 | functions: {'0': 1} 64 | statements: {'0': 1, '1': 1, '2': 1} 65 | 66 | - name: does not cover function 67 | args: [10] 68 | out: 15 69 | lines: {'1': 1, '2': 1, '4': 0} 70 | branches: {'0': [0, 1]} 71 | functions: {'0': 0} 72 | statements: {'0': 1, '1': 1, '2': 0} 73 | 74 | --- 75 | name: function decl in unreachable place 76 | code: | 77 | function foo(x) { 78 | return bar(x); 79 | function bar(y) { return y * 2 } 80 | } 81 | output = args[0] < 2 ? 2: foo(args[0]); 82 | tests: 83 | - name: covers declaration but not function 84 | args: [1] 85 | out: 2 86 | lines: { '2': 0, '3': 0, '5': 1} 87 | branches: {'0': [1, 0]} 88 | functions: {'0': 0, '1': 0} 89 | statements: {'0': 0, '1': 0, '2': 1} 90 | 91 | - name: covers declaration and function 92 | args: [10] 93 | out: 20 94 | lines: { '2': 1, '3': 1, '5': 1} 95 | branches: {'0': [0, 1]} 96 | functions: {'0': 1, '1': 1} 97 | statements: {'0': 1, '1': 1, '2': 1 } 98 | 99 | --- 100 | name: function declaration assignment name (top-level) 101 | guard: isInferredFunctionNameAvailable 102 | code: | 103 | const foo = function() {} 104 | var bar = function() {} 105 | output = foo.name + ' ' + bar.name; 106 | tests: 107 | - name: properly sets function name 108 | out: 'foo bar' 109 | lines: {'1': 1, '2': 1, '3': 1} 110 | functions: {'0': 0, '1': 0} 111 | statements: {'0': 1, '1': 1, '2': 1} 112 | guard: isInferredFunctionNameAvailable 113 | 114 | --- 115 | name: function declaration assignment name (in function) 116 | guard: isInferredFunctionNameAvailable 117 | code: | 118 | function a () { 119 | const foo = function () {} 120 | } 121 | function b () { 122 | const bar = function () {} 123 | return bar.name 124 | } 125 | output = b() 126 | tests: 127 | - name: properly sets function name 128 | out: 'bar' 129 | lines: {'2': 0, '5': 1, '6': 1, '8': 1} 130 | functions: {'0': 0, '1': 0, '2': 1, '3': 0} 131 | statements: {'0': 0, '1': 1, '2': 1, '3': 1} 132 | guard: isInferredFunctionNameAvailable 133 | 134 | --- 135 | name: function named Function 136 | code: | 137 | function Function () { 138 | this.x = 42 139 | } 140 | output = new Function().x 141 | tests: 142 | - name: does not fail if a function is called Function 143 | out: 42 144 | lines: {'2': 1, '4': 1} 145 | functions: {'0': 1} 146 | statements: {'0': 1, '1': 1} 147 | --- 148 | name: functions declared in an object 149 | code: | 150 | const computedIdx = 'computed'; 151 | const obj = { 152 | normal() { 153 | console.log('normal'); 154 | }, 155 | 'string'() { 156 | console.log('string literal'); 157 | }, 158 | 1() { 159 | console.log('number literal'); 160 | }, 161 | 2n() { 162 | console.log('bigint literal'); 163 | }, 164 | [computedIdx]() { 165 | console.log('computed property'); 166 | }, 167 | get getterFn() { 168 | console.log('getter function'); 169 | }, 170 | set setterFn(val) { 171 | console.log('setter function', val); 172 | }, 173 | get 'getterFn'() { 174 | console.log('getter function with string literal'); 175 | }, 176 | set 'setterStringLiteral'(val) { 177 | console.log('setter function with string literal', val); 178 | }, 179 | }; 180 | tests: 181 | - name: all functions in object are covered 182 | lines: {'1': 1, '2': 1, '4': 0, '7': 0, '10': 0, '13': 0, '16': 0, '19': 0, '22': 0, '25': 0, '28': 0} 183 | functions: {'0': 0, '1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0} 184 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0, '7': 0, '8': 0, '9': 0, '10': 0} -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/coverage_map.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | 3 | use crate::{CoverageSummary, FileCoverage}; 4 | 5 | /// a map of `FileCoverage` objects keyed by file paths 6 | #[derive(Clone, PartialEq, Default)] 7 | pub struct CoverageMap { 8 | inner: IndexMap, 9 | } 10 | 11 | impl CoverageMap { 12 | pub fn new() -> CoverageMap { 13 | CoverageMap { 14 | inner: Default::default(), 15 | } 16 | } 17 | 18 | pub fn default() -> CoverageMap { 19 | CoverageMap { 20 | inner: Default::default(), 21 | } 22 | } 23 | 24 | pub fn from_iter<'a>(value: impl IntoIterator) -> CoverageMap { 25 | let mut ret = CoverageMap { 26 | inner: Default::default(), 27 | }; 28 | 29 | for coverage in value.into_iter() { 30 | ret.add_coverage_for_file(coverage) 31 | } 32 | 33 | ret 34 | } 35 | 36 | /// Merges a second coverage map into this one 37 | pub fn merge(&mut self, map: &CoverageMap) { 38 | for (_, coverage) in map.inner.iter() { 39 | self.add_coverage_for_file(coverage); 40 | } 41 | } 42 | 43 | /// Filter the coverage map with a predicate. If the predicate returns false, 44 | /// the coverage is removed from the map. 45 | pub fn filter(&mut self, predicate: impl Fn(&FileCoverage) -> bool) { 46 | let mut filtered: IndexMap = Default::default(); 47 | 48 | for (_, coverage) in self.inner.drain(..) { 49 | if predicate(&coverage) { 50 | filtered.insert(coverage.path.clone(), coverage); 51 | } 52 | } 53 | 54 | self.inner = filtered; 55 | } 56 | 57 | pub fn to_json() { 58 | unimplemented!() 59 | } 60 | 61 | pub fn get_files(&self) -> Vec<&String> { 62 | self.inner.keys().collect() 63 | } 64 | 65 | pub fn get_coverage_for_file(&self, file_path: &str) -> Option<&FileCoverage> { 66 | self.inner.get(file_path) 67 | } 68 | 69 | pub fn add_coverage_for_file(&mut self, coverage: &FileCoverage) { 70 | if let Some(value) = self.inner.get_mut(coverage.path.as_str()) { 71 | value.merge(coverage); 72 | } else { 73 | self.inner.insert(coverage.path.clone(), coverage.clone()); 74 | } 75 | } 76 | 77 | pub fn get_coverage_summary(&self) -> CoverageSummary { 78 | let mut ret: CoverageSummary = Default::default(); 79 | 80 | for coverage in self.inner.values() { 81 | ret.merge(&coverage.to_summary()); 82 | } 83 | 84 | ret 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use crate::{CoverageMap, FileCoverage}; 91 | 92 | #[test] 93 | fn should_able_to_merge_another_coverage_map() { 94 | let mut base = CoverageMap::from_iter(vec![ 95 | &FileCoverage::from_file_path("foo.js".to_string(), false), 96 | &FileCoverage::from_file_path("bar.js".to_string(), false), 97 | ]); 98 | 99 | let second = CoverageMap::from_iter(vec![ 100 | &FileCoverage::from_file_path("foo.js".to_string(), false), 101 | &FileCoverage::from_file_path("baz.js".to_string(), false), 102 | ]); 103 | base.merge(&second); 104 | assert_eq!( 105 | base.get_files(), 106 | vec![ 107 | &"foo.js".to_string(), 108 | &"bar.js".to_string(), 109 | &"baz.js".to_string() 110 | ] 111 | ); 112 | } 113 | 114 | #[test] 115 | fn should_able_to_return_file_coverage() { 116 | let base = CoverageMap::from_iter(vec![ 117 | &FileCoverage::from_file_path("foo.js".to_string(), false), 118 | &FileCoverage::from_file_path("bar.js".to_string(), false), 119 | ]); 120 | 121 | assert!(base.get_coverage_for_file("foo.js").is_some()); 122 | assert!(base.get_coverage_for_file("bar.js").is_some()); 123 | 124 | assert!(base.get_coverage_for_file("baz.js").is_none()); 125 | } 126 | 127 | #[test] 128 | fn should_able_to_filter_coverage() { 129 | let mut base = CoverageMap::from_iter(vec![ 130 | &FileCoverage::from_file_path("foo.js".to_string(), false), 131 | &FileCoverage::from_file_path("bar.js".to_string(), false), 132 | ]); 133 | 134 | assert_eq!( 135 | base.get_files(), 136 | vec![&"foo.js".to_string(), &"bar.js".to_string()] 137 | ); 138 | 139 | base.filter(|x| x.path == "foo.js"); 140 | assert_eq!(base.get_files(), vec![&"foo.js".to_string()]); 141 | } 142 | 143 | #[test] 144 | fn should_return_coverage_summary_for_all_files() { 145 | let mut base = CoverageMap::from_iter(vec![ 146 | &FileCoverage::from_file_path("foo.js".to_string(), false), 147 | &FileCoverage::from_file_path("bar.js".to_string(), false), 148 | ]); 149 | 150 | base.add_coverage_for_file(&FileCoverage::from_file_path("foo.js".to_string(), false)); 151 | base.add_coverage_for_file(&FileCoverage::from_file_path("baz.js".to_string(), false)); 152 | 153 | let summary = base.get_coverage_summary(); 154 | assert_eq!(summary.statements.total, 0); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /spec/fixtures/switch.yaml: -------------------------------------------------------------------------------- 1 | name: empty switch 2 | code: | 3 | output = "unknown"; 4 | switch (args[0]) { 5 | } 6 | tests: 7 | - name: is benign 8 | args: ['1'] 9 | out: unknown 10 | lines: {'1': 1, '2': 1} 11 | statements: {'0': 1, '1': 1} 12 | 13 | --- 14 | name: 2 cases no default 15 | code: | 16 | output = "unknown"; 17 | switch (args[0]) { 18 | case "1": output = "one"; break; 19 | case "2": output = "two"; break; 20 | } 21 | tests: 22 | - name: first case 23 | args: ['1'] 24 | out: one 25 | lines: {'1': 1, '2': 1, '3': 1, '4': 0} 26 | branches: {'0': [1, 0]} 27 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 0, '5': 0} 28 | 29 | - name: second case 30 | args: ['2'] 31 | out: two 32 | lines: {'1': 1, '2': 1, '3': 0, '4': 1} 33 | branches: {'0': [0, 1]} 34 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 1, '5': 1} 35 | 36 | - name: no match 37 | args: ['3'] 38 | out: unknown 39 | lines: {'1': 1, '2': 1, '3': 0, '4': 0} 40 | branches: {'0': [0, 0]} 41 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 0, '5': 0} 42 | 43 | --- 44 | name: 2 cases with default 45 | code: | 46 | output = "unknown"; 47 | switch (args[0]) { 48 | case "1": output = "one"; break; 49 | case "2": output = "two"; break; 50 | default: output = "three"; 51 | } 52 | tests: 53 | - name: first case 54 | args: ['1'] 55 | out: one 56 | lines: {'1': 1, '2': 1, '3': 1, '4': 0, '5': 0} 57 | branches: {'0': [1, 0, 0]} 58 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 0, '5': 0, '6': 0} 59 | 60 | - name: second case 61 | args: ['2'] 62 | out: two 63 | lines: {'1': 1, '2': 1, '3': 0, '4': 1, '5': 0} 64 | branches: {'0': [0, 1, 0]} 65 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 1, '5': 1, '6': 0} 66 | 67 | - name: default case 68 | args: ['4'] 69 | out: three 70 | lines: {'1': 1, '2': 1, '3': 0, '4': 0, '5': 1} 71 | branches: {'0': [0, 0, 1]} 72 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 0, '5': 0, '6': 1} 73 | 74 | --- 75 | name: one line layout 76 | code: | 77 | output = "unknown"; 78 | switch (args[0]) { case "1": output = "one"; break; case "2": output = "two"; break; default: output = "three";} 79 | tests: 80 | - name: first case 81 | args: ['1'] 82 | out: one 83 | lines: {'1': 1, '2': 1} 84 | branches: {'0': [1, 0, 0]} 85 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 0, '5': 0, '6': 0} 86 | 87 | - name: second case 88 | args: ['2'] 89 | out: two 90 | lines: {'1': 1, '2': 1} 91 | branches: {'0': [0, 1, 0]} 92 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 1, '5': 1, '6': 0} 93 | 94 | - name: default case 95 | args: ['4'] 96 | out: three 97 | lines: {'1': 1, '2': 1} 98 | branches: {'0': [0, 0, 1]} 99 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 0, '5': 0, '6': 1} 100 | 101 | --- 102 | name: 2 cases with default and fallthru 103 | code: | 104 | output = ""; 105 | switch (args[0]) { 106 | case "1": output += "one"; 107 | case "2": output += "two"; 108 | default: output += "three"; 109 | } 110 | tests: 111 | - name: first case 112 | args: ['1'] 113 | out: onetwothree 114 | lines: {'1': 1, '2': 1, '3': 1, '4': 1, '5': 1} 115 | branches: {'0': [1, 1, 1]} 116 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 117 | 118 | - name: second case 119 | args: ['2'] 120 | out: twothree 121 | lines: {'1': 1, '2': 1, '3': 0, '4': 1, '5': 1} 122 | branches: {'0': [0, 1, 1]} 123 | statements: {'0': 1, '1': 1, '2': 0, '3': 1, '4': 1} 124 | 125 | - name: default case 126 | args: ['4'] 127 | out: three 128 | lines: {'1': 1, '2': 1, '3': 0, '4': 0, '5': 1} 129 | branches: {'0': [0, 0, 1]} 130 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 1} 131 | 132 | --- 133 | name: one-line layout with fallthru 134 | code: | 135 | output = ""; 136 | switch (args[0]) { case "1": output += "one"; case "2": output += "two"; default: output += "three";} 137 | tests: 138 | - name: first case 139 | args: ['1'] 140 | out: onetwothree 141 | lines: {'1': 1, '2': 1} 142 | branches: {'0': [1, 1, 1]} 143 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 144 | 145 | - name: second case 146 | args: ['2'] 147 | out: twothree 148 | lines: {'1': 1, '2': 1} 149 | branches: {'0': [0, 1, 1]} 150 | statements: {'0': 1, '1': 1, '2': 0, '3': 1, '4': 1} 151 | 152 | - name: default case 153 | args: ['4'] 154 | out: three 155 | lines: {'1': 1, '2': 1} 156 | branches: {'0': [0, 0, 1]} 157 | statements: {'0': 1, '1': 1, '2': 0, '3': 0, '4': 1} 158 | --- 159 | name: switch with ignore next (https://github.com/istanbuljs/istanbuljs/issues/64) 160 | code: | 161 | 'use strict' 162 | 163 | const test = foo => { 164 | switch (foo) { 165 | // the bug discussed in #64, seems to only 166 | // crop up when a function is invoked before 167 | // a single line comment, hence Date.now(). 168 | case 'ok': 169 | return Date.now() 170 | 171 | /* istanbul ignore next */ 172 | default: 173 | throw new Error('nope') 174 | } 175 | } 176 | 177 | test('ok') 178 | output = 'unknown' 179 | tests: 180 | - name: 100% coverage 181 | args: [] 182 | out: unknown 183 | lines: {'3': 1, '4': 1, '9': 1, '17': 1, '18': 1} 184 | statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1} 185 | functions: {'0': 1} 186 | branches: {'0': [1]} 187 | -------------------------------------------------------------------------------- /packages/istanbul-oxide/src/coverage_summary.rs: -------------------------------------------------------------------------------- 1 | use crate::percent; 2 | 3 | #[derive(Copy, Clone, PartialEq, Debug)] 4 | pub enum CoveragePercentage { 5 | Unknown, 6 | Value(f32), 7 | } 8 | 9 | impl Default for CoveragePercentage { 10 | fn default() -> Self { 11 | CoveragePercentage::Unknown 12 | } 13 | } 14 | 15 | #[derive(Default, Copy, Clone, PartialEq, Debug)] 16 | pub struct Totals { 17 | pub total: u32, 18 | pub covered: u32, 19 | pub skipped: u32, 20 | pub pct: CoveragePercentage, 21 | } 22 | 23 | impl Totals { 24 | pub fn new(total: u32, covered: u32, skipped: u32, pct: CoveragePercentage) -> Totals { 25 | Totals { 26 | total, 27 | covered, 28 | skipped, 29 | pct, 30 | } 31 | } 32 | 33 | pub fn default() -> Totals { 34 | Totals { 35 | total: 0, 36 | covered: 0, 37 | skipped: 0, 38 | pct: CoveragePercentage::Unknown, 39 | } 40 | } 41 | } 42 | 43 | #[derive(Default, Copy, Clone)] 44 | pub struct CoverageSummary { 45 | pub(crate) lines: Totals, 46 | pub(crate) statements: Totals, 47 | pub(crate) functions: Totals, 48 | pub(crate) branches: Totals, 49 | pub(crate) branches_true: Option, 50 | } 51 | 52 | impl CoverageSummary { 53 | pub fn new( 54 | lines: Totals, 55 | statements: Totals, 56 | functions: Totals, 57 | branches: Totals, 58 | branches_true: Option, 59 | ) -> CoverageSummary { 60 | CoverageSummary { 61 | lines, 62 | statements, 63 | functions, 64 | branches, 65 | branches_true, 66 | } 67 | } 68 | 69 | pub fn from(summary: &CoverageSummary) -> CoverageSummary { 70 | CoverageSummary { 71 | lines: summary.lines, 72 | statements: summary.statements, 73 | functions: summary.functions, 74 | branches: summary.branches, 75 | branches_true: summary.branches_true, 76 | } 77 | } 78 | 79 | pub fn default() -> CoverageSummary { 80 | CoverageSummary { 81 | lines: Default::default(), 82 | statements: Default::default(), 83 | functions: Default::default(), 84 | branches: Default::default(), 85 | branches_true: Some(Default::default()), 86 | } 87 | } 88 | 89 | /// Merges a second summary coverage object into this one 90 | pub fn merge(&mut self, summary: &CoverageSummary) { 91 | self.lines.total += summary.lines.total; 92 | self.lines.covered += summary.lines.covered; 93 | self.lines.skipped += summary.lines.skipped; 94 | self.lines.pct = CoveragePercentage::Value(percent(self.lines.covered, self.lines.total)); 95 | 96 | self.statements.total += summary.statements.total; 97 | self.statements.covered += summary.statements.covered; 98 | self.statements.skipped += summary.statements.skipped; 99 | self.statements.pct = 100 | CoveragePercentage::Value(percent(self.statements.covered, self.statements.total)); 101 | 102 | self.functions.total += summary.functions.total; 103 | self.functions.covered += summary.functions.covered; 104 | self.functions.skipped += summary.functions.skipped; 105 | self.functions.pct = 106 | CoveragePercentage::Value(percent(self.functions.covered, self.functions.total)); 107 | 108 | self.branches.total += summary.branches.total; 109 | self.branches.covered += summary.branches.covered; 110 | self.branches.skipped += summary.branches.skipped; 111 | self.branches.pct = 112 | CoveragePercentage::Value(percent(self.branches.covered, self.branches.total)); 113 | 114 | if let Some(branches_true) = summary.branches_true { 115 | let mut self_branches_true = if let Some(self_value) = self.branches_true { 116 | self_value 117 | } else { 118 | Default::default() 119 | }; 120 | 121 | self_branches_true.total += branches_true.total; 122 | self_branches_true.covered += branches_true.covered; 123 | self_branches_true.skipped += branches_true.skipped; 124 | self_branches_true.pct = CoveragePercentage::Value(percent( 125 | self_branches_true.covered, 126 | self_branches_true.total, 127 | )); 128 | 129 | self.branches_true = Some(self_branches_true); 130 | } 131 | } 132 | 133 | pub fn to_json(&self) { 134 | unimplemented!("Not implemented yet") 135 | } 136 | 137 | pub fn is_empty(&self) -> bool { 138 | self.lines.total == 0 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use crate::{CoveragePercentage, CoverageSummary, Totals}; 145 | 146 | #[test] 147 | fn should_able_to_create_empty() { 148 | let summary = CoverageSummary::default(); 149 | 150 | assert!(summary.is_empty()); 151 | } 152 | 153 | #[test] 154 | fn should_able_to_merge() { 155 | let basic = Totals::new(5, 4, 0, crate::CoveragePercentage::Value(80.0)); 156 | let empty = Totals::default(); 157 | 158 | let mut first = CoverageSummary::new(basic, basic, basic, empty, Some(empty)); 159 | let mut second = first.clone(); 160 | 161 | second.statements.covered = 5; 162 | first.merge(&second); 163 | 164 | assert_eq!( 165 | first.statements, 166 | Totals::new(10, 9, 0, CoveragePercentage::Value(90.0)) 167 | ); 168 | 169 | assert_eq!(first.branches.pct, CoveragePercentage::Value(100.0)); 170 | let branches_true = first.branches_true.expect("Should exist"); 171 | assert_eq!(branches_true.pct, CoveragePercentage::Value(100.0)); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /packages/swc-plugin-coverage/src/lib.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | ecma::{ast::Program, visit::*}, 3 | plugin::{ 4 | metadata::TransformPluginMetadataContextKind, plugin_transform, 5 | proxies::TransformPluginProgramMetadata, 6 | }, 7 | }; 8 | use swc_coverage_instrument::{ 9 | create_coverage_instrumentation_visitor, InstrumentLogOptions, InstrumentOptions, 10 | }; 11 | 12 | use tracing_subscriber::fmt::format::FmtSpan; 13 | use typed_path::Utf8TypedPath; 14 | use wax::Pattern; 15 | 16 | /// Normalize a file path to use forward slashes for consistent glob matching 17 | fn normalize_path(path: &str) -> String { 18 | let typed_path = Utf8TypedPath::derive(path); 19 | if typed_path.is_windows() { 20 | typed_path.with_unix_encoding().to_string() 21 | } else if path.contains('\\') { 22 | // Fallback: if the path contains backslashes but wasn't detected as Windows, 23 | // still normalize it by replacing backslashes with forward slashes 24 | path.replace('\\', "/") 25 | } else { 26 | path.to_string() 27 | } 28 | } 29 | 30 | fn initialize_instrumentation_log(log_options: &InstrumentLogOptions) { 31 | let log_level = match log_options.level.as_deref() { 32 | Some("error") => Some(tracing::Level::ERROR), 33 | Some("debug") => Some(tracing::Level::DEBUG), 34 | Some("info") => Some(tracing::Level::INFO), 35 | Some("warn") => Some(tracing::Level::WARN), 36 | Some("trace") => Some(tracing::Level::TRACE), 37 | _ => None, 38 | }; 39 | 40 | if let Some(log_level) = log_level { 41 | let builder = tracing_subscriber::fmt().with_max_level(log_level); 42 | 43 | let builder = if log_options.enable_trace { 44 | builder.with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE) 45 | } else { 46 | builder 47 | }; 48 | 49 | builder 50 | .with_ansi(false) 51 | .event_format(tracing_subscriber::fmt::format().pretty()) 52 | .init(); 53 | } 54 | } 55 | 56 | #[plugin_transform] 57 | pub fn process(program: Program, metadata: TransformPluginProgramMetadata) -> Program { 58 | let filename = metadata.get_context(&TransformPluginMetadataContextKind::Filename); 59 | let filename = if let Some(filename) = filename.as_deref() { 60 | filename 61 | } else { 62 | "unknown.js" 63 | }; 64 | 65 | let plugin_config = metadata.get_transform_plugin_config(); 66 | let instrument_options: InstrumentOptions = if let Some(plugin_config) = plugin_config { 67 | serde_json::from_str(&plugin_config).unwrap_or_else(|f| { 68 | println!("Could not deserialize instrumentation option"); 69 | println!("{:#?}", f); 70 | Default::default() 71 | }) 72 | } else { 73 | Default::default() 74 | }; 75 | 76 | // Unstable option to exclude files from coverage. If pattern is wax(https://crates.io/crates/wax) 77 | // compatible glob and the filename matches the pattern, the file will not be instrumented. 78 | // Note that the filename is provided by swc's core, may not be the full absolute path to the file name. 79 | if let Some(exclude) = &instrument_options.unstable_exclude { 80 | let normalized_patterns = exclude 81 | .iter() 82 | .map(|s| normalize_path(s)) 83 | .collect::>(); 84 | 85 | match wax::any(normalized_patterns.iter().map(|s| s.as_str())) { 86 | Ok(p) => { 87 | let normalized_filename = normalize_path(filename); 88 | if p.is_match(normalized_filename.as_str()) { 89 | return program; 90 | } 91 | } 92 | Err(e) => { 93 | println!("Could not parse unstable_exclude option, will be ignored"); 94 | println!("{:#?}", e); 95 | } 96 | } 97 | } 98 | 99 | initialize_instrumentation_log(&instrument_options.instrument_log); 100 | 101 | let visitor = create_coverage_instrumentation_visitor( 102 | std::sync::Arc::new(metadata.source_map), 103 | metadata.comments.as_ref(), 104 | instrument_options, 105 | filename.to_string(), 106 | ); 107 | 108 | program.apply(&mut visit_mut_pass(visitor)) 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | 115 | #[test] 116 | fn test_normalize_path_for_glob_matching() { 117 | // Test Windows paths are normalized to Unix-style 118 | let result = normalize_path(r"C:\Users\project\test\index.test.ts"); 119 | println!("Windows path result: {}", result); 120 | // The typed-path crate converts Windows paths to Unix format, but may strip the drive letter 121 | // The important thing is that backslashes are converted to forward slashes 122 | assert!(result.contains("/Users/project/test/index.test.ts")); 123 | 124 | // Test mixed separators are normalized 125 | let result = normalize_path(r"C:\Users/project\test/file.js"); 126 | println!("Mixed separators result: {}", result); 127 | assert!(result.contains("/Users/project/test/file.js")); 128 | 129 | // Test Unix paths remain unchanged 130 | assert_eq!( 131 | normalize_path("/home/user/project/src/utils/helper.js"), 132 | "/home/user/project/src/utils/helper.js" 133 | ); 134 | 135 | // Test relative Unix paths remain unchanged 136 | assert_eq!( 137 | normalize_path("src/components/Button.tsx"), 138 | "src/components/Button.tsx" 139 | ); 140 | 141 | // Test that backslashes are converted to forward slashes 142 | let windows_path = r"project\src\test\file.ts"; 143 | let result = normalize_path(windows_path); 144 | println!("Relative Windows path result: {}", result); 145 | assert!(result.contains("project/src/test/file.ts")); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /spec/fixtures/if-hints.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ignore else 3 | code: | 4 | output = -1; 5 | /* istanbul ignore else */ 6 | if (args[0] > args [1]) 7 | output = args[0]; 8 | tests: 9 | - args: [20, 10] 10 | out: 20 11 | lines: {'1': 1, '3': 1, '4': 1} 12 | branches: {'0': [1]} 13 | statements: {'0': 1, '1': 1, '2': 1} 14 | 15 | --- 16 | name: ignore if 17 | code: | 18 | output = -1; 19 | /* istanbul ignore if */ 20 | if (args[0] > args [1]) 21 | output = args[0]; 22 | tests: 23 | - args: [10, 20] 24 | out: -1 25 | lines: {'1': 1, '3': 1 } 26 | branches: {'0': [1]} 27 | statements: {'0': 1, '1': 1 } 28 | 29 | --- 30 | name: ignore else with if block 31 | code: | 32 | output = -1; 33 | /* istanbul ignore else */ 34 | if (args[0] > args [1]) { 35 | output = args[0]; 36 | } 37 | tests: 38 | - args: [20, 10] 39 | out: 20 40 | lines: {'1': 1, '3': 1, '4': 1} 41 | branches: {'0': [1]} 42 | statements: {'0': 1, '1': 1, '2': 1} 43 | 44 | --- 45 | name: ignore if with if block 46 | code: | 47 | output = -1; 48 | /* istanbul ignore if */ 49 | if (args[0] > args [1]) { 50 | output = args[0]; 51 | } 52 | tests: 53 | - args: [10, 20] 54 | out: -1 55 | lines: {'1': 1, '3': 1 } 56 | branches: {'0': [1]} 57 | statements: {'0': 1, '1': 1 } 58 | 59 | --- 60 | name: ignore else single line 61 | code: | 62 | output = -1; 63 | /* istanbul ignore else */ 64 | if (args[0] > args [1]) output = args[0]; 65 | tests: 66 | - args: [20, 10] 67 | out: 20 68 | lines: {'1': 1, '3': 1} 69 | branches: {'0': [1]} 70 | statements: {'0': 1, '1': 1, '2': 1} 71 | 72 | --- 73 | name: ignore if single line 74 | code: | 75 | output = -1; 76 | /* istanbul ignore if */ 77 | if (args[0] > args [1]) output = args[0]; 78 | tests: 79 | - args: [10, 20] 80 | out: -1 81 | lines: {'1': 1, '3': 1} 82 | branches: {'0': [1]} 83 | statements: {'0': 1, '1': 1 } 84 | 85 | --- 86 | name: ignore else if-block 87 | code: | 88 | output = -1; 89 | /* istanbul ignore else */ 90 | if (args[0] > args [1]) { output = args[0]; } 91 | tests: 92 | - args: [20, 10] 93 | out: 20 94 | lines: {'1': 1, '3': 1} 95 | branches: {'0': [1] } 96 | statements: {'0': 1, '1': 1, '2': 1} 97 | 98 | --- 99 | name: ignore if block 100 | code: | 101 | output = -1; 102 | /* istanbul ignore if */ 103 | if (args[0] > args [1]) { output = args[0]; } 104 | tests: 105 | - args: [10, 20] 106 | out: -1 107 | lines: {'1': 1, '3': 1} 108 | branches: {'0': [1]} 109 | statements: {'0': 1, '1': 1} 110 | branchSkips: { '0': [ true, false ]} 111 | statementSkips: { '2': true } 112 | 113 | --- 114 | name: ignore using next 115 | code: | 116 | output = -1; 117 | /* istanbul ignore next */ 118 | if (args[0] > args [1]) { output = args[0]; } 119 | tests: 120 | - args: [10, 20] 121 | out: -1 122 | lines: {'1': 1} 123 | branches: {} 124 | statements: {'0': 1 } 125 | 126 | --- 127 | name: ignore else using single line comment 128 | code: | 129 | // istanbul ignore else 130 | if (args[0] > args [1]) 131 | output = args[0]; 132 | else 133 | output = args[1]; 134 | tests: 135 | - args: [20, 10] 136 | out: 20 137 | lines: {'2': 1, '3': 1 } 138 | branches: {'0': [1]} 139 | statements: {'0': 1, '1': 1 } 140 | 141 | --- 142 | name: ignore if using single line comment 143 | code: | 144 | // istanbul ignore if 145 | if (args[0] > args [1]) 146 | output = args[0]; 147 | else 148 | output = args[1]; 149 | tests: 150 | - args: [10, 20] 151 | out: 20 152 | lines: {'2': 1, '5': 1} 153 | branches: {'0': [1]} 154 | statements: {'0': 1, '1': 1, } 155 | --- 156 | name: ignore chained if 157 | code: | 158 | if (args[0] === 1) { 159 | output = '1'; 160 | } else /* istanbul ignore if */ if (args[0] === 2) { 161 | output = '2'; 162 | } else { 163 | output = 'other'; 164 | } 165 | tests: 166 | - name: '1' 167 | args: [1] 168 | out: '1' 169 | lines: {'1': 1, '2': 1, '3': 0, '6': 0} 170 | branches: {'0': [1, 0], 1: [0]} 171 | statements: {'0': 1, '1': 1, '2': 0, '3': 0} 172 | - name: '2' 173 | args: [2] 174 | out: '2' 175 | lines: {'1': 1, '2': 0, '3': 1, '6': 0} 176 | branches: {'0': [0, 1], 1: [0]} 177 | statements: {'0': 1, '1': 0, '2': 1, '3': 0} 178 | - name: '3' 179 | args: [3] 180 | out: 'other' 181 | lines: {'1': 1, '2': 0, '3': 1, '6': 1} 182 | branches: {'0': [0, 1], 1: [1]} 183 | statements: {'0': 1, '1': 0, '2': 1, '3': 1} 184 | --- 185 | name: ignore chained else 186 | code: | 187 | if (args[0] === 1) { 188 | output = '1'; 189 | } else /* istanbul ignore else */ if (args[0] === 2) { 190 | output = '2'; 191 | } else if (args[0] === 3) { 192 | output = '3'; 193 | } else { 194 | output = 'other'; 195 | } 196 | tests: 197 | - name: '1' 198 | args: [1] 199 | out: '1' 200 | lines: {'1': 1, '2': 1, '3': 0, '4': 0} 201 | branches: {'0': [1, 0], 1: [0]} 202 | statements: {'0': 1, '1': 1, '2': 0, '3': 0} 203 | - name: '2' 204 | args: [2] 205 | out: '2' 206 | lines: {'1': 1, '2': 0, '3': 1, '4': 1} 207 | branches: {'0': [0, 1], 1: [1]} 208 | statements: {'0': 1, '1': 0, '2': 1, '3': 1} 209 | - name: '3' 210 | args: [3] 211 | out: '3' 212 | lines: {'1': 1, '2': 0, '3': 1, '4': 0} 213 | branches: {'0': [0, 1], 1: [0]} 214 | statements: {'0': 1, '1': 0, '2': 1, '3': 0} 215 | - name: '4' 216 | args: [4] 217 | out: 'other' 218 | lines: {'1': 1, '2': 0, '3': 1, '4': 0} 219 | branches: {'0': [0, 1], 1: [0]} 220 | statements: {'0': 1, '1': 0, '2': 1, '3': 0} 221 | --- 222 | name: ignore next chained if 223 | code: | 224 | if (args[0] === 1) { 225 | output = '1'; 226 | } else /* istanbul ignore next */ if (args[0] === 2) { 227 | output = '2'; 228 | } else if (args[0] === 3) { 229 | output = '3'; 230 | } else { 231 | output = 'other'; 232 | } 233 | tests: 234 | - name: '1' 235 | args: [1] 236 | out: '1' 237 | lines: {'1': 1, '2': 1} 238 | branches: {'0': [1, 0]} 239 | statements: {'0': 1, '1': 1} 240 | - name: '2' 241 | args: [2] 242 | out: '2' 243 | lines: {'1': 1, '2': 0} 244 | branches: {'0': [0, 1]} 245 | statements: {'0': 1, '1': 0} 246 | - name: '3' 247 | args: [3] 248 | out: '3' 249 | lines: {'1': 1, '2': 0} 250 | branches: {'0': [0, 1]} 251 | statements: {'0': 1, '1': 0} 252 | - name: '4' 253 | args: [4] 254 | out: 'other' 255 | lines: {'1': 1, '2': 0} 256 | branches: {'0': [0, 1]} 257 | statements: {'0': 1, '1': 0} 258 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/coverage_template/create_coverage_fn_decl.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::hash_map::DefaultHasher, 3 | hash::{Hash, Hasher}, 4 | }; 5 | 6 | use istanbul_oxide::FileCoverage; 7 | use once_cell::sync::OnceCell; 8 | use swc_core::{ 9 | common::{ 10 | comments::{Comment, CommentKind, Comments}, 11 | util::take::Take, 12 | Span, DUMMY_SP, 13 | }, 14 | ecma::ast::*, 15 | quote, 16 | }; 17 | 18 | use crate::constants::idents::*; 19 | 20 | use crate::{create_assignment_stmt, create_coverage_data_object}; 21 | 22 | pub static COVERAGE_FN_IDENT: OnceCell = OnceCell::new(); 23 | /// temporal ident being used for b_t true counter 24 | pub static COVERAGE_FN_TRUE_TEMP_IDENT: OnceCell = OnceCell::new(); 25 | 26 | /// Create a unique ident for the injected coverage counter fn, 27 | /// Stores it into a global scope. 28 | /// 29 | /// Do not use static value directly - create_instrumentation_visitor macro 30 | /// should inject this into a struct accordingly. 31 | pub fn create_coverage_fn_ident(value: &str) { 32 | let mut s = DefaultHasher::new(); 33 | value.hash(&mut s); 34 | let var_name_hash = format!("cov_{}", s.finish()); 35 | 36 | COVERAGE_FN_IDENT 37 | .get_or_init(|| Ident::new(var_name_hash.clone().into(), DUMMY_SP, Default::default())); 38 | COVERAGE_FN_TRUE_TEMP_IDENT.get_or_init(|| { 39 | Ident::new( 40 | format!("{}_temp", var_name_hash).into(), 41 | DUMMY_SP, 42 | Default::default(), 43 | ) 44 | }); 45 | } 46 | 47 | /// Creates a function declaration for actual coverage collection. 48 | pub fn create_coverage_fn_decl( 49 | coverage_variable: &str, 50 | coverage_template: Stmt, 51 | cov_fn_ident: &Ident, 52 | file_path: &str, 53 | coverage_data: &FileCoverage, 54 | comments: &C, 55 | attach_debug_comment: bool, 56 | ) -> Stmt { 57 | // Actual fn body statements will be injected 58 | let mut stmts = vec![]; 59 | 60 | // var path = $file_path; 61 | let path_stmt = create_assignment_stmt( 62 | &IDENT_PATH, 63 | Expr::Lit(Lit::Str(Str { 64 | value: file_path.into(), 65 | ..Str::dummy() 66 | })), 67 | ); 68 | stmts.push(path_stmt); 69 | 70 | let (hash, coverage_data_object) = create_coverage_data_object(coverage_data); 71 | 72 | // var hash = $HASH; 73 | let hash_stmt = 74 | create_assignment_stmt(&IDENT_HASH, Expr::Lit(Lit::Str(Str::from(hash.clone())))); 75 | stmts.push(hash_stmt); 76 | 77 | // var global = new Function("return $global_coverage_scope")(); 78 | stmts.push(coverage_template); 79 | 80 | // var gcv = ${coverage_variable}; 81 | let gcv_stmt = create_assignment_stmt( 82 | &IDENT_GCV, 83 | Expr::Lit(Lit::Str(Str { 84 | value: coverage_variable.into(), 85 | ..Str::dummy() 86 | })), 87 | ); 88 | stmts.push(gcv_stmt); 89 | 90 | // var coverageData = INITIAL; 91 | let coverage_data_stmt = create_assignment_stmt(&IDENT_COVERAGE_DATA, coverage_data_object); 92 | stmts.push(coverage_data_stmt); 93 | 94 | let coverage_ident = Ident::new("coverage".into(), DUMMY_SP, Default::default()); 95 | stmts.push(quote!( 96 | "var $coverage = $global[$gcv] || ($global[$gcv] = {})" as Stmt, 97 | coverage = coverage_ident.clone(), 98 | gcv = IDENT_GCV.clone(), 99 | global = IDENT_GLOBAL.clone() 100 | )); 101 | 102 | stmts.push(quote!( 103 | r#" 104 | if (!$coverage[$path] || $coverage[$path].$hash !== $hash) { 105 | $coverage[$path] = $coverage_data; 106 | } 107 | "# as Stmt, 108 | coverage = coverage_ident.clone(), 109 | path = IDENT_PATH.clone(), 110 | hash = IDENT_HASH.clone(), 111 | coverage_data = IDENT_COVERAGE_DATA.clone() 112 | )); 113 | 114 | // var actualCoverage = coverage[path]; 115 | let actual_coverage_ident = Ident::new("actualCoverage".into(), DUMMY_SP, Default::default()); 116 | stmts.push(quote!( 117 | "var $actual_coverage = $coverage[$path];" as Stmt, 118 | actual_coverage = actual_coverage_ident.clone(), 119 | coverage = coverage_ident.clone(), 120 | path = IDENT_PATH.clone() 121 | )); 122 | 123 | // 124 | //COVERAGE_FUNCTION = function () { 125 | // return actualCoverage; 126 | //} 127 | // TODO: need to add @ts-ignore leading comment 128 | let coverage_fn_assign_expr = Expr::Assign(AssignExpr { 129 | left: BindingIdent::from(cov_fn_ident.clone()).into(), 130 | right: Box::new(Expr::Fn(FnExpr { 131 | ident: None, 132 | function: Box::new(Function { 133 | body: Some(BlockStmt { 134 | span: DUMMY_SP, 135 | stmts: vec![Stmt::Return(ReturnStmt { 136 | span: DUMMY_SP, 137 | arg: Some(Box::new(Expr::Ident(actual_coverage_ident.clone()))), 138 | })], 139 | ..BlockStmt::dummy() 140 | }), 141 | ..Function::dummy() 142 | }), 143 | })), 144 | ..AssignExpr::dummy() 145 | }); 146 | 147 | stmts.push(Stmt::Block(BlockStmt { 148 | stmts: vec![Stmt::Expr(ExprStmt { 149 | span: DUMMY_SP, 150 | expr: Box::new(coverage_fn_assign_expr), 151 | })], 152 | ..BlockStmt::dummy() 153 | })); 154 | 155 | let ret = ReturnStmt { 156 | span: DUMMY_SP, 157 | arg: Some(Box::new(Expr::Ident(actual_coverage_ident.clone()))), 158 | }; 159 | 160 | if attach_debug_comment { 161 | let coverage_data_json_str = 162 | serde_json::to_string(coverage_data).expect("Should able to serialize coverage data"); 163 | 164 | // Append coverage data as stringified JSON comments at the bottom of transformed code. 165 | // Currently plugin does not have way to pass any other data to the host except transformed program. 166 | // This attaches arbitary data to the transformed code itself to retrieve it. 167 | comments.add_trailing( 168 | Span::dummy_with_cmt().hi, 169 | Comment { 170 | kind: CommentKind::Block, 171 | span: Span::dummy_with_cmt(), 172 | text: format!("__coverage_data_json_comment__::{}", coverage_data_json_str).into(), 173 | }, 174 | ); 175 | } 176 | 177 | stmts.push(Stmt::Return(ret)); 178 | 179 | // moduleitem for fn decl includes body defined above 180 | Stmt::Decl(Decl::Fn(FnDecl { 181 | ident: cov_fn_ident.clone(), 182 | declare: false, 183 | function: Box::new(Function { 184 | body: Some(BlockStmt { 185 | span: DUMMY_SP, 186 | stmts, 187 | ..BlockStmt::dummy() 188 | }), 189 | ..Function::dummy() 190 | }), 191 | })) 192 | } 193 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/instrument/create_increase_true_expr.rs: -------------------------------------------------------------------------------- 1 | use swc_core::{ 2 | common::{util::take::Take, DUMMY_SP}, 3 | ecma::ast::*, 4 | }; 5 | 6 | use super::create_increase_counter_expr::create_increase_counter_expr; 7 | use crate::constants::idents::IDENT_BT; 8 | 9 | /// Reads the logic expression conditions and conditionally increments truthy counter. 10 | /// This is always known to be b_t type counter does not need to accept what type of ident it'll create. 11 | pub fn create_increase_true_expr( 12 | id: u32, 13 | idx: u32, 14 | var_name: &Ident, 15 | temp_var_name: &Ident, 16 | expr: Expr, 17 | ) -> Expr { 18 | let member = MemberExpr { 19 | obj: Box::new(Expr::Call(CallExpr { 20 | callee: Callee::Expr(Box::new(Expr::Ident(var_name.clone()))), 21 | ..CallExpr::dummy() 22 | })), 23 | prop: MemberProp::Ident(temp_var_name.clone().into()), 24 | ..MemberExpr::dummy() 25 | }; 26 | 27 | let assignment = Expr::Assign(AssignExpr { 28 | op: AssignOp::Assign, 29 | left: member.clone().into(), 30 | right: Box::new(expr), // Only evaluates once. 31 | ..AssignExpr::dummy() 32 | }); 33 | 34 | let paren = Expr::Paren(ParenExpr { 35 | span: DUMMY_SP, 36 | expr: Box::new(Expr::Cond(CondExpr { 37 | test: Box::new(validate_true_non_trivial(var_name, temp_var_name)), 38 | cons: Box::new(create_increase_counter_expr( 39 | &IDENT_BT, 40 | id, 41 | var_name, 42 | Some(idx), 43 | )), 44 | alt: Box::new(Expr::Lit(Lit::Null(Null::dummy()))), 45 | ..CondExpr::dummy() 46 | })), 47 | }); 48 | 49 | let ret = Expr::Seq(SeqExpr { 50 | span: DUMMY_SP, 51 | exprs: vec![ 52 | Box::new(assignment), 53 | Box::new(paren), 54 | Box::new(Expr::Member(member)), 55 | ], 56 | }); 57 | 58 | ret 59 | } 60 | 61 | fn validate_true_non_trivial(var_name: &Ident, temp_var_name: &Ident) -> Expr { 62 | // TODO: duplicate code with create_increase_true_expr 63 | let member = Expr::Member(MemberExpr { 64 | obj: Box::new(Expr::Call(CallExpr { 65 | callee: Callee::Expr(Box::new(Expr::Ident(var_name.clone()))), 66 | ..CallExpr::dummy() 67 | })), 68 | prop: MemberProp::Ident(temp_var_name.clone().into()), 69 | ..MemberExpr::dummy() 70 | }); 71 | 72 | let left_for_right = Expr::Paren(ParenExpr { 73 | span: DUMMY_SP, 74 | expr: Box::new(Expr::Bin(BinExpr { 75 | op: BinaryOp::LogicalOr, 76 | left: Box::new(Expr::Unary(UnaryExpr { 77 | op: UnaryOp::Bang, 78 | arg: Box::new(Expr::Call(CallExpr { 79 | callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { 80 | obj: Box::new(Expr::Ident(Ident { 81 | sym: "Array".into(), 82 | ..Ident::dummy() 83 | })), 84 | prop: MemberProp::Ident(IdentName { 85 | sym: "isArray".into(), 86 | ..IdentName::dummy() 87 | }), 88 | ..MemberExpr::dummy() 89 | }))), 90 | args: vec![ExprOrSpread { 91 | expr: Box::new(member.clone()), 92 | spread: None, 93 | }], 94 | ..CallExpr::dummy() 95 | })), 96 | ..UnaryExpr::dummy() 97 | })), 98 | right: Box::new(Expr::Member(MemberExpr { 99 | obj: Box::new(member.clone()), 100 | prop: MemberProp::Ident(IdentName { 101 | sym: "length".into(), 102 | ..IdentName::dummy() 103 | }), 104 | ..MemberExpr::dummy() 105 | })), 106 | ..BinExpr::dummy() 107 | })), 108 | }); 109 | let right_for_right = Expr::Paren(ParenExpr { 110 | expr: Box::new(Expr::Bin(BinExpr { 111 | op: BinaryOp::LogicalOr, 112 | left: Box::new(Expr::Bin(BinExpr { 113 | op: BinaryOp::NotEqEq, 114 | left: Box::new(Expr::Call(CallExpr { 115 | callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { 116 | obj: Box::new(Expr::Ident(Ident { 117 | sym: "Object".into(), 118 | ..Ident::dummy() 119 | })), 120 | prop: MemberProp::Ident(IdentName { 121 | sym: "getPrototypeOf".into(), 122 | ..IdentName::dummy() 123 | }), 124 | ..MemberExpr::dummy() 125 | }))), 126 | args: vec![ExprOrSpread { 127 | spread: None, 128 | expr: Box::new(member.clone()), 129 | }], 130 | ..CallExpr::dummy() 131 | })), 132 | right: Box::new(Expr::Member(MemberExpr { 133 | obj: Box::new(Expr::Ident(Ident { 134 | sym: "Object".into(), 135 | ..Ident::dummy() 136 | })), 137 | prop: MemberProp::Ident(IdentName { 138 | sym: "prototype".into(), 139 | ..IdentName::dummy() 140 | }), 141 | ..MemberExpr::dummy() 142 | })), 143 | ..BinExpr::dummy() 144 | })), 145 | right: Box::new(Expr::Member(MemberExpr { 146 | obj: Box::new(Expr::Call(CallExpr { 147 | callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { 148 | obj: Box::new(Expr::Ident(Ident { 149 | sym: "Object".into(), 150 | ..Ident::dummy() 151 | })), 152 | prop: MemberProp::Ident(IdentName { 153 | sym: "values".into(), 154 | ..IdentName::dummy() 155 | }), 156 | ..MemberExpr::dummy() 157 | }))), 158 | args: vec![ExprOrSpread { 159 | expr: Box::new(member.clone()), 160 | spread: None, 161 | }], 162 | ..CallExpr::dummy() 163 | })), 164 | prop: MemberProp::Ident(IdentName { 165 | sym: "length".into(), 166 | ..IdentName::dummy() 167 | }), 168 | ..MemberExpr::dummy() 169 | })), 170 | ..BinExpr::dummy() 171 | })), 172 | ..ParenExpr::dummy() 173 | }); 174 | 175 | let right = Expr::Bin(BinExpr { 176 | op: BinaryOp::LogicalAnd, 177 | left: Box::new(left_for_right), 178 | right: Box::new(right_for_right), 179 | ..BinExpr::dummy() 180 | }); 181 | 182 | let ret = Expr::Bin(BinExpr { 183 | op: BinaryOp::LogicalAnd, 184 | left: Box::new(member), 185 | right: Box::new(right), 186 | ..BinExpr::dummy() 187 | }); 188 | ret 189 | } 190 | -------------------------------------------------------------------------------- /spec/util/verifier.ts: -------------------------------------------------------------------------------- 1 | import { Options, transformSync } from "@swc/core"; 2 | import * as path from "path"; 3 | import { assert } from "chai"; 4 | import { readInitialCoverage } from "./read-coverage"; 5 | import { EOL } from "os"; 6 | import { FileCoverageInterop } from "../swc-coverage-instrument-wasm/pkg/swc_coverage_instrument_wasm"; 7 | 8 | const clone: typeof import("lodash.clone") = require("lodash.clone"); 9 | 10 | const pluginBinary = path.resolve( 11 | __dirname, 12 | "../../target/wasm32-wasip1/debug/swc_plugin_coverage.wasm", 13 | ); 14 | 15 | /// Mimic instrumenter. 16 | const instrumentSync = ( 17 | code: string, 18 | filename: string, 19 | inputSourceMap?: object, 20 | instrumentOptions?: Record, 21 | transformOptions?: Options, 22 | ) => { 23 | const pluginOptions = inputSourceMap 24 | ? { 25 | ...(instrumentOptions ?? {}), 26 | inputSourceMap, 27 | } 28 | : instrumentOptions ?? {}; 29 | 30 | const options = { 31 | filename: filename ?? "unknown", 32 | jsc: { 33 | parser: { 34 | syntax: "ecmascript", 35 | jsx: true, 36 | }, 37 | target: "es2022", 38 | preserveAllComments: true, 39 | }, 40 | isModule: transformOptions?.isModule ?? true, 41 | module: { 42 | type: "commonjs", 43 | strict: transformOptions?.isModule ?? false, 44 | }, 45 | }; 46 | 47 | if (process.env.SWC_TRANSFORM_CUSTOM === "1") { 48 | const { transformSync } = require("../../spec/swc-coverage-custom-transform"); 49 | return transformSync( 50 | code, 51 | true, 52 | Buffer.from(JSON.stringify(options)), 53 | Buffer.from( 54 | JSON.stringify({ 55 | ...pluginOptions, 56 | debugInitialCoverageComment: true, 57 | }), 58 | ), 59 | ); 60 | } 61 | 62 | options.jsc.experimental = { 63 | plugins: [ 64 | [ 65 | pluginBinary, 66 | { 67 | ...pluginOptions, 68 | debugInitialCoverageComment: true, 69 | }, 70 | ], 71 | ], 72 | }; 73 | 74 | return transformSync(code, options); 75 | }; 76 | 77 | /** 78 | * Poorman's substitution for instrumenter::lastFileCoverage to get the coverage from instrumented codes. 79 | * SWC's plugin transform does not allow to pass arbiatary data other than transformed AST, using trailing comment 80 | * to grab out data from plugin. 81 | */ 82 | const lastFileCoverage = (code?: string) => { 83 | const lines = (code ?? "").split(EOL); 84 | const commentLine = lines 85 | .find((v) => v.includes("__coverage_data_json_comment__::")) 86 | ?.split("__coverage_data_json_comment__::")[1]; 87 | 88 | const data = commentLine?.substring(0, commentLine.indexOf("*/")); 89 | return data ? JSON.parse(data) : {}; 90 | }; 91 | 92 | type UnknownReserved = any; 93 | 94 | class Verifier { 95 | private result: UnknownReserved; 96 | 97 | constructor(result: UnknownReserved) { 98 | this.result = result; 99 | } 100 | 101 | async verify(args, expectedOutput, expectedCoverage) { 102 | assert.ok(!this.result.err, (this.result.err || {}).message); 103 | 104 | getGlobalObject()[this.result.coverageVariable] = clone( 105 | this.result.baseline, 106 | ); 107 | const actualOutput = await this.result.fn(args); 108 | const cov = this.getFileCoverage(); 109 | 110 | assert.ok( 111 | cov && typeof cov === "object", 112 | "No coverage found for [" + this.result.file + "]", 113 | ); 114 | assert.deepEqual(actualOutput, expectedOutput, "Output mismatch"); 115 | assert.deepEqual( 116 | Object.fromEntries(cov.getLineCoverage()), 117 | expectedCoverage.lines || {}, 118 | "Line coverage mismatch", 119 | ); 120 | assert.deepEqual( 121 | Object.fromEntries(cov.f()), 122 | expectedCoverage.functions || {}, 123 | "Function coverage mismatch", 124 | ); 125 | assert.deepEqual( 126 | Object.fromEntries(cov.b()), 127 | expectedCoverage.branches || {}, 128 | "Branch coverage mismatch", 129 | ); 130 | assert.deepEqual( 131 | Object.fromEntries(cov.bT() || new Map()), 132 | expectedCoverage.branchesTrue || {}, 133 | "Branch truthiness coverage mismatch", 134 | ); 135 | assert.deepEqual( 136 | Object.fromEntries(cov.s()), 137 | expectedCoverage.statements || {}, 138 | "Statement coverage mismatch", 139 | ); 140 | 141 | assert.deepEqual( 142 | cov.inputSourceMap(), 143 | expectedCoverage.inputSourceMap || undefined, 144 | "Input source map mismatch", 145 | ); 146 | 147 | const initial = readInitialCoverage( 148 | this.getGeneratedCode(), 149 | this.result.transformOptions, 150 | ); 151 | assert.ok(initial); 152 | assert.deepEqual(initial.coverageData, this.result.emptyCoverage); 153 | assert.ok(initial.path); 154 | if (this.result.file) { 155 | assert.equal(initial.path, this.result.file); 156 | } 157 | assert.equal(initial.gcv, this.result.coverageVariable); 158 | assert.ok(initial.hash); 159 | } 160 | 161 | getCoverage() { 162 | return getGlobalObject()[this.result.coverageVariable]; 163 | } 164 | 165 | getFileCoverage() { 166 | const cov = this.getCoverage(); 167 | 168 | const { _coverageSchema, hash, ...fileCoverage } = cov[Object.keys(cov)[0]]; 169 | 170 | return new FileCoverageInterop(fileCoverage); 171 | } 172 | 173 | getGeneratedCode() { 174 | return this.result.generatedCode; 175 | } 176 | 177 | compileError() { 178 | return this.result.err; 179 | } 180 | } 181 | 182 | function extractTestOption(options, name, defaultValue) { 183 | let v = defaultValue; 184 | if (Object.prototype.hasOwnProperty.call(options, name)) { 185 | v = options[name]; 186 | } 187 | return v; 188 | } 189 | 190 | const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; 191 | 192 | function pad(str, len) { 193 | const blanks = " "; 194 | if (str.length >= len) { 195 | return str; 196 | } 197 | return blanks.substring(0, len - str.length) + str; 198 | } 199 | 200 | function annotatedCode(code) { 201 | const codeArray = code.split("\n"); 202 | let line = 0; 203 | const annotated = codeArray.map((str) => { 204 | line += 1; 205 | return pad(line, 6) + ": " + str; 206 | }); 207 | return annotated.join("\n"); 208 | } 209 | 210 | function getGlobalObject() { 211 | return new Function("return this")(); 212 | } 213 | 214 | const create = (code, options = {}, instrumentOptions = {}, inputSourceMap) => { 215 | instrumentOptions.coverageVariable = 216 | instrumentOptions.coverageVariable || "__testing_coverage__"; 217 | 218 | const debug = extractTestOption(options, "debug", process.env.DEBUG === "1"); 219 | const file = extractTestOption(options, "file", __filename); 220 | const generateOnly = extractTestOption(options, "generateOnly", false); 221 | const noCoverage = extractTestOption(options, "noCoverage", false); 222 | const quiet = extractTestOption(options, "quiet", false); 223 | const coverageVariable = instrumentOptions.coverageVariable; 224 | const g = getGlobalObject(); 225 | 226 | let instrumenterOutput; 227 | let wrapped; 228 | let fn; 229 | let verror; 230 | 231 | if (debug) { 232 | instrumentOptions.compact = false; 233 | instrumentOptions.instrumentLog = { 234 | logLevel: "trace", 235 | enable_trace: true, 236 | }; 237 | } 238 | 239 | try { 240 | let out = instrumentSync( 241 | code, 242 | file, 243 | inputSourceMap, 244 | instrumentOptions, 245 | options.transformOptions, 246 | ); 247 | instrumenterOutput = out.code; 248 | 249 | if (debug) { 250 | console.log( 251 | "================== Original ============================================", 252 | ); 253 | console.log(annotatedCode(code)); 254 | console.log( 255 | "================== Generated ===========================================", 256 | ); 257 | console.log(instrumenterOutput); 258 | console.log( 259 | "========================================================================", 260 | ); 261 | } 262 | } catch (ex) { 263 | if (!quiet) { 264 | console.error(ex.stack); 265 | } 266 | verror = new Error( 267 | "Error instrumenting:\n" + 268 | annotatedCode(String(code)) + 269 | "\n" + 270 | ex.message, 271 | ); 272 | } 273 | if (!(verror || generateOnly)) { 274 | wrapped = 275 | "{ var exports={};\n var output;\n" + 276 | instrumenterOutput + 277 | "\nreturn output;\n}"; 278 | g[coverageVariable] = undefined; 279 | try { 280 | if (options.isAsync) { 281 | fn = new AsyncFunction("args", wrapped); 282 | } else { 283 | fn = new Function("args", wrapped); 284 | } 285 | } catch (ex) { 286 | console.error(ex.stack); 287 | verror = new Error( 288 | "Error compiling\n" + annotatedCode(code) + "\n" + ex.message, 289 | ); 290 | } 291 | } 292 | if (generateOnly || noCoverage) { 293 | if (verror) { 294 | throw verror; 295 | } 296 | } 297 | return new Verifier({ 298 | err: verror, 299 | debug, 300 | file, 301 | fn, 302 | code, 303 | generatedCode: instrumenterOutput, 304 | coverageVariable, 305 | baseline: clone(g[coverageVariable]), 306 | emptyCoverage: lastFileCoverage(instrumenterOutput), //instrumenter.getLastFileCoverage() 307 | transformOptions: options.transformOptions, 308 | }); 309 | }; 310 | 311 | export { create, instrumentSync }; 312 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/macros/create_instrumentation_visitor.rs: -------------------------------------------------------------------------------- 1 | /// Expand given struct to contain necessary common filed for the coverage visitor 2 | /// with common utility functions. 3 | /// 4 | /// This does not impl actual visitors (VisitMut) as each visitor may have different 5 | /// visitor logics. 6 | #[macro_export] 7 | macro_rules! create_instrumentation_visitor { 8 | ($name:ident { $($vis: vis $field:ident: $t:ty),* $(,)? }) => { 9 | #[allow(unused)] 10 | use swc_core::{common::{Span, Spanned}, ecma::ast::*}; 11 | 12 | // Declare a struct, expand fields commonly used for any instrumentation visitor. 13 | pub struct $name { 14 | // We may not need Arc in the plugin context - this is only to preserve isomorphic interface 15 | // between plugin & custom transform pass. 16 | source_map: std::sync::Arc, 17 | comments: C, 18 | cov: std::rc::Rc>, 19 | cov_fn_ident: Ident, 20 | cov_fn_temp_ident: Ident, 21 | instrument_options: crate::InstrumentOptions, 22 | // Current visitor state to hold stmts to be prepended by parent node. 23 | #[allow(dead_code)] pub before: Vec, 24 | nodes: Vec, 25 | should_ignore: Option, 26 | $($vis $field: $t,)* 27 | } 28 | 29 | impl $name { 30 | pub fn new( 31 | source_map: std::sync::Arc, 32 | comments: C, 33 | cov: std::rc::Rc>, 34 | instrument_options: crate::InstrumentOptions, 35 | nodes: Vec, 36 | should_ignore: Option, 37 | $($field: $t,)* 38 | ) -> $name { 39 | $name { 40 | source_map: source_map, 41 | comments: comments, 42 | cov: cov, 43 | cov_fn_ident: crate::COVERAGE_FN_IDENT.get().expect("Coverage fn Ident should be initialized already").clone(), 44 | cov_fn_temp_ident: crate::COVERAGE_FN_TRUE_TEMP_IDENT.get().expect("Coverage fn Ident should be initialized already").clone(), 45 | instrument_options: instrument_options, 46 | before: vec![], 47 | nodes: nodes, 48 | should_ignore, 49 | $($field,)* 50 | } 51 | } 52 | 53 | // Display current nodes. 54 | fn print_node(&self) -> String { 55 | if self.nodes.len() > 0 { 56 | format!( 57 | "{}", 58 | self.nodes 59 | .iter() 60 | .map(|n| n.to_string()) 61 | .collect::>() 62 | .join(":") 63 | ) 64 | } else { 65 | "".to_string() 66 | } 67 | } 68 | 69 | fn on_enter_with_span(&mut self, span: Option<&Span>) -> (Option, Option) { 70 | let old = self.should_ignore; 71 | let ret = match old { 72 | Some(crate::hint_comments::IgnoreScope::Next) => old, 73 | _ => { 74 | self.should_ignore = crate::hint_comments::should_ignore(&self.comments, span); 75 | self.should_ignore 76 | } 77 | }; 78 | 79 | (old, ret) 80 | } 81 | 82 | fn on_exit(&mut self, old: Option) { 83 | self.should_ignore = old; 84 | self.nodes.pop(); 85 | } 86 | } 87 | 88 | 89 | /// A trait expands to the ast types we want to use to determine if we need to ignore 90 | /// certain section of the code for the instrumentation. 91 | /// TODO: Can a macro like `on_visit_mut_expr` expands on_enter / exit automatically? 92 | /// `on_visit_mut_expr!(|expr| {self.xxx})` doesn't seem to work. 93 | trait CoverageInstrumentationMutVisitEnter { 94 | fn on_enter(&mut self, n: &mut N) -> (Option, Option); 95 | } 96 | 97 | // Macro generates trait impl for the type can access span directly. 98 | macro_rules! on_enter { 99 | ($N: tt) => { 100 | impl CoverageInstrumentationMutVisitEnter<$N> for $name { 101 | #[inline] 102 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::$N) -> (Option, Option) { 103 | self.nodes.push(crate::Node::$N); 104 | self.on_enter_with_span(Some(&n.span)) 105 | } 106 | } 107 | } 108 | } 109 | 110 | impl CoverageInstrumentationMutVisitEnter for $name { 111 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::Expr) -> (Option, Option) { 112 | self.nodes.push(crate::Node::Expr); 113 | let span = n.span(); 114 | self.on_enter_with_span(Some(&span)) 115 | } 116 | } 117 | 118 | impl CoverageInstrumentationMutVisitEnter for $name { 119 | fn on_enter(&mut self, n: &mut Stmt) -> (Option, Option) { 120 | self.nodes.push(crate::Node::Stmt); 121 | self.on_enter_with_span(Some(&n.span())) 122 | } 123 | } 124 | 125 | impl CoverageInstrumentationMutVisitEnter for $name { 126 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::ModuleDecl) -> (Option, Option) { 127 | self.nodes.push(crate::Node::ModuleDecl); 128 | let span = n.span(); 129 | 130 | self.on_enter_with_span(Some(&span)) 131 | } 132 | } 133 | 134 | impl CoverageInstrumentationMutVisitEnter for $name { 135 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::ClassDecl) -> (Option, Option) { 136 | self.nodes.push(crate::Node::ClassDecl); 137 | self.on_enter_with_span(Some(&n.class.span)) 138 | } 139 | } 140 | 141 | impl CoverageInstrumentationMutVisitEnter for $name { 142 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::FnExpr) -> (Option, Option) { 143 | self.nodes.push(crate::Node::FnExpr); 144 | self.on_enter_with_span(Some(&n.function.span)) 145 | } 146 | } 147 | 148 | impl CoverageInstrumentationMutVisitEnter for $name { 149 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::MethodProp) -> (Option, Option) { 150 | self.nodes.push(crate::Node::MethodProp); 151 | self.on_enter_with_span(Some(&n.function.span)) 152 | } 153 | } 154 | 155 | impl CoverageInstrumentationMutVisitEnter for $name { 156 | fn on_enter(&mut self, n: &mut swc_core::ecma::ast::FnDecl) -> (Option, Option) { 157 | self.nodes.push(crate::Node::FnDecl); 158 | self.on_enter_with_span(Some(&n.function.span)) 159 | } 160 | } 161 | 162 | on_enter!(BinExpr); 163 | on_enter!(VarDeclarator); 164 | on_enter!(VarDecl); 165 | on_enter!(CondExpr); 166 | on_enter!(ExprStmt); 167 | on_enter!(IfStmt); 168 | on_enter!(LabeledStmt); 169 | on_enter!(ContinueStmt); 170 | on_enter!(ClassProp); 171 | on_enter!(PrivateProp); 172 | on_enter!(ClassMethod); 173 | on_enter!(ArrowExpr); 174 | on_enter!(ForStmt); 175 | on_enter!(ForOfStmt); 176 | on_enter!(ForInStmt); 177 | on_enter!(WhileStmt); 178 | on_enter!(DoWhileStmt); 179 | on_enter!(SwitchStmt); 180 | on_enter!(SwitchCase); 181 | on_enter!(BreakStmt); 182 | on_enter!(ReturnStmt); 183 | on_enter!(BlockStmt); 184 | on_enter!(WithStmt); 185 | on_enter!(TryStmt); 186 | on_enter!(ThrowStmt); 187 | on_enter!(ExportDecl); 188 | on_enter!(ExportDefaultDecl); 189 | on_enter!(DebuggerStmt); 190 | on_enter!(AssignPat); 191 | on_enter!(GetterProp); 192 | on_enter!(SetterProp); 193 | on_enter!(TaggedTpl); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /packages/swc-coverage-instrument/src/visitors/coverage_visitor.rs: -------------------------------------------------------------------------------- 1 | use 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 | }; 9 | use tracing::instrument; 10 | 11 | use crate::{ 12 | create_instrumentation_visitor, instrumentation_counter_helper, 13 | instrumentation_stmt_counter_helper, instrumentation_visitor, InstrumentOptions, 14 | }; 15 | 16 | create_instrumentation_visitor!(CoverageVisitor { file_path: String }); 17 | 18 | /// Public interface to create a visitor performs transform to inject 19 | /// coverage instrumentation counter. 20 | pub fn create_coverage_instrumentation_visitor( 21 | source_map: std::sync::Arc, 22 | comments: C, 23 | instrument_options: InstrumentOptions, 24 | filename: String, 25 | ) -> CoverageVisitor { 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 | 43 | impl CoverageVisitor { 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 | 110 | impl VisitMut for CoverageVisitor { 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) { 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 | #[cfg(swc_ast_unknown)] 163 | _ => continue, 164 | }; 165 | 166 | // https://github.com/kwonoj/swc-plugin-coverage-instrument/issues/277 167 | // Add statement counter for export const declarations to match istanbul behavior 168 | // Istanbul treats export const and export var differently: 169 | // - export const: adds statement counter for the export declaration 170 | // - export var: only instruments the initializer, no separate export counter 171 | if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = &item { 172 | if let Decl::Var(var_decl) = &export_decl.decl { 173 | if var_decl.kind == VarDeclKind::Const { 174 | self.mark_prepend_stmt_counter(&export_decl.span); 175 | } 176 | } 177 | } 178 | 179 | item.visit_mut_children_with(self); 180 | 181 | new_items.extend(self.before.drain(..).map(|v| ModuleItem::Stmt(v))); 182 | new_items.push(item); 183 | self.on_exit(old); 184 | } 185 | *items = new_items; 186 | 187 | let (coverage_template, call_coverage_template_stmt) = self.get_coverage_templates(); 188 | 189 | // prepend template to the top of the code 190 | if items.len() >= 1 { 191 | items.insert(1, ModuleItem::Stmt(coverage_template)); 192 | items.insert(2, ModuleItem::Stmt(call_coverage_template_stmt)); 193 | } else { 194 | items.push(ModuleItem::Stmt(coverage_template)); 195 | items.push(ModuleItem::Stmt(call_coverage_template_stmt)); 196 | } 197 | 198 | if !root_exists { 199 | self.nodes.pop(); 200 | } 201 | } 202 | 203 | #[instrument(skip_all, fields(node = %self.print_node()))] 204 | fn visit_mut_script(&mut self, items: &mut Script) { 205 | if self.is_instrumented_already() { 206 | return; 207 | } 208 | 209 | let mut new_items = vec![]; 210 | for mut item in items.body.drain(..) { 211 | item.visit_mut_children_with(self); 212 | new_items.extend(self.before.drain(..)); 213 | new_items.push(item); 214 | } 215 | items.body = new_items; 216 | 217 | let (coverage_template, call_coverage_template_stmt) = self.get_coverage_templates(); 218 | 219 | // prepend template to the top of the code 220 | items.body.insert(0, coverage_template); 221 | items.body.insert(1, call_coverage_template_stmt); 222 | } 223 | 224 | // ExportDefaultDeclaration: entries(), // ignore processing only 225 | #[instrument(skip_all, fields(node = %self.print_node()))] 226 | fn visit_mut_export_default_decl(&mut self, export_default_decl: &mut ExportDefaultDecl) { 227 | let (old, ignore_current) = self.on_enter(export_default_decl); 228 | match ignore_current { 229 | Some(crate::hint_comments::IgnoreScope::Next) => {} 230 | _ => { 231 | // noop 232 | export_default_decl.visit_mut_children_with(self); 233 | } 234 | } 235 | self.on_exit(old); 236 | } 237 | 238 | // ExportNamedDeclaration: entries(), // ignore processing only 239 | #[instrument(skip_all, fields(node = %self.print_node()))] 240 | fn visit_mut_export_decl(&mut self, export_named_decl: &mut ExportDecl) { 241 | let (old, ignore_current) = self.on_enter(export_named_decl); 242 | match ignore_current { 243 | Some(crate::hint_comments::IgnoreScope::Next) => {} 244 | _ => { 245 | // noop 246 | export_named_decl.visit_mut_children_with(self); 247 | } 248 | } 249 | self.on_exit(old); 250 | } 251 | 252 | // DebuggerStatement: entries(coverStatement), 253 | #[instrument(skip_all, fields(node = %self.print_node()))] 254 | fn visit_mut_debugger_stmt(&mut self, debugger_stmt: &mut DebuggerStmt) { 255 | let (old, ignore_current) = self.on_enter(debugger_stmt); 256 | match ignore_current { 257 | Some(crate::hint_comments::IgnoreScope::Next) => {} 258 | _ => { 259 | debugger_stmt.visit_mut_children_with(self); 260 | } 261 | } 262 | self.on_exit(old); 263 | } 264 | } 265 | --------------------------------------------------------------------------------