org.openrewrite.staticanalysis.NoDoubleBraceInitialization Maven / Gradle / Ivy
Show all versions of rewrite-static-analysis Show documentation
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.staticanalysis;
import org.openrewrite.*;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.*;
import org.openrewrite.marker.Markers;
import org.openrewrite.marker.SearchResult;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
public class NoDoubleBraceInitialization extends Recipe {
private static final JavaType MAP_TYPE = JavaType.buildType("java.util.Map");
private static final JavaType LIST_TYPE = JavaType.buildType("java.util.List");
private static final JavaType SET_TYPE = JavaType.buildType("java.util.Set");
@Override
public String getDisplayName() {
return "No double brace initialization";
}
@Override
public String getDescription() {
return "Replace `List`, `Map`, and `Set` double brace initialization with an initialization block.";
}
@Override
public Set getTags() {
return new LinkedHashSet<>(Arrays.asList("RSPEC-S1171", "RSPEC-S3599"));
}
@Override
public Duration getEstimatedEffortPerOccurrence() {
return Duration.ofMinutes(30);
}
@Override
public TreeVisitor, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(
new UsesType<>("java.util.Map", false),
new UsesType<>("java.util.List", false),
new UsesType<>("java.util.Set", false)
),
new NoDoubleBraceInitializationVisitor()
);
}
private static class NoDoubleBraceInitializationVisitor extends JavaIsoVisitor {
private boolean isSupportedDoubleBraceInitialization(J.NewClass nc) {
if (getCursor().getParent() == null ||
getCursor().getParent().firstEnclosing(J.class) instanceof J.MethodInvocation ||
getCursor().getParent().firstEnclosing(J.class) instanceof J.NewClass) {
return false;
}
J.ClassDeclaration cd = getCursor().firstEnclosing(J.ClassDeclaration.class);
if (cd != null && cd.getKind() == J.ClassDeclaration.Kind.Type.Interface) {
return false;
}
if (nc.getBody() != null && !nc.getBody().getStatements().isEmpty() &&
nc.getBody().getStatements().size() == 1 &&
nc.getBody().getStatements().get(0) instanceof J.Block &&
getCursor().getParent(3) != null) {
return TypeUtils.isAssignableTo(MAP_TYPE, nc.getType()) ||
TypeUtils.isAssignableTo(LIST_TYPE, nc.getType()) ||
TypeUtils.isAssignableTo(SET_TYPE, nc.getType());
}
return false;
}
@Override
public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
J.NewClass nc = super.visitNewClass(newClass, ctx);
updateCursor(nc);
if (isSupportedDoubleBraceInitialization(newClass)) {
Cursor parentBlockCursor = getCursor().dropParentUntil(J.Block.class::isInstance);
J.VariableDeclarations.NamedVariable var = getCursor().firstEnclosing(J.VariableDeclarations.NamedVariable.class);
//noinspection ConstantConditions
J.Block secondBlock = (J.Block) nc.getBody().getStatements().get(0);
List initStatements = secondBlock.getStatements();
boolean maybeMistakenlyMissedAddingElement = !initStatements.isEmpty() &&
initStatements.stream().allMatch(J.NewClass.class::isInstance);
if (maybeMistakenlyMissedAddingElement) {
JavaType newClassType = nc.getType();
String addToCollectionMethod = TypeUtils.isAssignableTo(MAP_TYPE, newClassType) ? "put()" : "add()";
return nc.withBody(AddWarningMessage.addWarningComment(nc.getBody(), addToCollectionMethod));
}
// If not any method invocation (like add(), push(), etc) happened in the double brace to initialize
// the content of the collection, it means the intention of the code in the double brace is uncertain
// or maybe a custom code bug (like issue: https://github.com/openrewrite/rewrite/issues/2674),
// we don't want to rewrite code for this case to avoid introducing other warnings.
boolean hasMethodInvocationInDoubleBrace = FindMethodInvocationInDoubleBrace.find(secondBlock);
if (hasMethodInvocationInDoubleBrace && var != null && parentBlockCursor.getParent() != null) {
if (parentBlockCursor.getParent().getValue() instanceof J.ClassDeclaration) {
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(nc.getType());
if (fq != null && fq.getSupertype() != null) {
Cursor varDeclsCursor = getCursor().dropParentUntil(J.VariableDeclarations.class::isInstance);
Cursor namedVarCursor = getCursor().dropParentUntil(J.VariableDeclarations.NamedVariable.class::isInstance);
namedVarCursor.putMessage("DROP_INITIALIZER", Boolean.TRUE);
fq = fq.getSupertype();
String newInitializer = " new " + fq.getClassName() + "<>();";
JavaTemplate template = JavaTemplate.builder(newInitializer).imports(fq.getFullyQualifiedName()).build();
nc = template.apply(getCursor(), nc.getCoordinates().replace());
initStatements = addSelectToInitStatements(initStatements, var.getName(), ctx);
initStatements.add(0, new J.Assignment(Tree.randomId(), Space.EMPTY, Markers.EMPTY, var.getName().withId(UUID.randomUUID()), JLeftPadded.build(nc), fq));
parentBlockCursor.computeMessageIfAbsent("INIT_STATEMENTS", v -> new HashMap>()).put(varDeclsCursor.getValue(), initStatements);
}
} else if (parentBlockCursor.getParent().getValue() instanceof J.MethodDeclaration) {
initStatements = addSelectToInitStatements(initStatements, var.getName(), ctx);
Cursor varDeclsCursor = getCursor().dropParentUntil(J.VariableDeclarations.class::isInstance);
parentBlockCursor.computeMessageIfAbsent("METHOD_DECL_STATEMENTS", v -> new HashMap>()).put(varDeclsCursor.getValue(), initStatements);
nc = nc.withBody(null);
}
}
}
return nc;
}
private List addSelectToInitStatements(List statements, J.Identifier identifier, ExecutionContext ctx) {
AddSelectVisitor selectVisitor = new AddSelectVisitor(identifier);
return ListUtils.map(statements, statement -> (Statement) selectVisitor.visitNonNull(statement, ctx));
}
private static class AddSelectVisitor extends JavaIsoVisitor {
private final J.Identifier identifier;
public AddSelectVisitor(J.Identifier identifier) {
this.identifier = identifier;
}
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
if (mi.getSelect() == null || (mi.getSelect() instanceof J.Identifier && "this".equals(((J.Identifier) mi.getSelect()).getSimpleName()))) {
return mi.withSelect(identifier);
}
return mi;
}
}
@Override
public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, ExecutionContext ctx) {
J.VariableDeclarations.NamedVariable var = super.visitVariable(variable, ctx);
if (getCursor().pollMessage("DROP_INITIALIZER") != null) {
var = var.withInitializer(null);
}
return var;
}
@Override
public J.Block visitBlock(J.Block block, ExecutionContext ctx) {
J.Block bl = super.visitBlock(block, ctx);
Map> initStatements = getCursor().pollMessage("INIT_STATEMENTS");
Map> methodInitStatements = getCursor().pollMessage("METHOD_DECL_STATEMENTS");
if (initStatements != null) {
for (Map.Entry> objectListEntry : initStatements.entrySet()) {
Object statement = objectListEntry.getKey();
int statementIndex = bl.getStatements().indexOf(statement);
if (statementIndex > -1) {
JRightPadded isStatic;
if (objectListEntry.getKey() instanceof J.VariableDeclarations && J.Modifier.hasModifier(((J.VariableDeclarations) statement).getModifiers(), J.Modifier.Type.Static)) {
isStatic = JRightPadded.build(true).withAfter(Space.format(" "));
} else {
isStatic = JRightPadded.build(false);
}
J.Block initBlock = new J.Block(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
isStatic,
objectListEntry.getValue().stream().map(JRightPadded::build).collect(Collectors.toList()),
Space.EMPTY
);
//noinspection ConstantConditions
bl = maybeAutoFormat(bl, bl.withStatements(ListUtils.insertAll(bl.getStatements(), statementIndex + 1, Collections.singletonList(initBlock))),
initBlock, ctx, getCursor().getParent(2));
}
}
} else if (methodInitStatements != null) {
for (Map.Entry> objectListEntry : methodInitStatements.entrySet()) {
int statementIndex = bl.getStatements().indexOf(objectListEntry.getKey());
if (statementIndex > -1) {
//noinspection ConstantConditions
bl = maybeAutoFormat(bl, bl.withStatements(ListUtils.insertAll(bl.getStatements(), statementIndex + 1, objectListEntry.getValue())),
objectListEntry.getValue().get(objectListEntry.getValue().size() - 1), ctx, getCursor().getParent());
}
}
}
return bl;
}
}
private static class AddWarningMessage extends JavaIsoVisitor {
static T addWarningComment(T nc, String methodName) {
//noinspection unchecked
return (T) new AddWarningMessage().visitNonNull(nc, methodName);
}
@Override
public J.NewClass visitNewClass(J.NewClass newClass, String methodName) {
String comment = "Did you mean to invoke " + methodName + " method to the collection?";
return SearchResult.found(newClass, comment);
}
}
private static class FindMethodInvocationInDoubleBrace extends JavaIsoVisitor {
/**
* Find whether any collection content initialization method(e.g. add() or put()) is invoked in the double brace.
*
* @param j The subtree to search, supposed to be the 2nd brace (J.Block)
* @return true if any method invocation found in the double brace, otherwise false.
*/
static boolean find(J j) {
return new FindMethodInvocationInDoubleBrace()
.reduce(j, new AtomicBoolean()).get();
}
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, AtomicBoolean atomicBoolean) {
if (atomicBoolean.get() || method.getMethodType() == null) {
return method;
}
JavaType.FullyQualified declaring = method.getMethodType().getDeclaringType();
if (TypeUtils.isAssignableTo(MAP_TYPE, declaring) || TypeUtils.isAssignableTo(LIST_TYPE, declaring) || TypeUtils.isAssignableTo(SET_TYPE, declaring)) {
atomicBoolean.set(true);
return method;
}
return super.visitMethodInvocation(method, atomicBoolean);
}
}
}