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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/compiler-coverage-pragmas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/babel-plugin": patch
"@solidjs/compiler": patch
---

Preserve `/* istanbul ignore … */` and `/* c8 ignore … */` block comments written as JSX children on the generated component `children` getter. Line comments are not carried.
21 changes: 12 additions & 9 deletions packages/babel-plugin/src/shared/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isDynamic,
registerImportMethod,
filterChildren,
getCoverageIgnoreComments,
trimWhitespace,
transformCondition,
convertJSXIdentifier
Expand All @@ -22,7 +23,7 @@ type ComponentTransformResult = TransformResult & {
exprs: Array<t.Expression | t.Statement>;
};

type ComponentChildrenResult = [t.Expression, boolean] | undefined;
type ComponentChildrenResult = [t.Expression, boolean, t.Comment[]?] | undefined;

function isSimpleOptionalMemberExpression(
expression: t.Expression | t.JSXEmptyExpression
Expand Down Expand Up @@ -341,14 +342,14 @@ export default function transformComponent(
: t.isFunction(childResult[0])
? childResult[0].body
: childResult[0];
runningObject.push(
t.objectMethod(
"get",
t.identifier("children"),
[],
t.isExpression(body) ? t.blockStatement([t.returnStatement(body)]) : body
)
const getter = t.objectMethod(
"get",
t.identifier("children"),
[],
t.isExpression(body) ? t.blockStatement([t.returnStatement(body)]) : body
);
if (childResult[2]?.length) getter.leadingComments = childResult[2];
runningObject.push(getter);
} else runningObject.push(t.objectProperty(t.identifier("children"), childResult[0]));
}
if (runningObject.length || !props.length) props.push(propsLiteral(runningObject));
Expand Down Expand Up @@ -409,9 +410,11 @@ function transformComponentChildren(
if (!filteredChildren.length) return;
let dynamic = false;
let pathNodes: t.Node[] = [];
let coverageIgnoreComments: t.Comment[] | undefined;

let transformedChildren: t.Expression | t.Expression[] = filteredChildren.reduce(
(memo: t.Expression[], path: BabelPath<JSXNode>) => {
coverageIgnoreComments ||= getCoverageIgnoreComments(path);
if (t.isJSXText(path.node)) {
const v = decode(trimWhitespace((path.node.extra?.raw as string | undefined) ?? ""));
if (v.length) {
Expand Down Expand Up @@ -468,5 +471,5 @@ function transformComponentChildren(
transformedChildren = t.arrowFunctionExpression([], t.arrayExpression(transformedChildren));
dynamic = true;
}
return [transformedChildren as t.Expression, dynamic];
return [transformedChildren as t.Expression, dynamic, coverageIgnoreComments];
}
35 changes: 30 additions & 5 deletions packages/babel-plugin/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,38 @@ export function getStaticExpression(
);
}

const coverageIgnoreCommentData = "solid.coverageIgnoreComments";

export function getCoverageIgnoreComments(path: NodePath): t.Comment[] | undefined {
return path.getData(coverageIgnoreCommentData) as t.Comment[] | undefined;
}

// remove unnecessary JSX Text nodes
export function filterChildren<TPath extends NodePath>(children: TPath[]): TPath[] {
return children.filter(
({ node: child }) =>
!(t.isJSXExpressionContainer(child) && t.isJSXEmptyExpression(child.expression)) &&
(!t.isJSXText(child) || !/^[\r\n]\s*$/.test((child.extra?.raw as string | undefined) ?? ""))
);
const filtered: TPath[] = [];
let pendingCoverageIgnoreComments: t.Comment[] = [];

for (const path of children) {
const child = path.node;
if (t.isJSXExpressionContainer(child) && t.isJSXEmptyExpression(child.expression)) {
pendingCoverageIgnoreComments.push(
...(child.expression.innerComments?.filter(
comment =>
comment.type === "CommentBlock" && /^\s*(istanbul|c8)\s+ignore\b/.test(comment.value)
) ?? [])
);
continue;
}
if (t.isJSXText(child) && /^[\r\n]\s*$/.test((child.extra?.raw as string | undefined) ?? "")) {
continue;
}
if (pendingCoverageIgnoreComments.length) {
path.setData(coverageIgnoreCommentData, pendingCoverageIgnoreComments);
pendingCoverageIgnoreComments = [];
}
filtered.push(path);
}
return filtered;
}

export function checkLength(children: NodePath[]): boolean {
Expand Down
111 changes: 111 additions & 0 deletions packages/babel-plugin/test/__shared_fixtures__/coveragePragmas/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
export const istanbulPragma = (
<Show when={condition()}>
{/* istanbul ignore next */}
<div>Hello</div>
</Show>
);

export const c8Pragma = (
<Show when={condition()}>
{/* c8 ignore next */}
<div>Hello</div>
</Show>
);

export const expressionChild = (
<Show when={condition()}>
{/* c8 ignore next */}
{state.value}
</Show>
);

export const multipleChildren = (
<Comp>
{/* istanbul ignore next */}
<div>A</div>
<span>B</span>
</Comp>
);

export const nestedComponent = (
<Outer>
<Inner>
{/* c8 ignore next */}
<div>A</div>
</Inner>
</Outer>
);

export const twoContainers = (
<Comp>
{/* istanbul ignore next */}
{/* c8 ignore next */}
<div>A</div>
</Comp>
);

export const noteBesidePragma = (
<Comp>
{/* note */
/* c8 ignore next */}
<div>A</div>
</Comp>
);

export const textAfterPragma = (
<Comp>
{/* c8 ignore next */}text<div>A</div>
</Comp>
);

export const spaceAfterPragma = (
<Comp>
{/* c8 ignore next */} <div>A</div>
</Comp>
);

export const lineComment = (
<Comp>
{
// c8 ignore next
}
<div>A</div>
</Comp>
);

export const lineCommentMentioningBlock = (
<Comp>
{
// see /* c8 ignore next */
}
<div>A</div>
</Comp>
);

export const blockCommentMentioningLine = (
<Comp>
{/* see // c8 ignore next */}
<div>A</div>
</Comp>
);

export const trailingPragma = (
<Comp>
<div>A</div>
{/* c8 ignore next */}
</Comp>
);

export const notePragmaOnly = (
<Comp>
{/* just a note */}
<div>A</div>
</Comp>
);

export const lookalike = (
<Comp>
{/* c8 ignored */}
<div>A</div>
</Comp>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Shared by the Babel plugin and native compiler tests: the comments each
// export of `code.js` must print ahead of its `children` getters, in order.
const expected = {
istanbulPragma: ["/* istanbul ignore next */"],
c8Pragma: ["/* c8 ignore next */"],
expressionChild: ["/* c8 ignore next */"],
multipleChildren: ["/* istanbul ignore next */"],
nestedComponent: ["/* c8 ignore next */"],
twoContainers: ["/* istanbul ignore next */", "/* c8 ignore next */"],
noteBesidePragma: ["/* c8 ignore next */"],
textAfterPragma: ["/* c8 ignore next */"],
spaceAfterPragma: ["/* c8 ignore next */"],
lineComment: [],
lineCommentMentioningBlock: [],
blockCommentMentioningLine: [],
trailingPragma: [],
notePragmaOnly: [],
lookalike: []
};

exports.expectedFor = function expectedFor(generate) {
// SSR prop hoisting moves a nested component's static props into a shared
// descriptor, whose `get()` does not carry the getter's comments.
return generate === "ssr" ? { ...expected, nestedComponent: [] } : expected;
};

const comment = String.raw`\/\*(?:(?!\*\/)[\s\S])*\*\/|\/\/[^\n]*`;
const getterComments = new RegExp(String.raw`((?:(?:${comment})\s*)+)get children\(\)`, "g");

exports.pragmasByExport = function pragmasByExport(code) {
const result = {};
const sections = code.split(/^export const (\w+) = /m);
for (let i = 1; i < sections.length; i += 2) {
result[sections[i]] = [...sections[i + 1].matchAll(getterComments)].flatMap(([, group]) =>
group.match(new RegExp(comment, "g")).map(text => text.trim())
);
}
return result;
};
25 changes: 25 additions & 0 deletions packages/babel-plugin/test/coverage-pragmas.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const babel = require("@babel/core");
const fs = require("fs");
const path = require("path");
const plugin = require("../index");
const { expectedFor, pragmasByExport } = require("./__shared_fixtures__/coveragePragmas/expected");

const fixture = fs.readFileSync(
path.join(__dirname, "__shared_fixtures__", "coveragePragmas", "code.js"),
"utf8"
);

describe("coverage pragmas", () => {
test.each(["dom", "ssr", "universal"])(
"carries authored pragmas onto %s component children getters",
generate => {
const { code } = babel.transformSync(fixture, {
plugins: [[plugin, { generate, moduleName: "r-dom" }]],
configFile: false,
babelrc: false,
filename: "coveragePragmas.jsx"
});
expect(pragmasByExport(code)).toEqual(expectedFor(generate));
}
);
});
20 changes: 20 additions & 0 deletions packages/compiler/__tests__/transform.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ const fs = require("fs");
const path = require("path");

const babelDomFixtures = path.resolve(__dirname, "../../babel-plugin/test/__dom_fixtures__");
const coveragePragmasDir = path.resolve(
__dirname,
"../../babel-plugin/test/__shared_fixtures__/coveragePragmas"
);
const coveragePragmas = require(path.join(coveragePragmasDir, "expected"));

function readFixture(name) {
return fs.readFileSync(path.join(babelDomFixtures, name, "code.js"), "utf8");
Expand Down Expand Up @@ -89,6 +94,21 @@ describe("@solidjs/compiler transform", () => {
expect(result.code).toContain('_$createComponent(Child, { name: "Jake" });');
});

it.each(["dom", "ssr", "universal"])(
"carries authored coverage pragmas onto %s component children getters",
generate => {
const result = transform(fs.readFileSync(path.join(coveragePragmasDir, "code.js"), "utf8"), {
filename: "coveragePragmas.jsx",
moduleName: "r-dom",
generate
});

expect(coveragePragmas.pragmasByExport(result.code)).toEqual(
coveragePragmas.expectedFor(generate)
);
}
);

it("memoizes dynamic conditional component props by default", () => {
const result = transform("const view = <Comp render={state.dynamic ? good() : bad} />;", {
filename: "input.jsx",
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ fn compile_inner(source: &str, options: &CompileOptions) -> Result<CompileOutput
crate::tsrx::clear_generated_spans(&mut program, options.source_map);
}

crate::shared::component_children::anchor_coverage_pragmas(&mut program, source);

match options.generate {
Generate::Dom => {
let mut transform = AstDomTransform::new(
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/dom/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ impl<'a> crate::shared::component_children::ComponentChildLower<'a> for AstDomTr
}

impl<'a> ModeLower<'a> for AstDomTransform<'a, '_> {
fn source(&self) -> &str {
self.source
}

fn wrap_conditionals_enabled(&self) -> bool {
self.wrap_conditionals
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/shared/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ pub(crate) fn lower_component_with_setup<'a, C: ComponentLower<'a>>(
if children.needs_getter {
running_props.push(crate::shared::ast::object_getter_property_with_setup(
allocator,
element.span,
children.coverage_pragma_span.unwrap_or(element.span),
"children",
children.setup,
children.value,
Expand Down
Loading
Loading