2020import org .openrewrite .java .JavaIsoVisitor ;
2121import org .openrewrite .java .JavaTemplate ;
2222import org .openrewrite .java .MethodMatcher ;
23+ import org .openrewrite .java .search .DeclaresMethod ;
2324import org .openrewrite .java .service .AnnotationService ;
2425import org .openrewrite .java .tree .J ;
2526import org .openrewrite .java .tree .JavaType ;
27+ import org .openrewrite .staticanalysis .csharp .CSharpFileChecker ;
2628
27- import java .time .Duration ;
2829import java .util .Collections ;
2930import java .util .Comparator ;
3031import java .util .Set ;
31- import java .util .stream .Stream ;
3232
3333@ Incubating (since = "7.0.0" )
3434public class CovariantEquals extends Recipe {
3535
36+ private static final MethodMatcher EQUALS_MATCHER = new MethodMatcher ("* equals(..)" );
37+ private static final MethodMatcher EQUALS_OBJECT_MATCHER = new MethodMatcher ("* equals(java.lang.Object)" );
38+ private static final AnnotationMatcher OVERRIDE_ANNOTATION = new AnnotationMatcher ("@java.lang.Override" );
39+
3640 @ Override
3741 public String getDisplayName () {
3842 return "Covariant equals" ;
@@ -49,105 +53,79 @@ public Set<String> getTags() {
4953 return Collections .singleton ("RSPEC-S2162" );
5054 }
5155
52- @ Override
53- public Duration getEstimatedEffortPerOccurrence () {
54- return Duration .ofMinutes (5 );
55- }
56-
5756 @ Override
5857 public TreeVisitor <?, ExecutionContext > getVisitor () {
59- MethodMatcher objectEquals = new MethodMatcher ("* equals(java.lang.Object)" );
60- return new JavaIsoVisitor <ExecutionContext >() {
61-
58+ TreeVisitor <?, ExecutionContext > conditions = Preconditions .and (
59+ new DeclaresMethod <>(EQUALS_MATCHER ),
60+ Preconditions .not (new DeclaresMethod <>(EQUALS_OBJECT_MATCHER )),
61+ Preconditions .not (new CSharpFileChecker <>())
62+ );
63+ return Preconditions .check (conditions , Repeat .repeatUntilStable (new JavaIsoVisitor <ExecutionContext >() {
6264 @ Override
63- public J .ClassDeclaration visitClassDeclaration (J .ClassDeclaration classDecl , ExecutionContext ctx ) {
64- J .ClassDeclaration cd = super .visitClassDeclaration (classDecl , ctx );
65- Stream <J .MethodDeclaration > mds = cd .getBody ().getStatements ().stream ()
66- .filter (J .MethodDeclaration .class ::isInstance )
67- .map (J .MethodDeclaration .class ::cast );
68- if (cd .getKind () != J .ClassDeclaration .Kind .Type .Interface && mds .noneMatch (m -> objectEquals .matches (m , classDecl ))) {
69- cd = (J .ClassDeclaration ) new ChangeCovariantEqualsMethodVisitor (cd ).visit (cd , ctx , getCursor ().getParentOrThrow ());
70- assert cd != null ;
71- }
72- return cd ;
73- }
74-
75- class ChangeCovariantEqualsMethodVisitor extends JavaIsoVisitor <ExecutionContext > {
76- private final AnnotationMatcher OVERRIDE_ANNOTATION = new AnnotationMatcher ("@java.lang.Override" );
77-
78- private final J .ClassDeclaration enclosingClass ;
79-
80- public ChangeCovariantEqualsMethodVisitor (J .ClassDeclaration enclosingClass ) {
81- this .enclosingClass = enclosingClass ;
65+ public J .MethodDeclaration visitMethodDeclaration (J .MethodDeclaration method , ExecutionContext ctx ) {
66+ J .MethodDeclaration m = super .visitMethodDeclaration (method , ctx );
67+ J .ClassDeclaration enclosingClass = getCursor ().dropParentUntil (p -> p instanceof J .ClassDeclaration ).getValue ();
68+
69+ /*
70+ * Looking for "public boolean equals(EnclosingClassType)" as the method signature match.
71+ * We'll replace it with "public boolean equals(Object)"
72+ */
73+ JavaType .FullyQualified type = enclosingClass .getType ();
74+ if (type == null || type instanceof JavaType .Unknown ) {
75+ return m ;
8276 }
8377
84- @ Override
85- public J .MethodDeclaration visitMethodDeclaration (J .MethodDeclaration method , ExecutionContext ctx ) {
86- J .MethodDeclaration m = super .visitMethodDeclaration (method , ctx );
87- updateCursor (m );
88-
89- /*
90- * Looking for "public boolean equals(EnclosingClassType)" as the method signature match.
91- * We'll replace it with "public boolean equals(Object)"
92- */
93- JavaType .FullyQualified type = enclosingClass .getType ();
94- if (type == null || type instanceof JavaType .Unknown ) {
95- return m ;
96- }
97-
98- String ecfqn = type .getFullyQualifiedName ();
99- if (m .hasModifier (J .Modifier .Type .Public ) &&
100- m .getReturnTypeExpression () != null &&
78+ String ecfqn = type .getFullyQualifiedName ();
79+ if (m .hasModifier (J .Modifier .Type .Public ) && m .getReturnTypeExpression () != null &&
10180 JavaType .Primitive .Boolean .equals (m .getReturnTypeExpression ().getType ()) &&
10281 new MethodMatcher (ecfqn + " equals(" + ecfqn + ")" ).matches (m , enclosingClass )) {
10382
104- if (!service (AnnotationService .class ).matches (getCursor (), OVERRIDE_ANNOTATION )) {
105- m = JavaTemplate .builder ("@Override" ).build ()
106- .apply (updateCursor (m ),
107- m .getCoordinates ().addAnnotation (Comparator .comparing (J .Annotation ::getSimpleName )));
108- }
109-
110- /*
111- * Change parameter type to Object, and maybe change input parameter name representing the other object.
112- * This is because we prepend these type-checking replacement statements to the existing "equals(..)" body.
113- * Therefore we don't want to collide with any existing variable names.
114- */
115- J .VariableDeclarations .NamedVariable oldParamName = ((J .VariableDeclarations ) m .getParameters ().get (0 )).getVariables ().get (0 );
116- String paramName = "obj" .equals (oldParamName .getSimpleName ()) ? "other" : "obj" ;
117- m = JavaTemplate .builder ("Object #{}" ).build ()
83+ if (!service (AnnotationService .class ).matches (getCursor (), OVERRIDE_ANNOTATION )) {
84+ m = JavaTemplate .builder ("@Override" ).build ()
11885 .apply (updateCursor (m ),
119- m .getCoordinates ().replaceParameters (),
120- paramName );
121-
122- /*
123- * We'll prepend this type-check and type-cast to the beginning of the existing
124- * equals(..) method body statements, and let the existing equals(..) method definition continue
125- * with the logic doing what it was doing.
126- */
127- String equalsBodyPrefixTemplate = "if (#{} == this) return true;\n " +
128- "if (#{} == null || getClass() != #{}.getClass()) return false;\n " +
129- "#{} #{} = (#{}) #{};\n " ;
130- JavaTemplate equalsBodySnippet = JavaTemplate .builder (equalsBodyPrefixTemplate ).contextSensitive ().build ();
131-
132- assert m .getBody () != null ;
133- Object [] params = new Object []{
134- paramName ,
135- paramName ,
136- paramName ,
137- enclosingClass .getSimpleName (),
138- oldParamName .getSimpleName (),
139- enclosingClass .getSimpleName (),
140- paramName
141- };
142-
143- m = equalsBodySnippet .apply (new Cursor (getCursor ().getParent (), m ),
144- m .getBody ().getStatements ().get (0 ).getCoordinates ().before (),
145- params );
86+ m .getCoordinates ().addAnnotation (Comparator .comparing (J .Annotation ::getSimpleName )));
14687 }
14788
148- return m ;
89+ /*
90+ * Change parameter type to Object, and maybe change input parameter name representing the other object.
91+ * This is because we prepend these type-checking replacement statements to the existing "equals(..)" body.
92+ * Therefore we don't want to collide with any existing variable names.
93+ */
94+ J .VariableDeclarations .NamedVariable oldParamName = ((J .VariableDeclarations ) m .getParameters ().get (0 )).getVariables ().get (0 );
95+ String paramName = "obj" .equals (oldParamName .getSimpleName ()) ? "other" : "obj" ;
96+ m = JavaTemplate .builder ("Object #{}" ).build ()
97+ .apply (updateCursor (m ),
98+ m .getCoordinates ().replaceParameters (),
99+ paramName );
100+
101+ /*
102+ * We'll prepend this type-check and type-cast to the beginning of the existing
103+ * equals(..) method body statements, and let the existing equals(..) method definition continue
104+ * with the logic doing what it was doing.
105+ */
106+ String equalsBodyPrefixTemplate = "if (#{} == this) return true;\n " +
107+ "if (#{} == null || getClass() != #{}.getClass()) return false;\n " +
108+ "#{} #{} = (#{}) #{};\n " ;
109+ JavaTemplate equalsBodySnippet = JavaTemplate .builder (equalsBodyPrefixTemplate ).contextSensitive ().build ();
110+
111+ assert m .getBody () != null ;
112+ Object [] params = new Object []{
113+ paramName ,
114+ paramName ,
115+ paramName ,
116+ enclosingClass .getSimpleName (),
117+ oldParamName .getSimpleName (),
118+ enclosingClass .getSimpleName (),
119+ paramName
120+ };
121+
122+ m = equalsBodySnippet .apply (new Cursor (getCursor ().getParent (), m ),
123+ m .getBody ().getStatements ().get (0 ).getCoordinates ().before (),
124+ params );
149125 }
126+
127+ return m ;
150128 }
151- };
129+ })) ;
152130 }
153131}
0 commit comments