Skip to content

Commit ba1fbba

Browse files
graememorganError Prone Team
authored and
Error Prone Team
committed
Inliner: parse the expression and use the AST to construct a replacement by performing careful, AST-backed surgery on the replacement expression tree.
This fixes several bugs we have tests before even getting to the one I want to fix. PiperOrigin-RevId: 733026076
1 parent e71e0dd commit ba1fbba

File tree

3 files changed

+186
-140
lines changed

3 files changed

+186
-140
lines changed

check_api/src/main/java/com/google/errorprone/fixes/AppliedFix.java

+14-9
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,20 @@ public Applier(CharSequence source, EndPosTable endPositions) {
7070
return null;
7171
}
7272

73+
String replaced = applyReplacements(suggestedFix);
74+
75+
// Find the changed line containing the first edit
76+
String snippet = firstEditedLine(replaced, Iterables.get(replacements, 0));
77+
if (snippet.isEmpty()) {
78+
return new AppliedFix("to remove this line", /* isRemoveLine= */ true);
79+
}
80+
return new AppliedFix(snippet, /* isRemoveLine= */ false);
81+
}
82+
83+
public String applyReplacements(Fix fix) {
7384
StringBuilder replaced = new StringBuilder();
7485
int positionInOriginal = 0;
75-
for (Replacement repl : replacements) {
86+
for (Replacement repl : fix.getReplacements(endPositions)) {
7687
checkArgument(
7788
repl.endPosition() <= source.length(),
7889
"End [%s] should not exceed source length [%s]",
@@ -88,13 +99,7 @@ public Applier(CharSequence source, EndPosTable endPositions) {
8899
}
89100
// Flush out any remaining content after the final change
90101
replaced.append(source, positionInOriginal, source.length());
91-
92-
// Find the changed line containing the first edit
93-
String snippet = firstEditedLine(replaced, Iterables.get(replacements, 0));
94-
if (snippet.isEmpty()) {
95-
return new AppliedFix("to remove this line", /* isRemoveLine= */ true);
96-
}
97-
return new AppliedFix(snippet, /* isRemoveLine= */ false);
102+
return replaced.toString();
98103
}
99104

100105
/** Get the replacements in an appropriate order to apply correctly. */
@@ -110,7 +115,7 @@ private static ImmutableSet<Replacement> ascending(Set<Replacement> set) {
110115
* Error Prone have already been transformed from platform line endings to newlines (and even if
111116
* it didn't, the dangling \r characters would be handled by a trim() call).
112117
*/
113-
private static String firstEditedLine(StringBuilder content, Replacement firstEdit) {
118+
private static String firstEditedLine(String content, Replacement firstEdit) {
114119
// We subtract 1 here because we want to find the first newline *before* the edit, not one
115120
// at its beginning.
116121
int startOfFirstEditedLine = content.lastIndexOf("\n", firstEdit.startPosition() - 1);

core/src/main/java/com/google/errorprone/bugpatterns/inlineme/Inliner.java

+136-95
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
2222
import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
2323
import static com.google.errorprone.util.ASTHelpers.getReceiver;
24+
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
2425
import static com.google.errorprone.util.ASTHelpers.getSymbol;
2526
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
2627
import static com.google.errorprone.util.ASTHelpers.hasDirectAnnotationWithSimpleName;
@@ -36,33 +37,36 @@
3637
import com.google.common.collect.ImmutableList;
3738
import com.google.common.collect.ImmutableSet;
3839
import com.google.common.collect.Iterables;
39-
import com.google.common.collect.Range;
40-
import com.google.common.collect.RangeMap;
41-
import com.google.common.collect.TreeRangeMap;
4240
import com.google.errorprone.BugPattern;
4341
import com.google.errorprone.ErrorProneFlags;
4442
import com.google.errorprone.VisitorState;
4543
import com.google.errorprone.bugpatterns.BugChecker;
4644
import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher;
4745
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
4846
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
47+
import com.google.errorprone.fixes.AppliedFix;
4948
import com.google.errorprone.fixes.SuggestedFix;
5049
import com.google.errorprone.fixes.SuggestedFixes;
5150
import com.google.errorprone.matchers.Description;
5251
import com.google.errorprone.util.MoreAnnotations;
5352
import com.sun.source.tree.ExpressionStatementTree;
5453
import com.sun.source.tree.ExpressionTree;
54+
import com.sun.source.tree.IdentifierTree;
5555
import com.sun.source.tree.MemberReferenceTree;
5656
import com.sun.source.tree.MethodInvocationTree;
5757
import com.sun.source.tree.NewClassTree;
5858
import com.sun.source.tree.Tree;
59+
import com.sun.source.util.TreeScanner;
5960
import com.sun.tools.javac.code.Attribute;
6061
import com.sun.tools.javac.code.Symbol.MethodSymbol;
61-
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
62-
import java.util.HashMap;
62+
import com.sun.tools.javac.parser.JavacParser;
63+
import com.sun.tools.javac.parser.ParserFactory;
64+
import com.sun.tools.javac.tree.EndPosTable;
65+
import com.sun.tools.javac.tree.JCTree;
66+
import java.util.ArrayList;
6367
import java.util.List;
64-
import java.util.Map;
6568
import java.util.Optional;
69+
import java.util.function.BiConsumer;
6670
import java.util.regex.Matcher;
6771
import java.util.regex.Pattern;
6872
import java.util.stream.Stream;
@@ -213,19 +217,6 @@ private Description match(
213217
&& stringContainsComments(state.getSourceForNode(tree), state.context)) {
214218
return Description.NO_MATCH;
215219
}
216-
217-
SuggestedFix.Builder builder = SuggestedFix.builder();
218-
219-
Map<String, String> typeNames = new HashMap<>();
220-
for (String newImport : inlineMe.get().imports()) {
221-
String typeName = Iterables.getLast(PACKAGE_SPLITTER.split(newImport));
222-
String qualifiedTypeName = SuggestedFixes.qualifyType(state, builder, newImport);
223-
typeNames.put(typeName, qualifiedTypeName);
224-
}
225-
for (String newStaticImport : inlineMe.get().staticImports()) {
226-
builder.addStaticImport(newStaticImport);
227-
}
228-
229220
ImmutableList<String> varNames =
230221
symbol.getParameters().stream()
231222
.map(varSymbol -> varSymbol.getSimpleName().toString())
@@ -259,18 +250,55 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) {
259250
}
260251

261252
String replacement = inlineMe.get().replacement();
262-
int replacementStart = ((DiagnosticPosition) tree).getStartPosition();
253+
254+
JavacParser parser =
255+
ParserFactory.instance(state.context)
256+
.newParser(
257+
replacement,
258+
/* keepDocComments= */ true,
259+
/* keepEndPos= */ true,
260+
/* keepLineMap= */ true);
261+
var replacementExpression = parser.parseExpression();
262+
SuggestedFix.Builder replacementFixes = SuggestedFix.builder();
263+
264+
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
265+
266+
for (String newImport : inlineMe.get().imports()) {
267+
String typeName = Iterables.getLast(PACKAGE_SPLITTER.split(newImport));
268+
String qualifiedTypeName = SuggestedFixes.qualifyType(state, fixBuilder, newImport);
269+
270+
visitIdentifiers(
271+
replacementExpression,
272+
(node, unused) -> {
273+
if (node.getName().contentEquals(typeName)) {
274+
replacementFixes.replace(node, qualifiedTypeName);
275+
}
276+
});
277+
}
278+
for (String newStaticImport : inlineMe.get().staticImports()) {
279+
fixBuilder.addStaticImport(newStaticImport);
280+
}
281+
282+
int replacementStart = getStartPosition(tree);
263283
int replacementEnd = state.getEndPosition(tree);
264284

265285
// Special case replacements starting with "this." so the receiver portion is not included in
266286
// the replacement. This avoids overlapping replacement regions for fluent chains.
267-
if (replacement.startsWith("this.") && receiver != null) {
287+
boolean removedThisPrefix = replacement.startsWith("this.") && receiver != null;
288+
if (removedThisPrefix) {
289+
replacementFixes.replace(0, "this".length(), "");
268290
replacementStart = state.getEndPosition(receiver);
269-
replacement = replacement.substring("this".length());
270291
}
271292

272293
if (Strings.isNullOrEmpty(receiverString)) {
273-
replacement = replacement.replaceAll("\\bthis\\.\\b", "");
294+
visitIdentifiers(
295+
replacementExpression,
296+
(node, unused) -> {
297+
if (node.getName().contentEquals("this")) {
298+
replacementFixes.replace(
299+
getStartPosition(node), parser.getEndPos((JCTree) node) + 1, "");
300+
}
301+
});
274302
} else {
275303
if (replacement.equals("this")) { // e.g.: foo.b() -> foo
276304
Tree parent = state.getPath().getParentPath().getLeaf();
@@ -281,22 +309,17 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) {
281309
return describe(parent, SuggestedFix.delete(parent), api);
282310
}
283311
}
284-
replacement = replacement.replaceAll("\\bthis\\b", receiverString);
312+
visitIdentifiers(
313+
replacementExpression,
314+
(node, unused) -> {
315+
if ((!removedThisPrefix || getStartPosition(node) != 0)
316+
&& node.getName().contentEquals("this")) {
317+
replacementFixes.replace(
318+
getStartPosition(node), parser.getEndPos((JCTree) node), receiverString);
319+
}
320+
});
285321
}
286322

287-
// Qualify imports first, then replace parameter values to avoid clobbering source from the
288-
// inlined method.
289-
for (Map.Entry<String, String> typeName : typeNames.entrySet()) {
290-
// TODO(b/189535612): we'll need to be smarter about our replacements (to avoid clobbering
291-
// inline parameter comments like /* paramName= */
292-
replacement =
293-
replacement.replaceAll(
294-
"\\b" + Pattern.quote(typeName.getKey()) + "\\b",
295-
Matcher.quoteReplacement(typeName.getValue()));
296-
}
297-
298-
RangeMap<Integer, String> replacementsToApply = TreeRangeMap.create();
299-
300323
for (int i = 0; i < varNames.size(); i++) {
301324
String varName = varNames.get(i);
302325

@@ -305,79 +328,73 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) {
305328
return Description.NO_MATCH;
306329
}
307330

308-
// The replacement logic below assumes the existence of another token after the parameter
309-
// in the replacement string (ex: a trailing parens, comma, dot, etc.). However, in the case
310-
// where the replacement is _just_ one parameter, there isn't a trailing token. We just make
311-
// the direct replacement here.
312-
if (replacement.equals(varName)) {
313-
replacement = callingVarStrings.get(i);
314-
replacementsToApply.clear();
315-
break;
316-
}
317-
318331
// Ex: foo(int a, int... others) -> this.bar(a, others)
319332
// If caller passes 0 args in the varargs position, we want to remove the preceding comma to
320333
// make this.bar(a) (as opposed to "this.bar(a, )"
321334
boolean terminalVarargsReplacement = varargsWithEmptyArguments && i == varNames.size() - 1;
322-
String capturePrefixForVarargs = terminalVarargsReplacement ? "(?:,\\s*)?" : "\\b";
323-
// We want to avoid replacing a method invocation with the same name as the method.
324-
var extractArgAndNextToken =
325-
Pattern.compile(capturePrefixForVarargs + "(" + Pattern.quote(varName) + ")\\b([^(])");
326335
String replacementResult = terminalVarargsReplacement ? "" : callingVarStrings.get(i);
327336
boolean mayRequireParens =
328337
i < callingVars.size() && requiresParentheses(callingVars.get(i), state);
329-
String finalReplacement = replacement;
330-
extractArgAndNextToken
331-
.matcher(replacement)
332-
.results()
333-
.forEach(
334-
mr ->
335-
replacementsToApply.put(
336-
Range.closedOpen(mr.start(0), mr.end(1)),
337-
mightRequireParens(mr.start(1), mr.end(1), finalReplacement)
338-
&& mayRequireParens
339-
? "(" + replacementResult + ")"
340-
: replacementResult));
338+
339+
visitIdentifiers(
340+
replacementExpression,
341+
(node, path) -> {
342+
if (!node.getName().contentEquals(varName)) {
343+
return;
344+
}
345+
// Substituting into a method invocation never requires parens.
346+
boolean outerNeverRequiresParens =
347+
path.size() < 2 || getArguments(path.get(path.size() - 2)).contains(node);
348+
if (terminalVarargsReplacement) {
349+
var calledMethodArguments = getArguments(path.get(path.size() - 2));
350+
replacementFixes.replace(
351+
calledMethodArguments.indexOf(node) == 0
352+
? getStartPosition(node)
353+
: parser.getEndPos(
354+
(JCTree)
355+
calledMethodArguments.get(calledMethodArguments.indexOf(node) - 1)),
356+
parser.getEndPos((JCTree) node),
357+
replacementResult);
358+
} else {
359+
replacementFixes.replace(
360+
node,
361+
!outerNeverRequiresParens && mayRequireParens
362+
? "(" + replacementResult + ")"
363+
: replacementResult);
364+
}
365+
});
341366
}
342367

343-
replacement = applyReplacements(replacement, replacementsToApply);
368+
String fixedReplacement =
369+
AppliedFix.fromSource(
370+
replacement,
371+
new EndPosTable() {
372+
@Override
373+
public int getEndPos(JCTree tree) {
374+
return parser.getEndPos(tree);
375+
}
344376

345-
builder.replace(replacementStart, replacementEnd, replacement);
377+
@Override
378+
public void storeEnd(JCTree tree, int endpos) {}
346379

347-
SuggestedFix fix = builder.build();
380+
@Override
381+
public int replaceTree(JCTree oldtree, JCTree newtree) {
382+
return 0;
383+
}
384+
})
385+
.applyReplacements(replacementFixes.build());
348386

349-
return maybeCheckFixCompiles(tree, state, fix, api);
350-
}
387+
fixBuilder.replace(replacementStart, replacementEnd, fixedReplacement);
351388

352-
/**
353-
* Tries to establish whether substituting an expression into {@code replacement} between {@code
354-
* start} and {@code end} might require parenthesising.
355-
*
356-
* <p>The current heuristic is that the only things that are guaranteed not to are arguments to
357-
* methods, which we infer with string munging.
358-
*/
359-
private static boolean mightRequireParens(int start, int end, String replacement) {
360-
return !LOOKS_LIKE_METHOD_CALL_BEFORE.matcher(replacement.substring(0, start)).matches()
361-
|| !LOOKS_LIKE_METHOD_CALL_AFTER.matcher(replacement.substring(end)).matches();
389+
return maybeCheckFixCompiles(tree, state, fixBuilder.build(), api);
362390
}
363391

364-
private static final Pattern LOOKS_LIKE_METHOD_CALL_BEFORE = Pattern.compile(".*(\\(|,)\\s*$");
365-
366-
private static final Pattern LOOKS_LIKE_METHOD_CALL_AFTER = Pattern.compile("^\\s*(\\)|,).*");
367-
368-
private static String applyReplacements(
369-
String input, RangeMap<Integer, String> replacementsToApply) {
370-
// Replace in ascending order to avoid quadratic behaviour.
371-
int idx = 0;
372-
StringBuilder sb = new StringBuilder();
373-
for (Map.Entry<Range<Integer>, String> entry : replacementsToApply.asMapOfRanges().entrySet()) {
374-
Range<Integer> range = entry.getKey();
375-
String newText = entry.getValue();
376-
sb.append(input, idx, range.lowerEndpoint()).append(newText);
377-
idx = range.upperEndpoint();
378-
}
379-
sb.append(input, idx, input.length());
380-
return sb.toString();
392+
private static List<? extends ExpressionTree> getArguments(Tree tree) {
393+
return switch (tree) {
394+
case MethodInvocationTree mit -> mit.getArguments();
395+
case NewClassTree nct -> nct.getArguments();
396+
default -> ImmutableList.of();
397+
};
381398
}
382399

383400
private Description maybeCheckFixCompiles(
@@ -392,6 +409,30 @@ private Description maybeCheckFixCompiles(
392409
return describe(tree, fix, api);
393410
}
394411

412+
private static void visitIdentifiers(
413+
Tree tree, BiConsumer<IdentifierTree, List<Tree>> identifierConsumer) {
414+
new TreeScanner<Void, Void>() {
415+
// It'd be nice to use a TreePathScanner, but we don't have CompilationUnit-rooted AST.
416+
private final List<Tree> path = new ArrayList<>();
417+
418+
@Override
419+
public Void scan(Tree tree, Void unused) {
420+
if (tree != null) {
421+
path.add(tree);
422+
super.scan(tree, null);
423+
path.removeLast();
424+
}
425+
return null;
426+
}
427+
428+
@Override
429+
public Void visitIdentifier(IdentifierTree node, Void unused) {
430+
identifierConsumer.accept(node, path);
431+
return super.visitIdentifier(node, null);
432+
}
433+
}.scan(tree, null);
434+
}
435+
395436
private static ImmutableList<String> getStrings(Attribute.Compound attribute, String name) {
396437
return getValue(attribute, name)
397438
.map(MoreAnnotations::asStrings)

0 commit comments

Comments
 (0)