Skip to content

Commit 38de9c4

Browse files
cushonbutterunderflow
authored and
google-java-format Team
committed
Improve support for string templates
The initial implementation passed through the entire string unmodified, this allows formatting the Java expressions inside the `\{...}`. See #1010 Co-authored-by: butterunderflow <[email protected]> PiperOrigin-RevId: 592940163
1 parent 8afdfca commit 38de9c4

File tree

6 files changed

+103
-29
lines changed

6 files changed

+103
-29
lines changed

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

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
import com.sun.tools.javac.parser.Tokens.TokenKind;
2929
import com.sun.tools.javac.parser.UnicodeReader;
3030
import com.sun.tools.javac.util.Context;
31+
import java.util.ArrayList;
32+
import java.util.Collections;
33+
import java.util.Comparator;
34+
import java.util.HashSet;
35+
import java.util.List;
3136
import java.util.Objects;
3237
import java.util.Set;
3338

@@ -83,22 +88,53 @@ static boolean isStringFragment(TokenKind kind) {
8388
return STRINGFRAGMENT != null && Objects.equals(kind, STRINGFRAGMENT);
8489
}
8590

86-
/** Lex the input and return a list of {@link RawTok}s. */
87-
public static ImmutableList<RawTok> getTokens(
88-
String source, Context context, Set<TokenKind> stopTokens) {
91+
private static ImmutableList<Token> readAllTokens(
92+
String source, Context context, Set<Integer> nonTerminalStringFragments) {
8993
if (source == null) {
9094
return ImmutableList.of();
9195
}
9296
ScannerFactory fac = ScannerFactory.instance(context);
9397
char[] buffer = (source + EOF_COMMENT).toCharArray();
9498
Scanner scanner =
9599
new AccessibleScanner(fac, new CommentSavingTokenizer(fac, buffer, buffer.length));
100+
List<Token> tokens = new ArrayList<>();
101+
do {
102+
scanner.nextToken();
103+
tokens.add(scanner.token());
104+
} while (scanner.token().kind != TokenKind.EOF);
105+
for (int i = 0; i < tokens.size(); i++) {
106+
if (isStringFragment(tokens.get(i).kind)) {
107+
int start = i;
108+
while (isStringFragment(tokens.get(i).kind)) {
109+
i++;
110+
}
111+
for (int j = start; j < i - 1; j++) {
112+
nonTerminalStringFragments.add(tokens.get(j).pos);
113+
}
114+
}
115+
}
116+
// A string template is tokenized as a series of STRINGFRAGMENT tokens containing the string
117+
// literal values, followed by the tokens for the template arguments. For the formatter, we
118+
// want the stream of tokens to appear in order by their start position.
119+
if (Runtime.version().feature() >= 21) {
120+
Collections.sort(tokens, Comparator.comparingInt(t -> t.pos));
121+
}
122+
return ImmutableList.copyOf(tokens);
123+
}
124+
125+
/** Lex the input and return a list of {@link RawTok}s. */
126+
public static ImmutableList<RawTok> getTokens(
127+
String source, Context context, Set<TokenKind> stopTokens) {
128+
if (source == null) {
129+
return ImmutableList.of();
130+
}
131+
Set<Integer> nonTerminalStringFragments = new HashSet<>();
132+
ImmutableList<Token> javacTokens = readAllTokens(source, context, nonTerminalStringFragments);
133+
96134
ImmutableList.Builder<RawTok> tokens = ImmutableList.builder();
97135
int end = source.length();
98136
int last = 0;
99-
do {
100-
scanner.nextToken();
101-
Token t = scanner.token();
137+
for (Token t : javacTokens) {
102138
if (t.comments != null) {
103139
for (Comment c : Lists.reverse(t.comments)) {
104140
if (last < c.getSourcePos(0)) {
@@ -118,27 +154,12 @@ public static ImmutableList<RawTok> getTokens(
118154
if (last < t.pos) {
119155
tokens.add(new RawTok(null, null, last, t.pos));
120156
}
121-
int pos = t.pos;
122-
int endPos = t.endPos;
123157
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();
158+
int endPos = t.endPos;
159+
int pos = t.pos;
160+
if (nonTerminalStringFragments.contains(t.pos)) {
161+
// Include the \ escape from \{...} in the preceding string fragment
162+
endPos++;
142163
}
143164
tokens.add(new RawTok(source.substring(pos, endPos), t.kind, pos, endPos));
144165
last = endPos;
@@ -151,7 +172,7 @@ public static ImmutableList<RawTok> getTokens(
151172
t.endPos));
152173
last = t.endPos;
153174
}
154-
} while (scanner.token().kind != TokenKind.EOF);
175+
}
155176
if (last < end) {
156177
tokens.add(new RawTok(null, null, last, end));
157178
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,20 @@ public Void visitDeconstructionPattern(DeconstructionPatternTree node, Void unus
8282

8383
@SuppressWarnings("preview")
8484
@Override
85-
public Void visitStringTemplate(StringTemplateTree node, Void aVoid) {
85+
public Void visitStringTemplate(StringTemplateTree node, Void unused) {
8686
sync(node);
87+
builder.open(plusFour);
8788
scan(node.getProcessor(), null);
8889
token(".");
8990
token(builder.peekToken().get());
91+
for (int i = 0; i < node.getFragments().size() - 1; i++) {
92+
token("{");
93+
builder.breakOp();
94+
scan(node.getExpressions().get(i), null);
95+
token("}");
96+
token(builder.peekToken().get());
97+
}
98+
builder.close();
9099
return null;
91100
}
92101

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ public class FormatterIntegrationTest {
6060
"SwitchUnderscore",
6161
"I880",
6262
"Unnamed",
63-
"I981")
63+
"I981",
64+
"StringTemplate")
6465
.build();
6566

6667
@Parameters(name = "{index}: {0}")

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static com.google.common.truth.Truth.assertWithMessage;
1919
import static java.nio.charset.StandardCharsets.UTF_8;
2020
import static org.junit.Assert.fail;
21+
import static org.junit.Assume.assumeTrue;
2122

2223
import com.google.common.base.Joiner;
2324
import com.google.common.io.CharStreams;
@@ -492,4 +493,27 @@ public void removeTrailingTabsInComments() throws Exception {
492493
+ " }\n"
493494
+ "}\n");
494495
}
496+
497+
@Test
498+
public void stringTemplateTests() throws Exception {
499+
assumeTrue(Runtime.version().feature() >= 21);
500+
assertThat(
501+
new Formatter()
502+
.formatSource(
503+
"public class Foo {\n"
504+
+ " String test(){\n"
505+
+ " var simple = STR.\"mytemplate1XXXX \\{exampleXXXX.foo()}yyy\";\n"
506+
+ " var nested = STR.\"template \\{example. foo()+"
507+
+ " STR.\"templateInner\\{ example}\"}xxx }\";\n"
508+
+ " }\n"
509+
+ "}\n"))
510+
.isEqualTo(
511+
"public class Foo {\n"
512+
+ " String test() {\n"
513+
+ " var simple = STR.\"mytemplate1XXXX \\{exampleXXXX.foo()}yyy\";\n"
514+
+ " var nested = STR.\"template \\{example.foo() +"
515+
+ " STR.\"templateInner\\{example}\"}xxx }\";\n"
516+
+ " }\n"
517+
+ "}\n");
518+
}
495519
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
public class StringTemplates {
2+
void test(){
3+
var m = STR."template \{example}xxx";
4+
var nested = STR."template \{example.foo()+ STR."templateInner\{example}"}xxx }";
5+
var nestNested = STR."template \{example0.
6+
foo() +
7+
STR."templateInner\{example1.test(STR."\{example2
8+
}")}"}xxx }";
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public class StringTemplates {
2+
void test() {
3+
var m = STR."template \{example}xxx";
4+
var nested = STR."template \{example.foo() + STR."templateInner\{example}"}xxx }";
5+
var nestNested =
6+
STR."template \{
7+
example0.foo() + STR."templateInner\{example1.test(STR."\{example2}")}"}xxx }";
8+
}
9+
}

0 commit comments

Comments
 (0)