Skip to content

Commit

Permalink
extract tagged template and add more test
Browse files Browse the repository at this point in the history
  • Loading branch information
Kingwl committed May 26, 2018
1 parent c642abe commit e92b6bc
Show file tree
Hide file tree
Showing 20 changed files with 834 additions and 96 deletions.
12 changes: 11 additions & 1 deletion src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3523,8 +3523,18 @@ namespace ts {
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
case SyntaxKind.TemplateExpression:
if ((<NoSubstitutionTemplateLiteral | TemplateHead | TemplateMiddle | TemplateTail>node).templateFlags) {
transformFlags |= TransformFlags.AssertESNext;
break;
}
// falls through
case SyntaxKind.TaggedTemplateExpression:
if (hasInvalidEscape((<TaggedTemplateExpression>node).template)) {
transformFlags |= TransformFlags.AssertESNext;
break;
}
// falls through
case SyntaxKind.TemplateExpression:
case SyntaxKind.ShorthandPropertyAssignment:
case SyntaxKind.StaticKeyword:
case SyntaxKind.MetaProperty:
Expand Down
101 changes: 8 additions & 93 deletions src/compiler/transformers/es2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3643,75 +3643,14 @@ namespace ts {
* @param node A TaggedTemplateExpression node.
*/
function visitTaggedTemplateExpression(node: TaggedTemplateExpression) {
// Visit the tag expression
const tag = visitNode(node.tag, visitor, isExpression);

// Build up the template arguments and the raw and cooked strings for the template.
// We start out with 'undefined' for the first argument and revisit later
// to avoid walking over the template string twice and shifting all our arguments over after the fact.
const templateArguments: Expression[] = [undefined!];
const cookedStrings: Expression[] = [];
const rawStrings: Expression[] = [];
const template = node.template;
if (isNoSubstitutionTemplateLiteral(template)) {
cookedStrings.push(template.templateFlags ? createIdentifier("undefined") : createLiteral(template.text));
rawStrings.push(getRawLiteral(template));
}
else {
cookedStrings.push(template.head.templateFlags ? createIdentifier("undefined") : createLiteral(template.head.text));
rawStrings.push(getRawLiteral(template.head));
for (const templateSpan of template.templateSpans) {
cookedStrings.push(templateSpan.literal.templateFlags ? createIdentifier("undefined") : createLiteral(templateSpan.literal.text));
rawStrings.push(getRawLiteral(templateSpan.literal));
templateArguments.push(visitNode(templateSpan.expression, visitor, isExpression));
}
}

const helperCall = createTemplateObjectHelper(context, createArrayLiteral(cookedStrings), createArrayLiteral(rawStrings));

// Create a variable to cache the template object if we're in a module.
// Do not do this in the global scope, as any variable we currently generate could conflict with
// variables from outside of the current compilation. In the future, we can revisit this behavior.
if (isExternalModule(currentSourceFile)) {
const tempVar = createUniqueName("templateObject");
recordTaggedTemplateString(tempVar);
templateArguments[0] = createLogicalOr(
tempVar,
createAssignment(
tempVar,
helperCall)
);
}
else {
templateArguments[0] = helperCall;
}

return createCall(tag, /*typeArguments*/ undefined, templateArguments);
}

/**
* Creates an ES5 compatible literal from an ES6 template literal.
*
* @param node The ES6 template literal.
*/
function getRawLiteral(node: LiteralLikeNode) {
// 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));

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
text = text.replace(/\r\n?/g, "\n");
return setTextRange(createLiteral(text), node);
return processTaggedTemplateExpression(
context,
node,
visitor,
currentSourceFile,
recordTaggedTemplateString,
ProcessLevel.All
);
}

/**
Expand Down Expand Up @@ -4054,18 +3993,6 @@ namespace ts {
);
}

function createTemplateObjectHelper(context: TransformationContext, cooked: ArrayLiteralExpression, raw: ArrayLiteralExpression) {
context.requestEmitHelper(templateObjectHelper);
return createCall(
getHelperName("__makeTemplateObject"),
/*typeArguments*/ undefined,
[
cooked,
raw
]
);
}

