Skip to content

Commit

Permalink
feat(spoon): Add EqualsHashcode badsmell (#899)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinWitt authored Jul 25, 2023
1 parent f6de608 commit 83ae327
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance.AccessStaticViaInstance;
import io.github.martinwitt.spoon_analyzer.badsmells.array_can_be_replaced_with_enum_values.ArrayCanBeReplacedWithEnumValues;
import io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used.CharsetObjectCanBeUsed;
import io.github.martinwitt.spoon_analyzer.badsmells.equals_hashcode.EqualsHashcode;
import io.github.martinwitt.spoon_analyzer.badsmells.final_static_method.FinalStaticMethod;
import io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static.InnerClassMayBeStatic;
import io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class.NonProtectedConstructorInAbstractClass;
Expand Down Expand Up @@ -182,6 +183,14 @@ public AnalyzerResult visit(FinalStaticMethod badSmell) {
return toSpoonAnalyzerResult(badSmell, badSmell.getMethod().getPosition(), snippet);
}

@Override
public AnalyzerResult visit(EqualsHashcode badSmell) {
CtType<?> clone = badSmell.getAffectedType().clone();
clone.setTypeMembers(new ArrayList<>());
String snippet = clone.toString();
return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedType().getPosition(), snippet);
}

private Optional<String> trygetOriginalSourceCode(CtElement element) {
try {
File file = element.getPosition().getCompilationUnit().getFile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance.AccessStaticViaInstance;
import io.github.martinwitt.spoon_analyzer.badsmells.array_can_be_replaced_with_enum_values.ArrayCanBeReplacedWithEnumValues;
import io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used.CharsetObjectCanBeUsed;
import io.github.martinwitt.spoon_analyzer.badsmells.equals_hashcode.EqualsHashcode;
import io.github.martinwitt.spoon_analyzer.badsmells.final_static_method.FinalStaticMethod;
import io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static.InnerClassMayBeStatic;
import io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class.NonProtectedConstructorInAbstractClass;
Expand Down Expand Up @@ -63,6 +64,10 @@ default U visit(FinalStaticMethod badSmell) {
return emptyResult();
}

default U visit(EqualsHashcode badSmell) {
return emptyResult();
}

default U emptyResult() {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.martinwitt.spoon_analyzer.badsmells.equals_hashcode;

import io.github.martinwitt.spoon_analyzer.BadSmell;
import io.github.martinwitt.spoon_analyzer.BadSmellVisitor;
import spoon.reflect.declaration.CtType;

/**
* This badsmell means the class does not override equals() and hashcode() methods together.
* Overriding only one of them is not enough and can lead to unexpected behavior.
*/
public class EqualsHashcode implements BadSmell {

private CtType<?> affectedType;

Check warning on line 13 in spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/equals_hashcode/EqualsHashcode.java

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Field may be 'final'

Field `affectedType` may be 'final'

public EqualsHashcode(CtType<?> affectedType) {
this.affectedType = affectedType;
}

@Override
public String getName() {
return "EqualsHashcode";
}

@Override
public String getDescription() {
return "This class does not override equals() and hashcode() methods together.";
}

@Override
public CtType<?> getAffectedType() {
return affectedType;
}

@Override
public <T> T accept(BadSmellVisitor<T> visitor) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'accept'");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.github.martinwitt.spoon_analyzer.badsmells.equals_hashcode;

import io.github.martinwitt.spoon_analyzer.BadSmell;
import io.github.martinwitt.spoon_analyzer.LocalAnalyzer;
import java.util.List;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;

public class EqualsHashcodeAnalyzer implements LocalAnalyzer {

@Override
public List<BadSmell> analyze(CtType<?> clazz) {

boolean hasEquals = false;
boolean hasHashcode = false;
for (CtMethod<?> methods : clazz.getMethods()) {
if (isEqualsMethod(methods)) {
hasEquals = true;
}
if (isHashcodeMethod(methods)) {
hasHashcode = true;
}
}
// if only one of them is present, it is a badsmell, otherwise not
if ((!hasEquals && hasHashcode) || (hasEquals && !hasHashcode)) {
return List.of(new EqualsHashcode(clazz));
}
return List.of();
}

private boolean isEqualsMethod(CtMethod<?> method) {
return method.getSimpleName().equals("equals");
}

private boolean isHashcodeMethod(CtMethod<?> method) {
return method.getSimpleName().equals("hashCode");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.github.martinwitt.spoon_analyzer.badsmells.equals_hashcode;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import spoon.Launcher;
import spoon.reflect.declaration.CtType;
import spoon.support.compiler.VirtualFile;

public class EqualsHashcodeAnalyzerTest {

@Test
void noEqualsNoHashcode() {
String code = "public class A { }";

Launcher launcher = new Launcher();
launcher.addInputResource(new VirtualFile(code));
var model = launcher.buildModel();
EqualsHashcodeAnalyzer analyzer = new EqualsHashcodeAnalyzer();
CtType<?> simpleClass = model.getAllTypes().stream().findFirst().get();
var result = analyzer.analyze(simpleClass);
assertEquals(0, result.size());
}

@Test
void UpperClassEqualsNoHashcode() {
String code =
"""
class A {
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
}
class B extends A {
}
""";

Launcher launcher = new Launcher();
launcher.addInputResource(new VirtualFile(code));
var model = launcher.buildModel();
EqualsHashcodeAnalyzer analyzer = new EqualsHashcodeAnalyzer();
CtType<?> simpleClass = model.getAllTypes().stream().findFirst().get();
var result = analyzer.analyze(simpleClass);

assertEquals(1, result.size());
}
}

0 comments on commit 83ae327

Please sign in to comment.