Fixes JSX attribute escaping when parent pointers are missing (#35743)

* Fixes JSX attribute escaping when parent pointers are missing

* Fix whitespace change
This commit is contained in:
Ron Buckton 2020-01-30 16:52:31 -08:00 committed by GitHub
parent 8ed129771f
commit f24f36350b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 147 additions and 88 deletions

View File

@ -1157,6 +1157,10 @@ namespace ts {
return pipelineEmit(EmitHint.Expression, node);
}
function emitJsxAttributeValue(node: StringLiteral | JsxExpression): Node {
return pipelineEmit(isStringLiteral(node) ? EmitHint.JsxAttributeValue : EmitHint.Unspecified, node);
}
function pipelineEmit(emitHint: EmitHint, node: Node) {
const savedLastNode = lastNode;
const savedLastSubstitution = lastSubstitution;
@ -1224,6 +1228,7 @@ namespace ts {
Debug.assert(lastNode === node || lastSubstitution === node);
if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile));
if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier));
if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true);
if (hint === EmitHint.MappedTypeParameter) return emitMappedTypeParameter(cast(node, isTypeParameterDeclaration));
if (hint === EmitHint.EmbeddedStatement) {
Debug.assertNode(node, isEmptyStatement);
@ -1237,7 +1242,7 @@ namespace ts {
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
return emitLiteral(<LiteralExpression>node);
return emitLiteral(<LiteralExpression>node, /*jsxAttributeEscape*/ false);
case SyntaxKind.UnparsedSource:
case SyntaxKind.UnparsedPrepend:
@ -1556,7 +1561,7 @@ namespace ts {
case SyntaxKind.StringLiteral:
case SyntaxKind.RegularExpressionLiteral:
case SyntaxKind.NoSubstitutionTemplateLiteral:
return emitLiteral(<LiteralExpression>node);
return emitLiteral(<LiteralExpression>node, /*jsxAttributeEscape*/ false);
// Identifiers
case SyntaxKind.Identifier:
@ -1746,7 +1751,7 @@ namespace ts {
// SyntaxKind.NumericLiteral
// SyntaxKind.BigIntLiteral
function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) {
emitLiteral(node);
emitLiteral(node, /*jsxAttributeEscape*/ false);
}
// SyntaxKind.StringLiteral
@ -1755,8 +1760,8 @@ namespace ts {
// SyntaxKind.TemplateHead
// SyntaxKind.TemplateMiddle
// SyntaxKind.TemplateTail
function emitLiteral(node: LiteralLikeNode) {
const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape);
function emitLiteral(node: LiteralLikeNode, jsxAttributeEscape: boolean) {
const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape);
if ((printerOptions.sourceMap || printerOptions.inlineSourceMap)
&& (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) {
writeLiteral(text);
@ -2295,7 +2300,7 @@ namespace ts {
expression = skipPartiallyEmittedExpressions(expression);
if (isNumericLiteral(expression)) {
// check if numeric literal is a decimal literal that was originally written with a dot
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true);
const text = getLiteralTextOfNode(<LiteralExpression>expression, /*neverAsciiEscape*/ true, /*jsxAttributeEscape*/ false);
// If he number will be printed verbatim and it doesn't already contain a dot, add one
// if the expression doesn't have any comments that will be emitted.
return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!);
@ -3295,7 +3300,7 @@ namespace ts {
function emitJsxAttribute(node: JsxAttribute) {
emit(node.name);
emitNodeWithPrefix("=", writePunctuation, node.initializer!, emit); // TODO: GH#18217
emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue);
}
function emitJsxSpreadAttribute(node: JsxSpreadAttribute) {
@ -3828,7 +3833,7 @@ namespace ts {
}
}
function emitNodeWithPrefix(prefix: string, prefixWriter: (s: string) => void, node: Node, emit: (node: Node) => void) {
function emitNodeWithPrefix<T extends Node>(prefix: string, prefixWriter: (s: string) => void, node: T | undefined, emit: (node: T) => void) {
if (node) {
prefixWriter(prefix);
emit(node);
@ -4385,20 +4390,20 @@ namespace ts {
return getSourceTextOfNodeFromSourceFile(currentSourceFile!, node, includeTrivia);
}
function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined): string {
function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string {
if (node.kind === SyntaxKind.StringLiteral && (<StringLiteral>node).textSourceNode) {
const textSourceNode = (<StringLiteral>node).textSourceNode!;
if (isIdentifier(textSourceNode)) {
return neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ?
`"${escapeString(getTextOfNode(textSourceNode))}"` :
return jsxAttributeEscape ? `"${escapeJsxAttributeString(getTextOfNode(textSourceNode))}"` :
neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? `"${escapeString(getTextOfNode(textSourceNode))}"` :
`"${escapeNonAsciiString(getTextOfNode(textSourceNode))}"`;
}
else {
return getLiteralTextOfNode(textSourceNode, neverAsciiEscape);
return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape);
}
}
return getLiteralText(node, currentSourceFile!, neverAsciiEscape);
return getLiteralText(node, currentSourceFile!, neverAsciiEscape, jsxAttributeEscape);
}
/**

View File

@ -5842,6 +5842,7 @@ namespace ts {
MappedTypeParameter, // Emitting a TypeParameterDeclaration inside of a MappedTypeNode
Unspecified, // Emitting an otherwise unspecified node
EmbeddedStatement, // Emitting an embedded statement
JsxAttributeValue, // Emitting a JSX attribute value
}
/* @internal */

View File

@ -551,7 +551,7 @@ namespace ts {
return emitNode && emitNode.flags || 0;
}
export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined) {
export function getLiteralText(node: LiteralLikeNode, sourceFile: SourceFile, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean) {
// If we don't need to downlevel and we can reach the original source text using
// the node's parent reference, then simply get the text as it was originally written.
if (!nodeIsSynthesized(node) && node.parent && !(
@ -561,24 +561,29 @@ namespace ts {
return getSourceTextOfNodeFromSourceFile(sourceFile, node);
}
// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
// had to include a backslash: `not \${a} substitution`.
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString : escapeNonAsciiString;
// If we can't reach the original source text, use the canonical form if it's a number,
// or a (possibly escaped) quoted form of the original text if it's string-like.
switch (node.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.StringLiteral: {
const escapeText = jsxAttributeEscape ? escapeJsxAttributeString :
neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString :
escapeNonAsciiString;
if ((<StringLiteral>node).singleQuote) {
return "'" + escapeText(node.text, CharacterCodes.singleQuote) + "'";
}
else {
return '"' + escapeText(node.text, CharacterCodes.doubleQuote) + '"';
}
}
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.TemplateHead:
case SyntaxKind.TemplateMiddle:
case SyntaxKind.TemplateTail:
case SyntaxKind.TemplateTail: {
// If a NoSubstitutionTemplateLiteral appears to have a substitution in it, the original text
// had to include a backslash: `not \${a} substitution`.
const escapeText = neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? escapeString :
escapeNonAsciiString;
const rawText = (<TemplateLiteralLikeNode>node).rawText || escapeTemplateSubstitution(escapeText(node.text, CharacterCodes.backtick));
switch (node.kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
@ -591,6 +596,7 @@ namespace ts {
return "}" + rawText + "`";
}
break;
}
case SyntaxKind.NumericLiteral:
case SyntaxKind.BigIntLiteral:
case SyntaxKind.RegularExpressionLiteral:
@ -3384,6 +3390,25 @@ namespace ts {
"\u0085": "\\u0085" // nextLine
});
function encodeUtf16EscapeSequence(charCode: number): string {
const hexCharCode = charCode.toString(16).toUpperCase();
const paddedHexCode = ("0000" + hexCharCode).slice(-4);
return "\\u" + paddedHexCode;
}
function getReplacement(c: string, offset: number, input: string) {
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
const lookAhead = input.charCodeAt(offset + c.length);
if (lookAhead >= CharacterCodes._0 && lookAhead <= CharacterCodes._9) {
// 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)
return "\\x00";
}
// Otherwise, keep printing a literal \0 for the null character
return "\\0";
}
return escapedCharsMap.get(c) || encodeUtf16EscapeSequence(c.charCodeAt(0));
}
/**
* Based heavily on the abstract 'Quote'/'QuoteJSONString' operation from ECMA-262 (24.3.2.2),
* but augmented for a few select characters (e.g. lineSeparator, paragraphSeparator, nextLine)
@ -3397,6 +3422,46 @@ namespace ts {
return s.replace(escapedCharsRegExp, getReplacement);
}
const nonAsciiCharacters = /[^\u0000-\u007F]/g;
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
s = escapeString(s, quoteChar);
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
// Otherwise just return the original string.
return nonAsciiCharacters.test(s) ?
s.replace(nonAsciiCharacters, c => encodeUtf16EscapeSequence(c.charCodeAt(0))) :
s;
}
// This consists of the first 19 unprintable ASCII characters, JSX canonical escapes, lineSeparator,
// paragraphSeparator, and nextLine. The latter three are just desirable to suppress new lines in
// the language service. These characters should be escaped when printing, and if any characters are added,
// the map below must be updated.
const jsxDoubleQuoteEscapedCharsRegExp = /[\"\u0000-\u001f\u2028\u2029\u0085]/g;
const jsxSingleQuoteEscapedCharsRegExp = /[\'\u0000-\u001f\u2028\u2029\u0085]/g;
const jsxEscapedCharsMap = createMapFromTemplate({
"\"": "&quot;",
"\'": "&apos;"
});
function encodeJsxCharacterEntity(charCode: number): string {
const hexCharCode = charCode.toString(16).toUpperCase();
return "&#x" + hexCharCode + ";";
}
function getJsxAttributeStringReplacement(c: string) {
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
return "&#0;";
}
return jsxEscapedCharsMap.get(c) || encodeJsxCharacterEntity(c.charCodeAt(0));
}
export function escapeJsxAttributeString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote) {
const escapedCharsRegExp =
quoteChar === CharacterCodes.singleQuote ? jsxSingleQuoteEscapedCharsRegExp :
jsxDoubleQuoteEscapedCharsRegExp;
return s.replace(escapedCharsRegExp, getJsxAttributeStringReplacement);
}
/**
* Strip off existed surrounding single quotes, double quotes, or backticks from a given string
*
@ -3416,40 +3481,11 @@ namespace ts {
charCode === CharacterCodes.backtick;
}
function getReplacement(c: string, offset: number, input: string) {
if (c.charCodeAt(0) === CharacterCodes.nullCharacter) {
const lookAhead = input.charCodeAt(offset + c.length);
if (lookAhead >= CharacterCodes._0 && lookAhead <= CharacterCodes._9) {
// 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)
return "\\x00";
}
// Otherwise, keep printing a literal \0 for the null character
return "\\0";
}
return escapedCharsMap.get(c) || get16BitUnicodeEscapeSequence(c.charCodeAt(0));
}
export function isIntrinsicJsxName(name: __String | string) {
const ch = (name as string).charCodeAt(0);
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-");
}
function get16BitUnicodeEscapeSequence(charCode: number): string {
const hexCharCode = charCode.toString(16).toUpperCase();
const paddedHexCode = ("0000" + hexCharCode).slice(-4);
return "\\u" + paddedHexCode;
}
const nonAsciiCharacters = /[^\u0000-\u007F]/g;
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
s = escapeString(s, quoteChar);
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
// Otherwise just return the original string.
return nonAsciiCharacters.test(s) ?
s.replace(nonAsciiCharacters, c => get16BitUnicodeEscapeSequence(c.charCodeAt(0))) :
s;
}
const indentStrings: string[] = ["", " "];
export function getIndentString(level: number) {
if (indentStrings[level] === undefined) {

View File

@ -11,56 +11,58 @@ namespace ts {
describe("printFile", () => {
const printsCorrectly = makePrintsCorrectly("printsFileCorrectly");
// Avoid eagerly creating the sourceFile so that `createSourceFile` doesn't run unless one of these tests is run.
let sourceFile: SourceFile;
before(() => {
sourceFile = createSourceFile("source.ts", `
interface A<T> {
// comment1
readonly prop?: T;
describe("comment handling", () => {
// Avoid eagerly creating the sourceFile so that `createSourceFile` doesn't run unless one of these tests is run.
let sourceFile: SourceFile;
before(() => {
sourceFile = createSourceFile("source.ts", `
interface A<T> {
// comment1
readonly prop?: T;
// comment2
method(): void;
// comment2
method(): void;
// comment3
new <T>(): A<T>;
// comment3
new <T>(): A<T>;
// comment4
<T>(): A<T>;
}
// comment4
<T>(): A<T>;
}
// comment5
type B = number | string | object;
type C = A<number> & { x: string; }; // comment6
// comment5
type B = number | string | object;
type C = A<number> & { x: string; }; // comment6
// comment7
enum E1 {
// comment8
first
}
// comment7
enum E1 {
// comment8
first
}
const enum E2 {
second
}
const enum E2 {
second
}
// comment9
console.log(1 + 2);
// comment9
console.log(1 + 2);
// comment10
function functionWithDefaultArgValue(argument: string = "defaultValue"): void { }
`, ScriptTarget.ES2015);
// comment10
function functionWithDefaultArgValue(argument: string = "defaultValue"): void { }
`, ScriptTarget.ES2015);
});
printsCorrectly("default", {}, printer => printer.printFile(sourceFile));
printsCorrectly("removeComments", { removeComments: true }, printer => printer.printFile(sourceFile));
});
printsCorrectly("default", {}, printer => printer.printFile(sourceFile));
printsCorrectly("removeComments", { removeComments: true }, printer => printer.printFile(sourceFile));
// github #14948
// https://github.com/microsoft/TypeScript/issues/14948
// eslint-disable-next-line no-template-curly-in-string
printsCorrectly("templateLiteral", {}, printer => printer.printFile(createSourceFile("source.ts", "let greeting = `Hi ${name}, how are you?`;", ScriptTarget.ES2017)));
// github #18071
// https://github.com/microsoft/TypeScript/issues/18071
printsCorrectly("regularExpressionLiteral", {}, printer => printer.printFile(createSourceFile("source.ts", "let regex = /abc/;", ScriptTarget.ES2017)));
// github #22239
// https://github.com/microsoft/TypeScript/issues/22239
printsCorrectly("importStatementRemoveComments", { removeComments: true }, printer => printer.printFile(createSourceFile("source.ts", "import {foo} from 'foo';", ScriptTarget.ESNext)));
printsCorrectly("classHeritageClauses", {}, printer => printer.printFile(createSourceFile(
"source.ts",
@ -68,16 +70,28 @@ namespace ts {
ScriptTarget.ES2017
)));
// github #35093
// https://github.com/microsoft/TypeScript/issues/35093
printsCorrectly("definiteAssignmentAssertions", {}, printer => printer.printFile(createSourceFile(
"source.ts",
`class A {
prop!: string;
}
let x!: string;`,
ScriptTarget.ES2017
)));
// https://github.com/microsoft/TypeScript/issues/35054
printsCorrectly("jsx attribute escaping", {}, printer => {
debugger;
return printer.printFile(createSourceFile(
"source.ts",
String.raw`<a x='\\"'/>`,
ScriptTarget.ESNext,
/*setParentNodes*/ undefined,
ScriptKind.TSX
));
});
});
describe("printBundle", () => {

View File

@ -2984,7 +2984,8 @@ declare namespace ts {
IdentifierName = 2,
MappedTypeParameter = 3,
Unspecified = 4,
EmbeddedStatement = 5
EmbeddedStatement = 5,
JsxAttributeValue = 6
}
export interface TransformationContext {
/** Gets the compiler options supplied to the transformer. */

View File

@ -2984,7 +2984,8 @@ declare namespace ts {
IdentifierName = 2,
MappedTypeParameter = 3,
Unspecified = 4,
EmbeddedStatement = 5
EmbeddedStatement = 5,
JsxAttributeValue = 6
}
export interface TransformationContext {
/** Gets the compiler options supplied to the transformer. */

View File

@ -0,0 +1 @@
<a x="\\&quot;"/>;