Skip to content

Commit

Permalink
fix(rosetta): correctly emit multi-line string literals (#2419)
Browse files Browse the repository at this point in the history
This fixes hox multi-line string literals and template expressions are
rendered in all supported languages, leveraging idiomatic multi-line
literals where possible (i.e: everywhere but Java).



---

By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
  • Loading branch information
RomainMuller authored Jan 26, 2021
1 parent aeb624d commit a30a996
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 48 deletions.
31 changes: 24 additions & 7 deletions packages/jsii-rosetta/lib/languages/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class CSharpVisitor extends DefaultVisitor<CSharpLanguageContext> {
}

public identifier(
node: ts.Identifier | ts.StringLiteral,
node: ts.Identifier | ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
renderer: CSharpRenderer,
) {
let text = node.text;
Expand Down Expand Up @@ -339,12 +339,16 @@ export class CSharpVisitor extends DefaultVisitor<CSharpLanguageContext> {
}

public stringLiteral(
node: ts.StringLiteral,
node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
renderer: CSharpRenderer,
): OTree {
if (renderer.currentContext.stringAsIdentifier) {
return this.identifier(node, renderer);
}
if (node.text.includes('\n')) {
// Multi-line string literals (@"string") in C# do not do escaping. Only " needs to be doubled.
return new OTree(['@"', node.text.replace(/"/g, '""'), '"']);
}
return new OTree([JSON.stringify(node.text)]);
}

Expand Down Expand Up @@ -740,19 +744,32 @@ export class CSharpVisitor extends DefaultVisitor<CSharpLanguageContext> {
node: ts.TemplateExpression,
context: CSharpRenderer,
): OTree {
const parts = ['$"'];
// If this is a multi-line string literal, we need not quote much, as @"string" literals in C#
// do not perform any quoting. The literal quotes in the text however must be doubled.
const isMultiLine =
!!node.head.rawText?.includes('\n') ||
node.templateSpans.some((span) => span.literal.rawText?.includes('\n'));

const parts = new Array<string>();
if (node.head.rawText) {
parts.push(quoteStringLiteral(node.head.rawText));
parts.push(
isMultiLine
? node.head.rawText.replace(/"/g, '""')
: quoteStringLiteral(node.head.rawText),
);
}
for (const span of node.templateSpans) {
parts.push(`{${context.textOf(span.expression)}}`);
if (span.literal.rawText) {
parts.push(quoteStringLiteral(span.literal.rawText));
parts.push(
isMultiLine
? span.literal.rawText.replace(/"/g, '""')
: quoteStringLiteral(span.literal.rawText),
);
}
}
parts.push('"');

return new OTree([parts.join('')]);
return new OTree([isMultiLine ? '$@"' : '$"', ...parts, '"']);
}

protected argumentList(
Expand Down
11 changes: 2 additions & 9 deletions packages/jsii-rosetta/lib/languages/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,8 @@ export abstract class DefaultVisitor<C> implements AstHandler<C> {
}

public stringLiteral(
node: ts.StringLiteral,
_children: AstRenderer<C>,
): OTree {
return new OTree([JSON.stringify(node.text)]);
}

public noSubstitutionTemplateLiteral(
node: ts.NoSubstitutionTemplateLiteral,
_context: AstRenderer<C>,
node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
_renderer: AstRenderer<C>,
): OTree {
return new OTree([JSON.stringify(node.text)]);
}
Expand Down
21 changes: 14 additions & 7 deletions packages/jsii-rosetta/lib/languages/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,12 +476,14 @@ export class JavaVisitor extends DefaultVisitor<JavaContext> {
}

if (parameters.length === 0) {
return new OTree([`"${quoteStringLiteral(template)}"`]);
return new OTree([JSON.stringify(quoteStringLiteral(template))]);
}

return new OTree([
'String.format(',
`"${quoteStringLiteral(template)}"`,
`"${quoteStringLiteral(template)
// Java does not have multiline string literals, so we must replace literal newlines with %n
.replace(/\n/g, '%n')}"`,
...parameters.reduce(
(list, element) => list.concat(', ', element),
new Array<string | OTree>(),
Expand Down Expand Up @@ -677,14 +679,19 @@ export class JavaVisitor extends DefaultVisitor<JavaContext> {
return new OTree(parts);
}

public stringLiteral(node: ts.StringLiteral, renderer: JavaRenderer): OTree {
return renderer.currentContext.stringLiteralAsIdentifier
? this.identifier(node, renderer)
: super.stringLiteral(node, renderer);
public stringLiteral(
node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
renderer: JavaRenderer,
): OTree {
if (renderer.currentContext.stringLiteralAsIdentifier) {
return this.identifier(node, renderer);
}
// Java does not have multiline string literals, so we must replace literal newlines with \n
return new OTree([JSON.stringify(node.text).replace(/\n/g, '\\n')]);
}

public identifier(
node: ts.Identifier | ts.StringLiteral,
node: ts.Identifier | ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
renderer: JavaRenderer,
): OTree {
const nodeText = node.text;
Expand Down
26 changes: 23 additions & 3 deletions packages/jsii-rosetta/lib/languages/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,30 @@ export class PythonVisitor extends DefaultVisitor<PythonLanguageContext> {
return context.convert(node.expression);
}

public stringLiteral(
node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
_context: PythonVisitorContext,
): OTree {
const rawText = node.text;
if (rawText.includes('\n')) {
return new OTree([
'"""',
rawText
// Escape all occurrences of back-slash once more
.replace(/\\/g, '\\\\')
// Escape only the first one in triple-quotes
.replace(/"""/g, '\\"""'),
'"""',
]);
}
return new OTree([JSON.stringify(rawText)]);
}

public templateExpression(
node: ts.TemplateExpression,
context: PythonVisitorContext,
): OTree {
const parts = ['f"'];
const parts = new Array<string>();
if (node.head.rawText) {
parts.push(quoteStringLiteral(node.head.rawText));
}
Expand All @@ -714,9 +733,10 @@ export class PythonVisitor extends DefaultVisitor<PythonLanguageContext> {
parts.push(quoteStringLiteral(span.literal.rawText));
}
}
parts.push('"');

return new OTree([parts.join('')]);
const quote = parts.some((part) => part.includes('\n')) ? '"""' : '"';

return new OTree([`f${quote}`, ...parts, quote]);
}

public maskingVoidExpression(
Expand Down
9 changes: 1 addition & 8 deletions packages/jsii-rosetta/lib/languages/visualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class VisualizeAstVisitor implements AstHandler<void> {
}

public stringLiteral(
node: ts.StringLiteral,
node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
children: AstRenderer<void>,
): OTree {
return this.defaultNode('stringLiteral', node, children);
Expand Down Expand Up @@ -290,13 +290,6 @@ export class VisualizeAstVisitor implements AstHandler<void> {
return this.defaultNode('maskingVoidExpression', node, context);
}

public noSubstitutionTemplateLiteral(
node: ts.NoSubstitutionTemplateLiteral,
context: AstRenderer<void>,
): OTree {
return this.defaultNode('noSubstitutionTemplateLiteral', node, context);
}

private defaultNode(
handlerName: string,
node: ts.Node,
Expand Down
14 changes: 5 additions & 9 deletions packages/jsii-rosetta/lib/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class AstRenderer<C> {
this,
);
}
if (ts.isStringLiteral(tree)) {
if (ts.isStringLiteral(tree) || ts.isNoSubstitutionTemplateLiteral(tree)) {
return visitor.stringLiteral(tree, this);
}
if (ts.isFunctionDeclaration(tree)) {
Expand Down Expand Up @@ -295,9 +295,6 @@ export class AstRenderer<C> {
if (ts.isExpressionStatement(tree)) {
return visitor.expressionStatement(tree, this);
}
if (ts.isNoSubstitutionTemplateLiteral(tree)) {
return visitor.noSubstitutionTemplateLiteral(tree, this);
}
if (ts.isToken(tree)) {
return visitor.token(tree, this);
}
Expand Down Expand Up @@ -480,7 +477,10 @@ export interface AstHandler<C> {
sourceFile(node: ts.SourceFile, context: AstRenderer<C>): OTree;
commentRange(node: CommentSyntax, context: AstRenderer<C>): OTree;
importStatement(node: ImportStatement, context: AstRenderer<C>): OTree;
stringLiteral(node: ts.StringLiteral, children: AstRenderer<C>): OTree;
stringLiteral(
node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral,
children: AstRenderer<C>,
): OTree;
functionDeclaration(
node: ts.FunctionDeclaration,
children: AstRenderer<C>,
Expand Down Expand Up @@ -572,10 +572,6 @@ export interface AstHandler<C> {
node: ts.VoidExpression,
context: AstRenderer<C>,
): OTree;
noSubstitutionTemplateLiteral(
node: ts.NoSubstitutionTemplateLiteral,
context: AstRenderer<C>,
): OTree;

// Not a node, called when we recognize a spread element/assignment that is only
// '...' and nothing else.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
string x = "world";
string y = "well";
Console.WriteLine($"Hello, {x}, it works {y}!");
Console.WriteLine($"Hello, {x}, it works {y}!");

// And now a multi-line expression
Console.WriteLine($@"
Hello, {x}.
It works {y}!
");
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
String x = "world";
String y = "well";
System.out.println(String.format("Hello, %s, it works %s!", x, y));

// And now a multi-line expression
System.out.println(String.format("%nHello, %s.%n%nIt works %s!%n", x, y));
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
x = "world"
y = "well"
print(f"Hello, {x}, it works {y}!")
print(f"Hello, {x}, it works {y}!")

# And now a multi-line expression
print(f"""
Hello, {x}.
It works {y}!
""")
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
const x = "world";
const y = "well";
console.log(`Hello, ${x}, it works ${y}!`);
const x = 'world';
const y = 'well';
console.log(`Hello, ${x}, it works ${y}!`);

// And now a multi-line expression
console.log(`
Hello, ${x}.
It works ${y}!
`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
string literal = @"
This si a multiline string literal.
""It's cool!"".
YEAH BABY!!
Litteral \n right here (not a newline!)
";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
String literal = "\nThis si a multiline string literal.\n\n\"It's cool!\".\n\nYEAH BABY!!\n\nLitteral \\n right here (not a newline!)\n";
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
literal = """
This si a multiline string literal.
"It's cool!".
YEAH BABY!!
Litteral \\n right here (not a newline!)
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const literal = `
This si a multiline string literal.
"It's cool!".
YEAH BABY!!
Litteral \\n right here (not a newline!)
`;

0 comments on commit a30a996

Please sign in to comment.