mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-05 08:11:30 -06:00
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:
parent
8ed129771f
commit
f24f36350b
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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({
|
||||
"\"": """,
|
||||
"\'": "'"
|
||||
});
|
||||
|
||||
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 "�";
|
||||
}
|
||||
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) {
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<a x="\\""/>;
|
||||
Loading…
x
Reference in New Issue
Block a user