Skip to content

Do not escape actual template literals on emit #32844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2019
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
86 changes: 78 additions & 8 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,30 +1329,100 @@ namespace ts {
: node;
}

export function createTemplateHead(text: string) {
const node = <TemplateHead>createSynthesizedNode(SyntaxKind.TemplateHead);
let rawTextScanner: Scanner | undefined;
const invalidValueSentinel: object = {};

function getCookedText(kind: TemplateLiteralToken["kind"], rawText: string) {
if (!rawTextScanner) {
rawTextScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard);
}
switch (kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
rawTextScanner.setText("`" + rawText + "`");
break;
case SyntaxKind.TemplateHead:
// tslint:disable-next-line no-invalid-template-strings
rawTextScanner.setText("`" + rawText + "${");
break;
case SyntaxKind.TemplateMiddle:
// tslint:disable-next-line no-invalid-template-strings
rawTextScanner.setText("}" + rawText + "${");
break;
case SyntaxKind.TemplateTail:
rawTextScanner.setText("}" + rawText + "`");
break;
}

let token = rawTextScanner.scan();
if (token === SyntaxKind.CloseBracketToken) {
token = rawTextScanner.reScanTemplateToken();
}

if (rawTextScanner.isUnterminated()) {
rawTextScanner.setText(undefined);
return invalidValueSentinel;
}

let tokenValue: string | undefined;
switch (token) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
tokenValue = rawTextScanner.getTokenValue();
break;
}

if (rawTextScanner.scan() !== SyntaxKind.EndOfFileToken) {
rawTextScanner.setText(undefined);
return invalidValueSentinel;
}

rawTextScanner.setText(undefined);
return tokenValue;
}

function createTemplateLiteralLikeNode(kind: TemplateLiteralToken["kind"], text: string, rawText: string | undefined) {
const node = <TemplateLiteralLikeNode>createSynthesizedNode(kind);
node.text = text;
if (rawText === undefined || text === rawText) {
node.rawText = rawText;
}
else {
const cooked = getCookedText(kind, rawText);
if (typeof cooked === "object") {
return Debug.fail("Invalid raw text");
}

Debug.assert(text === cooked, "Expected argument 'text' to be the normalized (i.e. 'cooked') version of argument 'rawText'.");
node.rawText = rawText;
}
return node;
}

