From 518a5d3a48c18ff83d3709a7122adbc243256a67 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Wed, 15 Oct 2014 17:12:13 -0700 Subject: [PATCH] Rudimentary template support, excluding tagging. --- src/compiler/checker.ts | 85 ++------ src/compiler/core.ts | 19 +- .../diagnosticInformationMap.generated.ts | 1 + src/compiler/diagnosticMessages.json | 6 +- src/compiler/emitter.ts | 101 ++++++++- src/compiler/parser.ts | 155 +++++++++++++- src/compiler/scanner.ts | 201 ++++++++++++------ src/compiler/types.ts | 30 ++- 8 files changed, 445 insertions(+), 153 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6927be355a8..dc5e3a7d906 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4103,11 +4103,11 @@ module ts { } return createArrayType(elementType); } - + function isNumericName(name: string) { return (name !== "") && !isNaN(name); } - + function checkObjectLiteral(node: ObjectLiteral, contextualMapper?: TypeMapper): Type { var members = node.symbol.members; var properties: SymbolTable = {}; @@ -5227,6 +5227,12 @@ module ts { return resultType; } + function checkTemplateExpression(node: TemplateExpression): void { + forEach((node).templateSpans, templateSpan => { + checkExpression(templateSpan.expression); + }); + } + function checkExpressionWithContextualType(node: Expression, contextualType: Type, contextualMapper?: TypeMapper): Type { var saveContextualType = node.contextualType; node.contextualType = contextualType; @@ -5280,7 +5286,11 @@ module ts { return booleanType; case SyntaxKind.NumericLiteral: return numberType; + case SyntaxKind.TemplateExpression: + checkTemplateExpression(node); + // fall through case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: return stringType; case SyntaxKind.RegularExpressionLiteral: return globalRegExpType; @@ -7229,77 +7239,6 @@ module ts { return node.parent && node.parent.kind === SyntaxKind.TypeReference; } - function isExpression(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ThisKeyword: - case SyntaxKind.SuperKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.RegularExpressionLiteral: - case SyntaxKind.ArrayLiteral: - case SyntaxKind.ObjectLiteral: - case SyntaxKind.PropertyAccess: - case SyntaxKind.IndexedAccess: - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.TypeAssertion: - case SyntaxKind.ParenExpression: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.PrefixOperator: - case SyntaxKind.PostfixOperator: - case SyntaxKind.BinaryExpression: - case SyntaxKind.ConditionalExpression: - case SyntaxKind.OmittedExpression: - return true; - case SyntaxKind.QualifiedName: - while (node.parent.kind === SyntaxKind.QualifiedName) node = node.parent; - return node.parent.kind === SyntaxKind.TypeQuery; - case SyntaxKind.Identifier: - if (node.parent.kind === SyntaxKind.TypeQuery) { - return true; - } - // Fall through - case SyntaxKind.NumericLiteral: - case SyntaxKind.StringLiteral: - var parent = node.parent; - switch (parent.kind) { - case SyntaxKind.VariableDeclaration: - case SyntaxKind.Parameter: - case SyntaxKind.Property: - case SyntaxKind.EnumMember: - case SyntaxKind.PropertyAssignment: - return (parent).initializer === node; - case SyntaxKind.ExpressionStatement: - case SyntaxKind.IfStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.CaseClause: - case SyntaxKind.ThrowStatement: - case SyntaxKind.SwitchStatement: - return (parent).expression === node; - case SyntaxKind.ForStatement: - return (parent).initializer === node || - (parent).condition === node || - (parent).iterator === node; - case SyntaxKind.ForInStatement: - return (parent).variable === node || - (parent).expression === node; - case SyntaxKind.TypeAssertion: - return node === (parent).operand; - default: - if (isExpression(parent)) { - return true; - } - } - } - return false; - } - function isTypeNode(node: Node): boolean { if (SyntaxKind.FirstTypeNode <= node.kind && node.kind <= SyntaxKind.LastTypeNode) { return true; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 3da94137fc8..6a4f4beaa33 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -5,6 +5,12 @@ module ts { [index: string]: T; } + export enum Comparison { + LessThan = -1, + EqualTo = 0, + GreaterThan = 1 + } + export interface StringSet extends Map { } export function forEach(array: T[], callback: (element: T) => U): U { @@ -79,6 +85,7 @@ module ts { export function concatenate(array1: T[], array2: T[]): T[] { if (!array2 || !array2.length) return array1; if (!array1 || !array1.length) return array2; + return array1.concat(array2); } @@ -304,11 +311,11 @@ module ts { }; } - export function compareValues(a: T, b: T): number { - if (a === b) return 0; - if (a === undefined) return -1; - if (b === undefined) return 1; - return a < b ? -1 : 1; + export function compareValues(a: T, b: T): Comparison { + if (a === b) return Comparison.EqualTo; + if (a === undefined) return Comparison.LessThan; + if (b === undefined) return Comparison.GreaterThan; + return a < b ? Comparison.LessThan : Comparison.GreaterThan; } function getDiagnosticFilename(diagnostic: Diagnostic): string { @@ -333,7 +340,7 @@ module ts { var previousDiagnostic = diagnostics[0]; for (var i = 1; i < diagnostics.length; i++) { var currentDiagnostic = diagnostics[i]; - var isDupe = compareDiagnostics(currentDiagnostic, previousDiagnostic) === 0; + var isDupe = compareDiagnostics(currentDiagnostic, previousDiagnostic) === Comparison.EqualTo; if (!isDupe) { newDiagnostics.push(currentDiagnostic); previousDiagnostic = currentDiagnostic; diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index c9d296d8bcc..a38da38eccf 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -122,6 +122,7 @@ module ts { const_must_be_declared_inside_a_block: { code: 1156, category: DiagnosticCategory.Error, key: "const must be declared inside a block." }, let_must_be_declared_inside_a_block: { code: 1157, category: DiagnosticCategory.Error, key: "let must be declared inside a block." }, Only_var_declarations_can_be_exported: { code: 1158, category: DiagnosticCategory.Error, key: "Only var declarations can be exported." }, + Invalid_template_literal_expected: { code: 1159, category: DiagnosticCategory.Error, key: "Invalid template literal; expected '}'" }, Duplicate_identifier_0: { code: 2300, category: DiagnosticCategory.Error, key: "Duplicate identifier '{0}'." }, Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor: { code: 2301, category: DiagnosticCategory.Error, key: "Initializer of instance member variable '{0}' cannot reference identifier '{1}' declared in the constructor." }, Static_members_cannot_reference_class_type_parameters: { code: 2302, category: DiagnosticCategory.Error, key: "Static members cannot reference class type parameters." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 68b5395224a..41eae297be5 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -449,7 +449,7 @@ }, "An enum member cannot have a numeric name.": { "category": "Error", - "code": 1151 + "code": 1151 }, "'var', 'let' or 'const' expected.": { "category": "Error", @@ -479,6 +479,10 @@ "category": "Error", "code": 1158 }, + "Invalid template literal; expected '}'": { + "category": "Error", + "code": 1159 + }, "Duplicate identifier '{0}'.": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 8c0dd818d32..eb1eafbf9b2 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -792,14 +792,101 @@ module ts { } } - function emitLiteral(node: LiteralExpression) { - var text = getSourceTextOfLocalNode(node); - if (node.kind === SyntaxKind.StringLiteral && compilerOptions.sourceMap) { + function emitLiteral(node: LiteralExpression): void { + var text = getLiteralText(); + + if (compilerOptions.sourceMap && (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) { writer.writeLiteral(text); } else { write(text); } + + function getLiteralText() { + if (compilerOptions.target < ScriptTarget.ES6 && isTemplateLiteralKind(node.kind)) { + return getTemplateLiteralAsStringLiteral(node) + } + + return getSourceTextOfLocalNode(node); + } + } + + function getTemplateLiteralAsStringLiteral(node: LiteralExpression): string { + return "\"" + escapeString(node.text) + "\""; + } + + function emitTemplateExpression(node: TemplateExpression): void { + if (compilerOptions.target >= ScriptTarget.ES6) { + forEachChild(node, emitNode); + return; + } + + var templateNeedsParens = isExpression(node.parent) && + comparePrecedenceToBinaryPlus(node.parent) !== Comparison.LessThan; + + if (templateNeedsParens) { + write("("); + } + + emitLiteral(node.head); + + forEach(node.templateSpans, templateSpan => { + var needsParens = comparePrecedenceToBinaryPlus(templateSpan.expression) !== Comparison.GreaterThan; + + write(" + "); + + if (needsParens) { + write("("); + } + emit(templateSpan.expression); + if (needsParens) { + write(")"); + } + + write(" + ") + emitLiteral(templateSpan.literal); + }); + + if (templateNeedsParens) { + write(")"); + } + + /** + * Returns whether the expression has lesser, greater, + * or equal precedence to the binary '+' operator + */ + function comparePrecedenceToBinaryPlus(expression: Expression): Comparison { + // All binary expressions have lower precedence than '+' apart from '*', '/', and '%'. + // All unary operators have a higher precedence apart from yield. + // Arrow functions and conditionals have a lower precedence, + // although we convert the former into regular function expressions in ES5 mode, + // and in ES6 mode this function won't get called anyway. + // + // TODO (drosen): Note that we need to account for the upcoming 'yield' and + // spread ('...') unary operators that are anticipated for ES6. + switch (expression.kind) { + case SyntaxKind.BinaryExpression: + switch ((expression).operator) { + case SyntaxKind.AsteriskToken: + case SyntaxKind.SlashToken: + case SyntaxKind.PercentToken: + return Comparison.GreaterThan; + case SyntaxKind.PlusToken: + return Comparison.EqualTo; + default: + return Comparison.LessThan; + } + case SyntaxKind.ConditionalExpression: + return Comparison.LessThan; + default: + return Comparison.GreaterThan; + } + } + + } + + function emitTemplateSpan(span: TemplateSpan) { + forEachChild(span, emitNode); } // This function specifically handles numeric/string literals for enum and accessor 'identifiers'. @@ -2091,7 +2178,15 @@ module ts { case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: case SyntaxKind.RegularExpressionLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateHead: + case SyntaxKind.TemplateMiddle: + case SyntaxKind.TemplateTail: return emitLiteral(node); + case SyntaxKind.TemplateExpression: + return emitTemplateExpression(node); + case SyntaxKind.TemplateSpan: + return emitTemplateSpan(node); case SyntaxKind.QualifiedName: return emitPropertyAccess(node); case SyntaxKind.ArrayLiteral: diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index b011220095c..f6ea10c4384 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -344,6 +344,10 @@ module ts { child((node).externalModuleName); case SyntaxKind.ExportAssignment: return child((node).exportName); + case SyntaxKind.TemplateExpression: + return child((node).head) || children((node).templateSpans); + case SyntaxKind.TemplateSpan: + return child((node).expression) || child((node).literal); } } @@ -448,10 +452,95 @@ module ts { } } + export function isExpression(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ThisKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.RegularExpressionLiteral: + case SyntaxKind.ArrayLiteral: + case SyntaxKind.ObjectLiteral: + case SyntaxKind.PropertyAccess: + case SyntaxKind.IndexedAccess: + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.TypeAssertion: + case SyntaxKind.ParenExpression: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.PrefixOperator: + case SyntaxKind.PostfixOperator: + case SyntaxKind.BinaryExpression: + case SyntaxKind.ConditionalExpression: + case SyntaxKind.TemplateExpression: + case SyntaxKind.OmittedExpression: + return true; + case SyntaxKind.QualifiedName: + while (node.parent.kind === SyntaxKind.QualifiedName) node = node.parent; + return node.parent.kind === SyntaxKind.TypeQuery; + case SyntaxKind.Identifier: + if (node.parent.kind === SyntaxKind.TypeQuery) { + return true; + } + // fall through + case SyntaxKind.NumericLiteral: + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + var parent = node.parent; + switch (parent.kind) { + case SyntaxKind.VariableDeclaration: + case SyntaxKind.Parameter: + case SyntaxKind.Property: + case SyntaxKind.EnumMember: + case SyntaxKind.PropertyAssignment: + return (parent).initializer === node; + case SyntaxKind.ExpressionStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.CaseClause: + case SyntaxKind.ThrowStatement: + case SyntaxKind.SwitchStatement: + return (parent).expression === node; + case SyntaxKind.ForStatement: + return (parent).initializer === node || + (parent).condition === node || + (parent).iterator === node; + case SyntaxKind.ForInStatement: + return (parent).variable === node || + (parent).expression === node; + case SyntaxKind.TypeAssertion: + return node === (parent).operand; + default: + if (isExpression(parent)) { + return true; + } + } + } + return false; + } + export function hasRestParameters(s: SignatureDeclaration): boolean { return s.parameters.length > 0 && (s.parameters[s.parameters.length - 1].flags & NodeFlags.Rest) !== 0; } + export function isLiteralKind(kind: SyntaxKind): boolean { + return SyntaxKind.FirstLiteralToken <= kind && kind <= SyntaxKind.LastLiteralToken; + } + + export function isTextualLiteralKind(kind: SyntaxKind): boolean { + return kind === SyntaxKind.StringLiteral || kind === SyntaxKind.NoSubstitutionTemplateLiteral; + } + + export function isTemplateLiteralKind(kind: SyntaxKind): boolean { + return SyntaxKind.FirstTemplateToken <= kind && kind <= SyntaxKind.LastTemplateToken; + } + export function isInAmbientContext(node: Node): boolean { while (node) { if (node.flags & (NodeFlags.Ambient | NodeFlags.DeclarationFile)) return true; @@ -460,6 +549,7 @@ module ts { return false; } + export function isDeclaration(node: Node): boolean { switch (node.kind) { case SyntaxKind.TypeParameter: @@ -875,6 +965,10 @@ module ts { return token = scanner.reScanSlashToken(); } + function reScanTemplateToken(): SyntaxKind { + return token = scanner.reScanTemplateToken(); + } + function lookAheadHelper(callback: () => T, alwaysResetState: boolean): T { // Keep track of the state we'll need to rollback to if lookahead fails (or if the // caller asked us to always reset our state). @@ -1021,7 +1115,9 @@ module ts { } function isPropertyName(): boolean { - return token >= SyntaxKind.Identifier || token === SyntaxKind.StringLiteral || token === SyntaxKind.NumericLiteral; + return token >= SyntaxKind.Identifier || + token === SyntaxKind.StringLiteral || + token === SyntaxKind.NumericLiteral; } function parsePropertyName(): Identifier { @@ -1296,7 +1392,44 @@ module ts { return finishNode(node); } - function parseLiteralNode(internName?:boolean): LiteralExpression { + function parseTemplateExpression() { + var template = createNode(SyntaxKind.TemplateExpression); + + template.head = parseLiteralNode(); + Debug.assert(template.head.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind"); + + var templateSpans: TemplateSpan[] = []; + do { + templateSpans.push(parseTemplateSpan()); + } + while (templateSpans[templateSpans.length - 1].literal.kind === SyntaxKind.TemplateMiddle) + + template.templateSpans = templateSpans; + return finishNode(template); + } + + function parseTemplateSpan(): TemplateSpan { + var span = createNode(SyntaxKind.TemplateSpan); + span.expression = parseExpression(/*noIn*/ false); + + var literal: LiteralExpression; + + if (token === SyntaxKind.CloseBraceToken) { + reScanTemplateToken() + literal = parseLiteralNode(); + } + else { + error(Diagnostics.Invalid_template_literal_expected); + literal = createMissingNode(); + literal.text = ""; + } + + span.literal = literal; + + return finishNode(span); + } + + function parseLiteralNode(internName?: boolean): LiteralExpression { var node = createNode(token); var text = scanner.getTokenValue(); node.text = internName ? internIdentifier(text) : text; @@ -1308,7 +1441,7 @@ module ts { // Octal literals are not allowed in strict mode or ES5 // Note that theoretically the following condition would hold true literals like 009, // which is not octal.But because of how the scanner separates the tokens, we would - // never get a token like this.Instead, we would get 00 and 9 as two separate tokens. + // never get a token like this. Instead, we would get 00 and 9 as two separate tokens. // We also do not need to check for negatives because any prefix operator would be part of a // parent unary expression. if (node.kind === SyntaxKind.NumericLiteral @@ -1327,7 +1460,9 @@ module ts { } function parseStringLiteral(): LiteralExpression { - if (token === SyntaxKind.StringLiteral) return parseLiteralNode(/*internName:*/ true); + if (token === SyntaxKind.StringLiteral) { + return parseLiteralNode(/*internName:*/ true); + } error(Diagnostics.String_literal_expected); return createMissingNode(); } @@ -1755,6 +1890,8 @@ module ts { case SyntaxKind.FalseKeyword: case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateHead: case SyntaxKind.OpenParenToken: case SyntaxKind.OpenBracketToken: case SyntaxKind.OpenBraceToken: @@ -1846,8 +1983,8 @@ module ts { } // Now see if we might be in cases '2' or '3'. - // If the expression was a LHS expression, and we have an assignment operator, then - // we're in '2' or '3'. Consume the assignment and return. + // If the expression was a LHS expression, and we have an assignment operator, then + // we're in '2' or '3'. Consume the assignment and return. if (isLeftHandSideExpression(expr) && isAssignmentOperator()) { if (isInStrictMode && isEvalOrArgumentsIdentifier(expr)) { // ECMA 262 (Annex C) The identifier eval or arguments may not appear as the LeftHandSideExpression of an @@ -1879,6 +2016,8 @@ module ts { case SyntaxKind.RegularExpressionLiteral: case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateExpression: case SyntaxKind.FalseKeyword: case SyntaxKind.NullKeyword: case SyntaxKind.ThisKeyword: @@ -2343,6 +2482,7 @@ module ts { return parseTokenNode(); case SyntaxKind.NumericLiteral: case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: return parseLiteralNode(); case SyntaxKind.OpenParenToken: return parseParenExpression(); @@ -2360,6 +2500,9 @@ module ts { return parseLiteralNode(); } break; + case SyntaxKind.TemplateHead: + return parseTemplateExpression(); + default: if (isIdentifier()) { return parseIdentifier(); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 81d16b5f487..0d207b1ae92 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -24,6 +24,7 @@ module ts { isReservedWord(): boolean; reScanGreaterToken(): SyntaxKind; reScanSlashToken(): SyntaxKind; + reScanTemplateToken(): SyntaxKind; scan(): SyntaxKind; setText(text: string): void; setTextPos(textPos: number): void; @@ -465,7 +466,7 @@ module ts { var len: number; // Length of text var startPos: number; // Start position of whitespace before current token var tokenPos: number; // Start position of text of current token - var token: number; + var token: SyntaxKind; var tokenValue: string; var precedingLineBreak: boolean; @@ -518,10 +519,10 @@ module ts { return +(text.substring(start, pos)); } - function scanHexDigits(count: number, exact?: boolean): number { + function scanHexDigits(count: number, useExactCount?: boolean): number { var digits = 0; var value = 0; - while (digits < count || !exact) { + while (digits < count || !useExactCount) { var ch = text.charCodeAt(pos); if (ch >= CharacterCodes._0 && ch <= CharacterCodes._9) { value = value * 16 + ch - CharacterCodes._0; @@ -562,60 +563,7 @@ module ts { } if (ch === CharacterCodes.backslash) { result += text.substring(start, pos); - pos++; - if (pos >= len) { - error(Diagnostics.Unexpected_end_of_text); - break; - } - ch = text.charCodeAt(pos++); - switch (ch) { - case CharacterCodes._0: - result += "\0"; - break; - case CharacterCodes.b: - result += "\b"; - break; - case CharacterCodes.t: - result += "\t"; - break; - case CharacterCodes.n: - result += "\n"; - break; - case CharacterCodes.v: - result += "\v"; - break; - case CharacterCodes.f: - result += "\f"; - break; - case CharacterCodes.r: - result += "\r"; - break; - case CharacterCodes.singleQuote: - result += "\'"; - break; - case CharacterCodes.doubleQuote: - result += "\""; - break; - case CharacterCodes.x: - case CharacterCodes.u: - var ch = scanHexDigits(ch === CharacterCodes.x ? 2 : 4, true); - if (ch >= 0) { - result += String.fromCharCode(ch); - } - else { - error(Diagnostics.Hexadecimal_digit_expected); - } - break; - case CharacterCodes.carriageReturn: - if (pos < len && text.charCodeAt(pos) === CharacterCodes.lineFeed) pos++; - break; - case CharacterCodes.lineFeed: - case CharacterCodes.lineSeparator: - case CharacterCodes.paragraphSeparator: - break; - default: - result += String.fromCharCode(ch); - } + result += scanEscapeSequence(); start = pos; continue; } @@ -629,13 +577,134 @@ module ts { return result; } + /** + * Sets the current 'tokenValue' and returns a NoSubstitutionTemplateLiteral or + * a literal component of a TemplateExpression. + */ + function scanTemplateAndSetTokenValue(): SyntaxKind { + var isStartOfTemplate = text.charCodeAt(pos) === CharacterCodes.backtick; + + pos++; + var start = pos; + var contents = "" + var resultingToken = SyntaxKind.Unknown; + + while (true) { + if (pos >= len) { + contents += text.substring(start, pos); + error(Diagnostics.Unexpected_end_of_text); + resultingToken = isStartOfTemplate ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; + break; + } + + var currChar = text.charCodeAt(pos); + + // '`' + if (currChar === CharacterCodes.backtick) { + contents += text.substring(start, pos); + pos++; + resultingToken = isStartOfTemplate ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; + break; + } + + // '${' + if (currChar === CharacterCodes.$ && pos + 1 < len && text.charCodeAt(pos + 1) === CharacterCodes.openBrace) { + contents += text.substring(start, pos); + pos += 2; + resultingToken = isStartOfTemplate ? SyntaxKind.TemplateHead : SyntaxKind.TemplateMiddle; + break; + } + + // Escape character + if (currChar === CharacterCodes.backslash) { + contents += text.substring(start, pos); + contents += scanEscapeSequence(); + start = pos; + continue; + } + + // Speculated ECMAScript 6 Spec 11.8.6.1: + // and LineTerminatorSequences are normalized to for Template Values + // An explicit EscapeSequence is needed to include a or sequence. + if (currChar === CharacterCodes.carriageReturn) { + contents += text.substring(start, pos); + + if (pos + 1 < len && text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { + pos++; + } + pos++; + contents += "\n"; + start = pos; + continue; + } + + pos++; + } + + tokenValue = contents; + return resultingToken; + } + + function scanEscapeSequence(): string { + pos++; + if (pos >= len) { + error(Diagnostics.Unexpected_end_of_text); + return ""; + } + var ch = text.charCodeAt(pos++); + switch (ch) { + case CharacterCodes._0: + return "\0"; + case CharacterCodes.b: + return "\b"; + case CharacterCodes.t: + return "\t"; + case CharacterCodes.n: + return "\n"; + case CharacterCodes.v: + return "\v"; + case CharacterCodes.f: + return "\f"; + case CharacterCodes.r: + return "\r"; + case CharacterCodes.singleQuote: + return "\'"; + case CharacterCodes.doubleQuote: + return "\""; + case CharacterCodes.x: + case CharacterCodes.u: + var ch = scanHexDigits(ch === CharacterCodes.x ? 2 : 4, /*useExactCount*/ true); + if (ch >= 0) { + return String.fromCharCode(ch); + } + else { + error(Diagnostics.Hexadecimal_digit_expected); + return "" + } + + // when encountering a LineContinuation (i.e. a backslash and a line terminator sequence), + // the line terminator is interpreted to be "the empty code unit sequence". + case CharacterCodes.carriageReturn: + if (pos < len && text.charCodeAt(pos) === CharacterCodes.lineFeed) { + pos++; + } + // fall through + case CharacterCodes.lineFeed: + case CharacterCodes.lineSeparator: + case CharacterCodes.paragraphSeparator: + return "" + default: + return String.fromCharCode(ch); + } + } + // Current character is known to be a backslash. Check for Unicode escape of the form '\uXXXX' // and return code point value if valid Unicode escape is found. Otherwise return -1. function peekUnicodeEscape(): number { if (pos + 5 < len && text.charCodeAt(pos + 1) === CharacterCodes.u) { var start = pos; pos += 2; - var value = scanHexDigits(4, true); + var value = scanHexDigits(4, /*useExactCount*/ true); pos = start; return value; } @@ -734,6 +803,8 @@ module ts { case CharacterCodes.singleQuote: tokenValue = scanString(); return token = SyntaxKind.StringLiteral; + case CharacterCodes.backtick: + return token = scanTemplateAndSetTokenValue() case CharacterCodes.percent: if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.PercentEqualsToken; @@ -851,7 +922,7 @@ module ts { case CharacterCodes._0: if (pos + 2 < len && (text.charCodeAt(pos + 1) === CharacterCodes.X || text.charCodeAt(pos + 1) === CharacterCodes.x)) { pos += 2; - var value = scanHexDigits(1, false); + var value = scanHexDigits(1, /*useExactCount*/ false); if (value < 0) { error(Diagnostics.Hexadecimal_digit_expected); value = 0; @@ -1037,6 +1108,15 @@ module ts { return token; } + /** + * Unconditionally back up and scan a template expression portion. + */ + function reScanTemplateToken(): SyntaxKind { + Debug.assert("'reScanTemplateToken' should only be called on a '}'"); + pos = tokenPos; + return token = scanTemplateAndSetTokenValue(); + } + function tryScan(callback: () => T): T { var savePos = pos; var saveStartPos = startPos; @@ -1085,10 +1165,11 @@ module ts { isReservedWord: () => token >= SyntaxKind.FirstReservedWord && token <= SyntaxKind.LastReservedWord, reScanGreaterToken: reScanGreaterToken, reScanSlashToken: reScanSlashToken, + reScanTemplateToken: reScanTemplateToken, scan: scan, setText: setText, setTextPos: setTextPos, - tryScan: tryScan + tryScan: tryScan, }; } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 09cd613e8f3..e7400f28bf9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -20,6 +20,11 @@ module ts { NumericLiteral, StringLiteral, RegularExpressionLiteral, + NoSubstitutionTemplateLiteral, + // Pseudo-literals + TemplateHead, + TemplateMiddle, + TemplateTail, // Punctuation OpenBraceToken, CloseBraceToken, @@ -170,6 +175,8 @@ module ts { PostfixOperator, BinaryExpression, ConditionalExpression, + TemplateExpression, + TemplateSpan, OmittedExpression, // Element Block, @@ -230,7 +237,11 @@ module ts { FirstToken = EndOfFileToken, LastToken = StringKeyword, FirstTriviaToken = SingleLineCommentTrivia, - LastTriviaToken = WhitespaceTrivia + LastTriviaToken = WhitespaceTrivia, + FirstLiteralToken = NumericLiteral, + LastLiteralToken = NoSubstitutionTemplateLiteral, + FirstTemplateToken = NoSubstitutionTemplateLiteral, + LastTemplateToken = TemplateTail } export enum NodeFlags { @@ -369,13 +380,23 @@ module ts { body: Node; // Required, whereas the member inherited from FunctionDeclaration is optional } - // The text property of a LiteralExpression stores the interpreted value of the literal in text form. For a StringLiteral - // 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". + // 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". export interface LiteralExpression extends Expression { text: string; } + export interface TemplateExpression extends Expression { + head: LiteralExpression; + templateSpans: TemplateSpan[] + } + + export interface TemplateSpan extends Node { + expression: Expression; + literal: LiteralExpression; + } + export interface ParenExpression extends Expression { expression: Expression; } @@ -1189,6 +1210,7 @@ module ts { asterisk = 0x2A, // * at = 0x40, // @ backslash = 0x5C, // \ + backtick = 0x60, // ` bar = 0x7C, // | caret = 0x5E, // ^ closeBrace = 0x7D, // }