diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java index c55cae923..5b0181ed6 100644 --- a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/TransformationEngine.java @@ -40,6 +40,7 @@ import xyz.keksdose.spoon.code_solver.transformations.qodana.ArraysToString; import xyz.keksdose.spoon.code_solver.transformations.qodana.EmptyStringCheck; import xyz.keksdose.spoon.code_solver.transformations.self.StringBuilderDirectUse; +import xyz.keksdose.spoon.code_solver.transformations.self.ThreadLocalWithInitial; public class TransformationEngine { @@ -80,6 +81,7 @@ protected void addProcessors(ProcessingManager pm, ChangeListener listener) { pm.addProcessor(new ArraysToString(listener)); pm.addProcessor(new EmptyStringCheck(listener)); pm.addProcessor(new StringBuilderDirectUse(listener)); + pm.addProcessor(new ThreadLocalWithInitial(listener)); // pm.addProcessor(new AssertTrueEqualsCheck(listener)); // pm.addProcessor(new AssertFalseEqualsCheck(listener)); // pm.addProcessor(new AssertNullTransformation(listener)); @@ -138,8 +140,9 @@ protected void printChangedTypes(PrettyPrinter prettyPrinter, ChangeListener lis private static Environment setEnvironmentOptions(Launcher launcher) { Environment environment = launcher.getEnvironment(); environment.setNoClasspath(true); + environment.setComplianceLevel(11); environment.setIgnoreDuplicateDeclarations(true); - environment.setIgnoreSyntaxErrors(true); + environment.setPreserveLineNumbers(true); environment.setPrettyPrinterCreator(() -> new ImportAwareSniperPrinter(environment)); return environment; } diff --git a/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/transformations/self/ThreadLocalWithInitial.java b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/transformations/self/ThreadLocalWithInitial.java new file mode 100644 index 000000000..8bdf0a35e --- /dev/null +++ b/code-transformation/src/main/java/xyz/keksdose/spoon/code_solver/transformations/self/ThreadLocalWithInitial.java @@ -0,0 +1,126 @@ + +package xyz.keksdose.spoon.code_solver.transformations.self; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLambda; +import spoon.reflect.code.CtNewClass; +import spoon.reflect.code.CtReturn; +import spoon.reflect.code.CtStatement; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtType; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtTypeReference; +import xyz.keksdose.spoon.code_solver.history.Change; +import xyz.keksdose.spoon.code_solver.history.ChangeListener; +import xyz.keksdose.spoon.code_solver.history.Link; +import xyz.keksdose.spoon.code_solver.history.MarkdownString; +import xyz.keksdose.spoon.code_solver.transformations.BadSmell; +import xyz.keksdose.spoon.code_solver.transformations.TransformationProcessor; + +public class ThreadLocalWithInitial extends TransformationProcessor> { + + private static final BadSmell threadLocalWithInitalValue = new BadSmell() { + @Override + public MarkdownString getName() { + return MarkdownString.fromRaw("ThreadLocalWithInitialValue"); + } + + @Override + public MarkdownString getDescription() { + String rawText = "ThreadLocal with initialValue override shall be replaced by ThreadLocal.withInitialValue"; + String markdown = "`ThreadLocal` with initialValue override shall be replaced by `ThreadLocal.withInitialValue`"; + return MarkdownString.fromMarkdown(rawText, markdown); + } + + @Override + public List getLinks() { + return List.of(new Link("https://rules.sonarsource.com/java/RSPEC-4065")); + } + + }; + public ThreadLocalWithInitial(ChangeListener listener) { + super(listener); + } + + @Override + public void process(CtNewClass threadLocal) { + if (threadLocal.getType() != null && threadLocal.getType().getQualifiedName().equals("java.lang.ThreadLocal")) { + CtClass innerClass = threadLocal.getAnonymousClass(); + if (hasNoFields(innerClass) && hasOnlyConstructorAndSingleMethod(innerClass)) { + Optional> initalValueMethod = findInitalValueMethod(innerClass); + if (initalValueMethod.isPresent()) { + CtLambda lambda = createSupplier(initalValueMethod.get()); + CtInvocation invocation = createInitalMethod(threadLocal, lambda); + invocation.setArguments(List.of(lambda)); + notifyChangeListener(threadLocal, lambda, invocation); + threadLocal.replace(invocation); + } + } + } + } + + private void notifyChangeListener(CtNewClass threadLocal, CtLambda lambda, CtInvocation invocation) { + CtElement element = lambda.getBody() == null ? lambda.getExpression() : lambda.getBody(); + String rawText = String.format( + "ThreadLocal with initialValue %s was replaced by ThreadLocal.withInitialValue(%s)", element, invocation); + String markdown = String.format( + "`ThreadLocal` with initialValue `%s` was replaced by `ThreadLocal.withInitialValue(%s)`", element, + invocation); + setChanged(threadLocal.getParent(CtType.class).getTopLevelType(), new Change(threadLocalWithInitalValue, + MarkdownString.fromMarkdown(rawText, markdown), threadLocal.getParent(CtType.class).getTopLevelType())); + } + + private CtInvocation createInitalMethod(CtNewClass threadLocal, CtLambda lambda) { + return getFactory().createInvocation(getFactory().createTypeAccess(createThreadLocalRef()), + getFactory().Executable() + .createReference(threadLocal.getType(), true, threadLocal.getType(), "withInitial", + List.of(lambda.getType()))); + } + + private CtTypeReference createThreadLocalRef() { + return getFactory().createCtTypeReference(ThreadLocal.class); + } + + private CtLambda createSupplier(CtExecutableReference initalValueMethod) { + CtLambda lambda = getFactory().createLambda(); + if (initalValueMethod.getDeclaration().getBody().getStatements().size() == 1) { + CtStatement statement = initalValueMethod.getDeclaration().getBody().getStatement(0); + if (statement instanceof CtReturn) { + lambda.setExpression(getReturnStatement(statement)); + } + else { + lambda.setBody(initalValueMethod.getDeclaration().getBody()); + } + } + else { + lambda.setBody(initalValueMethod.getDeclaration().getBody()); + } + lambda.setType(getFactory().createCtTypeReference(Supplier.class)); + return lambda; + } + + private CtExpression getReturnStatement(CtStatement statement) { + return ((CtReturn) statement).getReturnedExpression(); + } + + private Optional> findInitalValueMethod(CtClass innerClass) { + return innerClass.getDeclaredExecutables() + .stream() + .filter(v -> v.getSimpleName().equals("initialValue")) + .findFirst(); + } + + private boolean hasOnlyConstructorAndSingleMethod(CtClass innerClass) { + return innerClass.getDeclaredExecutables().size() == 2; + } + + private boolean hasNoFields(CtClass innerClass) { + return innerClass.getDeclaredFields().isEmpty(); + } +}