All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.staticanalysis.UseAsBuilder Maven / Gradle / Ivy

The newest version!
/*
 * 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 lombok.EqualsAndHashCode; import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.search.UsesMethod; import org.openrewrite.java.tree.*; import java.util.*; import static org.openrewrite.Validated.notBlank; import static org.openrewrite.java.format.TabsAndIndents.formatTabsAndIndents; @Value @EqualsAndHashCode(callSuper = false) public class UseAsBuilder extends Recipe { @Option( displayName = "Builder Type", description = "Fully qualified name of the Builder", example = "org.example.Buildable.Builder") String builderType; @Option( displayName = "Immutable state", description = "The builder is immutable if you must assign the result of calls to intermediate variables " + "or use directly. Defaults to true as many purpose-built builders will be immutable.", required = false) @Nullable Boolean immutable; @Option( displayName = "Builder creator method", description = "The method that creates the builder instance, which may not be a method of the builder itself.", required = false, example = "org.example.Buildable builder()") @Nullable String builderCreator; @Override public String getDisplayName() { return "Chain calls to builder methods"; } @Override public String getDescription() { return "Chain calls to builder methods that are on separate lines into one chain of builder calls."; } @Override public Validated validate() { return super.validate().and(notBlank("builderType", builderType)); } @Override public TreeVisitor getVisitor() { JavaIsoVisitor v = new JavaIsoVisitor() { final MethodMatcher builderCall = new MethodMatcher(builderType + " *(..)"); @Override public J.Block visitBlock(J.Block block, ExecutionContext ctx) { J.Block b = super.visitBlock(block, ctx); Map> builderCalls = collectBuilderMethodsByVariable(b); if (builderCalls.values().stream().map(List::size).max(Integer::sum).orElse(0) > 1) { List statements = consolidateAllBuilderCalls(block, builderCalls); return b.withStatements(statements); } return b; } private List consolidateAllBuilderCalls(J.Block block, Map> builderCalls) { List statements = new ArrayList<>(); // intermediate statements between builder methods that should move ahead of the builder List beforeBuilder = new ArrayList<>(); List afterBuilder = new ArrayList<>(); J.VariableDeclarations consolidatedBuilder = null; Iterator builderCallIter = builderCalls.values() .stream().flatMap(List::stream).iterator(); Statement currentBuilderCall = builderCallIter.next(); for (Statement statement : block.getStatements()) { if (currentBuilderCall != null) { if (statement == currentBuilderCall) { if (statement instanceof J.VariableDeclarations) { if (consolidatedBuilder != null) { statements.addAll(beforeBuilder); statements.add(consolidatedBuilder); statements.addAll(afterBuilder); beforeBuilder.clear(); afterBuilder.clear(); } consolidatedBuilder = (J.VariableDeclarations) statement; } else { assert consolidatedBuilder != null; if (statement instanceof J.Assignment) { J.Assignment assign = (J.Assignment) statement; consolidatedBuilder = consolidateBuilder(consolidatedBuilder, (J.MethodInvocation) assign.getAssignment()); } else if (statement instanceof J.MethodInvocation) { consolidatedBuilder = consolidateBuilder(consolidatedBuilder, (J.MethodInvocation) statement); } beforeBuilder.addAll(afterBuilder); afterBuilder.clear(); } currentBuilderCall = builderCallIter.hasNext() ? builderCallIter.next() : null; } else { // we consider it to be "after" the builder until we can prove // that there is a subsequent builder call, at which point it shifts // to the "before" builder list. afterBuilder.add(statement); } } else { afterBuilder.add(statement); } } statements.addAll(beforeBuilder); if (consolidatedBuilder != null) { statements.add(consolidatedBuilder); } statements.addAll(afterBuilder); return statements; } private Map> collectBuilderMethodsByVariable(J.Block b) { Map> builderCalls = new LinkedHashMap<>(); for (Statement stat : b.getStatements()) { if (stat instanceof J.VariableDeclarations) { J.VariableDeclarations varDecs = (J.VariableDeclarations) stat; for (J.VariableDeclarations.NamedVariable namedVar : varDecs.getVariables()) { if (matchesBuilder(namedVar.getInitializer())) { builderCalls.computeIfAbsent(namedVar.getSimpleName(), n -> new ArrayList<>()) .add(stat); } } } else if (stat instanceof J.Assignment) { J.Assignment assign = (J.Assignment) stat; if (matchesBuilder(assign.getAssignment())) { builderCalls.computeIfAbsent(assign.getVariable().printTrimmed(getCursor()), n -> new ArrayList<>()).add(stat); } } else if (!Boolean.FALSE.equals(immutable)) { if (stat instanceof J.MethodInvocation) { J.MethodInvocation method = (J.MethodInvocation) stat; if (matchesBuilder(method) && method.getSelect() != null) { builderCalls.computeIfAbsent(method.getSelect().printTrimmed(getCursor()), n -> new ArrayList<>()).add(stat); } } } } return builderCalls; } private boolean matchesBuilder(@Nullable Expression j) { return builderCall.matches(j) || (builderCreator != null && new MethodMatcher(builderCreator).matches(j)); } private J.VariableDeclarations consolidateBuilder(J.VariableDeclarations consolidatedBuilder, J.MethodInvocation builderCall) { J.VariableDeclarations cb = consolidatedBuilder.withVariables( ListUtils.map(consolidatedBuilder.getVariables(), nv -> { Expression init = nv.getInitializer(); assert init != null; return nv .withInitializer(builderCall .getPadding() .withSelect(JRightPadded .build((Expression) init.withPrefix(Space.EMPTY)) .withAfter(Space.format("\n"))) ); }) ); cb = formatTabsAndIndents(cb, getCursor()); return cb; } }; return builderCreator == null ? v : Preconditions.check(new UsesMethod<>(builderCreator), v); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy