Skip to content

Commit e946e82

Browse files
cushongoogle-java-format Team
authored and
google-java-format Team
committed
Work around a crash on comments inside string template arguments
PiperOrigin-RevId: 609732753
1 parent ce3cb59 commit e946e82

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

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

Lines changed: 56 additions & 6 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 com.google.common.base.Preconditions.checkElementIndex;
1819
import static java.util.Arrays.stream;
1920

2021
import com.google.common.collect.ImmutableList;
@@ -28,16 +29,18 @@
2829
import com.sun.tools.javac.parser.Tokens.TokenKind;
2930
import com.sun.tools.javac.parser.UnicodeReader;
3031
import com.sun.tools.javac.util.Context;
32+
import java.lang.reflect.Method;
3133
import java.util.ArrayList;
3234
import java.util.Collections;
3335
import java.util.Comparator;
3436
import java.util.HashSet;
3537
import java.util.List;
3638
import java.util.Objects;
3739
import java.util.Set;
40+
import org.checkerframework.checker.nullness.qual.Nullable;
3841

3942
/** A wrapper around javac's lexer. */
40-
class JavacTokens {
43+
final class JavacTokens {
4144

4245
/** The lexer eats terminal comments, so feed it one we don't care about. */
4346
// TODO(b/33103797): fix javac and remove the work-around
@@ -51,6 +54,8 @@ static class RawTok {
5154
private final int endPos;
5255

5356
RawTok(String stringVal, TokenKind kind, int pos, int endPos) {
57+
checkElementIndex(pos, endPos, "pos");
58+
checkArgument(pos < endPos, "expected pos (%s) < endPos (%s)", pos, endPos);
5459
this.stringVal = stringVal;
5560
this.kind = kind;
5661
this.pos = pos;
@@ -136,13 +141,30 @@ public static ImmutableList<RawTok> getTokens(
136141
int last = 0;
137142
for (Token t : javacTokens) {
138143
if (t.comments != null) {
144+
// javac accumulates comments in reverse order
139145
for (Comment c : Lists.reverse(t.comments)) {
140-
if (last < c.getSourcePos(0)) {
141-
tokens.add(new RawTok(null, null, last, c.getSourcePos(0)));
146+
int pos = c.getSourcePos(0);
147+
int length;
148+
if (pos == -1) {
149+
// We've found a comment whose position hasn't been recorded. Deduce its position as the
150+
// first `/` character after the end of the previous token.
151+
//
152+
// javac creates a new JavaTokenizer to process string template arguments, so
153+
// CommentSavingTokenizer doesn't get a chance to preprocess those comments and save
154+
// their text and positions.
155+
//
156+
// TODO: consider always using this approach once the minimum supported JDK is 16 and
157+
// we can assume BasicComment#getRawCharacters is always available.
158+
pos = source.indexOf('/', last);
159+
length = CommentSavingTokenizer.commentLength(c);
160+
} else {
161+
length = c.getText().length();
142162
}
143-
tokens.add(
144-
new RawTok(null, null, c.getSourcePos(0), c.getSourcePos(0) + c.getText().length()));
145-
last = c.getSourcePos(0) + c.getText().length();
163+
if (last < pos) {
164+
tokens.add(new RawTok(null, null, last, pos));
165+
}
166+
tokens.add(new RawTok(null, null, pos, pos + length));
167+
last = pos + length;
146168
}
147169
}
148170
if (stopTokens.contains(t.kind)) {
@@ -181,6 +203,32 @@ public static ImmutableList<RawTok> getTokens(
181203

182204
/** A {@link JavaTokenizer} that saves comments. */
183205
static class CommentSavingTokenizer extends JavaTokenizer {
206+
207+
private static final Method GET_RAW_CHARACTERS_METHOD = getRawCharactersMethod();
208+
209+
private static @Nullable Method getRawCharactersMethod() {
210+
try {
211+
// This is a method in PositionTrackingReader, but that class is not public.
212+
return BasicComment.class.getMethod("getRawCharacters");
213+
} catch (NoSuchMethodException e) {
214+
return null;
215+
}
216+
}
217+
218+
static int commentLength(Comment comment) {
219+
if (comment instanceof BasicComment && GET_RAW_CHARACTERS_METHOD != null) {
220+
// If we've seen a BasicComment instead of a CommentWithTextAndPosition, getText() will
221+
// be null, so we deduce the length using getRawCharacters. See also the comment at the
222+
// usage of this method in getTokens.
223+
try {
224+
return ((char[]) GET_RAW_CHARACTERS_METHOD.invoke(((BasicComment) comment))).length;
225+
} catch (ReflectiveOperationException e) {
226+
throw new LinkageError(e.getMessage(), e);
227+
}
228+
}
229+
return comment.getText().length();
230+
}
231+
184232
CommentSavingTokenizer(ScannerFactory fac, char[] buffer, int length) {
185233
super(fac, buffer, length);
186234
}
@@ -288,4 +336,6 @@ protected AccessibleReader(ScannerFactory fac, char[] buffer, int length) {
288336
super(fac, buffer, length);
289337
}
290338
}
339+
340+
private JavacTokens() {}
291341
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ public class FormatterIntegrationTest {
6363
"I981",
6464
"StringTemplate",
6565
"I1020",
66-
"I1037")
66+
"I1037",
67+
"TextBlockTemplates")
6768
.build();
6869

6970
@Parameters(name = "{index}: {0}")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
abstract class RSLs {
2+
abstract String f();
3+
4+
String a = STR."""
5+
lorem
6+
foo\{ /** a method */ f( // line comment
7+
/* TODO */
8+
)}bar
9+
ipsum
10+
""";
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
abstract class RSLs {
2+
abstract String f();
3+
4+
String a =
5+
STR."""
6+
lorem
7+
foo\{
8+
/** a method */
9+
f( // line comment
10+
/* TODO */
11+
)}bar
12+
ipsum
13+
""";
14+
}

0 commit comments

Comments
 (0)