export function createTemplateMiddle(text: string) {
const node = <TemplateMiddle>createSynthesizedNode(SyntaxKind.TemplateMiddle);
export function createTemplateHead(text: string, rawText?: string) {
const node = <TemplateHead>createTemplateLiteralLikeNode(SyntaxKind.TemplateHead, text, rawText);
node.text = text;
return node;
}

export function createTemplateTail(text: string) {
const node = <TemplateTail>createSynthesizedNode(SyntaxKind.TemplateTail);
export function createTemplateMiddle(text: string, rawText?: string) {
const node = <TemplateMiddle>createTemplateLiteralLikeNode(SyntaxKind.TemplateMiddle, text, rawText);
node.text = text;
return node;
}

export function createNoSubstitutionTemplateLiteral(text: string) {
const node = <NoSubstitutionTemplateLiteral>createSynthesizedNode(SyntaxKind.NoSubstitutionTemplateLiteral);
export function createTemplateTail(text: string, rawText?: string) {
const node = <TemplateTail>createTemplateLiteralLikeNode(SyntaxKind.TemplateTail, text, rawText);
node.text = text;
return node;
}

export function createNoSubstitutionTemplateLiteral(text: string, rawText?: string) {
const node = <NoSubstitutionTemplateLiteral>createTemplateLiteralLikeNode(SyntaxKind.NoSubstitutionTemplateLiteral, text, rawText);
return node;
}

export function createYield(expression?: Expression): YieldExpression;
export function createYield(asteriskToken: AsteriskToken | undefined, expression: Expression): YieldExpression;
export function createYield(asteriskTokenOrExpression?: AsteriskToken | undefined | Expression, expression?: Expression) {
Expand Down
14 changes: 12 additions & 2 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2291,9 +2291,19 @@ namespace ts {
return <TemplateMiddle | TemplateTail>fragment;
}

function parseLiteralLikeNode(kind: SyntaxKind): LiteralExpression | LiteralLikeNode {
const node = <LiteralExpression>createNode(kind);
function parseLiteralLikeNode(kind: SyntaxKind): LiteralLikeNode {
const node = <LiteralLikeNode>createNode(kind);
node.text = scanner.getTokenValue();
switch (kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
const isLast = kind === SyntaxKind.NoSubstitutionTemplateLiteral || kind === SyntaxKind.TemplateTail;
const tokenText = scanner.getTokenText();
(<TemplateLiteralLikeNode>node).rawText = tokenText.substring(1, tokenText.length - (scanner.isUnterminated() ? 0 : isLast ? 1 : 2));
break;
}

if (scanner.hasExtendedUnicodeEscape()) {
node.hasExtendedUnicodeEscape = true;
Expand Down
21 changes: 12 additions & 9 deletions src/compiler/transformers/es2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3993,18 +3993,21 @@ namespace ts {
*
* @param node The ES6 template literal.
*/
function getRawLiteral(node: LiteralLikeNode) {
function getRawLiteral(node: TemplateLiteralLikeNode) {
// Find original source text, since we need to emit the raw strings of the tagged template.
// The raw strings contain the (escaped) strings of what the user wrote.
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
let text = getSourceTextOfNodeFromSourceFile(currentSourceFile, node);

// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
// thus we need to remove those characters.
// First template piece starts with "`", others with "}"
// Last template piece ends with "`", others with "${"
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
text = text.substring(1, text.length - (isLast ? 1 : 2));
let text = node.rawText;
if (text === undefined) {
text = getSourceTextOfNodeFromSourceFile(currentSourceFile, node);

// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
// thus we need to remove those characters.
// First template piece starts with "`", others with "}"
// Last template piece ends with "`", others with "${"
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
text = text.substring(1, text.length - (isLast ? 1 : 2));
}

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
Expand Down
12 changes: 8 additions & 4 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,10 @@ namespace ts {
hasExtendedUnicodeEscape?: boolean;
}

export interface TemplateLiteralLikeNode extends LiteralLikeNode {
rawText?: string;
}

// The text property of a LiteralExpression stores the interpreted value of the literal in text form. For a StringLiteral,
// or any literal of a template, this means quotes have been removed and escapes have been converted to actual characters.
// For a NumericLiteral, the stored value is the toString() representation of the number. For example 1, 1.00, and 1e0 are all stored as just "1".
Expand All @@ -1657,7 +1661,7 @@ namespace ts {
kind: SyntaxKind.RegularExpressionLiteral;
}

export interface NoSubstitutionTemplateLiteral extends LiteralExpression {
export interface NoSubstitutionTemplateLiteral extends LiteralExpression, TemplateLiteralLikeNode {
kind: SyntaxKind.NoSubstitutionTemplateLiteral;
}

Expand Down Expand Up @@ -1694,17 +1698,17 @@ namespace ts {
kind: SyntaxKind.BigIntLiteral;
}

export interface TemplateHead extends LiteralLikeNode {
export interface TemplateHead extends TemplateLiteralLikeNode {
kind: SyntaxKind.TemplateHead;
parent: TemplateExpression;
}

export interface TemplateMiddle extends LiteralLikeNode {
export interface TemplateMiddle extends TemplateLiteralLikeNode {
kind: SyntaxKind.TemplateMiddle;
parent: TemplateSpan;
}

export interface TemplateTail extends LiteralLikeNode {
export interface TemplateTail extends TemplateLiteralLikeNode {
kind: SyntaxKind.TemplateTail;
parent: TemplateSpan;
}
Expand Down
29 changes: 17 additions & 12 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,6 @@ namespace ts {
return emitNode && emitNode.flags || 0;
}

const escapeNoSubstitutionTemplateLiteralText = compose(escapeString, escapeTemplateSubstitution);
const escapeNonAsciiNoSubstitutionTemplateLiteralText = compose(escapeNonAsciiString, escapeTemplateSubstitution);
export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined) {
// If we don't need to downlevel and we can reach the original source text using
// the node's parent reference, then simply get the text as it was originally written.
Expand All @@ -580,9 +578,7 @@ namespace ts {

// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
// had to include a backslash: `not \${a} substitution`.
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ?
node.kind === SyntaxKind.NoSubstitutionTemplateLiteral ? escapeNoSubstitutionTemplateLiteralText : escapeString :
node.kind === SyntaxKind.NoSubstitutionTemplateLiteral ? escapeNonAsciiNoSubstitutionTemplateLiteralText : escapeNonAsciiString;
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString : escapeNonAsciiString;

// If we can't reach the original source text, use the canonical form if it's a number,
// or a (possibly escaped) quoted form of the original text if it's string-like.
Expand All @@ -595,15 +591,23 @@ namespace ts {
return '"' + escapeText(node.text, CharacterCodes.doubleQuote) + '"';
}
case SyntaxKind.NoSubstitutionTemplateLiteral:
return "`" + escapeText(node.text, CharacterCodes.backtick) + "`";
case SyntaxKind.TemplateHead:
// tslint:disable-next-line no-invalid-template-strings
return "`" + escapeText(node.text, CharacterCodes.backtick) + "${";
case SyntaxKind.TemplateMiddle:
// tslint:disable-next-line no-invalid-template-strings
return "}" + escapeText(node.text, CharacterCodes.backtick) + "${";
case SyntaxKind.TemplateTail:
return "}" + escapeText(node.text, CharacterCodes.backtick) + "`";
const rawText = (<TemplateLiteralLikeNode>node).rawText || escapeTemplateSubstitution(escapeText(node.text, CharacterCodes.backtick));
switch (node.kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
return "`" + rawText + "`";
case SyntaxKind.TemplateHead:
// tslint:disable-next-line no-invalid-template-strings
return "`" + rawText + "${";
case SyntaxKind.TemplateMiddle:
// tslint:disable-next-line no-invalid-template-strings
return "}" + rawText + "${";
case SyntaxKind.TemplateTail:
return "}" + rawText + "`";
}
break;
case SyntaxKind.NumericLiteral:
case SyntaxKind.BigIntLiteral:
case SyntaxKind.RegularExpressionLiteral:
Expand Down Expand Up @@ -3137,7 +3141,8 @@ namespace ts {
// There is no reason for this other than that JSON.stringify does not handle it either.
const doubleQuoteEscapedCharsRegExp = /[\\\"\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
const singleQuoteEscapedCharsRegExp = /[\\\'\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
const backtickQuoteEscapedCharsRegExp = /[\\\`\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
// Template strings should be preserved as much as possible
const backtickQuoteEscapedCharsRegExp = /[\\\`]/g;
const escapedCharsMap = createMapFromTemplate({
"\t": "\\t",
"\v": "\\v",
Expand Down
24 changes: 13 additions & 11 deletions src/harness/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace evaluator {
declare var Symbol: SymbolConstructor;

const sourceFile = vpath.combine(vfs.srcFolder, "source.ts");
const sourceFileJs = vpath.combine(vfs.srcFolder, "source.js");

function compile(sourceText: string, options?: ts.CompilerOptions) {
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
Expand Down Expand Up @@ -32,9 +33,8 @@ namespace evaluator {
// Add "asyncIterator" if missing
if (!ts.hasProperty(FakeSymbol, "asyncIterator")) Object.defineProperty(FakeSymbol, "asyncIterator", { value: Symbol.for("Symbol.asyncIterator"), configurable: true });

function evaluate(result: compiler.CompilationResult, globals?: Record<string, any>) {
globals = { Symbol: FakeSymbol, ...globals };

export function evaluateTypeScript(sourceText: string, options?: ts.CompilerOptions, globals?: Record<string, any>) {
const result = compile(sourceText, options);
if (ts.some(result.diagnostics)) {
assert.ok(/*value*/ false, "Syntax error in evaluation source text:\n" + ts.formatDiagnostics(result.diagnostics, {
getCanonicalFileName: file => file,
Expand All @@ -46,6 +46,12 @@ namespace evaluator {
const output = result.getOutput(sourceFile, "js")!;
assert.isDefined(output);

return evaluateJavaScript(output.text, globals, output.file);
}

export function evaluateJavaScript(sourceText: string, globals?: Record<string, any>, sourceFile = sourceFileJs) {
globals = { Symbol: FakeSymbol, ...globals };

const globalNames: string[] = [];
const globalArgs: any[] = [];
for (const name in globals) {
Expand All @@ -55,15 +61,11 @@ namespace evaluator {
}
}

const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${output.text} })`;
// tslint:disable-next-line:no-eval
const evaluateThunk = eval(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void;
const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${sourceText} })`;
// tslint:disable-next-line:no-eval no-unused-expression
const evaluateThunk = (void 0, eval)(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void;
const module: { exports: any; } = { exports: {} };
evaluateThunk.call(globals, module, module.exports, noRequire, vpath.dirname(output.file), output.file, FakeSymbol, ...globalArgs);
evaluateThunk.call(globals, module, module.exports, noRequire, vpath.dirname(sourceFile), sourceFile, FakeSymbol, ...globalArgs);
return module.exports;
}

export function evaluateTypeScript(sourceText: string, options?: ts.CompilerOptions, globals?: Record<string, any>) {
return evaluate(compile(sourceText, options), globals);
}
}
43 changes: 43 additions & 0 deletions src/testRunner/unittests/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ namespace ts {
});
}

function testBaselineAndEvaluate(testName: string, test: () => string, onEvaluate: (exports: any) => void) {
describe(testName, () => {
let sourceText!: string;
before(() => {
sourceText = test();
});
after(() => {
sourceText = undefined!;
});
it("compare baselines", () => {
Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, sourceText);
});
it("evaluate", () => {
onEvaluate(evaluator.evaluateJavaScript(sourceText));
});
});
}

testBaseline("substitution", () => {
return transformSourceFile(`var a = undefined;`, [replaceUndefinedWithVoid0]);
});
Expand Down Expand Up @@ -440,6 +458,31 @@ namespace Foo {

});

testBaselineAndEvaluate("templateSpans", () => {
return transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", {
compilerOptions: {
target: ScriptTarget.ESNext,
newLine: NewLineKind.CarriageReturnLineFeed,
},
transformers: {
before: [transformSourceFile]
}
}).outputText;

function transformSourceFile(context: TransformationContext): Transformer<SourceFile> {
function visitor(node: Node): VisitResult<Node> {
if (isNoSubstitutionTemplateLiteral(node)) {
return createNoSubstitutionTemplateLiteral(node.text, node.rawText);
}
else {
return visitEachChild(node, visitor, context);
}
}
return sourceFile => visitNode(sourceFile, visitor, isSourceFile);
}
}, exports => {
assert.equal(exports.stringLength, 5);
});
});
}

Loading