Skip to content

Commit 38de9c4

Browse files
cushonbutterunderflow
authored andcommitted
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)