Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8638eb4
Added recipe to remove @VisibleForTesting annotation when used from p…
JohannisK Apr 2, 2025
6610f25
Added license
JohannisK Apr 2, 2025
03926fa
Renamed executionContext to ctx
JohannisK Apr 2, 2025
5ea0619
Improvements based on review
JohannisK Apr 2, 2025
76e54c2
Removed JetBrains annotations
JohannisK Apr 2, 2025
47b9f7d
Removed unused import
JohannisK Apr 2, 2025
918489b
Review, TestCases generics and method references, Fix method references
JohannisK Apr 3, 2025
7eac321
rename to ctx
JohannisK Apr 3, 2025
68ac454
Added generic test
JohannisK Apr 3, 2025
83a021e
Added tests proving scope of support for generics, added scope to des…
JohannisK Apr 4, 2025
9efb086
Merge branch 'main' into 224-remove-visiblefortesting-if-variable-is-…
timtebeek Apr 4, 2025
ab231a0
Review
JohannisK Apr 4, 2025
0005511
remove newline
JohannisK Apr 4, 2025
8e076d8
removed import
JohannisK Apr 4, 2025
f63c13b
Remove `forEach` and `stream()` to avoid object allocations
timtebeek Apr 4, 2025
bd2d2bf
Add UsesType precondition on `getVisitor`
timtebeek Apr 4, 2025
6973da1
Verify that we remove the import when last annotation removed
timtebeek Apr 4, 2025
7832d93
Added maybeRemoveImport
JohannisK Apr 7, 2025
d0413dd
Removed wrong test
JohannisK Apr 7, 2025
f53a28b
Run `RemoveVisibleForTestingAnnotationWhenUsedInProduction` as part o…
timtebeek Apr 7, 2025
c11c00a
Merge branch 'main' into 224-remove-visiblefortesting-if-variable-is-…
timtebeek Apr 7, 2025
d11ef63
Added private filter
JohannisK Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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.java.testing.cleanup;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.openrewrite.ExecutionContext;
import org.openrewrite.ScanningRecipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.RemoveAnnotation;
import org.openrewrite.java.marker.JavaSourceSet;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

public class RemoveVisibleForTestingAnnotationWhenUsedInProduction extends ScanningRecipe<RemoveVisibleForTestingAnnotationWhenUsedInProduction.Scanned> {

public static class Scanned {
List<String> methods = new ArrayList<>();
List<String> fields = new ArrayList<>();
List<String> classes = new ArrayList<>();
}

@Override
public String getDisplayName() {
return "Remove `@VisibleForTesting` annotation when target is used in production";
}

@Override
public String getDescription() {
return "The `@VisibleForTesting` annotation is used when a method or a field has been made more accessible then it would normally be, solely for testing purposes. " +
"This recipe removes the annotation where such an element is used from production classes. It identifies production classes as classes in `src/main` and test classes as classes in `src/test`. " +
"It will remove the `@VisibleForTesting` from methods, fields (both member fields and constants), constructors and inner classes. " +
"This recipe should not be used in an environment where QA tooling acts on the `@VisibleForTesting` annotation.";
}

@Override
public Scanned getInitialValue(ExecutionContext ctx) {
return new Scanned();
}

@Override
public TreeVisitor<?, ExecutionContext> getScanner(Scanned acc) {
return new JavaIsoVisitor<ExecutionContext>() {

@Override
public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) {
// Mark classes
if (fieldAccess.getTarget().getType() instanceof JavaType.Class) {
checkAndRegister(acc.classes, fieldAccess.getTarget().getType());
}
// Mark fields
if (fieldAccess.getName().getFieldType() != null) {
checkAndRegister(acc.fields, fieldAccess.getName().getFieldType());
}
return super.visitFieldAccess(fieldAccess, ctx);
}

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
if (method.getMethodType() != null) {
checkAndRegister(acc.methods, method.getMethodType());
}
return super.visitMethodInvocation(method, ctx);
}

@Override
public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
// Mark constructors
if (newClass.getConstructorType() != null) {
checkAndRegister(acc.methods, newClass.getConstructorType());
}
// Mark classes
if (newClass.getClazz() != null && newClass.getClazz().getType() != null && newClass.getClazz().getType() instanceof JavaType.Class) {
checkAndRegister(acc.classes, newClass.getClazz().getType());
}
return super.visitNewClass(newClass, ctx);
}

private void checkAndRegister(List<String> target, JavaType type) {
if (!target.contains(TypeUtils.toString(type))) {
getAnnotations(type).forEach(annotation -> {
if ("VisibleForTesting".equals(annotation.getClassName())) {
J.CompilationUnit compilationUnit = getCursor().firstEnclosing(J.CompilationUnit.class);
if (compilationUnit != null) {
compilationUnit
.getMarkers()
.findFirst(JavaSourceSet.class)
.filter(elem -> "main".equals(elem.getName()))
.ifPresent(sourceSet -> target.add(TypeUtils.toString(type)));
}
}
});
}
}

private @NotNull List<JavaType.FullyQualified> getAnnotations(JavaType type) {
if (type instanceof JavaType.Class) {
return ((JavaType.Class) type).getAnnotations();
} else if (type instanceof JavaType.Variable) {
return ((JavaType.Variable) type).getAnnotations();
} else if (type instanceof JavaType.Method) {
return ((JavaType.Method) type).getAnnotations();
}
return Collections.emptyList();
}
};
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Scanned acc) {
return new JavaIsoVisitor<ExecutionContext>() {

@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
J.VariableDeclarations variableDeclarations = super.visitVariableDeclarations(multiVariable, ctx);
if (!variableDeclarations.getVariables().isEmpty()) {
// if none of the variables in the declaration are used from production code, the annotation should be kept
boolean keepAnnotation = variableDeclarations.getVariables().stream()
.filter(elem -> elem.getVariableType() != null)
.noneMatch(elem -> acc.fields.contains(TypeUtils.toString(elem.getVariableType())));
if (!keepAnnotation) {
return (J.VariableDeclarations) getElement(ctx, variableDeclarations.getLeadingAnnotations(), variableDeclarations);
}
}
return variableDeclarations;
}

@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(method, ctx);
if (methodDeclaration.getMethodType() != null && acc.methods.contains(TypeUtils.toString(methodDeclaration.getMethodType()))) {
return (J.MethodDeclaration) getElement(ctx, methodDeclaration.getLeadingAnnotations(), methodDeclaration);
}
return methodDeclaration;
}

@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, ctx);
if (classDeclaration.getType() != null && acc.classes.contains(TypeUtils.toString(classDeclaration.getType()))) {
return (J.ClassDeclaration) getElement(ctx, classDeclaration.getLeadingAnnotations(), classDeclaration);
}
return classDeclaration;
}

private <@Nullable T extends J> J getElement(ExecutionContext ctx, List<J.Annotation> leadingAnnotations, T target) {
Optional<J.Annotation> annotation = leadingAnnotations.stream()
.filter(elem -> "VisibleForTesting".equals(elem.getSimpleName()))
.findFirst();
if (annotation.isPresent() && annotation.get().getType() instanceof JavaType.Class) {
JavaType.Class type = (JavaType.Class) annotation.get().getType();
return new RemoveAnnotation("@" + type.getFullyQualifiedName()).getVisitor().visitNonNull(target, ctx, getCursor().getParentOrThrow());
}
return target;
}
};
}
}
Loading
Loading