Skip to content

Commit f24f363

Browse files
authored
Fixes JSX attribute escaping when parent pointers are missing (microsoft#35743)
* Fixes JSX attribute escaping when parent pointers are missing * Fix whitespace change
1 parent 8ed1297 commit f24f363

File tree

7 files changed

+147
-88
lines changed

7 files changed

+147
-88
lines changed

src/compiler/emitter.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,10 @@ namespace ts {
11571157
return pipelineEmit(EmitHint.Expression, node);
11581158
}
11591159

1160+
function emitJsxAttributeValue(node: StringLiteral | JsxExpression): Node {
1161+
return pipelineEmit(isStringLiteral(node) ? EmitHint.JsxAttributeValue : EmitHint.Unspecified, node);
1162+
}
1163+
11601164
function pipelineEmit(emitHint: EmitHint, node: Node) {
11611165
const savedLastNode = lastNode;
11621166
const savedLastSubstitution = lastSubstitution;
@@ -1224,6 +1228,7 @@ namespace ts {
12241228
Debug.assert(lastNode === node || lastSubstitution === node);
12251229
if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
12261230
if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier));
1231+
if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true);
12271232
if (hint === EmitHint.MappedTypeParameter) return emitMappedTypeParameter(cast(node, isTypeParameterDeclaration));
12281233
if (hint === EmitHint.EmbeddedStatement) {
12291234
Debug.assertNode(node, isEmptyStatement);
@@ -1237,7 +1242,7 @@ namespace ts {
12371242
case SyntaxKind.TemplateHead:
12381243
case SyntaxKind.TemplateMiddle:
12391244
case SyntaxKind.TemplateTail:
1240-
return emitLiteral(<LiteralExpression>node);
1245+
return emitLiteral(<LiteralExpression>node, /*jsxAttributeEscape*/ false);
12411246

12421247
case SyntaxKind.UnparsedSource:
12431248
case SyntaxKind.UnparsedPrepend:
@@ -1556,7 +1561,7 @@ namespace ts {
15561561
case SyntaxKind.StringLiteral:
15571562
case SyntaxKind.RegularExpressionLiteral:
15581563
case SyntaxKind.NoSubstitutionTemplateLiteral:
1559-
return emitLiteral(<LiteralExpression>node);
1564+
return emitLiteral(<LiteralExpression>node, /*jsxAttributeEscape*/ false);
15601565

15611566
// Identifiers
15621567
case SyntaxKind.Identifier:
@@ -1746,7 +1751,7 @@ namespace ts {
17461751
// SyntaxKind.NumericLiteral
17471752
// SyntaxKind.BigIntLiteral
17481753
function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) {
1749-
emitLiteral(node);
1754+
emitLiteral(node, /*jsxAttributeEscape*/ false);
17501755
}
17511756

17521757
// SyntaxKind.StringLiteral
@@ -1755,8 +1760,8 @@ namespace ts {
17551760
// SyntaxKind.TemplateHead
17561761
// SyntaxKind.TemplateMiddle
17571762
// SyntaxKind.TemplateTail
1758-
function emitLiteral(node: LiteralLikeNode) {
1759-
const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape);
1763+
function emitLiteral(node: LiteralLikeNode, jsxAttributeEscape: boolean) {
1764+
const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape);
17601765
if ((printerOptions.sourceMap || printerOptions.inlineSourceMap)
17611766
&& (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) {
17621767
writeLiteral(text);
@@ -2295,7 +2300,7 @@ namespace ts {
22952300
expression = skipPartiallyEmittedExpressions(expression);
22962301
if (isNumericLiteral(expression)) {
22972302
// check if numeric literal is a decimal literal that was originally written with a dot
2298-
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true);
2303+
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true, /*jsxAttributeEscape*/ false);
22992304
// If he number will be printed verbatim and it doesn't already contain a dot, add one
23002305
// if the expression doesn't have any comments that will be emitted.
23012306
return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!);
@@ -3295,7 +3300,7 @@ namespace ts {
32953300

32963301
function emitJsxAttribute(node: JsxAttribute) {
32973302
emit(node.name);
3298-
emitNodeWithPrefix("=", writePunctuation, node.initializer!, emit); // TODO: GH#18217
3303+
emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue);
32993304
}
33003305

33013306
function emitJsxSpreadAttribute(node: JsxSpreadAttribute) {
@@ -3828,7 +3833,7 @@ namespace ts {
38283833
}
38293834
}
38303835

3831-
function emitNodeWithPrefix(prefix: string, prefixWriter: (s: string) => void, node: Node, emit: (node: Node) => void) {
3836+
function emitNodeWithPrefix<T extends Node>(prefix: string, prefixWriter: (s: string) => void, node: T | undefined, emit: (node: T) => void) {
38323837
if (node) {
38333838
prefixWriter(prefix);
38343839
emit(node);
@@ -4385,20 +4390,20 @@ namespace ts {
43854390
return getSourceTextOfNodeFromSourceFile(currentSourceFile!, node, includeTrivia);
43864391
}
43874392

4388-
function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined): string {
4393+
function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string {
43894394
if (node.kind === SyntaxKind.StringLiteral && (<StringLiteral>node).textSourceNode) {
43904395
const textSourceNode = (<StringLiteral>node).textSourceNode!;
43914396
if (isIdentifier(textSourceNode)) {
4392-
return neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ?
4393-
`"${escapeString(getTextOfNode(textSourceNode))}"` :
4397+
return jsxAttributeEscape ? `"${escapeJsxAttributeString(getTextOfNode(textSourceNode))}"` :
4398+
neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? `"${escapeString(getTextOfNode(textSourceNode))}"` :
43944399
`"${escapeNonAsciiString(getTextOfNode(textSourceNode))}"`;
43954400
}
43964401
else {
4397-
return getLiteralTextOfNode(textSourceNode, neverAsciiEscape);
4402+
return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape);
43984403
}
43994404
}
44004405

4401-
return getLiteralText(node, currentSourceFile!, neverAsciiEscape);
4406+
return getLiteralText(node, currentSourceFile!, neverAsciiEscape, jsxAttributeEscape);
44024407
}
44034408

44044409
/**

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5842,6 +5842,7 @@ namespace ts {
58425842
MappedTypeParameter, // Emitting a TypeParameterDeclaration inside of a MappedTypeNode
58435843
Unspecified, // Emitting an otherwise unspecified node
58445844
EmbeddedStatement, // Emitting an embedded statement
5845+
JsxAttributeValue, // Emitting a JSX attribute value
58455846
}
58465847

58475848
/* @internal */

src/compiler/utilities.ts

+72-36
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ namespace ts {
551551
return emitNode && emitNode.flags || 0;
552552
}
553553

554-
export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined) {
554+
export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean) {
555555
// If we don't need to downlevel and we can reach the original source text using
556556
// the node's parent reference, then simply get the text as it was originally written.
557557
if (!nodeIsSynthesized(node) && node.parent && !(
@@ -561,24 +561,29 @@ namespace ts {
561561
return getSourceTextOfNodeFromSourceFile(sourceFile, node);
562562
}
563563

564-
// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
565-
// had to include a backslash: `not \${a} substitution`.
566-
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString : escapeNonAsciiString;
567-
568564
// If we can't reach the original source text, use the canonical form if it's a number,
569565
// or a (possibly escaped) quoted form of the original text if it's string-like.
570566
switch (node.kind) {
571-
case SyntaxKind.StringLiteral:
567+
case SyntaxKind.StringLiteral: {
568+
const escapeText = jsxAttributeEscape ? escapeJsxAttributeString :
569+
neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString :
570+
escapeNonAsciiString;
572571
if ((<StringLiteral>node).singleQuote) {
573572
return "'" + escapeText(node.text, CharacterCodes.singleQuote) + "'";
574573
}
575574
else {
576575
return '"' + escapeText(node.text, CharacterCodes.doubleQuote) + '"';
577576
}
577+
}
578578
case SyntaxKind.NoSubstitutionTemplateLiteral:
579579
case SyntaxKind.TemplateHead:
580580
case SyntaxKind.TemplateMiddle:
581-
case SyntaxKind.TemplateTail:
581+
case SyntaxKind.TemplateTail: {
582+
// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
583+
// had to include a backslash: `not \${a} substitution`.
584+
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString :
585+
escapeNonAsciiString;
586+
582587
const rawText = (<TemplateLiteralLikeNode>node).rawText || escapeTemplateSubstitution(escapeText(node.text, CharacterCodes.backtick));
583588
switch (node.kind) {
584589
case SyntaxKind.NoSubstitutionTemplateLiteral:
@@ -591,6 +596,7 @@ namespace ts {
591596
return "}" + rawText + "`";
592597
}
593598
break;
599+
}
594600
case SyntaxKind.NumericLiteral:
595601
case SyntaxKind.BigIntLiteral:
596602
case SyntaxKind.RegularExpressionLiteral:
@@ -3384,6 +3390,25 @@ namespace ts {
33843390
"\u0085": "\\u0085" // nextLine
33853391
});
33863392

3393+
function encodeUtf16EscapeSequence(charCode: number): string {
3394+
const hexCharCode = charCode.toString(16).toUpperCase();
3395+
const paddedHexCode = ("0000" + hexCharCode).slice(-4);
3396+
return "\\u" + paddedHexCode;
3397+
}
3398+
3399+
function getReplacement(c: string, offset: number, input: string) {
3400+
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
3401+
const lookAhead = input.charCodeAt(offset + c.length);
3402+
if (lookAhead >= CharacterCodes._0 && lookAhead <= CharacterCodes._9) {
3403+
// If the null character is followed by digits, print as a hex escape to prevent the result from parsing as an octal (which is forbidden in strict mode)
3404+
return "\\x00";
3405+
}
3406+
// Otherwise, keep printing a literal \0 for the null character
3407+
return "\\0";
3408+
}
3409+
return escapedCharsMap.get(c) || encodeUtf16EscapeSequence(c.charCodeAt(0));
3410+
}
3411+
33873412
/**
33883413
* Based heavily on the abstract 'Quote'/'QuoteJSONString' operation from ECMA-262 (24.3.2.2),
33893414
* but augmented for a few select characters (e.g. lineSeparator, paragraphSeparator, nextLine)
@@ -3397,6 +3422,46 @@ namespace ts {
33973422
return s.replace(escapedCharsRegExp, getReplacement);
33983423
}
33993424

3425+
const nonAsciiCharacters = /[^\u0000-\u007F]/g;
3426+
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
3427+
s = escapeString(s, quoteChar);
3428+
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
3429+
// Otherwise just return the original string.
3430+
return nonAsciiCharacters.test(s) ?
3431+
s.replace(nonAsciiCharacters, c => encodeUtf16EscapeSequence(c.charCodeAt(0))) :
3432+
s;
3433+
}
3434+
3435+
// This consists of the first 19 unprintable ASCII characters, JSX canonical escapes, lineSeparator,
3436+
// paragraphSeparator, and nextLine. The latter three are just desirable to suppress new lines in
3437+
// the language service. These characters should be escaped when printing, and if any characters are added,
3438+
// the map below must be updated.
3439+
const jsxDoubleQuoteEscapedCharsRegExp = /[\"\u0000-\u001f\u2028\u2029\u0085]/g;
3440+
const jsxSingleQuoteEscapedCharsRegExp = /[\'\u0000-\u001f\u2028\u2029\u0085]/g;
3441+
const jsxEscapedCharsMap = createMapFromTemplate({
3442+
"\"": "&quot;",
3443+
"\'": "&apos;"
3444+
});
3445+
3446+
function encodeJsxCharacterEntity(charCode: number): string {
3447+
const hexCharCode = charCode.toString(16).toUpperCase();
3448+
return "&#x" + hexCharCode + ";";
3449+
}
3450+
3451+
function getJsxAttributeStringReplacement(c: string) {
3452+
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
3453+
return "&#0;";
3454+
}
3455+
return jsxEscapedCharsMap.get(c) || encodeJsxCharacterEntity(c.charCodeAt(0));
3456+
}
3457+
3458+
export function escapeJsxAttributeString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote) {
3459+
const escapedCharsRegExp =
3460+
quoteChar === CharacterCodes.singleQuote ? jsxSingleQuoteEscapedCharsRegExp :
3461+
jsxDoubleQuoteEscapedCharsRegExp;
3462+
return s.replace(escapedCharsRegExp, getJsxAttributeStringReplacement);
3463+
}
3464+
34003465
/**
34013466
* Strip off existed surrounding single quotes, double quotes, or backticks from a given string
34023467
*
@@ -3416,40 +3481,11 @@ namespace ts {
34163481
charCode === CharacterCodes.backtick;
34173482
}
34183483

3419-
function getReplacement(c: string, offset: number, input: string) {
3420-
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
3421-
const lookAhead = input.charCodeAt(offset + c.length);
3422-
if (lookAhead >= CharacterCodes._0 && lookAhead <= CharacterCodes._9) {
3423-
// If the null character is followed by digits, print as a hex escape to prevent the result from parsing as an octal (which is forbidden in strict mode)
3424-
return "\\x00";
3425-
}
3426-
// Otherwise, keep printing a literal \0 for the null character
3427-
return "\\0";
3428-
}
3429-
return escapedCharsMap.get(c) || get16BitUnicodeEscapeSequence(c.charCodeAt(0));
3430-
}
3431-
34323484
export function isIntrinsicJsxName(name: __String | string) {
34333485
const ch = (name as string).charCodeAt(0);
34343486
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-");
34353487
}
34363488

3437-
function get16BitUnicodeEscapeSequence(charCode: number): string {
3438-
const hexCharCode = charCode.toString(16).toUpperCase();
3439-
const paddedHexCode = ("0000" + hexCharCode).slice(-4);
3440-
return "\\u" + paddedHexCode;
3441-
}
3442-
3443-
const nonAsciiCharacters = /[^\u0000-\u007F]/g;
3444-
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
3445-
s = escapeString(s, quoteChar);
3446-
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
3447-
// Otherwise just return the original string.
3448-
return nonAsciiCharacters.test(s) ?
3449-
s.replace(nonAsciiCharacters, c => get16BitUnicodeEscapeSequence(c.charCodeAt(0))) :
3450-
s;
3451-
}
3452-
34533489
const indentStrings: string[] = ["", " "];
34543490
export function getIndentString(level: number) {
34553491
if (indentStrings[level] === undefined) {

0 commit comments

Comments
 (0)