diff --git a/src/parse.js b/src/parse.js index 2654df8..3125858 100644 --- a/src/parse.js +++ b/src/parse.js @@ -361,10 +361,16 @@ function toPlaceholderJS(source, parseResults) { parts.push(source.slice(cursor, start)); + // Blank out backticks and dollar signs instead of backslash-escaping + // them: escaping grows the content, and once the growth exceeds the + // padding slack the placeholder no longer lines up with the original + // region — matchPlaceholder then rejects it and the raw placeholder + // leaks into the AST (ember-tooling/ember-eslint-parser#230). The + // content is discarded when the Glimmer AST is spliced in, so only + // its length and line structure matter. const content = source .slice(pr.contentRange.startUtf16Codepoint, pr.contentRange.endUtf16Codepoint) - .replace(/`/g, "\\`") - .replace(/\$/g, "\\$"); + .replace(/[`$]/g, " "); if (pr.type === "class-member") { const spaces = tplLength - content.length - 10; // "static{`" + "`}" = 10 diff --git a/tests/parse.test.js b/tests/parse.test.js index c04e210..38d76a8 100644 --- a/tests/parse.test.js +++ b/tests/parse.test.js @@ -149,4 +149,65 @@ class B extends Component { const templates = findAllNodes(ast, "GlimmerTemplate"); expect(templates.length).toBe(2); }); + + // ember-tooling/ember-eslint-parser#230: backticks/dollars in template + // content used to be backslash-escaped in the placeholder JS, growing it + // past the original region once the escapes exceeded the padding slack + // (11 chars for class members, 19 for expressions). The end-range check + // in matchPlaceholder then failed and the raw placeholder StaticBlock/ + // TemplateLiteral leaked into the AST with every later offset shifted. + describe("placeholder-hostile template content (` and $)", () => { + function expectSingleTemplate(source, expected) { + const ast = parse(source); + const template = findNode(ast, "GlimmerTemplate"); + expect(template).toBeTruthy(); + expect(source.substring(template.start, template.end)).toBe(expected); + expect(findNode(ast, "StaticBlock")).toBeNull(); + expect(findNode(ast, "TemplateLiteral")).toBeNull(); + } + + it("class body template with many backticks in a comment", () => { + const tpl = ``; + const source = `export default class MyComponent extends Component { + ${tpl} +}`; + const ast = parse(source); + + const classDecl = findNode(ast, "ClassDeclaration"); + expect(classDecl.body.body.length).toBe(1); + expect(classDecl.body.body[0].type).toBe("GlimmerTemplate"); + expect(source.substring(classDecl.body.body[0].start, classDecl.body.body[0].end)).toBe(tpl); + }); + + it("class body template with many dollar signs", () => { + const tpl = ``; + const source = `export default class MyComponent extends Component { + ${tpl} +}`; + expectSingleTemplate(source, tpl); + }); + + it("expression template with many backticks", () => { + const tpl = ``; + const source = `const x = ${tpl};`; + expectSingleTemplate(source, tpl); + }); + + it("keeps offsets accurate for code after a backtick-heavy template", () => { + const tpl = ``; + const source = `export default class MyComponent extends Component { + ${tpl} +} +const after = 1;`; + const ast = parse(source); + + const decl = findNode(ast, "VariableDeclaration"); + expect(decl).toBeTruthy(); + expect(source.substring(decl.start, decl.end)).toBe("const after = 1;"); + }); + }); });