Skip to content

Better template literals support in checker #32064

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
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
2 changes: 1 addition & 1 deletion src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ namespace ts {
return expr.kind === SyntaxKind.Identifier || expr.kind === SyntaxKind.ThisKeyword || expr.kind === SyntaxKind.SuperKeyword ||
(isPropertyAccessExpression(expr) || isNonNullExpression(expr) || isParenthesizedExpression(expr)) && isNarrowableReference(expr.expression) ||
isElementAccessExpression(expr) &&
(isStringLiteral(expr.argumentExpression) || isNumericLiteral(expr.argumentExpression)) &&
isStringOrNumericLiteralLike(expr.argumentExpression) &&
isNarrowableReference(expr.expression);
}

Expand Down
20 changes: 11 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6599,7 +6599,7 @@ namespace ts {
}

function isStringConcatExpression(expr: Node): boolean {
if (expr.kind === SyntaxKind.StringLiteral) {
if (isStringLiteralLike(expr)) {
return true;
}
else if (expr.kind === SyntaxKind.BinaryExpression) {
Expand All @@ -6616,6 +6616,7 @@ namespace ts {
switch (expr.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.NumericLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
return true;
case SyntaxKind.PrefixUnaryExpression:
return (<PrefixUnaryExpression>expr).operator === SyntaxKind.MinusToken &&
Expand All @@ -6638,7 +6639,7 @@ namespace ts {
for (const declaration of symbol.declarations) {
if (declaration.kind === SyntaxKind.EnumDeclaration) {
for (const member of (<EnumDeclaration>declaration).members) {
if (member.initializer && member.initializer.kind === SyntaxKind.StringLiteral) {
if (member.initializer && isStringLiteralLike(member.initializer)) {
return links.enumKind = EnumKind.Literal;
}
if (!isLiteralEnumMember(member)) {
Expand Down Expand Up @@ -16590,7 +16591,7 @@ namespace ts {

function getAccessedPropertyName(access: AccessExpression): __String | undefined {
return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText :
isStringLiteral(access.argumentExpression) || isNumericLiteral(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) :
isStringOrNumericLiteralLike(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) :
undefined;
}

Expand Down Expand Up @@ -16988,8 +16989,8 @@ namespace ts {
const witnesses: (string | undefined)[] = [];
for (const clause of switchStatement.caseBlock.clauses) {
if (clause.kind === SyntaxKind.CaseClause) {
if (clause.expression.kind === SyntaxKind.StringLiteral) {
witnesses.push((clause.expression as StringLiteral).text);
if (isStringLiteralLike(clause.expression)) {
witnesses.push(clause.expression.text);
continue;
}
return emptyArray;
Expand Down Expand Up @@ -21556,7 +21557,7 @@ namespace ts {
return objectType;
}

if (isConstEnumObjectType(objectType) && indexExpression.kind !== SyntaxKind.StringLiteral) {
if (isConstEnumObjectType(objectType) && !isStringLiteralLike(indexExpression)) {
error(indexExpression, Diagnostics.A_const_enum_member_can_only_be_accessed_using_a_string_literal);
return errorType;
}
Expand Down Expand Up @@ -30232,7 +30233,8 @@ namespace ts {
}
break;
case SyntaxKind.StringLiteral:
return (<StringLiteral>expr).text;
case SyntaxKind.NoSubstitutionTemplateLiteral:
return (<StringLiteralLike>expr).text;
case SyntaxKind.NumericLiteral:
checkGrammarNumericLiteral(<NumericLiteral>expr);
return +(<NumericLiteral>expr).text;
Expand Down Expand Up @@ -30285,7 +30287,7 @@ namespace ts {
return node.kind === SyntaxKind.Identifier ||
node.kind === SyntaxKind.PropertyAccessExpression && isConstantMemberAccess((<PropertyAccessExpression>node).expression) ||
node.kind === SyntaxKind.ElementAccessExpression && isConstantMemberAccess((<ElementAccessExpression>node).expression) &&
(<ElementAccessExpression>node).argumentExpression.kind === SyntaxKind.StringLiteral;
isStringLiteralLike((<ElementAccessExpression>node).argumentExpression);
}

function checkEnumDeclaration(node: EnumDeclaration) {
Expand Down Expand Up @@ -33838,7 +33840,7 @@ namespace ts {
}

function isStringOrNumberLiteralExpression(expr: Expression) {
return expr.kind === SyntaxKind.StringLiteral || expr.kind === SyntaxKind.NumericLiteral ||
return isStringOrNumericLiteralLike(expr) ||
expr.kind === SyntaxKind.PrefixUnaryExpression && (<PrefixUnaryExpression>expr).operator === SyntaxKind.MinusToken &&
(<PrefixUnaryExpression>expr).operand.kind === SyntaxKind.NumericLiteral;
}
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3249,9 +3249,10 @@ namespace ts {

const substitute = createLiteral(constantValue);
if (!compilerOptions.removeComments) {
const propertyName = isPropertyAccessExpression(node)
? declarationNameToString(node.name)
: getTextOfNode(node.argumentExpression);
const originalNode = getOriginalNode(node, isAccessExpression);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, what was this one needed for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template literals and string literals with extended Unicode escapes will be first processed with es2015 transformer which will replace them with Synthesized string literals that don't have reference to parent element or to original node.

Under the hood getTextOfNode(node.argumentExpression) will call getSourceFileOfNode on Synthesized string literal which will return undefined (because node.parent is undefined) and then getSourceTextOfNodeFromSourceFile that will try to access sourceFile.text and exception will be thrown.

On the other hand ElementAccessExpression even if will be replaced with Synthesized node, it will have link to original node. Therefore originalNode.argumentExpression will have proper parent references up to source file and text will be retrieved correctly.

const propertyName = isPropertyAccessExpression(originalNode)
? declarationNameToString(originalNode.name)
: getTextOfNode(originalNode.argumentExpression);

addSyntheticTrailingComment(substitute, SyntaxKind.MultiLineCommentTrivia, ` ${propertyName} `);
}
Expand Down
15 changes: 9 additions & 6 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1692,7 +1692,6 @@ namespace ts {
case SyntaxKind.ConditionalExpression:
case SyntaxKind.SpreadElement:
case SyntaxKind.TemplateExpression:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.OmittedExpression:
case SyntaxKind.JsxElement:
case SyntaxKind.JsxSelfClosingElement:
Expand All @@ -1715,6 +1714,7 @@ namespace ts {
case SyntaxKind.NumericLiteral:
case SyntaxKind.BigIntLiteral:
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.ThisKeyword:
return isInExpressionContext(node);
default:
Expand Down Expand Up @@ -2529,6 +2529,7 @@ namespace ts {
const parent = name.parent;
switch (name.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.NumericLiteral:
if (isComputedPropertyName(parent)) return parent.parent;
// falls through
Expand Down Expand Up @@ -2556,7 +2557,7 @@ namespace ts {
}

export function isLiteralComputedPropertyDeclarationName(node: Node) {
return (node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NumericLiteral) &&
return isStringOrNumericLiteralLike(node) &&
node.parent.kind === SyntaxKind.ComputedPropertyName &&
isDeclaration(node.parent.parent);
}
Expand Down Expand Up @@ -3220,20 +3221,22 @@ namespace ts {
}

/**
* Strip off existed single quotes or double quotes from a given string
* Strip off existed surrounding single quotes, double quotes, or backticks from a given string
*
* @return non-quoted string
*/
export function stripQuotes(name: string) {
const length = name.length;
if (length >= 2 && name.charCodeAt(0) === name.charCodeAt(length - 1) && startsWithQuote(name)) {
if (length >= 2 && name.charCodeAt(0) === name.charCodeAt(length - 1) && isQuoteOrBacktick(name.charCodeAt(0))) {
return name.substring(1, length - 1);
}
return name;
}

export function startsWithQuote(name: string): boolean {
return isSingleOrDoubleQuote(name.charCodeAt(0));
function isQuoteOrBacktick(charCode: number) {
return charCode === CharacterCodes.singleQuote ||
charCode === CharacterCodes.doubleQuote ||
charCode === CharacterCodes.backtick;
}

function getReplacement(c: string, offset: number, input: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2086,7 +2086,7 @@ namespace ts.Completions {
if (name === undefined
// If the symbol is external module, don't show it in the completion list
// (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there)
|| symbol.flags & SymbolFlags.Module && startsWithQuote(name)
|| symbol.flags & SymbolFlags.Module && isSingleOrDoubleQuote(name.charCodeAt(0))
// If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@"
|| isKnownSymbol(symbol)) {
return undefined;
Expand Down
5 changes: 3 additions & 2 deletions src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ namespace ts.FindAllReferences {
function getTextSpan(node: Node, sourceFile: SourceFile, endNode?: Node): TextSpan {
let start = node.getStart(sourceFile);
let end = (endNode || node).getEnd();
if (node.kind === SyntaxKind.StringLiteral) {
if (isStringLiteralLike(node)) {
Debug.assert(endNode === undefined);
start += 1;
end -= 1;
Expand Down Expand Up @@ -1234,8 +1234,9 @@ namespace ts.FindAllReferences.Core {
case SyntaxKind.Identifier:
return (node as Identifier).text.length === searchSymbolName.length;

case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.StringLiteral: {
const str = node as StringLiteral;
const str = node as StringLiteralLike;
return (isLiteralNameOfPropertyDeclarationOrIndexAccess(str) || isNameOfModuleDeclaration(node) || isExpressionOfExternalModuleImportEqualsDeclaration(node) || (isCallExpression(node.parent) && isBindableObjectDefinePropertyCall(node.parent) && node.parent.arguments[1] === node)) &&
str.text.length === searchSymbolName.length;
}
Expand Down
3 changes: 2 additions & 1 deletion src/services/rename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ namespace ts.Rename {
function createTriggerSpanForNode(node: Node, sourceFile: SourceFile) {
let start = node.getStart(sourceFile);
let width = node.getWidth(sourceFile);
if (node.kind === SyntaxKind.StringLiteral) {
if (isStringLiteralLike(node)) {
// Exclude the quotes
start += 1;
width -= 2;
Expand All @@ -93,6 +93,7 @@ namespace ts.Rename {
switch (node.kind) {
case SyntaxKind.Identifier:
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.ThisKeyword:
return true;
case SyntaxKind.NumericLiteral:
Expand Down
1 change: 1 addition & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,7 @@ namespace ts {
function getContainingObjectLiteralElementWorker(node: Node): ObjectLiteralElement | undefined {
switch (node.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.NumericLiteral:
if (node.parent.kind === SyntaxKind.ComputedPropertyName) {
return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ namespace ts {
isFunctionLike(node.parent) && (<FunctionLikeDeclaration>node.parent).name === node;
}

export function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: StringLiteral | NumericLiteral): boolean {
export function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: StringLiteral | NumericLiteral | NoSubstitutionTemplateLiteral): boolean {
switch (node.parent.kind) {
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertySignature:
Expand Down
28 changes: 28 additions & 0 deletions tests/baselines/reference/ambientModuleWithTemplateLiterals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// [ambientModuleWithTemplateLiterals.ts]
declare module Foo {
enum Bar {
a = `1`,
b = '2',
c = '3'
}

export const a = 'string';
export const b = `template`;

export const c = Bar.a;
export const d = Bar['b'];
export const e = Bar[`c`];
}

Foo.a;
Foo.b;
Foo.c;
Foo.d;
Foo.e;

//// [ambientModuleWithTemplateLiterals.js]
Foo.a;
Foo.b;
Foo.c;
Foo.d;
Foo.e;
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
=== tests/cases/compiler/ambientModuleWithTemplateLiterals.ts ===
declare module Foo {
>Foo : Symbol(Foo, Decl(ambientModuleWithTemplateLiterals.ts, 0, 0))

enum Bar {
>Bar : Symbol(Bar, Decl(ambientModuleWithTemplateLiterals.ts, 0, 20))

a = `1`,
>a : Symbol(Bar.a, Decl(ambientModuleWithTemplateLiterals.ts, 1, 14))

b = '2',
>b : Symbol(Bar.b, Decl(ambientModuleWithTemplateLiterals.ts, 2, 16))

c = '3'
>c : Symbol(Bar.c, Decl(ambientModuleWithTemplateLiterals.ts, 3, 16))
}

export const a = 'string';
>a : Symbol(a, Decl(ambientModuleWithTemplateLiterals.ts, 7, 16))

export const b = `template`;
>b : Symbol(b, Decl(ambientModuleWithTemplateLiterals.ts, 8, 16))

export const c = Bar.a;
>c : Symbol(c, Decl(ambientModuleWithTemplateLiterals.ts, 10, 16))
>Bar.a : Symbol(Bar.a, Decl(ambientModuleWithTemplateLiterals.ts, 1, 14))
>Bar : Symbol(Bar, Decl(ambientModuleWithTemplateLiterals.ts, 0, 20))
>a : Symbol(Bar.a, Decl(ambientModuleWithTemplateLiterals.ts, 1, 14))

export const d = Bar['b'];
>d : Symbol(d, Decl(ambientModuleWithTemplateLiterals.ts, 11, 16))
>Bar : Symbol(Bar, Decl(ambientModuleWithTemplateLiterals.ts, 0, 20))
>'b' : Symbol(Bar.b, Decl(ambientModuleWithTemplateLiterals.ts, 2, 16))

export const e = Bar[`c`];
>e : Symbol(e, Decl(ambientModuleWithTemplateLiterals.ts, 12, 16))
>Bar : Symbol(Bar, Decl(ambientModuleWithTemplateLiterals.ts, 0, 20))
>`c` : Symbol(Bar.c, Decl(ambientModuleWithTemplateLiterals.ts, 3, 16))
}

Foo.a;
>Foo.a : Symbol(Foo.a, Decl(ambientModuleWithTemplateLiterals.ts, 7, 16))
>Foo : Symbol(Foo, Decl(ambientModuleWithTemplateLiterals.ts, 0, 0))
>a : Symbol(Foo.a, Decl(ambientModuleWithTemplateLiterals.ts, 7, 16))

Foo.b;
>Foo.b : Symbol(Foo.b, Decl(ambientModuleWithTemplateLiterals.ts, 8, 16))
>Foo : Symbol(Foo, Decl(ambientModuleWithTemplateLiterals.ts, 0, 0))
>b : Symbol(Foo.b, Decl(ambientModuleWithTemplateLiterals.ts, 8, 16))

Foo.c;
>Foo.c : Symbol(Foo.c, Decl(ambientModuleWithTemplateLiterals.ts, 10, 16))
>Foo : Symbol(Foo, Decl(ambientModuleWithTemplateLiterals.ts, 0, 0))
>c : Symbol(Foo.c, Decl(ambientModuleWithTemplateLiterals.ts, 10, 16))

Foo.d;
>Foo.d : Symbol(Foo.d, Decl(ambientModuleWithTemplateLiterals.ts, 11, 16))
>Foo : Symbol(Foo, Decl(ambientModuleWithTemplateLiterals.ts, 0, 0))
>d : Symbol(Foo.d, Decl(ambientModuleWithTemplateLiterals.ts, 11, 16))

Foo.e;
>Foo.e : Symbol(Foo.e, Decl(ambientModuleWithTemplateLiterals.ts, 12, 16))
>Foo : Symbol(Foo, Decl(ambientModuleWithTemplateLiterals.ts, 0, 0))
>e : Symbol(Foo.e, Decl(ambientModuleWithTemplateLiterals.ts, 12, 16))

72 changes: 72 additions & 0 deletions tests/baselines/reference/ambientModuleWithTemplateLiterals.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
=== tests/cases/compiler/ambientModuleWithTemplateLiterals.ts ===
declare module Foo {
>Foo : typeof Foo

enum Bar {
>Bar : Bar

a = `1`,
>a : Bar.a
>`1` : "1"

b = '2',
>b : Bar.b
>'2' : "2"

c = '3'
>c : Bar.c
>'3' : "3"
}

export const a = 'string';
>a : "string"
>'string' : "string"

export const b = `template`;
>b : "template"
>`template` : "template"

export const c = Bar.a;
>c : Bar.a
>Bar.a : Bar.a
>Bar : typeof Bar
>a : Bar.a

export const d = Bar['b'];
>d : Bar.b
>Bar['b'] : Bar.b
>Bar : typeof Bar
>'b' : "b"

export const e = Bar[`c`];
>e : Bar.c
>Bar[`c`] : Bar.c
>Bar : typeof Bar
>`c` : "c"
}

Foo.a;
>Foo.a : "string"
>Foo : typeof Foo
>a : "string"

Foo.b;
>Foo.b : "template"
>Foo : typeof Foo
>b : "template"

Foo.c;
>Foo.c : Foo.Bar.a
>Foo : typeof Foo
>c : Foo.Bar.a

Foo.d;
>Foo.d : Foo.Bar.b
>Foo : typeof Foo
>d : Foo.Bar.b

Foo.e;
>Foo.e : Foo.Bar.c
>Foo : typeof Foo
>e : Foo.Bar.c

Loading