diff --git a/src/index.d.ts b/src/index.d.ts
index 38a469b..f5e62f8 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -32,7 +32,8 @@ export interface ParseOptions {
templateOnly?: boolean;
/**
* Custom JS/TS parser. Called with the placeholder JS string
- * (templates replaced with backtick expressions of equal length).
+ * (templates replaced with `void `...`` expressions, or `static{`...`}`
+ * blocks for class members, of equal length).
* Must return at least `{ ast }`.
*/
parser?: (placeholderJS: string) => { ast: ASTNode; [key: string]: unknown };
diff --git a/src/parse.js b/src/parse.js
index a41491e..4ce476c 100644
--- a/src/parse.js
+++ b/src/parse.js
@@ -55,7 +55,7 @@ const preprocessor = new Preprocessor();
const PLACEHOLDER_TYPES = new Set([
"ExpressionStatement",
"StaticBlock",
- "TemplateLiteral",
+ "UnaryExpression",
"ExportDefaultDeclaration",
]);
@@ -250,7 +250,7 @@ export function toTree(source, options = {}) {
setParent(ast, parent);
// Recurse into the Glimmer subtree so visitors fire on its nodes too.
// The Glimmer root's parentPath reflects its true JS parent — the
- // placeholder (TemplateLiteral / StaticBlock) is an internal artifact.
+ // placeholder (UnaryExpression / StaticBlock) is an internal artifact.
if (hasVisitors) walkWithKeys(ast, parentPath);
return;
}
@@ -348,9 +348,16 @@ export const parse = toTree;
* Replaces ... regions with placeholder expressions
* of the same character length that are valid JS/TS.
*
- * Expression templates become: `content ` (backtick, space-padded)
+ * Expression templates become: void `content ` (void + backtick, space-padded)
* Class member templates become: static{`content `} (static block, space-padded)
*
+ * The `void` prefix matters at a statement start: a bare backtick literal
+ * after an unterminated expression statement continues it as a tagged
+ * template (`const y = x\n`...`` reads as `x`...``), whereas `void` cannot
+ * continue an expression, so automatic semicolon insertion applies exactly
+ * as it does for the real ``. `void` is a keyword, so the
+ * placeholder also adds no identifier reference for scope analysis to see.
+ *
* This format is compatible with all JS/TS parsers including
* oxc-parser, @typescript-eslint/parser, and @babel/eslint-parser.
*/
@@ -381,8 +388,8 @@ function toPlaceholderJS(source, parseResults) {
const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10
parts.push(`static{\`${content}${" ".repeat(Math.max(0, spaces))}\`}`);
} else {
- const spaces = tplLength - content.length - 2; // "`" + "`" = 2
- parts.push(`\`${content}${" ".repeat(Math.max(0, spaces))}\``);
+ const spaces = tplLength - content.length - 7; // "void `" + "`" = 7
+ parts.push(`void \`${content}${" ".repeat(Math.max(0, spaces))}\``);
}
cursor = end;
diff --git a/tests/custom-parser-ts.test.js b/tests/custom-parser-ts.test.js
index 35ed5aa..d3dfcab 100644
--- a/tests/custom-parser-ts.test.js
+++ b/tests/custom-parser-ts.test.js
@@ -26,7 +26,7 @@ describe("toTree — custom parser (typescript-eslint) — node identity", () =>
it("preserves outer-AST node identity after a standalone template splice", () => {
const source = `const Bar = () => ;`;
// Snapshot ancestor references before toTree gets a chance to splice,
- // and verify the placeholder is still a TemplateLiteral at that point.
+ // and verify the placeholder is still a `void` expression at that point.
let origProgram, origDecl, origDtor, origArrow, origPlaceholder;
const result = toTree(source, {
parser: (js) => {
@@ -35,8 +35,8 @@ describe("toTree — custom parser (typescript-eslint) — node identity", () =>
origDecl = origProgram.body[0]; // VariableDeclaration
origDtor = origDecl.declarations[0]; // VariableDeclarator
origArrow = origDtor.init; // ArrowFunctionExpression
- origPlaceholder = origArrow.body; // TemplateLiteral (placeholder)
- expect(origPlaceholder.type).toBe("TemplateLiteral");
+ origPlaceholder = origArrow.body; // UnaryExpression (placeholder)
+ expect(origPlaceholder.type).toBe("UnaryExpression");
return parsed;
},
});
@@ -121,9 +121,9 @@ describe("toTree — custom parser (typescript-eslint) — templateInfos.placeho
const result = toTree(source, {
parser: (js) => {
const parsed = tsParseForESLint(js);
- // Capture the ArrowFunctionExpression body's placeholder (TemplateLiteral)
+ // Capture the ArrowFunctionExpression body's placeholder (UnaryExpression)
capturedPlaceholder = parsed.ast.body[0].declarations[0].init.body;
- expect(capturedPlaceholder.type).toBe("TemplateLiteral");
+ expect(capturedPlaceholder.type).toBe("UnaryExpression");
return parsed;
},
});
@@ -148,8 +148,8 @@ describe("toTree — custom parser (typescript-eslint) — templateInfos.placeho
// After: the GlimmerTemplate resolves to the placeholder's TS node.
const glimmerTS = esMap.get(result.templateInfos[0].ast);
expect(glimmerTS).toBeDefined();
- // The placeholder was a TemplateLiteral (backtick) whose TS node is a
- // string-typed expression — good enough for typed rules.
+ // The placeholder was a `void `...`` expression; its TS node stands in
+ // for the template for typed rules.
expect(glimmerTS).toBe(esMap.get(result.templateInfos[0].placeholder));
});
});
diff --git a/tests/parse.test.js b/tests/parse.test.js
index bd31f4b..cc37a64 100644
--- a/tests/parse.test.js
+++ b/tests/parse.test.js
@@ -161,6 +161,29 @@ describe("parse", () => {
]);
});
+ it("keeps a statement-start template separate from an unterminated expression before it", () => {
+ // Without a semicolon after `x`, a bare backtick placeholder would read
+ // as the tagged template `x`...``, swallowing the template.
+ const source = ["const x = 1", "const y = x", "{{x}}", ""].join("\n");
+ const ast = parse(source);
+
+ expect(ast.program.body.map((node) => node.type)).toEqual([
+ "VariableDeclaration",
+ "VariableDeclaration",
+ "GlimmerTemplate",
+ ]);
+ });
+
+ it("keeps a statement-start template separate from a call before it", () => {
+ const source = ["setup()", "hi", ""].join("\n");
+ const ast = parse(source);
+
+ expect(ast.program.body.map((node) => node.type)).toEqual([
+ "ExpressionStatement",
+ "GlimmerTemplate",
+ ]);
+ });
+
it("handles multiple classes with templates", () => {
const source = `class A extends Component {
A