1+ /*
2+ * Copyright 2023 the original author or authors.
3+ * <p>
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ * <p>
8+ * https://www.apache.org/licenses/LICENSE-2.0
9+ * <p>
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+ package org .openrewrite .java .migrate .guava ;
17+
18+ import org .openrewrite .ExecutionContext ;
19+ import org .openrewrite .Preconditions ;
20+ import org .openrewrite .Recipe ;
21+ import org .openrewrite .TreeVisitor ;
22+ import org .openrewrite .internal .lang .Nullable ;
23+ import org .openrewrite .java .JavaTemplate ;
24+ import org .openrewrite .java .JavaVisitor ;
25+ import org .openrewrite .java .MethodMatcher ;
26+ import org .openrewrite .java .search .UsesJavaVersion ;
27+ import org .openrewrite .java .search .UsesType ;
28+ import org .openrewrite .java .tree .*;
29+
30+ import java .time .Duration ;
31+ import java .util .Objects ;
32+ import java .util .stream .Collectors ;
33+
34+ abstract class AbstractNoGuavaImmutableOf extends Recipe {
35+
36+ private final String guavaType ;
37+ private final String javaType ;
38+
39+ AbstractNoGuavaImmutableOf (String guavaType , String javaType ) {
40+ this .guavaType = guavaType ;
41+ this .javaType = javaType ;
42+ }
43+
44+ private String getShortType (String fullyQualifiedType ) {
45+ return fullyQualifiedType .substring (javaType .lastIndexOf ("." ) + 1 );
46+ }
47+
48+ @ Override
49+ public String getDisplayName () {
50+ return "Prefer `" + getShortType (javaType ) + ".of(..)` in Java 9 or higher" ;
51+ }
52+
53+ @ Override
54+ public String getDescription () {
55+ return "Replaces `" + getShortType (guavaType ) + ".of(..)` if the returned type is immediately down-cast." ;
56+ }
57+
58+ @ Override
59+ public Duration getEstimatedEffortPerOccurrence () {
60+ return Duration .ofMinutes (10 );
61+ }
62+
63+ @ Override
64+ public TreeVisitor <?, ExecutionContext > getVisitor () {
65+ TreeVisitor <?, ExecutionContext > check = Preconditions .and (new UsesJavaVersion <>(9 ),
66+ new UsesType <>(guavaType , false ));
67+ final MethodMatcher IMMUTABLE_MATCHER = new MethodMatcher (guavaType + " of(..)" );
68+ return Preconditions .check (check , new JavaVisitor <ExecutionContext >() {
69+ @ Override
70+ public J visitMethodInvocation (J .MethodInvocation method , ExecutionContext ctx ) {
71+ if (IMMUTABLE_MATCHER .matches (method ) && isParentTypeDownCast ()) {
72+ maybeRemoveImport (guavaType );
73+ maybeAddImport (javaType );
74+
75+ String template = method .getArguments ().stream ()
76+ .map (arg -> {
77+ if (arg .getType () instanceof JavaType .Primitive ) {
78+ String type = "" ;
79+ if (JavaType .Primitive .Boolean == arg .getType ()) {
80+ type = "Boolean" ;
81+ } else if (JavaType .Primitive .Byte == arg .getType ()) {
82+ type = "Byte" ;
83+ } else if (JavaType .Primitive .Char == arg .getType ()) {
84+ type = "Character" ;
85+ } else if (JavaType .Primitive .Double == arg .getType ()) {
86+ type = "Double" ;
87+ } else if (JavaType .Primitive .Float == arg .getType ()) {
88+ type = "Float" ;
89+ } else if (JavaType .Primitive .Int == arg .getType ()) {
90+ type = "Integer" ;
91+ } else if (JavaType .Primitive .Long == arg .getType ()) {
92+ type = "Long" ;
93+ } else if (JavaType .Primitive .Short == arg .getType ()) {
94+ type = "Short" ;
95+ } else if (JavaType .Primitive .String == arg .getType ()) {
96+ type = "String" ;
97+ }
98+ return TypeUtils .asFullyQualified (JavaType .buildType ("java.lang." + type ));
99+ } else {
100+ return TypeUtils .asFullyQualified (arg .getType ());
101+ }
102+ })
103+ .filter (Objects ::nonNull )
104+ .map (type -> "#{any(" + type .getFullyQualifiedName () + ")}" )
105+ .collect (Collectors .joining ("," , getShortType (javaType ) + ".of(" , ")" ));
106+
107+ return JavaTemplate .builder (template )
108+ .contextSensitive ()
109+ .imports (javaType )
110+ .build ()
111+ .apply (getCursor (),
112+ method .getCoordinates ().replace (),
113+ method .getArguments ().get (0 ) instanceof J .Empty ? new Object []{} : method .getArguments ().toArray ());
114+ }
115+ return super .visitMethodInvocation (method , ctx );
116+ }
117+
118+ private boolean isParentTypeDownCast () {
119+ J parent = getCursor ().dropParentUntil (J .class ::isInstance ).getValue ();
120+ boolean isParentTypeDownCast = false ;
121+ if (parent instanceof J .VariableDeclarations .NamedVariable ) {
122+ isParentTypeDownCast = isParentTypeMatched (((J .VariableDeclarations .NamedVariable ) parent ).getType ());
123+ } else if (parent instanceof J .Assignment ) {
124+ J .Assignment a = (J .Assignment ) parent ;
125+ if (a .getVariable () instanceof J .Identifier && ((J .Identifier ) a .getVariable ()).getFieldType () != null ) {
126+ isParentTypeDownCast = isParentTypeMatched (((J .Identifier ) a .getVariable ()).getFieldType ().getType ());
127+ } else if (a .getVariable () instanceof J .FieldAccess ) {
128+ isParentTypeDownCast = isParentTypeMatched (a .getVariable ().getType ());
129+ }
130+ } else if (parent instanceof J .Return ) {
131+ // Does not currently support returns in lambda expressions.
132+ J j = getCursor ().dropParentUntil (is -> is instanceof J .MethodDeclaration || is instanceof J .CompilationUnit ).getValue ();
133+ if (j instanceof J .MethodDeclaration ) {
134+ TypeTree returnType = ((J .MethodDeclaration ) j ).getReturnTypeExpression ();
135+ if (returnType != null ) {
136+ isParentTypeDownCast = isParentTypeMatched (returnType .getType ());
137+ }
138+ }
139+ } else if (parent instanceof J .MethodInvocation ) {
140+ J .MethodInvocation m = (J .MethodInvocation ) parent ;
141+ if (m .getMethodType () != null ) {
142+ int index = 0 ;
143+ for (Expression argument : m .getArguments ()) {
144+ if (IMMUTABLE_MATCHER .matches (argument )) {
145+ break ;
146+ }
147+ index ++;
148+ }
149+ isParentTypeDownCast = isParentTypeMatched (m .getMethodType ().getParameterTypes ().get (index ));
150+ }
151+ } else if (parent instanceof J .NewClass ) {
152+ J .NewClass c = (J .NewClass ) parent ;
153+ int index = 0 ;
154+ if (c .getConstructorType () != null ) {
155+ for (Expression argument : c .getArguments ()) {
156+ if (IMMUTABLE_MATCHER .matches (argument )) {
157+ break ;
158+ }
159+ index ++;
160+ }
161+ if (c .getConstructorType () != null ) {
162+ isParentTypeDownCast = isParentTypeMatched (c .getConstructorType ().getParameterTypes ().get (index ));
163+ }
164+ }
165+ } else if (parent instanceof J .NewArray ) {
166+ J .NewArray a = (J .NewArray ) parent ;
167+ JavaType arrayType = a .getType ();
168+ while (arrayType instanceof JavaType .Array ) {
169+ arrayType = ((JavaType .Array ) arrayType ).getElemType ();
170+ }
171+
172+ isParentTypeDownCast = isParentTypeMatched (arrayType );
173+ }
174+ return isParentTypeDownCast ;
175+ }
176+
177+ private boolean isParentTypeMatched (@ Nullable JavaType type ) {
178+ JavaType .FullyQualified fq = TypeUtils .asFullyQualified (type );
179+ return TypeUtils .isOfClassType (fq , javaType )
180+ || TypeUtils .isOfClassType (fq , "java.lang.Object" );
181+ }
182+ });
183+ }
184+ }
0 commit comments