diff --git a/code-transformation/build.gradle b/code-transformation/build.gradle index bf40411e1..a7404861b 100644 --- a/code-transformation/build.gradle +++ b/code-transformation/build.gradle @@ -23,6 +23,7 @@ dependencies { implementation "com.contrastsecurity:java-sarif:+" testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' implementation project(":commons") + implementation project(":spoon-analyzer") implementation 'io.github.java-diff-utils:java-diff-utils:4.12' } diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/AnalyzerResultVisitor.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/AnalyzerResultVisitor.java new file mode 100644 index 000000000..89ce70095 --- /dev/null +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/AnalyzerResultVisitor.java @@ -0,0 +1,141 @@ +package xyz.keksdose.spoon.code_solver.analyzer.spoon; + +import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; +import io.github.martinwitt.laughing_train.domain.value.Position; +import io.github.martinwitt.laughing_train.domain.value.RuleId; +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.BadSmellVisitor; +import io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains.IndexOfReplaceableByContains; +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.innerclass_may_be_static.InnerClassMayBeStatic; +import io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class.NonProtectedConstructorInAbstractClass; +import io.github.martinwitt.spoon_analyzer.badsmells.private_final_method.PrivateFinalMethod; +import io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty.SizeReplaceableByIsEmpty; +import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements.UnnecessaryImplements; +import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_tostring.UnnecessaryTostring; +import java.util.ArrayList; +import java.util.Optional; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.declaration.CtType; + +class AnalyzerResultVisitor implements BadSmellVisitor { + + private static final AnalyzerResultVisitor analyzerResultVisitor = new AnalyzerResultVisitor(); + + public static Optional toAnalyzerResult(BadSmell badSmell) { + return Optional.ofNullable(badSmell.accept(analyzerResultVisitor)); + } + + private AnalyzerResultVisitor() {} + + @Override + public AnalyzerResult visit(IndexOfReplaceableByContains badSmell) { + return toSpoonAnalyzerResult( + badSmell, + badSmell.getIndexOfCall().getPosition(), + badSmell.getIndexOfCall() + .getParent(CtBinaryOperator.class) + .getOriginalSourceFragment() + .toString()); + } + + private String getAbsolutePath(BadSmell badSmell) { + return badSmell.getAffectedType().getPosition().getFile().getAbsolutePath(); + } + + private Position toPosition(SourcePosition position) { + int sourceStart = position.getSourceStart(); + int sourceEnd = position.getSourceEnd(); + int line = position.getLine(); + int column = position.getColumn(); + int endColumn = position.getEndColumn(); + int endLine = position.getEndLine(); + return new Position(line, endLine, column, endColumn, sourceStart, sourceEnd - sourceStart); + } + + public AnalyzerResult toSpoonAnalyzerResult(BadSmell badSmell, SourcePosition position, String snippet) { + String absolutePath = getAbsolutePath(badSmell); + RuleId ruleId = new RuleId(badSmell.getName()); + return new SpoonAnalyzerResult( + ruleId, + absolutePath, + toPosition(position), + badSmell.getDescription(), + badSmell.getDescription(), + snippet); + } + + @Override + public AnalyzerResult visit(AccessStaticViaInstance badSmell) { + String snippet = + badSmell.getAffectedCtInvocation().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult( + badSmell, badSmell.getAffectedCtInvocation().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(ArrayCanBeReplacedWithEnumValues badSmell) { + String snippet = + badSmell.getAffectedElement().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedElement().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(CharsetObjectCanBeUsed badSmell) { + if (badSmell.getInvocation() != null) { + String snippet = + badSmell.getInvocation().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getInvocation().getPosition(), snippet); + } else { + String snippet = badSmell.getCtorCall().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getCtorCall().getPosition(), snippet); + } + } + + @Override + public AnalyzerResult visit(InnerClassMayBeStatic badSmell) { + CtType clone = badSmell.getAffectedType().clone(); + clone.setTypeMembers(new ArrayList<>()); + String snippet = clone.toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedType().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(NonProtectedConstructorInAbstractClass badSmell) { + String snippet = badSmell.getCtConstructor().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getCtConstructor().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(PrivateFinalMethod badSmell) { + String snippet = + badSmell.getAffectedMethod().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedMethod().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(SizeReplaceableByIsEmpty badSmell) { + String snippet = + badSmell.getSizeInvocation().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getSizeInvocation().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(UnnecessaryImplements badSmell) { + + CtType clone = badSmell.getAffectedType().clone(); + clone.setTypeMembers(new ArrayList<>()); + String snippet = clone.toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getAffectedType().getPosition(), snippet); + } + + @Override + public AnalyzerResult visit(UnnecessaryTostring badSmell) { + String snippet = + badSmell.getNotNeededTostring().getOriginalSourceFragment().toString(); + return toSpoonAnalyzerResult(badSmell, badSmell.getNotNeededTostring().getPosition(), snippet); + } +} diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonAnalyzerResult.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonAnalyzerResult.java new file mode 100644 index 000000000..c7347439f --- /dev/null +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonAnalyzerResult.java @@ -0,0 +1,15 @@ +package xyz.keksdose.spoon.code_solver.analyzer.spoon; + +import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; +import io.github.martinwitt.laughing_train.domain.value.Position; +import io.github.martinwitt.laughing_train.domain.value.RuleId; + +public record SpoonAnalyzerResult( + RuleId ruleID, String filePath, Position position, String message, String messageMarkdown, String snippet) + implements AnalyzerResult { + + @Override + public String getAnalyzer() { + return "Spoon"; + } +} diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonBasedAnalyzer.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonBasedAnalyzer.java new file mode 100644 index 000000000..63e811593 --- /dev/null +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/analyzer/spoon/SpoonBasedAnalyzer.java @@ -0,0 +1,20 @@ +package xyz.keksdose.spoon.code_solver.analyzer.spoon; + +import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.SpoonAnalyzer; +import java.nio.file.Path; +import java.util.List; + +public class SpoonBasedAnalyzer { + + public List analyze(Path sourceRoot) { + SpoonAnalyzer analyzer = new SpoonAnalyzer(); + List analyze = analyzer.analyze(sourceRoot.toAbsolutePath().toString()); + return analyze.stream() + .map(AnalyzerResultVisitor::toAnalyzerResult) + .filter(v -> v.isPresent()) + .map(v -> v.get()) + .toList(); + } +} diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/AnalyzerRequest.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/request/AnalyzerRequest.java similarity index 59% rename from github-bot/src/main/java/io/github/martinwitt/laughing_train/data/AnalyzerRequest.java rename to github-bot/src/main/java/io/github/martinwitt/laughing_train/data/request/AnalyzerRequest.java index 957977a78..eebd0c623 100644 --- a/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/AnalyzerRequest.java +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/request/AnalyzerRequest.java @@ -1,5 +1,6 @@ -package io.github.martinwitt.laughing_train.data; +package io.github.martinwitt.laughing_train.data.request; +import io.github.martinwitt.laughing_train.data.Project; import java.io.Serializable; public sealed interface AnalyzerRequest extends Serializable { diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/result/CodeAnalyzerResult.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/result/CodeAnalyzerResult.java new file mode 100644 index 000000000..49d3706b0 --- /dev/null +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/data/result/CodeAnalyzerResult.java @@ -0,0 +1,13 @@ +package io.github.martinwitt.laughing_train.data.result; + +import io.github.martinwitt.laughing_train.data.Project; +import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; +import java.io.Serializable; +import java.util.List; + +public interface CodeAnalyzerResult extends Serializable { + + record Success(List results, Project project) implements CodeAnalyzerResult {} + + record Failure(String message) implements CodeAnalyzerResult {} +} diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/MiningPrinter.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/MiningPrinter.java index 44706c30a..cfa1a7f1b 100644 --- a/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/MiningPrinter.java +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/MiningPrinter.java @@ -2,7 +2,6 @@ import io.github.martinwitt.laughing_train.Config; import io.github.martinwitt.laughing_train.MarkdownPrinter; -import io.github.martinwitt.laughing_train.data.Project; import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; import io.github.martinwitt.laughing_train.domain.value.RuleId; import jakarta.enterprise.context.ApplicationScoped; @@ -25,7 +24,7 @@ public class MiningPrinter { @Inject Config config; - public String printAllResults(List results, Project project) { + public String printAllResults(List results) { StringBuilder sb = new StringBuilder(); List ruleIds = config.getRules().keySet().stream() .map(QodanaRules::getRuleId) diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/PeriodicMiner.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/PeriodicMiner.java index 82f6fdc15..61acecbcb 100644 --- a/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/PeriodicMiner.java +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/mining/PeriodicMiner.java @@ -1,15 +1,18 @@ package io.github.martinwitt.laughing_train.mining; import com.google.common.flogger.FluentLogger; -import io.github.martinwitt.laughing_train.data.AnalyzerRequest; import io.github.martinwitt.laughing_train.data.ProjectRequest; import io.github.martinwitt.laughing_train.data.ProjectResult; +import io.github.martinwitt.laughing_train.data.ProjectResult.Success; import io.github.martinwitt.laughing_train.data.QodanaResult; +import io.github.martinwitt.laughing_train.data.request.AnalyzerRequest; +import io.github.martinwitt.laughing_train.data.result.CodeAnalyzerResult; import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; import io.github.martinwitt.laughing_train.domain.entity.Project; import io.github.martinwitt.laughing_train.persistence.repository.ProjectRepository; import io.github.martinwitt.laughing_train.services.ProjectService; import io.github.martinwitt.laughing_train.services.QodanaService; +import io.github.martinwitt.laughing_train.services.SpoonAnalyzerService; import io.micrometer.core.instrument.MeterRegistry; import io.quarkus.runtime.StartupEvent; import io.vertx.core.Vertx; @@ -17,8 +20,8 @@ import jakarta.enterprise.event.Observes; import java.io.IOException; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.Queue; import java.util.Random; import java.util.concurrent.TimeUnit; @@ -37,7 +40,7 @@ public class PeriodicMiner { final ProjectRepository projectRepository; final QodanaService qodanaService; final ProjectService projectService; - + final SpoonAnalyzerService spoonAnalyzerService; MeterRegistry registry; private final Random random = new Random(); @@ -50,7 +53,8 @@ public PeriodicMiner( ProjectRepository projectRepository, QodanaService qodanaService, ProjectService projectService, - MiningPrinter miningPrinter) { + MiningPrinter miningPrinter, + SpoonAnalyzerService spoonAnalyzerService) { this.registry = registry; this.vertx = vertx; this.searchProjectService = searchProjectService; @@ -58,6 +62,7 @@ public PeriodicMiner( this.qodanaService = qodanaService; this.projectService = projectService; this.miningPrinter = miningPrinter; + this.spoonAnalyzerService = spoonAnalyzerService; } private Project getRandomProject() throws IOException { @@ -97,17 +102,32 @@ private void mineRandomRepo() { } logger.atInfo().log("Successfully checked out project %s", success.project()); var qodanaResult = analyzeProject(success); + var spoonResult = analyzeProjectWithSpoon(success); + List results = new ArrayList<>(); if (qodanaResult instanceof QodanaResult.Failure failure) { logger.atWarning().log("Failed to analyze project %s", failure.message()); - registry.counter("mining.qodana.error").increment(); tryDeleteProject(success); - return; + } + if (spoonResult instanceof CodeAnalyzerResult.Failure error) { + logger.atWarning().log("Failed to analyze project with spoon %s", error.message()); + tryDeleteProject(success); + } + if (spoonResult instanceof CodeAnalyzerResult.Success successResult) { + results.addAll(successResult.results()); } if (qodanaResult instanceof QodanaResult.Success successResult) { logger.atInfo().log("Successfully analyzed project %s", success.project()); - saveQodanaResults(successResult); - addOrUpdateCommitHash(success); + results.addAll(successResult.result()); + } + if (results.isEmpty()) { + logger.atWarning().log("No results for project %s", success.project()); + tryDeleteProject(success); + mineRandomRepo(); + return; } + saveResults(results, project); + addOrUpdateCommitHash(success); + tryDeleteProject(success); } } catch (Exception e) { logger.atWarning().withCause(e).log("Failed to mine random repo"); @@ -119,6 +139,10 @@ private void mineRandomRepo() { } } + private CodeAnalyzerResult analyzeProjectWithSpoon(Success success) { + return spoonAnalyzerService.analyze(new AnalyzerRequest.WithProject(success.project())); + } + private boolean isAlreadyMined(ProjectResult.Success success, String commitHash) { return projectRepository.findByProjectUrl(success.project().url()).stream() .anyMatch(it -> !it.getCommitHashes().contains(commitHash)); @@ -157,28 +181,18 @@ private void addOrUpdateCommitHash(ProjectResult.Success projectResult) { } } - private void saveQodanaResults(QodanaResult.Success success) { - success.project().runInContext(() -> { - try { - List results = success.result(); - registry.summary("mining.qodana", "result", Integer.toString(results.size())); - if (results.isEmpty()) { - logger.atInfo().log("No results for %s", success); - return Optional.empty(); - } - String content = printFormattedResults(success, results); - var laughingRepo = getLaughingRepo(); - updateOrCreateContent(laughingRepo, success.project().name(), content); - } catch (Exception e) { - logger.atSevere().withCause(e).log("Error while updating content"); - } - return Optional.empty(); - }); + private void saveResults(List results, Project project) { + try { + String content = printFormattedResults(project, results); + var laughingRepo = getLaughingRepo(); + updateOrCreateContent(laughingRepo, project.getProjectName(), content); + } catch (Exception e) { + logger.atSevere().withCause(e).log("Error while updating content"); + } } - private String printFormattedResults(QodanaResult.Success success, List results) { - return "# %s %n %s" - .formatted(success.project().name(), miningPrinter.printAllResults(results, success.project())); + private String printFormattedResults(Project project, List results) { + return "# %s %n %s".formatted(project.getProjectName(), miningPrinter.printAllResults(results)); } private GHRepository getLaughingRepo() throws IOException { diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/AnalyzerResultPersistenceService.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/AnalyzerResultPersistenceService.java index 3fad3d64e..984d0ec20 100644 --- a/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/AnalyzerResultPersistenceService.java +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/AnalyzerResultPersistenceService.java @@ -3,6 +3,7 @@ import com.google.common.flogger.FluentLogger; import io.github.martinwitt.laughing_train.data.Project; import io.github.martinwitt.laughing_train.data.QodanaResult; +import io.github.martinwitt.laughing_train.data.result.CodeAnalyzerResult; import io.github.martinwitt.laughing_train.persistence.BadSmell; import io.github.martinwitt.laughing_train.persistence.repository.BadSmellRepository; import io.smallrye.mutiny.Multi; @@ -34,4 +35,22 @@ void persistResults(QodanaResult result) { "Persisted %d qodana bad smells for project %s", badSmell, project.name())); } } + + void persistResults(CodeAnalyzerResult result) { + if (result instanceof CodeAnalyzerResult.Success success) { + Project project = success.project(); + Multi.createFrom() + .iterable(success.results()) + .map(badSmell -> new BadSmell(badSmell, project.name(), project.url(), project.commitHash())) + .filter(v -> badSmellRepository + .findByIdentifier(v.getIdentifier()) + .isEmpty()) + .map(badSmellRepository::save) + .collect() + .with(Collectors.counting()) + .subscribe() + .with(badSmell -> + logger.atInfo().log("Persisted %d bad smells for project %s", badSmell, project.name())); + } + } } diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/QodanaService.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/QodanaService.java index 3123ef03a..273ede3bd 100644 --- a/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/QodanaService.java +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/QodanaService.java @@ -4,10 +4,10 @@ import com.google.common.flogger.FluentLogger; import io.github.martinwitt.laughing_train.Config; import io.github.martinwitt.laughing_train.Constants; -import io.github.martinwitt.laughing_train.data.AnalyzerRequest; -import io.github.martinwitt.laughing_train.data.AnalyzerRequest.WithProject; import io.github.martinwitt.laughing_train.data.FindProjectConfigRequest; import io.github.martinwitt.laughing_train.data.QodanaResult; +import io.github.martinwitt.laughing_train.data.request.AnalyzerRequest; +import io.github.martinwitt.laughing_train.data.request.AnalyzerRequest.WithProject; import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; import io.github.martinwitt.laughing_train.domain.entity.ProjectConfig; import io.smallrye.health.api.AsyncHealthCheck; diff --git a/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/SpoonAnalyzerService.java b/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/SpoonAnalyzerService.java new file mode 100644 index 000000000..e585b268b --- /dev/null +++ b/github-bot/src/main/java/io/github/martinwitt/laughing_train/services/SpoonAnalyzerService.java @@ -0,0 +1,44 @@ +package io.github.martinwitt.laughing_train.services; + +import com.google.common.base.Strings; +import com.google.common.flogger.FluentLogger; +import io.github.martinwitt.laughing_train.data.request.AnalyzerRequest; +import io.github.martinwitt.laughing_train.data.result.CodeAnalyzerResult; +import io.github.martinwitt.laughing_train.domain.entity.AnalyzerResult; +import jakarta.enterprise.context.ApplicationScoped; +import java.io.File; +import java.util.List; +import xyz.keksdose.spoon.code_solver.analyzer.spoon.SpoonBasedAnalyzer; + +@ApplicationScoped +public class SpoonAnalyzerService { + + final AnalyzerResultPersistenceService analyzerResultPersistenceService; + final ProjectConfigService projectConfigService; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + SpoonAnalyzerService( + AnalyzerResultPersistenceService analyzerResultPersistenceService, + ProjectConfigService projectConfigService) { + this.analyzerResultPersistenceService = analyzerResultPersistenceService; + this.projectConfigService = projectConfigService; + } + + public CodeAnalyzerResult analyze(AnalyzerRequest request) { + logger.atInfo().log("Received request %s", request); + try { + if (request instanceof AnalyzerRequest.WithProject project) { + File folder = project.project().folder(); + SpoonBasedAnalyzer analyzer = new SpoonBasedAnalyzer(); + List analyze = analyzer.analyze(folder.toPath()); + CodeAnalyzerResult.Success success = new CodeAnalyzerResult.Success(analyze, project.project()); + analyzerResultPersistenceService.persistResults(success); + return success; + } else { + return new CodeAnalyzerResult.Failure("Unknown request type"); + } + } catch (Exception e) { + return new CodeAnalyzerResult.Failure(Strings.nullToEmpty(e.getMessage())); + } + } +} diff --git a/settings.gradle b/settings.gradle index 6569ddd21..55b242992 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'laughing-train-project' -include(':code-transformation',":commons", ":github-bot", ":application", ":matcher") \ No newline at end of file +include(':code-transformation',":commons", ":github-bot", ":application", ":matcher", ":spoon-analyzer") \ No newline at end of file diff --git a/spoon-analyzer/build.gradle b/spoon-analyzer/build.gradle new file mode 100644 index 000000000..5259d1dd7 --- /dev/null +++ b/spoon-analyzer/build.gradle @@ -0,0 +1,8 @@ + + +plugins { + id 'xyz.keksdose.spoon.code_solver.java-common-conventions' +} +dependencies { + implementation project (":matcher") +} \ No newline at end of file diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/BadSmell.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/BadSmell.java new file mode 100644 index 000000000..332b40ab7 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/BadSmell.java @@ -0,0 +1,31 @@ +package io.github.martinwitt.spoon_analyzer; + +import spoon.reflect.declaration.CtType; + +public interface BadSmell { + + String getName(); + + String getDescription(); + + CtType getAffectedType(); + + default T accept(BadSmellVisitor visitor) { + return visitor.visit(this); + } + + /** + * Fixes the bad smell. Fixing means changing the source code in a way that the bad smell is not present anymore. + * This method is not supported by default. Call {@link #isFixable()} to check if the bad smell is fixable. + */ + default void fix() { + throw new UnsupportedOperationException("Fixing this bad smell is not supported."); + } + + /** + * @return true if the bad smell is fixable, false otherwise. + */ + default boolean isFixable() { + return false; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/BadSmellVisitor.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/BadSmellVisitor.java new file mode 100644 index 000000000..a0d1bc174 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/BadSmellVisitor.java @@ -0,0 +1,73 @@ +package io.github.martinwitt.spoon_analyzer; + +import io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains.IndexOfReplaceableByContains; +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.innerclass_may_be_static.InnerClassMayBeStatic; +import io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class.NonProtectedConstructorInAbstractClass; +import io.github.martinwitt.spoon_analyzer.badsmells.private_final_method.PrivateFinalMethod; +import io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty.SizeReplaceableByIsEmpty; +import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements.UnnecessaryImplements; +import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_tostring.UnnecessaryTostring; + +/** + * A visitor interface for bad smells in code. + * + * @param the return type of the visitor methods + */ +public interface BadSmellVisitor extends Visitor { + + /** + * Visits a generic bad smell, which is not further specified. This method is called if no other method is more specific. + * @param badSmell the bad smell to visit + * @return the result of the visit + */ + default U visit(BadSmell badSmell) { + return emptyResult(); + } + + default U visit(IndexOfReplaceableByContains badSmell) { + return emptyResult(); + } + + default U visit(AccessStaticViaInstance badSmell) { + return emptyResult(); + } + + default U visit(ArrayCanBeReplacedWithEnumValues badSmell) { + return emptyResult(); + } + + default U visit(CharsetObjectCanBeUsed badSmell) { + return emptyResult(); + } + + default U visit(InnerClassMayBeStatic badSmell) { + return emptyResult(); + } + + default U visit(NonProtectedConstructorInAbstractClass badSmell) { + return emptyResult(); + } + + default U visit(PrivateFinalMethod badSmell) { + return emptyResult(); + } + + default U visit(SizeReplaceableByIsEmpty badSmell) { + return emptyResult(); + } + + default U visit(UnnecessaryImplements badSmell) { + return emptyResult(); + } + + default U visit(UnnecessaryTostring badSmell) { + return emptyResult(); + } + + default U emptyResult() { + return null; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/LocalAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/LocalAnalyzer.java new file mode 100644 index 000000000..87b11db95 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/LocalAnalyzer.java @@ -0,0 +1,8 @@ +package io.github.martinwitt.spoon_analyzer; + +import java.util.List; +import spoon.reflect.declaration.CtType; + +public interface LocalAnalyzer { + List analyze(CtType clazz); +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/LocalRefactor.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/LocalRefactor.java new file mode 100644 index 000000000..1781b94bf --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/LocalRefactor.java @@ -0,0 +1,9 @@ +package io.github.martinwitt.spoon_analyzer; + +public interface LocalRefactor { + + /** + * Refactors the given bad smell. + */ + void refactor(T badSmell); +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/PathUtils.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/PathUtils.java new file mode 100644 index 000000000..233b7bd8c --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/PathUtils.java @@ -0,0 +1,49 @@ +package io.github.martinwitt.spoon_analyzer; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class PathUtils { + + /** + * Removes paths that are already covered by a deeper one. + * @param paths the list of paths to filter + * @return a new list of paths with duplicates removed + */ + public static List removeRedundantPaths(List paths) { + List result = new ArrayList<>(); + List sorted = new ArrayList<>(paths); + sorted.sort((p1, p2) -> p1.getNameCount() - p2.getNameCount()); + for (Path path : sorted) { + boolean isRedundant = false; + for (Path other : result) { + if (path.startsWith(other)) { + isRedundant = true; + break; + } + } + if (!isRedundant) { + result.add(path); + } + } + return result; + } + + /** + * Filters input paths that contain resources or test files. + * @param paths the list of paths to filter + * @return a new list of paths without resources or test files + */ + public static List filterResourcePaths(List paths) { + return paths.stream() + .filter(path -> { + String pathString = path.toString(); + return !pathString.contains("src/main/resources") + && !pathString.contains("src/test/resources") + && !pathString.contains("src/test/java"); + }) + .collect(Collectors.toList()); + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/SpoonAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/SpoonAnalyzer.java new file mode 100644 index 000000000..3000691c5 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/SpoonAnalyzer.java @@ -0,0 +1,70 @@ +package io.github.martinwitt.spoon_analyzer; + +import io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains.IndexOfReplaceableByContainsAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance.AccessStaticViaInstanceAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.array_can_be_replaced_with_enum_values.ArrayCanBeReplacedWithEnumValuesAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used.CharsetObjectCanBeUsedAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static.InnerClassMayBeStaticAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class.NonProtectedConstructorInAbstractClassAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.private_final_method.PrivateFinalMethodAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty.SizeReplaceableByIsEmptyAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements.UnnecessaryImplementsAnalyzer; +import io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_tostring.UnnecessaryTostringAnalyzer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import spoon.Launcher; +import spoon.reflect.declaration.CtType; + +public class SpoonAnalyzer { + + private final List localAnalyzers; + + public SpoonAnalyzer() { + this.localAnalyzers = new ArrayList<>(); + localAnalyzers.add(new IndexOfReplaceableByContainsAnalyzer()); + localAnalyzers.add(new AccessStaticViaInstanceAnalyzer()); + localAnalyzers.add(new ArrayCanBeReplacedWithEnumValuesAnalyzer()); + localAnalyzers.add(new CharsetObjectCanBeUsedAnalyzer()); + localAnalyzers.add(new InnerClassMayBeStaticAnalyzer()); + localAnalyzers.add(new UnnecessaryImplementsAnalyzer()); + localAnalyzers.add(new UnnecessaryTostringAnalyzer()); + localAnalyzers.add(new NonProtectedConstructorInAbstractClassAnalyzer()); + localAnalyzers.add(new PrivateFinalMethodAnalyzer()); + localAnalyzers.add(new SizeReplaceableByIsEmptyAnalyzer()); + } + + public List analyze(String path) { + + List badSmells = new ArrayList<>(); + try { + List files = + Files.walk(Path.of(path)).filter(v -> Files.isDirectory(v)).toList(); + files = PathUtils.removeRedundantPaths(PathUtils.filterResourcePaths(files)); + + Launcher launcher = new Launcher(); + for (Path p : files) { + launcher.addInputResource(p.toString()); + } + launcher.getEnvironment().setAutoImports(true); + launcher.getEnvironment().setIgnoreDuplicateDeclarations(true); + launcher.getEnvironment().setNoClasspath(true); + launcher.getEnvironment().setComplianceLevel(17); + launcher.getEnvironment().setIgnoreSyntaxErrors(true); + var model = launcher.buildModel(); + System.out.println("Found " + model.getAllTypes().size() + " types."); + for (CtType type : model.getAllTypes()) { + for (LocalAnalyzer analyzer : localAnalyzers) { + var badSmell = analyzer.analyze(type); + if (badSmell != null) { + badSmells.addAll(badSmell); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return badSmells; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/Visitor.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/Visitor.java new file mode 100644 index 000000000..681c5b529 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/Visitor.java @@ -0,0 +1,3 @@ +package io.github.martinwitt.spoon_analyzer; + +public interface Visitor {} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContains.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContains.java new file mode 100644 index 000000000..d0fb25c6d --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContains.java @@ -0,0 +1,63 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.code.CtExpression; +import spoon.reflect.declaration.CtType; + +public class IndexOfReplaceableByContains implements BadSmell { + + private final CtType affectedType; + private final CtExpression indexOfCall; + private final CtExpression minusOne; + + public IndexOfReplaceableByContains(CtType affectedType, CtExpression indexOfCall, CtExpression minusOne) { + this.affectedType = affectedType; + this.indexOfCall = indexOfCall; + this.minusOne = minusOne; + } + + @Override + public String getName() { + return "IndexOfReplaceableByContains"; + } + + @Override + public String getDescription() { + + return "The indexOf method returns -1 if the substring is not found. This can be replaced by the contains method."; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + /** + * @return the indexOfCall + */ + public CtExpression getIndexOfCall() { + return indexOfCall; + } + /** + * @return the minusOne + */ + public CtExpression getMinusOne() { + return minusOne; + } + + @Override + public String toString() { + return "IndexOfReplaceableByContains [affectedType=" + affectedType.getQualifiedName() + ", indexOfCall=" + + indexOfCall + ", minusOne=" + minusOne + "]"; + } + + @Override + public void fix() { + new IndexOfReplaceableByContainsAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContainsAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContainsAnalyzer.java new file mode 100644 index 000000000..b5b8e1bbf --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContainsAnalyzer.java @@ -0,0 +1,76 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains; + +import io.github.martinwitt.laughing_train.spoonutils.InvocationMatcher; +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.UnaryOperatorKind; +import spoon.reflect.declaration.CtType; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.visitor.filter.TypeFilter; + +public class IndexOfReplaceableByContainsAnalyzer + implements LocalAnalyzer, LocalRefactor { + + private InvocationMatcher matcher; + + public IndexOfReplaceableByContainsAnalyzer() { + matcher = new InvocationMatcher("java.lang.String", "indexOf"); + } + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + List> list = clazz.getElements(new TypeFilter<>(CtBinaryOperator.class)); + for (CtBinaryOperator ctBinaryOperator : list) { + CtExpression rightHandOperand = ctBinaryOperator.getRightHandOperand(); + CtExpression leftHandOperand = ctBinaryOperator.getLeftHandOperand(); + if (isIndexOfCall(leftHandOperand) && isMinusOne(rightHandOperand)) { + badSmells.add(new IndexOfReplaceableByContains(clazz, leftHandOperand, rightHandOperand)); + } else if (isIndexOfCall(rightHandOperand) && isMinusOne(leftHandOperand)) { + badSmells.add(new IndexOfReplaceableByContains(clazz, rightHandOperand, leftHandOperand)); + } + } + return badSmells; + } + + private boolean isIndexOfCall(CtExpression expression) { + if (expression instanceof CtInvocation innvocation) { + return matcher.matches(innvocation); + } + return false; + } + + private boolean isMinusOne(CtExpression expression) { + if (expression instanceof CtUnaryOperator operator + && operator.getKind().equals(UnaryOperatorKind.NEG)) { + return operator.getOperand() instanceof CtLiteral literal + && literal.getValue().equals(1); + } + return false; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void refactor(IndexOfReplaceableByContains badSmell) { + Factory factory = badSmell.getIndexOfCall().getFactory(); + CtExpression indexOfCall = badSmell.getIndexOfCall(); + CtBinaryOperator parent = indexOfCall.getParent(CtBinaryOperator.class); + if (indexOfCall instanceof CtInvocation invocation && parent != null) { + CtExecutableReference containsCalls = factory.createExecutableReference() + .setDeclaringType(factory.Type().createReference("java.lang.String")) + .setSimpleName("contains"); + invocation.setExecutable(containsCalls); + parent.replace( + factory.createUnaryOperator().setKind(UnaryOperatorKind.NOT).setOperand(invocation)); + } + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstance.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstance.java new file mode 100644 index 000000000..cc1e881b4 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstance.java @@ -0,0 +1,57 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.declaration.CtType; + +public class AccessStaticViaInstance implements BadSmell { + + private static final String description = + "Accessing static methods should be done via the class name, not via an instance."; + + private final CtType affectedCtType; + private final CtInvocation affectedCtInvocation; + + public AccessStaticViaInstance(CtType affectedCtType, CtInvocation affectedCtInvocation) { + this.affectedCtType = affectedCtType; + this.affectedCtInvocation = affectedCtInvocation; + } + + @Override + public String getName() { + return "AccessStaticViaInstance"; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public CtType getAffectedType() { + return affectedCtType; + } + + /** + * @return the affectedCtInvocation + */ + public CtInvocation getAffectedCtInvocation() { + return affectedCtInvocation; + } + + @Override + public String toString() { + return "AccessStaticViaInstance [affectedCtType=" + affectedCtType.getQualifiedName() + + ", affectedCtInvocation=" + affectedCtInvocation + "]"; + } + + @Override + public void fix() { + new AccessStaticViaInstanceAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstanceAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstanceAnalyzer.java new file mode 100644 index 000000000..fee020a61 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstanceAnalyzer.java @@ -0,0 +1,53 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtTypeAccess; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtType; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.Filter; + +public class AccessStaticViaInstanceAnalyzer implements LocalAnalyzer, LocalRefactor { + + @Override + public List analyze(CtType clazz) { + List> elements = clazz.getElements(new StaticInvocationFilter()); + List results = new ArrayList<>(); + for (CtInvocation element : elements) { + results.add(new AccessStaticViaInstance(clazz, element)); + } + return results; + } + + private final class StaticInvocationFilter implements Filter> { + + @Override + public boolean matches(CtInvocation element) { + if (!Optional.ofNullable(element.getExecutable()) + .map(v -> v.getExecutableDeclaration()) + .filter(v -> v instanceof CtMethod) + .map(v -> (CtMethod) v) + .map(v -> v.isStatic()) + .orElse(false)) { + return false; + } + return element.getTarget() != null && !(element.getTarget() instanceof CtTypeAccess); + } + } + + @Override + public void refactor(AccessStaticViaInstance badSmell) { + CtInvocation affectedCtInvocation = badSmell.getAffectedCtInvocation(); + CtTypeReference target = affectedCtInvocation.getTarget().getType(); + CtTypeAccess typeAccess = + badSmell.getAffectedType().getFactory().Code().createTypeAccess(target); + typeAccess.getAccessedType().setSimplyQualified(true); + affectedCtInvocation.setTarget(typeAccess); + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/array_can_be_replaced_with_enum_values/ArrayCanBeReplacedWithEnumValues.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/array_can_be_replaced_with_enum_values/ArrayCanBeReplacedWithEnumValues.java new file mode 100644 index 000000000..28e995085 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/array_can_be_replaced_with_enum_values/ArrayCanBeReplacedWithEnumValues.java @@ -0,0 +1,40 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.array_can_be_replaced_with_enum_values; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.code.CtNewArray; +import spoon.reflect.declaration.CtType; + +// https://github.com/JetBrains/intellij-community/blob/master/plugins/InspectionGadgets/src/inspectionDescriptions/ArrayCanBeReplacedWithEnumValues.html +public class ArrayCanBeReplacedWithEnumValues implements BadSmell { + + private static final String NAME = "ArrayCanBeReplacedWithEnumValues"; + private static final String DESCRIPTION = + "Instead of listing all enum values in an array, you can use the `Enum.values() directly. This makes the code more readable and less error prone. There are no updates needed if there is a new enum value."; + + private final CtType affectedType; + private final CtNewArray affectedElement; + + public ArrayCanBeReplacedWithEnumValues(CtType affectedType, CtNewArray affectedElement) { + this.affectedType = affectedType; + this.affectedElement = affectedElement; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + public CtNewArray getAffectedElement() { + return affectedElement; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/array_can_be_replaced_with_enum_values/ArrayCanBeReplacedWithEnumValuesAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/array_can_be_replaced_with_enum_values/ArrayCanBeReplacedWithEnumValuesAnalyzer.java new file mode 100644 index 000000000..bb3559056 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/array_can_be_replaced_with_enum_values/ArrayCanBeReplacedWithEnumValuesAnalyzer.java @@ -0,0 +1,50 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.array_can_be_replaced_with_enum_values; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtNewArray; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtType; +import spoon.reflect.visitor.Filter; + +public class ArrayCanBeReplacedWithEnumValuesAnalyzer implements LocalAnalyzer { + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + EnumArrayInitializer filter = new EnumArrayInitializer(); + for (CtNewArray element : clazz.getElements(filter)) { + badSmells.add(new ArrayCanBeReplacedWithEnumValues(clazz, element)); + } + return badSmells; + } + + private static final class EnumArrayInitializer implements Filter> { + + @Override + public boolean matches(CtNewArray element) { + List> elements = element.getElements(); + if (elements.isEmpty()) { + return false; + } + // check if all elements are enum values + for (CtExpression ctExpression : elements) { + if (!Optional.ofNullable(ctExpression.getType()) + .map(v -> v.isEnum()) + .orElse(false)) { + return false; + } + } + long count = elements.stream().map(CtExpression::getType).distinct().count(); + CtEnum declaration = (CtEnum) elements.get(0).getType().getTypeDeclaration(); + if (declaration == null) { + return false; + } + return !(declaration.getEnumValues().size() != count); + } + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsed.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsed.java new file mode 100644 index 000000000..e13ec39da --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsed.java @@ -0,0 +1,74 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.code.CtConstructorCall; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.declaration.CtType; + +public class CharsetObjectCanBeUsed implements BadSmell { + + private static final String NAME = "CharsetObjectCanBeUsed"; + private static final String DESCRIPTION = "The Charset object can be used instead of the String object."; + + private final CtType affectedType; + private CtInvocation invocation; + private CtConstructorCall ctorCall; + + public CharsetObjectCanBeUsed(CtType affectedType, CtInvocation invocation) { + this.affectedType = affectedType; + this.invocation = invocation; + } + + public CharsetObjectCanBeUsed(CtType affectedType, CtConstructorCall invocation) { + this.affectedType = affectedType; + this.ctorCall = invocation; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + /** + * @return the invocation + */ + public CtInvocation getInvocation() { + return invocation; + } + /** + * @return the ctorCall + */ + public CtConstructorCall getCtorCall() { + return ctorCall; + } + + @Override + public void fix() { + new CharsetObjectCanBeUsedAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } + + @Override + public String toString() { + if (invocation == null) { + return "CharsetObjectCanBeUsed [affectedType=" + affectedType.getQualifiedName() + ", invocation=" + + ctorCall + "]"; + } + return "CharsetObjectCanBeUsed [affectedType=" + affectedType.getQualifiedName() + ", invocation=" + invocation + + "]"; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsedAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsedAnalyzer.java new file mode 100644 index 000000000..2fccf60d0 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsedAnalyzer.java @@ -0,0 +1,130 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used; + +import static java.util.Map.entry; + +import io.github.martinwitt.laughing_train.spoonutils.ConstructorMatcher; +import io.github.martinwitt.laughing_train.spoonutils.InvocationMatcher; +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.declaration.CtType; + +public class CharsetObjectCanBeUsedAnalyzer implements LocalAnalyzer, LocalRefactor { + + private static final Map SUPPORTED_CHARSETS = Map.ofEntries( + entry("US-ASCII", "US_ASCII"), + entry("ASCII", "US_ASCII"), + entry("ISO646-US", "US_ASCII"), + entry("ISO-8859-1", "ISO_8859_1"), + entry("8859_1", "ISO_8859_1"), + entry("UTF-8", "UTF_8"), + entry("UTF8", "UTF_8"), + entry("UTF-16BE", "UTF_16BE"), + entry("UTF16BE", "UTF_16BE"), + entry("UTF-16LE", "UTF_16LE"), + entry("UTF16LE", "UTF_16LE"), + entry("UTF-16", "UTF_16"), + entry("UTF16", "UTF_16")); + + private static final List matcher = List.of( + new InvocationMatcher("java.lang.String", "getBytes", "java.lang.String"), + new InvocationMatcher("java.io.ByteArrayOutputStream", "toString", "java.lang.String"), + new InvocationMatcher("java.net.URLDecoder", "decode", "java.lang.String", "java.lang.String"), + new InvocationMatcher("java.net.URLEncoder", "encode", "java.lang.String", "java.lang.String"), + new InvocationMatcher( + "java.nio.channels.Channels", + "newReader", + "java.nio.channels.ReadableByteChannel", + "java.lang.String"), + new InvocationMatcher( + "java.nio.channels.Channels", + "newWriter", + "java.nio.channels.WritableByteChannel", + "java.lang.String"), + new InvocationMatcher( + "java.util.Properties", + "storeToXML", + "java.io.OutputStream", + "java.lang.String", + "java.lang.String")); + private static final List ctorMatcher = List.of( + new ConstructorMatcher("java.lang.InputStreamerReader", "java.io.InputStream", "java.lang.String"), + new ConstructorMatcher("java.lang.OutputStreamWriter", "java.io.OutputStream", "java.lang.String"), + new ConstructorMatcher("java.lang.String", "byte[]", "int", "int", "java.lang.String"), + new ConstructorMatcher("java.lang.String", "byte[]", "java.lang.String"), + new ConstructorMatcher("java.util.Scanner", "java.io.InputStream", "java.lang.String"), + new ConstructorMatcher("java.util.Scanner", "java.io.File", "java.lang.String"), + new ConstructorMatcher("java.util.Scanner", "java.nio.file.Path", "java.lang.String"), + new ConstructorMatcher("java.util.Scanner", "java.nio.channels.ReadableByteChannel", "java.lang.String"), + new ConstructorMatcher("java.io.PrintStream", "java.io.OutputStream", "boolean", "java.lang.String"), + new ConstructorMatcher("java.io.PrintStream", "java.lang.String", "java.lang.String"), + new ConstructorMatcher("java.io.PrintStream", "java.io.File", "java.lang.String"), + new ConstructorMatcher("java.io.PrintWriter", "java.io.OutputStream", "boolean", "java.lang.String"), + new ConstructorMatcher("java.io.PrintWriter", "java.lang.String", "java.lang.String"), + new ConstructorMatcher("java.io.PrintWriter", "java.io.File", "java.lang.String")); + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + for (InvocationMatcher invocationMatcher : matcher) { + clazz.getElements(invocationMatcher).stream() + .map(v -> new CharsetObjectCanBeUsed(clazz, v)) + .forEach(badSmells::add); + } + for (ConstructorMatcher constructorMatcher : ctorMatcher) { + clazz.getElements(constructorMatcher).stream() + .map(v -> new CharsetObjectCanBeUsed(clazz, v)) + .forEach(badSmells::add); + } + return badSmells; + } + // https://github.com/JetBrains/intellij-community/blob/3bdfb90acd447c2c70304ab4a2d53e19204aec5a/java/java-impl-inspections/src/com/intellij/codeInspection/CharsetObjectCanBeUsedInspection.java#LL73C1-L86C33 + /* + new CharsetConstructorMatcher("java.io.InputStreamReader", "java.io.InputStream", ""), + new CharsetConstructorMatcher("java.io.OutputStreamWriter", "java.io.OutputStream", ""), + new CharsetConstructorMatcher(JAVA_LANG_STRING, "byte[]", "int", "int", ""), + new CharsetConstructorMatcher(JAVA_LANG_STRING, "byte[]", ""), + new CharsetConstructorMatcher("java.util.Scanner", "java.io.InputStream", ""), + new CharsetConstructorMatcher("java.util.Scanner", JAVA_IO_FILE, ""), + new CharsetConstructorMatcher("java.util.Scanner", "java.nio.file.Path", ""), + new CharsetConstructorMatcher("java.util.Scanner", "java.nio.channels.ReadableByteChannel", ""), + new CharsetConstructorMatcher("java.io.PrintStream", "java.io.OutputStream", "boolean", ""), + new CharsetConstructorMatcher("java.io.PrintStream", JAVA_LANG_STRING, ""), + new CharsetConstructorMatcher("java.io.PrintStream", JAVA_IO_FILE, ""), + new CharsetConstructorMatcher("java.io.PrintWriter", "java.io.OutputStream", "boolean", ""), + new CharsetConstructorMatcher("java.io.PrintWriter", JAVA_LANG_STRING, ""), + new CharsetConstructorMatcher("java.io.PrintWriter", JAVA_IO_FILE, ""), + + new CharsetMethodMatcher(JAVA_LANG_STRING, "getBytes", ""), + new CharsetMethodMatcher("java.io.ByteArrayOutputStream", "toString", ""), + new CharsetMethodMatcher("java.net.URLDecoder", "decode", JAVA_LANG_STRING, ""), + new CharsetMethodMatcher("java.net.URLEncoder", "encode", JAVA_LANG_STRING, ""), + new CharsetMethodMatcher("java.nio.channels.Channels", "newReader", "java.nio.channels.ReadableByteChannel", ""), + new CharsetMethodMatcher("java.nio.channels.Channels", "newWriter", "java.nio.channels.WritableByteChannel", ""), + new CharsetMethodMatcher(JAVA_UTIL_PROPERTIES, "storeToXML", "java.io.OutputStream", JAVA_LANG_STRING, ""), + */ + + @Override + public void refactor(CharsetObjectCanBeUsed badSmell) { + List> arguments = Optional.ofNullable(badSmell.getInvocation()) + .map(v -> v.getArguments()) + .orElse(badSmell.getCtorCall().getArguments()); + CtExpression ctExpression = arguments.get(arguments.size() - 1); + if (ctExpression instanceof CtLiteral) { + CtLiteral ctLiteral = (CtLiteral) ctExpression; + String charset = ctLiteral.getValue().toString(); + String string = SUPPORTED_CHARSETS.get(charset); + if (string != null) { + ctExpression.replace(ctExpression + .getFactory() + .createCodeSnippetExpression("java.nio.charset.StandardCharsets." + string)); + } + } + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStatic.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStatic.java new file mode 100644 index 000000000..283e2f8ef --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStatic.java @@ -0,0 +1,53 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.declaration.CtType; + +public class InnerClassMayBeStatic implements BadSmell { + + private final CtType affectedType; + private final CtType innerClass; + private static final String name = "Inner Class May Be Static"; + private static final String description = + "This class is an inner class and may be static. Static inner classes dont need the reference to the outer class."; + + public InnerClassMayBeStatic(CtType affectedType, CtType innerClass) { + this.affectedType = affectedType; + this.innerClass = innerClass; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + public CtType getInnerClass() { + return innerClass; + } + + @Override + public String toString() { + return "InnerClassMayBeStatic [affectedType=" + affectedType.getQualifiedName() + ", innerClass=" + + innerClass.getQualifiedName() + "]"; + } + + @Override + public void fix() { + new InnerClassMayBeStaticAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStaticAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStaticAnalyzer.java new file mode 100644 index 000000000..88ca8eeb5 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStaticAnalyzer.java @@ -0,0 +1,97 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtFieldReference; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.filter.TypeFilter; + +public class InnerClassMayBeStaticAnalyzer implements LocalAnalyzer, LocalRefactor { + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + for (CtType innerClazz : clazz.getNestedTypes()) { + if (innerClazz.isStatic()) { + continue; + } + // get all refereced executables + if (referencesParentWithField(innerClazz)) { + continue; + } + if (referencesParentWithMethod(innerClazz)) { + continue; + } + if (referencedInnerClassesAreStatic(innerClazz)) { + continue; + } + Set> referencedTypes = innerClazz.getReferencedTypes(); + if (referencedTypes.stream().anyMatch(v -> innerClazz.hasParent(v))) { + continue; + } + badSmells.add(new InnerClassMayBeStatic(clazz, innerClazz)); + } + return badSmells; + } + + private boolean referencesParentWithMethod(CtType innerClazz) { + for (CtExecutableReference referencedExecutables : + innerClazz.getElements(new TypeFilter<>(CtExecutableReference.class))) { + CtTypeReference declaringType = referencedExecutables.getDeclaringType(); + if (declaringType == null) { + continue; + } + CtType declaration = declaringType.getDeclaration(); + if (declaration == null) { + continue; + } + if (innerClazz.hasParent(declaration)) { + return true; + } + } + return false; + } + + private boolean referencesParentWithField(CtType innerClazz) { + for (CtFieldReference fieldReference : innerClazz.getElements(new TypeFilter<>(CtFieldReference.class))) { + CtTypeReference declaringTypeRef = fieldReference.getDeclaringType(); + if (declaringTypeRef == null) { + continue; + } + CtType type = declaringTypeRef.getTypeDeclaration(); + if (type == null) { + continue; + } + if (innerClazz.hasParent(type)) { + return true; + } + } + return false; + } + + private boolean referencedInnerClassesAreStatic(CtType innerClazz) { + for (CtTypeReference referencedType : innerClazz.getReferencedTypes()) { + CtType referencedTypeDeclaration = referencedType.getTypeDeclaration(); + if (referencedTypeDeclaration == null) { + continue; + } + if (referencedTypeDeclaration.isStatic() && !referencedTypeDeclaration.isTopLevel()) { + continue; + } + return false; + } + return true; + } + + @Override + public void refactor(InnerClassMayBeStatic badSmell) { + badSmell.getInnerClass().addModifier(ModifierKind.STATIC); + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClass.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClass.java new file mode 100644 index 000000000..f906150c6 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClass.java @@ -0,0 +1,54 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.declaration.CtConstructor; +import spoon.reflect.declaration.CtType; + +public class NonProtectedConstructorInAbstractClass implements BadSmell { + + private final CtType clazz; + private final CtConstructor ctConstructor; + + public NonProtectedConstructorInAbstractClass(CtType clazz, CtConstructor ctConstructor) { + this.clazz = clazz; + this.ctConstructor = ctConstructor; + } + + @Override + public String getName() { + return "NonProtectedConstructorInAbstractClass"; + } + + @Override + public String getDescription() { + return "A non-protected constructor in an abstract class is not accessible from outside the package. Adding the modifier public is not needed, as only subclasses can call the constructor."; + } + + @Override + public CtType getAffectedType() { + return clazz; + } + + @Override + public String toString() { + return "NonProtectedConstructorInAbstractClass [clazz=" + clazz.getQualifiedName() + ", ctConstructor=" + + ctConstructor.getSignature() + "]"; + } + + /** + * @return the ctConstructor + */ + public CtConstructor getCtConstructor() { + return ctConstructor; + } + + @Override + public void fix() { + new NonProtectedConstructorInAbstractClassAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClassAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClassAnalyzer.java new file mode 100644 index 000000000..a038fde0b --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClassAnalyzer.java @@ -0,0 +1,36 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import spoon.reflect.declaration.CtConstructor; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.visitor.filter.TypeFilter; + +public class NonProtectedConstructorInAbstractClassAnalyzer + implements LocalAnalyzer, LocalRefactor { + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + if (!clazz.isAbstract()) { + return badSmells; + } + List> elements = clazz.getElements(new TypeFilter<>(CtConstructor.class)); + for (CtConstructor ctConstructor : elements) { + if (!ctConstructor.isProtected() && ctConstructor.isPublic()) { + badSmells.add(new NonProtectedConstructorInAbstractClass(clazz, ctConstructor)); + } + } + return badSmells; + } + + @Override + public void refactor(NonProtectedConstructorInAbstractClass badSmell) { + badSmell.getCtConstructor().removeModifier(ModifierKind.PUBLIC); + badSmell.getCtConstructor().addModifier(ModifierKind.PROTECTED); + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethod.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethod.java new file mode 100644 index 000000000..adacd683b --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethod.java @@ -0,0 +1,54 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.private_final_method; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtType; + +public class PrivateFinalMethod implements BadSmell { + + private static final String NAME = "PrivateFinalMethod"; + + private static final String DESCRIPTION = + "Private method cant be overridden, so there is no reason for the final. Less modifiers are easier to read."; + + private CtType affectedType; + private CtMethod affectedMethod; + + /** + * @param affectedType + * @param affectedMethod + */ + public PrivateFinalMethod(CtType affectedType, CtMethod affectedMethod) { + this.affectedType = affectedType; + this.affectedMethod = affectedMethod; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + public CtMethod getAffectedMethod() { + return affectedMethod; + } + + @Override + public void fix() { + new PrivateFinalMethodAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethodAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethodAnalyzer.java new file mode 100644 index 000000000..85ca47a60 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethodAnalyzer.java @@ -0,0 +1,39 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.private_final_method; + +import io.github.martinwitt.laughing_train.spoonutils.matcher.Matchers; +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.visitor.Filter; + +public class PrivateFinalMethodAnalyzer implements LocalAnalyzer, LocalRefactor { + + private Filter> filter = new PrivateFinalMethodFilter(); + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + for (CtMethod method : clazz.getElements(filter)) { + badSmells.add(new PrivateFinalMethod(clazz, method)); + } + return badSmells; + } + + private static class PrivateFinalMethodFilter implements Filter> { + + @Override + public boolean matches(CtMethod element) { + return Matchers.allOf(Matchers.isPrivate(), Matchers.isFinal()).matches(element); + } + } + + @Override + public void refactor(PrivateFinalMethod badSmell) { + badSmell.getAffectedMethod().removeModifier(ModifierKind.FINAL); + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/size_replaceable_by_is_empty/SizeReplaceableByIsEmpty.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/size_replaceable_by_is_empty/SizeReplaceableByIsEmpty.java new file mode 100644 index 000000000..f711a422d --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/size_replaceable_by_is_empty/SizeReplaceableByIsEmpty.java @@ -0,0 +1,65 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.declaration.CtType; + +public class SizeReplaceableByIsEmpty implements BadSmell { + private static final String NAME = "SizeReplaceableByIsEmpty"; + private static final String DESCRIPTION = + "Checking if an collection is empty by comparing its size to 0 is redundant. Use isEmpty() instead."; + private final CtType affectedType; + private final CtBinaryOperator element; + private final CtInvocation sizeInvocation; + private final CtExpression zeroLiteral; + + public SizeReplaceableByIsEmpty( + CtType affectedType, + CtBinaryOperator element, + CtInvocation sizeInvocation, + CtExpression zeroLiteral) { + this.affectedType = affectedType; + this.element = element; + this.sizeInvocation = sizeInvocation; + this.zeroLiteral = zeroLiteral; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + public CtBinaryOperator getElement() { + return element; + } + + public CtInvocation getSizeInvocation() { + return sizeInvocation; + } + + public CtExpression getZeroLiteral() { + return zeroLiteral; + } + + @Override + public void fix() { + new SizeReplaceableByIsEmptyAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/size_replaceable_by_is_empty/SizeReplaceableByIsEmptyAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/size_replaceable_by_is_empty/SizeReplaceableByIsEmptyAnalyzer.java new file mode 100644 index 000000000..1afcccfe9 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/size_replaceable_by_is_empty/SizeReplaceableByIsEmptyAnalyzer.java @@ -0,0 +1,89 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty; + +import io.github.martinwitt.laughing_train.spoonutils.matcher.Matchers; +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import spoon.reflect.code.BinaryOperatorKind; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.UnaryOperatorKind; +import spoon.reflect.declaration.CtType; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.visitor.Filter; + +public class SizeReplaceableByIsEmptyAnalyzer implements LocalAnalyzer, LocalRefactor { + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + List> elements = clazz.getElements(new SizeIsEmptyFilter()); + for (CtBinaryOperator ctBinaryOperator : elements) { + CtInvocation sizeInvocation = getSizeInvocation(ctBinaryOperator); + CtExpression zeroLiteral = getZeroLiteral(ctBinaryOperator).orElse(null); + if (sizeInvocation != null && zeroLiteral != null) { + badSmells.add(new SizeReplaceableByIsEmpty(clazz, ctBinaryOperator, sizeInvocation, zeroLiteral)); + } + } + return badSmells; + } + + private final class SizeIsEmptyFilter implements Filter> { + + @Override + public boolean matches(CtBinaryOperator element) { + CtExpression zeroLiteral = getZeroLiteral(element).orElse(null); + CtExpression sizeInvocation = getSizeInvocation(element); + return (sizeInvocation != null && zeroLiteral != null); + } + } + + private Optional> isSizeInvocation(CtExpression expression) { + if (expression instanceof CtInvocation invocation) { + if (invocation.getExecutable() != null) { + if (invocation.getExecutable().getSimpleName().equals("size")) { + return Optional.of(invocation); + } + } + } + return Optional.empty(); + } + + private CtInvocation getSizeInvocation(CtBinaryOperator element) { + return isSizeInvocation(element.getLeftHandOperand()) + .orElse(isSizeInvocation(element.getRightHandOperand()).orElse(null)); + } + + private Optional> getZeroLiteral(CtBinaryOperator element) { + if (Matchers.isLiteral(0).matches(element.getRightHandOperand())) { + return Optional.of(element.getRightHandOperand()); + } else if (Matchers.isLiteral(0).matches(element.getLeftHandOperand())) { + return Optional.of(element.getLeftHandOperand()); + } else { + return Optional.empty(); + } + } + + @Override + public void refactor(SizeReplaceableByIsEmpty badSmell) { + Factory factory = badSmell.getAffectedType().getFactory(); + CtExpression target = badSmell.getSizeInvocation().getTarget(); + CtExecutableReference isEmpty = + factory.Executable().createReference(target.getType(), false, factory.Type().BOOLEAN, "isEmpty"); + CtInvocation newInvocation = factory.Code().createInvocation(target, isEmpty); + CtBinaryOperator parent = badSmell.getSizeInvocation().getParent(CtBinaryOperator.class); + if (parent.getKind().equals(BinaryOperatorKind.EQ)) { + badSmell.getSizeInvocation().getParent(CtBinaryOperator.class).replace(newInvocation); + } else { + parent.replace(factory.Core() + .createUnaryOperator() + .setKind(UnaryOperatorKind.NOT) + .setOperand(newInvocation)); + } + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplements.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplements.java new file mode 100644 index 000000000..2f29a6ed4 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplements.java @@ -0,0 +1,66 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.declaration.CtType; +import spoon.reflect.reference.CtTypeReference; + +public class UnnecessaryImplements implements BadSmell { + + private final String name = "Unnecessary Implements"; + private final String description = "This class has 1 or more interfaces which are already implemented."; + + private final CtTypeReference lowerType; + private final CtTypeReference notNeededImplements; + private final CtType affectedType; + + public UnnecessaryImplements( + CtTypeReference lowerType, CtTypeReference notNeededImplements, CtType affectedType) { + this.lowerType = lowerType; + this.notNeededImplements = notNeededImplements; + this.affectedType = affectedType; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + /** + * @return the lowerType + */ + public CtTypeReference getLowerType() { + return lowerType; + } + /** + * @return the notNeededImplements + */ + public CtTypeReference getNotNeededImplements() { + return notNeededImplements; + } + + @Override + public String toString() { + return "UnnecessaryImplements [name=" + name + ", description=" + description + + ", lowerType=" + lowerType + ", notNeededImplements=" + notNeededImplements + + ", affectedType=" + affectedType.getQualifiedName() + "]"; + } + + @Override + public void fix() { + new UnnecessaryImplementsAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplementsAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplementsAnalyzer.java new file mode 100644 index 000000000..b3ea27d6e --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplementsAnalyzer.java @@ -0,0 +1,40 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import spoon.reflect.declaration.CtType; +import spoon.reflect.reference.CtTypeReference; + +public class UnnecessaryImplementsAnalyzer implements LocalAnalyzer, LocalRefactor { + + @Override + public List analyze(CtType clazz) { + Set> superInterfaces = clazz.getSuperInterfaces(); + ; + if (superInterfaces.isEmpty()) { + return List.of(); + } + List badSmells = new ArrayList<>(); + for (CtTypeReference ctTypeReference : superInterfaces) { + for (CtTypeReference needed : superInterfaces) { + if (ctTypeReference.equals(needed)) { + continue; + } + if (ctTypeReference.isSubtypeOf(needed)) { + badSmells.add(new UnnecessaryImplements(ctTypeReference, needed, clazz)); + } + } + } + return badSmells; + } + + @Override + public void refactor(UnnecessaryImplements badSmell) { + CtTypeReference notNeededImplements = badSmell.getNotNeededImplements(); + badSmell.getAffectedType().getSuperInterfaces().remove(notNeededImplements); + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_tostring/UnnecessaryTostring.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_tostring/UnnecessaryTostring.java new file mode 100644 index 000000000..d992c62a7 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_tostring/UnnecessaryTostring.java @@ -0,0 +1,50 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_tostring; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.declaration.CtType; + +public class UnnecessaryTostring implements BadSmell { + + private static final String name = "UnnecessaryTostring"; + private static final String description = "Calling to String on a String object is unnecessary."; + private final CtType affectedType; + private final CtInvocation notNeededTostring; + + public UnnecessaryTostring(CtType affectedType, CtInvocation notNeededTostring) { + this.affectedType = affectedType; + this.notNeededTostring = notNeededTostring; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public CtType getAffectedType() { + return affectedType; + } + + /** + * @return the notNeededTostring + */ + public CtInvocation getNotNeededTostring() { + return notNeededTostring; + } + + @Override + public void fix() { + new UnnecessaryTostringAnalyzer().refactor(this); + } + + @Override + public boolean isFixable() { + return true; + } +} diff --git a/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_tostring/UnnecessaryTostringAnalyzer.java b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_tostring/UnnecessaryTostringAnalyzer.java new file mode 100644 index 000000000..8f3b8abe2 --- /dev/null +++ b/spoon-analyzer/src/main/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_tostring/UnnecessaryTostringAnalyzer.java @@ -0,0 +1,33 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_tostring; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import io.github.martinwitt.spoon_analyzer.LocalRefactor; +import java.util.ArrayList; +import java.util.List; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.declaration.CtType; +import spoon.reflect.visitor.filter.TypeFilter; + +public class UnnecessaryTostringAnalyzer implements LocalAnalyzer, LocalRefactor { + + @Override + public List analyze(CtType clazz) { + List badSmells = new ArrayList<>(); + List> elements = clazz.getElements(new TypeFilter<>(CtInvocation.class)); + for (CtInvocation invocation : elements) { + if (invocation.getTarget() != null + && invocation.getTarget().getType() != null + && invocation.getTarget().getType().getSimpleName().equals("String") + && invocation.getExecutable().getSimpleName().equals("toString")) { + badSmells.add(new UnnecessaryTostring(clazz, invocation)); + } + } + return badSmells; + } + + @Override + public void refactor(UnnecessaryTostring badSmell) { + badSmell.getNotNeededTostring().delete(); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContainsAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContainsAnalyzerTest.java new file mode 100644 index 000000000..bdb5629b3 --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/Index_off_replaceable_by_contains/IndexOfReplaceableByContainsAnalyzerTest.java @@ -0,0 +1,86 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.Index_off_replaceable_by_contains; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class IndexOfReplaceableByContainsAnalyzerTest { + + @Test + public void testAnalyzeInnerSimpleClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = + """ + public class SimpleClass { + void bar { + String foo = "foo"; + var bar = foo.indexOf("f") != -1; + } + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new IndexOfReplaceableByContainsAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(1, badSmells.size()); + assertEquals("IndexOfReplaceableByContains", badSmells.get(0).getName()); + } + + @Test + public void testAnalyzeInnerSimpleClassRefactor() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = + """ + public class SimpleClass { + void bar() { + String foo = "foo"; + var bar = foo.indexOf("f") != -1; + } + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new IndexOfReplaceableByContainsAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(1, badSmells.size()); + badSmells.get(0).fix(); + assertEquals("IndexOfReplaceableByContains", badSmells.get(0).getName()); + Assertions.assertThat(simpleClass.toString()).contains("!foo.contains(\"f\");"); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstanceAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstanceAnalyzerTest.java new file mode 100644 index 000000000..42c72f2d8 --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/access_static_via_instance/AccessStaticViaInstanceAnalyzerTest.java @@ -0,0 +1,61 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.access_static_via_instance; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import java.util.List; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class AccessStaticViaInstanceAnalyzerTest { + + @Test + public void testAnalyzeInnerSimpleClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = + """ + public class SimpleClass { + void bar() { + int a = new SimpleClass().foo(); + System.out.println(new SimpleClass().foo()); + System.out.println(3); + String f = bar2(); + } + + static int foo() { + return 1; + } + String bar2() { + return "bar"; + } + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new AccessStaticViaInstanceAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(2, badSmells.size()); + assertEquals("AccessStaticViaInstance", badSmells.get(0).getName()); + badSmells.get(0).fix(); + assertTrue(simpleClass.toString().contains("SimpleClass.foo()")); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsedAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsedAnalyzerTest.java new file mode 100644 index 000000000..320ed8df3 --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/charset_object_can_be_used/CharsetObjectCanBeUsedAnalyzerTest.java @@ -0,0 +1,261 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.charset_object_can_be_used; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class CharsetObjectCanBeUsedAnalyzerTest { + + @Test + void testMethodCalls() { + String code = + """ + import java.io.ByteArrayOutputStream; + import java.io.OutputStream; + import java.net.URLDecoder; + import java.net.URLEncoder; + import java.nio.ByteBuffer; + import java.nio.channels.Channels; + import java.nio.channels.ReadableByteChannel; + import java.nio.channels.WritableByteChannel; + import java.util.Properties; + + public class CharsetExample { + public static void main(String[] args) throws Exception { + // Get bytes from a string using UTF-8 encoding + String input = "Hello, world!"; + byte[] bytes = input.getBytes("UTF-8"); + System.out.println("Bytes: " + bytes); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void testByteArrayOutputStream() { + String code = + """ + import java.io.ByteArrayOutputStream; + import java.io.OutputStream; + import java.net.URLDecoder; + import java.net.URLEncoder; + import java.nio.ByteBuffer; + import java.nio.channels.Channels; + import java.nio.channels.ReadableByteChannel; + import java.nio.channels.WritableByteChannel; + import java.util.Properties; + + public class CharsetExample { + public static void main(String[] args) throws Exception { + // Convert bytes to a string using UTF-8 encoding + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + WritableByteChannel channel = Channels.newChannel(baos); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + channel.write(buffer); + String output = baos.toString("UTF-8"); + System.out.println("Output: " + output) + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void testURLDecoder() { + String code = + """ + import java.io.ByteArrayOutputStream; + import java.io.OutputStream; + import java.net.URLDecoder; + import java.net.URLEncoder; + import java.nio.ByteBuffer; + import java.nio.channels.Channels; + import java.nio.channels.ReadableByteChannel; + import java.nio.channels.WritableByteChannel; + import java.util.Properties; + + public class CharsetExample { + public static void main(String[] args) throws Exception { + // Decode a URL-encoded string using UTF-8 encoding + String encoded = "https%3A%2F%2Fexample.com%2F"; + String decoded = URLDecoder.decode(encoded, "UTF-8"); + System.out.println("Decoded: " + decoded); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void testUrlEncoder() { + String code = + """ + import java.io.ByteArrayOutputStream; + import java.io.OutputStream; + import java.net.URLDecoder; + import java.net.URLEncoder; + import java.nio.ByteBuffer; + import java.nio.channels.Channels; + import java.nio.channels.ReadableByteChannel; + import java.nio.channels.WritableByteChannel; + import java.util.Properties; + + public class CharsetExample { + public static void main(String[] args) throws Exception { + + // Encode a string using UTF-8 encoding + String toEncode = "https://example.com/"; + String encoded2 = URLEncoder.encode(toEncode, "UTF-8"); + System.out.println("Encoded: " + encoded2); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void testNewWriter() { + String code = + """ +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; + + public class CharsetExample { + public static void main(String[] args) throws Exception { + // Create a file channel for a new file + File file = new File("output.txt"); + FileOutputStream fos = new FileOutputStream(file); + FileChannel channel = fos.getChannel(); + + // Create a writer for the file channel using UTF-8 encoding + String charsetName = "UTF-8"; + WritableByteChannel writer = Channels.newWriter(channel, charsetName); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void teststoreToXML() { + String code = + """ + import java.io.ByteArrayOutputStream; + import java.io.OutputStream; + import java.net.URLDecoder; + import java.net.URLEncoder; + import java.nio.ByteBuffer; + import java.nio.channels.Channels; + import java.nio.channels.ReadableByteChannel; + import java.nio.channels.WritableByteChannel; + import java.util.Properties; + + public class CharsetExample { + public static void main(String[] args) throws Exception { + // Store properties to an output stream using UTF-8 encoding + Properties props = new Properties(); + props.setProperty("foo", "bar"); + OutputStream os = System.out; + String comment = "This is a comment"; + props.storeToXML(os, comment, "UTF-8"); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void testFileWriter() { + String code = + """ + import java.io.File; + import java.util.Scanner; + class Foo { + + public void bar() throws Exception { + File file = new File("output.txt"); + Scanner scanner = new Scanner(file, "UTF-8"); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + System.out.println(simpleClass); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + } + + @Test + void testRefactorFileWriter() { + String code = + """ + import java.io.File; + import java.util.Scanner; + class Foo { + + public void bar() throws Exception { + File file = new File("output.txt"); + Scanner scanner = new Scanner(file, "UTF-8"); + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + var model = launcher.buildModel(); + CharsetObjectCanBeUsedAnalyzer analyzer = new CharsetObjectCanBeUsedAnalyzer(); + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + var result = analyzer.analyze(simpleClass); + assertEquals(1, result.size()); + result.get(0).fix(); + Assertions.assertThat(simpleClass.toString()).contains("java.nio.charset.StandardCharsets.UTF_8"); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStaticAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStaticAnalyzerTest.java new file mode 100644 index 000000000..551075f9b --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/innerclass_may_be_static/InnerClassMayBeStaticAnalyzerTest.java @@ -0,0 +1,133 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.innerclass_may_be_static; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import java.util.List; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class InnerClassMayBeStaticAnalyzerTest { + + @Test + public void testAnalyzeSimpleClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = """ + public class SimpleClass { + + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new InnerClassMayBeStaticAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(0, badSmells.size()); + } + + @Test + public void testAnalyzeInnerSimpleClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = + """ + public class SimpleClass { + class Foo { + void bar { + + } + } + + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new InnerClassMayBeStaticAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(1, badSmells.size()); + } + + @Test + public void testAnalyzeInnerDeepClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = + """ +public class OuterClass { + private static int staticField; + + public static class InnerClass1 { + private int field1; + + public class InnerClass2 { + private int field2; + + public class InnerClass3 { + private int field3; + + public class InnerClass4 { + private int field4; + + public class InnerClass5 { + private int field5; + + public void method() { + System.out.println(staticField + field1 + field2 + field3 + field4 + field5); + } + } + } + } + } + } +} + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new InnerClassMayBeStaticAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(0, badSmells.size()); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClassAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClassAnalyzerTest.java new file mode 100644 index 000000000..659cbd0fe --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/non_protected_constructor_In_abstract_class/NonProtectedConstructorInAbstractClassAnalyzerTest.java @@ -0,0 +1,75 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.non_protected_constructor_In_abstract_class; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.LocalAnalyzer; +import java.util.List; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class NonProtectedConstructorInAbstractClassAnalyzerTest { + + @Test + public void testNonAbstractClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = """ + public class SimpleClass { + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new NonProtectedConstructorInAbstractClassAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(0, badSmells.size()); + } + + @Test + public void testAbstractClass() { + // Create a Spoon launcher + Launcher launcher = new Launcher(); + String code = + """ + public abstract class SimpleClass { + + public SimpleClass() { + } + } + """; + // Add the source directory to the classpath + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + launcher.buildModel(); + CtModel model = launcher.getModel(); + + // Get the CtClass object for the SimpleClass class + CtType simpleClass = model.getAllTypes().stream().findFirst().get(); + + // Create an instance of the InnerClassMayBeStaticAnalyzer class + LocalAnalyzer analyzer = new NonProtectedConstructorInAbstractClassAnalyzer(); + + // Analyze the SimpleClass class for bad smells + List badSmells = analyzer.analyze(simpleClass); + + // Check that no bad smells were found + assertEquals(1, badSmells.size()); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethodAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethodAnalyzerTest.java new file mode 100644 index 000000000..163d0b286 --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/private_final_method/PrivateFinalMethodAnalyzerTest.java @@ -0,0 +1,26 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.private_final_method; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class PrivateFinalMethodAnalyzerTest { + + @Test + void simplePrivateFinalMethodTest() { + String code = "class A { private final void foo() {} }"; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + CtModel model = launcher.buildModel(); + PrivateFinalMethodAnalyzer analyzer = new PrivateFinalMethodAnalyzer(); + CtType type = model.getAllTypes().iterator().next(); + List analyze = analyzer.analyze(type); + Assertions.assertEquals(1, analyze.size()); + Assertions.assertEquals("PrivateFinalMethod", analyze.get(0).getName()); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/size_is_replaceable_by_is_empty/SizeReplaceableByIsEmptyAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/size_is_replaceable_by_is_empty/SizeReplaceableByIsEmptyAnalyzerTest.java new file mode 100644 index 000000000..158be7c52 --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/size_is_replaceable_by_is_empty/SizeReplaceableByIsEmptyAnalyzerTest.java @@ -0,0 +1,32 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.size_is_replaceable_by_is_empty; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import io.github.martinwitt.spoon_analyzer.badsmells.size_replaceable_by_is_empty.SizeReplaceableByIsEmptyAnalyzer; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.support.compiler.VirtualFile; + +public class SizeReplaceableByIsEmptyAnalyzerTest { + @Test + void simpleSizeIsZero() { + String code = + """ + class A { + public void a() { + List list = new ArrayList<>(); + if (list.size() == 0) { + System.out.println("Hello World"); + } + } + } + """; + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + SizeReplaceableByIsEmptyAnalyzer analyzer = new SizeReplaceableByIsEmptyAnalyzer(); + List analyze = + analyzer.analyze(launcher.buildModel().getAllTypes().iterator().next()); + Assertions.assertEquals(1, analyze.size()); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplementsAnalyzerTest.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplementsAnalyzerTest.java new file mode 100644 index 000000000..1bdd36dcf --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/badsmells/unnecessary_implements/UnnecessaryImplementsAnalyzerTest.java @@ -0,0 +1,85 @@ +package io.github.martinwitt.spoon_analyzer.badsmells.unnecessary_implements; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import java.util.List; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; +import spoon.support.compiler.VirtualFile; + +public class UnnecessaryImplementsAnalyzerTest { + + @Test + public void testAnalyze() { + String code = + """ + import java.util.List; + import java.util.Collection; + + public class Foo implements List, Collection { + } + """; + // Create a Spoon launcher and set the input source directory + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + CtModel model = launcher.buildModel(); + + // Get the factory and the class to analyze + CtType clazz = model.getAllTypes().stream().findFirst().get(); + + // Create the analyzer and run the analysis + UnnecessaryImplementsAnalyzer analyzer = new UnnecessaryImplementsAnalyzer(); + List badSmells = analyzer.analyze(clazz); + + // Check that the analysis found the expected number of bad smells + assertEquals(1, badSmells.size()); + + // Check that the bad smell has the expected properties + UnnecessaryImplements badSmell = (UnnecessaryImplements) badSmells.get(0); + assertEquals("java.util.List", badSmell.getLowerType().getQualifiedName()); + assertEquals("java.util.Collection", badSmell.getNotNeededImplements().getQualifiedName()); + } + + @Test + public void testAnalyze_multipleBadSmells() { + String code = + """ + import java.util.List; + import java.util.Collection; + import java.util.Set; + + public class Foo implements List, Collection, Set { + } + """; + // Create a Spoon launcher and set the input source directory + Launcher launcher = new Launcher(); + launcher.addInputResource(new VirtualFile(code)); + + // Build the Spoon model + CtModel model = launcher.buildModel(); + + // Get the factory and the class to analyze + CtType clazz = model.getAllTypes().stream().findFirst().get(); + + // Create the analyzer and run the analysis + UnnecessaryImplementsAnalyzer analyzer = new UnnecessaryImplementsAnalyzer(); + List badSmells = analyzer.analyze(clazz); + + // Check that the analysis found the expected number of bad smells + assertEquals(2, badSmells.size()); + + // Check that the bad smells have the expected properties + UnnecessaryImplements badSmell1 = (UnnecessaryImplements) badSmells.get(0); + assertEquals("java.util.List", badSmell1.getLowerType().getQualifiedName()); + assertEquals("java.util.Collection", badSmell1.getNotNeededImplements().getQualifiedName()); + + UnnecessaryImplements badSmell2 = (UnnecessaryImplements) badSmells.get(1); + assertEquals("java.util.Set", badSmell2.getLowerType().getQualifiedName()); + assertEquals("java.util.Collection", badSmell2.getNotNeededImplements().getQualifiedName()); + } +} diff --git a/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/docgen/GenerateBadSmells.java b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/docgen/GenerateBadSmells.java new file mode 100644 index 000000000..09401dd1e --- /dev/null +++ b/spoon-analyzer/src/test/java/io/github/martinwitt/spoon_analyzer/docgen/GenerateBadSmells.java @@ -0,0 +1,48 @@ +package io.github.martinwitt.spoon_analyzer.docgen; + +import io.github.martinwitt.spoon_analyzer.BadSmell; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.instancio.Instancio; +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtType; + +public class GenerateBadSmells { + + @Test + void generateBadSmellTable() throws InvocationTargetException, IOException { + Launcher launcher = new Launcher(); + launcher.addInputResource("src/main/java"); + launcher.getEnvironment().setAutoImports(true); + launcher.getEnvironment().setNoClasspath(true); + CtModel model = launcher.buildModel(); + StringBuilder sb = new StringBuilder(); + sb.append("| Bad Smell | Description |").append(System.lineSeparator()); + sb.append("| --- | --- |").append(System.lineSeparator()); + for (CtType type : model.getAllTypes()) { + if (type.getSuperInterfaces().stream() + .anyMatch(v -> v.getQualifiedName().equals("io.github.martinwitt.spoon_analyzer.BadSmell"))) { + try { + Class clazz = (Class) type.getActualClass(); + BadSmell instance = Instancio.create(clazz); + String name = instance.getName(); + String description = instance.getDescription(); + sb.append("| "); + sb.append(name); + sb.append(" | "); + sb.append(description); + sb.append(" | ").append(System.lineSeparator()); + } catch (IllegalArgumentException | SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + Files.writeString(Path.of("../doc/BadSmells.md"), sb.toString()); + } + ; + } + } +}