Skip to content

Commit b5feefe

Browse files
cushongoogle-java-format Team
authored and
google-java-format Team
committed
Initial support for string templates
Fixes #981 PiperOrigin-RevId: 591982309
1 parent dc8b461 commit b5feefe

File tree

6 files changed

+93
-13
lines changed

6 files changed

+93
-13
lines changed

core/src/main/java/com/google/googlejavaformat/java/JavaInput.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,14 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept
387387
final boolean isNumbered; // Is this tok numbered? (tokens and comments)
388388
String extraNewline = null; // Extra newline at end?
389389
List<String> strings = new ArrayList<>();
390-
if (Character.isWhitespace(tokText0)) {
390+
if (tokText.startsWith("'")
391+
|| tokText.startsWith("\"")
392+
|| JavacTokens.isStringFragment(t.kind())) {
393+
// Perform this check first, STRINGFRAGMENT tokens can start with arbitrary characters.
394+
isToken = true;
395+
isNumbered = true;
396+
strings.add(originalTokText);
397+
} else if (Character.isWhitespace(tokText0)) {
391398
isToken = false;
392399
isNumbered = false;
393400
Iterator<String> it = Newlines.lineIterator(originalTokText);
@@ -404,10 +411,6 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept
404411
strings.add(line);
405412
}
406413
}
407-
} else if (tokText.startsWith("'") || tokText.startsWith("\"")) {
408-
isToken = true;
409-
isNumbered = true;
410-
strings.add(originalTokText);
411414
} else if (tokText.startsWith("//") || tokText.startsWith("/*")) {
412415
// For compatibility with an earlier lexer, the newline after a // comment is its own tok.
413416
if (tokText.startsWith("//")

core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.googlejavaformat.java;
1616

1717
import static com.google.common.base.Preconditions.checkArgument;
18+
import static java.util.Arrays.stream;
1819

1920
import com.google.common.collect.ImmutableList;
2021
import com.google.common.collect.Lists;
@@ -27,6 +28,7 @@
2728
import com.sun.tools.javac.parser.Tokens.TokenKind;
2829
import com.sun.tools.javac.parser.UnicodeReader;
2930
import com.sun.tools.javac.util.Context;
31+
import java.util.Objects;
3032
import java.util.Set;
3133

3234
/** A wrapper around javac's lexer. */
@@ -71,6 +73,16 @@ public String stringVal() {
7173
}
7274
}
7375

76+
private static final TokenKind STRINGFRAGMENT =
77+
stream(TokenKind.values())
78+
.filter(t -> t.name().contentEquals("STRINGFRAGMENT"))
79+
.findFirst()
80+
.orElse(null);
81+
82+
static boolean isStringFragment(TokenKind kind) {
83+
return STRINGFRAGMENT != null && Objects.equals(kind, STRINGFRAGMENT);
84+
}
85+
7486
/** Lex the input and return a list of {@link RawTok}s. */
7587
public static ImmutableList<RawTok> getTokens(
7688
String source, Context context, Set<TokenKind> stopTokens) {
@@ -106,13 +118,39 @@ public static ImmutableList<RawTok> getTokens(
106118
if (last < t.pos) {
107119
tokens.add(new RawTok(null, null, last, t.pos));
108120
}
109-
tokens.add(
110-
new RawTok(
111-
t.kind == TokenKind.STRINGLITERAL ? "\"" + t.stringVal() + "\"" : null,
112-
t.kind,
113-
t.pos,
114-
t.endPos));
115-
last = t.endPos;
121+
int pos = t.pos;
122+
int endPos = t.endPos;
123+
if (isStringFragment(t.kind)) {
124+
// A string template is tokenized as a series of STRINGFRAGMENT tokens containing the string
125+
// literal values, followed by the tokens for the template arguments. For the formatter, we
126+
// want the stream of tokens to appear in order by their start position, and also to have
127+
// all the content from the original source text (including leading and trailing ", and the
128+
// \ escapes from template arguments). This logic processes the token stream from javac to
129+
// meet those requirements.
130+
while (isStringFragment(t.kind)) {
131+
endPos = t.endPos;
132+
scanner.nextToken();
133+
t = scanner.token();
134+
}
135+
// Read tokens for the string template arguments, until we read the end of the string
136+
// template. The last token in a string template is always a trailing string fragment. Use
137+
// lookahead to defer reading the token after the template until the next iteration of the
138+
// outer loop.
139+
while (scanner.token(/* lookahead= */ 1).endPos < endPos) {
140+
scanner.nextToken();
141+
t = scanner.token();
142+
}
143+
tokens.add(new RawTok(source.substring(pos, endPos), t.kind, pos, endPos));
144+
last = endPos;
145+
} else {
146+
tokens.add(
147+
new RawTok(
148+
t.kind == TokenKind.STRINGLITERAL ? "\"" + t.stringVal() + "\"" : null,
149+
t.kind,
150+
t.pos,
151+
t.endPos));
152+
last = t.endPos;
153+
}
116154
} while (scanner.token().kind != TokenKind.EOF);
117155
if (last < end) {
118156
tokens.add(new RawTok(null, null, last, end));

core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.sun.source.tree.ExpressionTree;
2424
import com.sun.source.tree.PatternCaseLabelTree;
2525
import com.sun.source.tree.PatternTree;
26+
import com.sun.source.tree.StringTemplateTree;
2627
import javax.lang.model.element.Name;
2728

2829
/**
@@ -60,6 +61,7 @@ public Void visitConstantCaseLabel(ConstantCaseLabelTree node, Void aVoid) {
6061

6162
@Override
6263
public Void visitDeconstructionPattern(DeconstructionPatternTree node, Void unused) {
64+
sync(node);
6365
scan(node.getDeconstructor(), null);
6466
builder.open(plusFour);
6567
token("(");
@@ -78,6 +80,16 @@ public Void visitDeconstructionPattern(DeconstructionPatternTree node, Void unus
7880
return null;
7981
}
8082

83+
@SuppressWarnings("preview")
84+
@Override
85+
public Void visitStringTemplate(StringTemplateTree node, Void aVoid) {
86+
sync(node);
87+
scan(node.getProcessor(), null);
88+
token(".");
89+
token(builder.peekToken().get());
90+
return null;
91+
}
92+
8193
@Override
8294
protected void variableName(Name name) {
8395
if (name.isEmpty()) {

core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ public class FormatterIntegrationTest {
5959
"SwitchDouble",
6060
"SwitchUnderscore",
6161
"I880",
62-
"Unnamed")
62+
"Unnamed",
63+
"I981")
6364
.build();
6465

6566
@Parameters(name = "{index}: {0}")
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Foo {
2+
private static final int X = 42;
3+
private static final String A = STR."\{X} = \{X}";
4+
private static final String B = STR."";
5+
private static final String C = STR."\{X}";
6+
private static final String D = STR."\{X}\{X}";
7+
private static final String E = STR."\{X}\{X}\{X}";
8+
private static final String F = STR." \{X}";
9+
private static final String G = STR."\{X} ";
10+
private static final String H = STR."\{X} one long incredibly unbroken sentence moving from "+"topic to topic so that no-one had a chance to interrupt";
11+
private static final String I = STR."\{X} \uD83D\uDCA9 ";
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Foo {
2+
private static final int X = 42;
3+
private static final String A = STR."\{X} = \{X}";
4+
private static final String B = STR."";
5+
private static final String C = STR."\{X}";
6+
private static final String D = STR."\{X}\{X}";
7+
private static final String E = STR."\{X}\{X}\{X}";
8+
private static final String F = STR." \{X}";
9+
private static final String G = STR."\{X} ";
10+
private static final String H =
11+
STR."\{X} one long incredibly unbroken sentence moving from "
12+
+ "topic to topic so that no-one had a chance to interrupt";
13+
private static final String I = STR."\{X} \uD83D\uDCA9 ";
14+
}

0 commit comments

Comments
 (0)