├── 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 |
--------------------------------------------------------------------------------