const extendsHelper: EmitHelper = {
name: "typescript:extends",
scoped: false,
Expand All @@ -4082,16 +4009,4 @@ namespace ts {
};
})();`
};

const templateObjectHelper: EmitHelper = {
name: "typescript:makeTemplateObject",
scoped: false,
priority: 0,
text: `
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
};`
};

}
37 changes: 35 additions & 2 deletions src/compiler/transformers/esnext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,27 @@ namespace ts {
let enabledSubstitutions: ESNextSubstitutionFlags;
let enclosingFunctionFlags: FunctionFlags;
let enclosingSuperContainerFlags: NodeCheckFlags = 0;
let currentSourceFile: SourceFile;
let taggedTemplateStringDeclarations: VariableDeclaration[];

return chainBundle(transformSourceFile);

function recordTaggedTemplateString(temp: Identifier) {
taggedTemplateStringDeclarations = append(
taggedTemplateStringDeclarations,
createVariableDeclaration(temp));
}

function transformSourceFile(node: SourceFile) {
if (node.isDeclarationFile) {
return node;
}

const visited = visitEachChild(node, visitor, context);
currentSourceFile = node;
const visited = visitSourceFile(node);
addEmitHelpers(visited, context.readEmitHelpers());

currentSourceFile = undefined!;
taggedTemplateStringDeclarations = undefined!;
return visited;
}

Expand Down Expand Up @@ -99,6 +110,8 @@ namespace ts {
return visitParenthesizedExpression(node as ParenthesizedExpression, noDestructuringValue);
case SyntaxKind.CatchClause:
return visitCatchClause(node as CatchClause);
case SyntaxKind.TaggedTemplateExpression:
return visitTaggedTemplateExpression(node as TaggedTemplateExpression);
default:
return visitEachChild(node, visitor, context);
}
Expand Down Expand Up @@ -637,6 +650,26 @@ namespace ts {
return updated;
}

function visitSourceFile(node: SourceFile): SourceFile {
const visited = visitEachChild(node, visitor, context);
const statement = concatenate(visited.statements, taggedTemplateStringDeclarations && [
createVariableStatement(/*modifiers*/ undefined,
createVariableDeclarationList(taggedTemplateStringDeclarations))
]);
return updateSourceFileNode(visited, setTextRange(createNodeArray(statement), node.statements));
}

function visitTaggedTemplateExpression (node: TaggedTemplateExpression) {
return processTaggedTemplateExpression(
context,
node,
visitor,
currentSourceFile,
recordTaggedTemplateString,
ProcessLevel.LiftRestriction
);
}

function transformAsyncGeneratorFunctionBody(node: MethodDeclaration | AccessorDeclaration | FunctionDeclaration | FunctionExpression): FunctionBody {
resumeLexicalEnvironment();
const statements: Statement[] = [];
Expand Down
116 changes: 116 additions & 0 deletions src/compiler/transformers/taggedTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*@internal*/
namespace ts {
export enum ProcessLevel {
LiftRestriction,
All
}

export function processTaggedTemplateExpression(
context: TransformationContext,
node: TaggedTemplateExpression,
visitor: ((node: Node) => VisitResult<Node>) | undefined,
currentSourceFile: SourceFile,
recordTaggedTemplateString: (temp: Identifier) => void,
level: ProcessLevel) {

// Visit the tag expression
const tag = visitNode(node.tag, visitor, isExpression);

// Build up the template arguments and the raw and cooked strings for the template.
// We start out with 'undefined' for the first argument and revisit later
// to avoid walking over the template string twice and shifting all our arguments over after the fact.
const templateArguments: Expression[] = [undefined!];
const cookedStrings: Expression[] = [];
const rawStrings: Expression[] = [];
const template = node.template;

if (level === ProcessLevel.LiftRestriction && !hasInvalidEscape(template)) return tag;

if (isNoSubstitutionTemplateLiteral(template)) {
cookedStrings.push(createTemplateCooked(template));
rawStrings.push(getRawLiteral(template, currentSourceFile));
}
else {
cookedStrings.push(createTemplateCooked(template.head));
rawStrings.push(getRawLiteral(template.head, currentSourceFile));
for (const templateSpan of template.templateSpans) {
cookedStrings.push(createTemplateCooked(templateSpan.literal));
rawStrings.push(getRawLiteral(templateSpan.literal, currentSourceFile));
templateArguments.push(visitNode(templateSpan.expression, visitor, isExpression));
}
}

const helperCall = createTemplateObjectHelper(context, createArrayLiteral(cookedStrings), createArrayLiteral(rawStrings));

// Create a variable to cache the template object if we're in a module.
// Do not do this in the global scope, as any variable we currently generate could conflict with
// variables from outside of the current compilation. In the future, we can revisit this behavior.
if (isExternalModule(currentSourceFile)) {
const tempVar = createUniqueName("templateObject");
recordTaggedTemplateString(tempVar);
templateArguments[0] = createLogicalOr(
tempVar,
createAssignment(
tempVar,
helperCall)
);
}
else {
templateArguments[0] = helperCall;
}

return createCall(tag, /*typeArguments*/ undefined, templateArguments);
}

function createTemplateCooked (template: TemplateHead | TemplateMiddle | TemplateTail | NoSubstitutionTemplateLiteral) {
return template.templateFlags ? createIdentifier("undefined") : createLiteral(template.text);
}

/**
* Creates an ES5 compatible literal from an ES6 template literal.
*
* @param node The ES6 template literal.
*/
function getRawLiteral(node: LiteralLikeNode, currentSourceFile: SourceFile) {
// 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));

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
text = text.replace(/\r\n?/g, "\n");
return setTextRange(createLiteral(text), node);
}

function createTemplateObjectHelper(context: TransformationContext, cooked: ArrayLiteralExpression, raw: ArrayLiteralExpression) {
context.requestEmitHelper(templateObjectHelper);
return createCall(
getHelperName("__makeTemplateObject"),
/*typeArguments*/ undefined,
[
cooked,
raw
]
);
}

const templateObjectHelper: EmitHelper = {
name: "typescript:makeTemplateObject",
scoped: false,
priority: 0,
text: `
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
};`
};
}
1 change: 1 addition & 0 deletions src/compiler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"visitor.ts",
"transformers/utilities.ts",
"transformers/destructuring.ts",
"transformers/taggedTemplate.ts",
"transformers/ts.ts",
"transformers/es2017.ts",
"transformers/esnext.ts",
Expand Down
7 changes: 7 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6433,4 +6433,11 @@ namespace ts {
export function isNamedImportsOrExports(node: Node): node is NamedImportsOrExports {
return node.kind === SyntaxKind.NamedImports || node.kind === SyntaxKind.NamedExports;
}

/** @internal */
export function hasInvalidEscape(template: TemplateLiteral): boolean {
return template && !!(isNoSubstitutionTemplateLiteral(template)
? template.templateFlags
: (template.head.templateFlags || some(template.templateSpans, span => !!span.literal.templateFlags)));
}
}
1 change: 1 addition & 0 deletions src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"../compiler/visitor.ts",
"../compiler/transformers/utilities.ts",
"../compiler/transformers/destructuring.ts",
"../compiler/transformers/taggedTemplate.ts",
"../compiler/transformers/ts.ts",
"../compiler/transformers/es2017.ts",
"../compiler/transformers/esnext.ts",
Expand Down
1 change: 1 addition & 0 deletions src/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"../compiler/visitor.ts",
"../compiler/transformers/utilities.ts",
"../compiler/transformers/destructuring.ts",
"../compiler/transformers/taggedTemplate.ts",
"../compiler/transformers/ts.ts",
"../compiler/transformers/es2017.ts",
"../compiler/transformers/esnext.ts",
Expand Down
1 change: 1 addition & 0 deletions src/server/tsconfig.library.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"../compiler/visitor.ts",
"../compiler/transformers/utilities.ts",
"../compiler/transformers/destructuring.ts",
"../compiler/transformers/taggedTemplate.ts",
"../compiler/transformers/ts.ts",
"../compiler/transformers/es2017.ts",
"../compiler/transformers/esnext.ts",
Expand Down
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"../compiler/visitor.ts",
"../compiler/transformers/utilities.ts",
"../compiler/transformers/destructuring.ts",
"../compiler/transformers/taggedTemplate.ts",
"../compiler/transformers/ts.ts",
"../compiler/transformers/es2017.ts",
"../compiler/transformers/esnext.ts",
Expand Down
Loading

0 comments on commit e92b6bc

Please sign in to comment.