21
21
import static com .google .errorprone .BugPattern .SeverityLevel .WARNING ;
22
22
import static com .google .errorprone .util .ASTHelpers .enclosingPackage ;
23
23
import static com .google .errorprone .util .ASTHelpers .getReceiver ;
24
+ import static com .google .errorprone .util .ASTHelpers .getStartPosition ;
24
25
import static com .google .errorprone .util .ASTHelpers .getSymbol ;
25
26
import static com .google .errorprone .util .ASTHelpers .hasAnnotation ;
26
27
import static com .google .errorprone .util .ASTHelpers .hasDirectAnnotationWithSimpleName ;
36
37
import com .google .common .collect .ImmutableList ;
37
38
import com .google .common .collect .ImmutableSet ;
38
39
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 ;
42
40
import com .google .errorprone .BugPattern ;
43
41
import com .google .errorprone .ErrorProneFlags ;
44
42
import com .google .errorprone .VisitorState ;
45
43
import com .google .errorprone .bugpatterns .BugChecker ;
46
44
import com .google .errorprone .bugpatterns .BugChecker .MemberReferenceTreeMatcher ;
47
45
import com .google .errorprone .bugpatterns .BugChecker .MethodInvocationTreeMatcher ;
48
46
import com .google .errorprone .bugpatterns .BugChecker .NewClassTreeMatcher ;
47
+ import com .google .errorprone .fixes .AppliedFix ;
49
48
import com .google .errorprone .fixes .SuggestedFix ;
50
49
import com .google .errorprone .fixes .SuggestedFixes ;
51
50
import com .google .errorprone .matchers .Description ;
52
51
import com .google .errorprone .util .MoreAnnotations ;
53
52
import com .sun .source .tree .ExpressionStatementTree ;
54
53
import com .sun .source .tree .ExpressionTree ;
54
+ import com .sun .source .tree .IdentifierTree ;
55
55
import com .sun .source .tree .MemberReferenceTree ;
56
56
import com .sun .source .tree .MethodInvocationTree ;
57
57
import com .sun .source .tree .NewClassTree ;
58
58
import com .sun .source .tree .Tree ;
59
+ import com .sun .source .util .TreeScanner ;
59
60
import com .sun .tools .javac .code .Attribute ;
60
61
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 ;
63
67
import java .util .List ;
64
- import java .util .Map ;
65
68
import java .util .Optional ;
69
+ import java .util .function .BiConsumer ;
66
70
import java .util .regex .Matcher ;
67
71
import java .util .regex .Pattern ;
68
72
import java .util .stream .Stream ;
@@ -213,19 +217,6 @@ private Description match(
213
217
&& stringContainsComments (state .getSourceForNode (tree ), state .context )) {
214
218
return Description .NO_MATCH ;
215
219
}
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
-
229
220
ImmutableList <String > varNames =
230
221
symbol .getParameters ().stream ()
231
222
.map (varSymbol -> varSymbol .getSimpleName ().toString ())
@@ -259,18 +250,55 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) {
259
250
}
260
251
261
252
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 );
263
283
int replacementEnd = state .getEndPosition (tree );
264
284
265
285
// Special case replacements starting with "this." so the receiver portion is not included in
266
286
// 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 (), "" );
268
290
replacementStart = state .getEndPosition (receiver );
269
- replacement = replacement .substring ("this" .length ());
270
291
}
271
292
272
293
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
+ });
274
302
} else {
275
303
if (replacement .equals ("this" )) { // e.g.: foo.b() -> foo
276
304
Tree parent = state .getPath ().getParentPath ().getLeaf ();
@@ -281,22 +309,17 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) {
281
309
return describe (parent , SuggestedFix .delete (parent ), api );
282
310
}
283
311
}
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
+ });
285
321
}
286
322
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
-
300
323
for (int i = 0 ; i < varNames .size (); i ++) {
301
324
String varName = varNames .get (i );
302
325
@@ -305,79 +328,73 @@ && stringContainsComments(state.getSourceForNode(tree), state.context)) {
305
328
return Description .NO_MATCH ;
306
329
}
307
330
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
-
318
331
// Ex: foo(int a, int... others) -> this.bar(a, others)
319
332
// If caller passes 0 args in the varargs position, we want to remove the preceding comma to
320
333
// make this.bar(a) (as opposed to "this.bar(a, )"
321
334
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([^(])" );
326
335
String replacementResult = terminalVarargsReplacement ? "" : callingVarStrings .get (i );
327
336
boolean mayRequireParens =
328
337
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
+ });
341
366
}
342
367
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
+ }
344
376
345
- builder .replace (replacementStart , replacementEnd , replacement );
377
+ @ Override
378
+ public void storeEnd (JCTree tree , int endpos ) {}
346
379
347
- SuggestedFix fix = builder .build ();
380
+ @ Override
381
+ public int replaceTree (JCTree oldtree , JCTree newtree ) {
382
+ return 0 ;
383
+ }
384
+ })
385
+ .applyReplacements (replacementFixes .build ());
348
386
349
- return maybeCheckFixCompiles (tree , state , fix , api );
350
- }
387
+ fixBuilder .replace (replacementStart , replacementEnd , fixedReplacement );
351
388
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 );
362
390
}
363
391
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
+ };
381
398
}
382
399
383
400
private Description maybeCheckFixCompiles (
@@ -392,6 +409,30 @@ private Description maybeCheckFixCompiles(
392
409
return describe (tree , fix , api );
393
410
}
394
411
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
+
395
436
private static ImmutableList <String > getStrings (Attribute .Compound attribute , String name ) {
396
437
return getValue (attribute , name )
397
438
.map (MoreAnnotations ::asStrings )
0 commit comments