Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f24f363

Browse filesBrowse 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
Copy full SHA for f24f363

File tree

Expand file treeCollapse file tree

7 files changed

+147
-88
lines changed
Filter options
Expand file treeCollapse file tree

7 files changed

+147
-88
lines changed

‎src/compiler/emitter.ts

Copy file name to clipboardExpand all lines: src/compiler/emitter.ts
+18-13Lines changed: 18 additions & 13 deletions
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

Copy file name to clipboardExpand all lines: src/compiler/types.ts
+1Lines changed: 1 addition & 0 deletions
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

Copy file name to clipboardExpand all lines: src/compiler/utilities.ts
+72-36Lines changed: 72 additions & 36 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.