Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,6 @@ macro_rules! create_instrumentation_visitor {
on_enter!(AssignPat);
on_enter!(GetterProp);
on_enter!(SetterProp);
on_enter!(TaggedTpl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@ macro_rules! instrumentation_counter_helper {
}

fn cover_statement(&mut self, expr: &mut Expr) {
// Special handling for tagged template expressions (like emotion styled components)
// to preserve the template relationship and avoid wrapping in sequence expressions
if let Expr::TaggedTpl(_) = expr {
// For tagged templates, prepend statement counter instead of wrapping
// This preserves the template relationship
let span = expr.span();
self.mark_prepend_stmt_counter(&span);
// Still visit children to instrument any inner expressions
expr.visit_mut_children_with(self);
return;
}

let span = expr.span();
// This is ugly, poor man's substitute to istanbul's `insertCounter` to determine
// when to replace givn expr to wrapped Paren or prepend stmt counter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,31 @@ macro_rules! instrumentation_visitor {
self.on_exit(old);
}

// TaggedTemplateExpression: special handling to preserve template relationship
#[tracing::instrument(skip_all, fields(node = %self.print_node()))]
fn visit_mut_tagged_tpl(&mut self, tagged_tpl: &mut TaggedTpl) {
let (old, ignore_current) = self.on_enter(tagged_tpl);

match ignore_current {
Some(crate::hint_comments::IgnoreScope::Next) => {}
_ => {
// For tagged template expressions like styled(...)`template`,
// we need to instrument the entire expression rather than wrapping
// the tag part in a sequence expression, which would break the
// template relationship and cause emotion to lose label information.

// Instead of calling cover_statement on the tag (which would wrap it),
// we mark to prepend a statement counter before the entire tagged template
self.mark_prepend_stmt_counter(&tagged_tpl.span);

// Visit children normally to instrument any inner expressions
tagged_tpl.visit_mut_children_with(self);
}
}

self.on_exit(old);
}

// ReturnStatement: entries(coverStatement),
#[tracing::instrument(skip_all, fields(node = %self.print_node()))]
fn visit_mut_return_stmt(&mut self, return_stmt: &mut ReturnStmt) {
Expand Down
1 change: 1 addition & 0 deletions packages/swc-coverage-instrument/src/utils/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub enum Node {
ExportDefaultDecl,
BlockStmt,
AssignPat,
TaggedTpl,
}

impl Display for Node {
Expand Down
79 changes: 79 additions & 0 deletions spec/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,83 @@ ${code}
`,
);
});

it("should preserve emotion styled component labels with template literals", () => {
// This reproduces the issue from GitHub #247
// Input: code AFTER emotion processing (as shown in the GitHub issue)
const code = `export var TabsList = /*#__PURE__*/ styled(TabsListCore, {
target: "ebt2y835",
label: "TabsList"
})("margin:0 auto;width:fit-content;");`;

const output = instrumentSync(code, "test-emotion.js");

// Expected output: should preserve label like v0.0.20 did (from GitHub issue)
// The key difference: label should remain "TabsList", not become ""
const expectedOutput = `"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "TabsList", {
enumerable: true,
get: function() {
return TabsList;
}
});
var TabsList = (cov_14220330533750098279().s[0]++, /*#__PURE__*/ styled(TabsListCore, {
target: "ebt2y835",
label: "TabsList"
})("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":{}}*/
function cov_14220330533750098279() {
var path = "test-emotion.js";
var hash = "15339889637910252771";
var global = new ((function(){}).constructor)("return this")();
var gcv = "__coverage__";
var coverageData = {
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: {},
_coverageSchema: "11020577277169172593",
hash: "15339889637910252771"
};
var coverage = global[gcv] || (global[gcv] = {});
if (!coverage[path] || coverage[path].$hash !== hash) {
coverage[path] = coverageData;
}
var actualCoverage = coverage[path];
{
cov_14220330533750098279 = function() {
return actualCoverage;
};
}
return actualCoverage;
}
cov_14220330533750098279();`;

// Compare whole output.code to the raw output as requested
// This ensures emotion labels are preserved without explicitly asserting them
assert.equal(
output.code.trim(),
expectedOutput.trim(),
"Instrumented code should preserve emotion styled component label property",
);
});
});