org.openrewrite.staticanalysis.HiddenFieldVisitor Maven / Gradle / Ivy
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.openrewrite.Cursor;
import org.openrewrite.Incubating;
import org.openrewrite.Tree;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.RenameVariable;
import org.openrewrite.java.cleanup.RenameJavaDocParamNameVisitor;
import org.openrewrite.java.style.HiddenFieldStyle;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaSourceFile;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeUtils;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Value
@EqualsAndHashCode(callSuper = false)
@Incubating(since = "7.6.0")
public class HiddenFieldVisitor
extends JavaIsoVisitor
{
private static final Pattern NEXT_NAME_PATTERN = Pattern.compile("(.+)(\\d+)");
HiddenFieldStyle style;
/**
* Returns either the current block or a J.Type that may create a reference to a variable.
* I.E. for(int target = 0; target < N; target++) creates a new name scope for `target`.
* The name scope in the next J.Block `{}` cannot create new variables with the name `target`.
*
* J.* types that may only reference an existing name and do not create a new name scope are excluded.
*
* Kindly borrowed from {@link RenameVariable}
*/
private static Cursor getCursorToParentScope(Cursor cursor) {
return cursor.dropParentUntil(is ->
is instanceof J.Block ||
is instanceof J.MethodDeclaration ||
is instanceof J.ForLoop ||
is instanceof J.ForEachLoop ||
is instanceof J.Case ||
is instanceof J.Try ||
is instanceof J.Try.Catch ||
is instanceof J.MultiCatch ||
is instanceof J.Lambda
);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {
List classFields = classDecl.getBody().getStatements().stream()
.filter(J.VariableDeclarations.class::isInstance)
.map(J.VariableDeclarations.class::cast)
.flatMap(vd -> vd.getVariables().stream())
.collect(Collectors.toList());
classFields.forEach(cf -> FindNameShadows.find(classDecl, cf, classDecl, style)
.forEach(shadow -> doAfterVisit(new RenameShadowedName<>(shadow, style))));
return super.visitClassDeclaration(classDecl, p);
}
private static class FindExistingVariableDeclarations extends JavaIsoVisitor> {
private final Cursor childTargetReference;
private final String childTargetName;
private FindExistingVariableDeclarations(Cursor childTargetReference, String childTargetName) {
this.childTargetReference = childTargetReference;
this.childTargetName = childTargetName;
}
/**
* In the context of {@link HiddenFieldVisitor}, this is used to determine whether there is an existing variable definition
* within the same name scope as the provided {@param childTargetReference}. This ensures that when we want to increment
* the name of a variable we're renaming, we aren't renaming it to something that will cause a name collision with existing variable declarations.
*
* @param j The subtree to search.
* @param childTargetReference The location of the variable declaration of our original search target.
* @param childTargetName The name of the {@param childTargetReference} we'd like to see if anything exists.
* @return A set of existing variable definition of the {@param childTargetName} within the same name scope as the {@param childTargetName}.
*/
public static Set find(J j, Cursor childTargetReference, String childTargetName) {
Set references = new LinkedHashSet<>();
new FindExistingVariableDeclarations(childTargetReference, childTargetName).visit(j, references);
return references;
}
@Override
public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, Set ctx) {
if (variable.getSimpleName().equals(childTargetName) && isInSameNameScope(getCursor(), childTargetReference)) {
ctx.add(variable);
}
return super.visitVariable(variable, ctx);
}
}
private static class RenameShadowedName extends JavaIsoVisitor
{
private final J.VariableDeclarations.NamedVariable targetVariable;
private final HiddenFieldStyle hiddenFieldStyle;
public RenameShadowedName(J.VariableDeclarations.NamedVariable targetVariable, HiddenFieldStyle hiddenFieldStyle) {
this.targetVariable = targetVariable;
this.hiddenFieldStyle = hiddenFieldStyle;
}
private static String nextName(String name) {
Matcher nameMatcher = NEXT_NAME_PATTERN.matcher(name);
return nameMatcher.matches() ?
nameMatcher.group(1) + (Integer.parseInt(nameMatcher.group(2)) + 1) :
name + "1";
}
@Override
public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, P p) {
J.VariableDeclarations.NamedVariable v = super.visitVariable(variable, p);
if (v.isScope(targetVariable)) {
String nextName = nextName(v.getSimpleName());
JavaSourceFile enclosingCU = getCursor().firstEnclosingOrThrow(JavaSourceFile.class);
Cursor parentScope = getCursorToParentScope(getCursor());
J.ClassDeclaration enclosingClass = getCursor().firstEnclosing(J.ClassDeclaration.class);
if (enclosingClass == null) {
return v;
}
while (// don't use a variable name of any existing variable "downstream" of the renamed variable's scope
!FindNameShadows.find(parentScope.getValue(), v.withName(v.getName().withSimpleName(nextName)), enclosingClass, hiddenFieldStyle).isEmpty() ||
// don't use a variable name of any existing variables already defined in the "upstream" cursor path of the renamed variable's scope
!FindExistingVariableDeclarations.find(enclosingCU, getCursor(), nextName).isEmpty()
) {
nextName = nextName(nextName);
}
doAfterVisit(new RenameVariable<>(v, nextName));
if (parentScope.getValue() instanceof J.MethodDeclaration) {
Optional variableParameter = ((J.MethodDeclaration) parentScope.getValue()).getParameters().stream()
.filter(J.VariableDeclarations.class::isInstance)
.map(J.VariableDeclarations.class::cast)
.filter(it -> it.getVariables().contains(v))
.findFirst();
if (variableParameter.isPresent()) {
doAfterVisit(new RenameJavaDocParamNameVisitor<>(parentScope.getValue(), v.getSimpleName(), nextName));
}
}
}
return v;
}
}
private static class FindNameShadows extends JavaIsoVisitor> {
private final J.VariableDeclarations.NamedVariable targetVariable;
private final J.ClassDeclaration targetVariableEnclosingClass;
private final HiddenFieldStyle hiddenFieldStyle;
public FindNameShadows(J.VariableDeclarations.NamedVariable targetVariable, J.ClassDeclaration targetVariableEnclosingClass, HiddenFieldStyle hiddenFieldStyle) {
this.targetVariable = targetVariable;
this.targetVariableEnclosingClass = targetVariableEnclosingClass;
this.hiddenFieldStyle = hiddenFieldStyle;
}
/**
* Find {@link J.VariableDeclarations.NamedVariable} definitions within the searched tree which "hide" the target variable definition
* from an outer tree. Specifically, used to find local variables or method parameters which shadow a class field.
*
* @param j The subtree to search.
* @param targetVariable The {@link J.VariableDeclarations.NamedVariable} to identify whether any other variables shadow it.
* @param targetVariableEnclosingClass The enclosing class of where the {@param targetVariable} is defined.
* @param hiddenFieldStyle The {@link HiddenFieldStyle} to use as part of search criteria.
* @return A set representing any found {@link J.VariableDeclarations.NamedVariable} which shadow the provided {@param targetVariable}.
*/
public static Set find(J j, J.VariableDeclarations.NamedVariable targetVariable, J.ClassDeclaration targetVariableEnclosingClass, HiddenFieldStyle hiddenFieldStyle) {
Set references = new LinkedHashSet<>();
new FindNameShadows(targetVariable, targetVariableEnclosingClass, hiddenFieldStyle).visit(j, references);
return references;
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Set ctx) {
// do not go into static inner classes, interfaces, or enums which have a different name scope
if (!(classDecl.getKind().equals(J.ClassDeclaration.Kind.Type.Class)) || classDecl.hasModifier(J.Modifier.Type.Static)) {
return classDecl;
}
return super.visitClassDeclaration(classDecl, ctx);
}
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Set ctx) {
// do not go into static methods-- local variables of static methods don't hide instance fields
if (method.hasModifier(J.Modifier.Type.Static)) {
return method;
}
return super.visitMethodDeclaration(method, ctx);
}
@Override
public J.Block visitBlock(J.Block block, Set ctx) {
// do not go into static initialization blocks-- local variables of static initializers don't hide instance fields
if (block.isStatic()) {
return block;
}
return super.visitBlock(block, ctx);
}
@Override
public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, Set ctx) {
J.VariableDeclarations.NamedVariable v = super.visitVariable(variable, ctx);
// skip if we are visiting the original target variable, or else this will consider a variable to be a shadow of itself.
if (v.getSimpleName().equals(targetVariable.getSimpleName()) && !v.isScope(targetVariable)) {
Tree maybeMethodDecl = getCursor()
.getParentTreeCursor() // J.VariableDeclarations
.getParentTreeCursor() // maybe J.MethodDeclaration
.getValue();
boolean isIgnorableConstructorParam = hiddenFieldStyle.getIgnoreConstructorParameter();
if (isIgnorableConstructorParam) {
isIgnorableConstructorParam = maybeMethodDecl instanceof J.MethodDeclaration && ((J.MethodDeclaration) maybeMethodDecl).isConstructor();
}
boolean isIgnorableSetter = hiddenFieldStyle.getIgnoreSetter();
if (isIgnorableSetter &= maybeMethodDecl instanceof J.MethodDeclaration) {
J.MethodDeclaration md = (J.MethodDeclaration) maybeMethodDecl;
boolean doesSetterReturnItsClass = md.getReturnTypeExpression() != null && TypeUtils.isOfType(targetVariableEnclosingClass.getType(), md.getReturnTypeExpression().getType());
boolean isSetterVoid = md.getReturnTypeExpression() != null && JavaType.Primitive.Void.equals(md.getReturnTypeExpression().getType());
boolean doesMethodNameCorrespondToVariable = md.getSimpleName().startsWith("set") && md.getSimpleName().toLowerCase().endsWith(variable.getSimpleName().toLowerCase());
isIgnorableSetter = doesMethodNameCorrespondToVariable &&
(hiddenFieldStyle.getSetterCanReturnItsClass() ? (doesSetterReturnItsClass || isSetterVoid) : isSetterVoid);
}
boolean isIgnorableAbstractMethod = hiddenFieldStyle.getIgnoreAbstractMethods();
if (isIgnorableAbstractMethod) {
isIgnorableAbstractMethod = maybeMethodDecl instanceof J.MethodDeclaration && ((J.MethodDeclaration) maybeMethodDecl).isAbstract();
}
if (!isIgnorableSetter && !isIgnorableConstructorParam && !isIgnorableAbstractMethod) {
ctx.add(v);
}
}
return v;
}
}
}