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

org.openrewrite.staticanalysis.HideUtilityClassConstructorVisitor 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 org.openrewrite.Cursor; import org.openrewrite.Incubating; import org.openrewrite.java.*; import org.openrewrite.java.service.AnnotationService; import org.openrewrite.java.style.HideUtilityClassConstructorStyle; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; import org.openrewrite.java.tree.Statement; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; /** * HideUtilityClassConstructorVisitor will perform the following operations on a Utility Class: *

    *
  • Change any Public constructors to Private
  • *
  • Change any Package-Private ("Default", no modifiers) to Private
  • *
  • If the Implicit Default Constructor is used (as in, no explicit constructors defined), add a Private constructor
  • *
*

* HideUtilityClassConstructorVisitor will NOT perform operations on a Utility Class under these circumstances: *

    *
  • Will NOT change any Protected constructors to Private
  • *
  • * HideUtilityClassConstructorVisitor will ignore classes with a Main method signature ({@code public static void main(String[] args)}. * This prevents HideUtilityClassConstructorVisitor from generating a Private constructor on classes which only * serve as application entry points, though they are technically a Utility Class. *
  • *
  • * HideUtilityClassConstructorVisitor can be configured with a list of fully-qualified "ignorable Annotations" strings. * These are used with {@link AnnotationMatcher} to check for the presence of annotations on the class. * HideUtilityClassConstructorVisitor will ignore classes which have any of the configured Annotations. * This is valuable for situations such as Lombok Utility classes, which generate Private constructors in bytecode. *
  • *
*/ @Incubating(since = "7.0.0") public class HideUtilityClassConstructorVisitor

extends JavaIsoVisitor

{ private static final EnumSet EXCLUDE_CLASS_TYPES = EnumSet.of(J.ClassDeclaration.Kind.Type.Interface, J.ClassDeclaration.Kind.Type.Record); private final UtilityClassMatcher utilityClassMatcher; public HideUtilityClassConstructorVisitor(HideUtilityClassConstructorStyle style) { this.utilityClassMatcher = new UtilityClassMatcher(style.getIgnoreIfAnnotatedBy()); } @SuppressWarnings("ConstantConditions") @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) { J.ClassDeclaration c = super.visitClassDeclaration(classDecl, p); if (!EXCLUDE_CLASS_TYPES.contains(c.getKind()) && !c.hasModifier(J.Modifier.Type.Abstract) && utilityClassMatcher.isRefactorableUtilityClass(getCursor())) { /* * Note, it's a deliberate choice to have these be their own respective visitors rather than putting * all the logic in one visitor. It's conceptually easier to distinguish what each are doing. * And some linters which deal with "utility classes", such as IntelliJ, Checkstyle, etc., treat * "change public constructor to private" and "generate a private constructor" as different toggleable flags. * Here we're just saying "do both", but having the logic be implemented as separate visitors does mean * it could be simpler to break these out into their own visitors in the future. Maybe. For now, to defer that decision * and keep the API surface area small, we're keeping these private. * * But, first and foremost, the main rationale is because it's hopefully conceptually easier to distinguish the steps * required for HideUtilityClassConstructorVisitor to work. */ c = (J.ClassDeclaration) new UtilityClassWithImplicitDefaultConstructorVisitor().visit(c, p, getCursor().getParentOrThrow()); c = (J.ClassDeclaration) new UtilityClassWithExposedConstructorInspectionVisitor<>(c).visit(c, p, getCursor().getParentOrThrow()); } return c; } /** * Adds an empty private constructor if the class has zero explicit constructors. This hides the default implicit constructor. */ private class UtilityClassWithImplicitDefaultConstructorVisitor extends JavaIsoVisitor

{ @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) { if (utilityClassMatcher.hasImplicitDefaultConstructor(classDecl) && !J.ClassDeclaration.Kind.Type.Enum.equals(classDecl.getKind())) { classDecl = JavaTemplate.builder("private #{}() {}") .contextSensitive() .build() .apply(getCursor(), classDecl.getBody().getCoordinates().lastStatement(), classDecl.getSimpleName()); } return classDecl; } } /** * We consider a Utility Class to have an "exposed" constructor if the constructor is Public or Package-Private. * The constructor may be "Protected" in cases where it's desirable to subclass the Utility Class. */ private static class UtilityClassWithExposedConstructorInspectionVisitor

extends JavaIsoVisitor

{ private final J.ClassDeclaration utilityClass; public UtilityClassWithExposedConstructorInspectionVisitor(J.ClassDeclaration utilityClass) { this.utilityClass = utilityClass; } @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) { return classDecl == utilityClass ? super.visitClassDeclaration(classDecl, p) : classDecl; } @Override public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P p) { J.MethodDeclaration md = super.visitMethodDeclaration(method, p); if (md.getMethodType() == null || !md.isConstructor() || (md.hasModifier(J.Modifier.Type.Private) || md.hasModifier(J.Modifier.Type.Protected) || md.getMethodType().getDeclaringType().getKind().equals(JavaType.Class.Kind.Enum))) { return md; } ChangeMethodAccessLevelVisitor

changeMethodAccessLevelVisitor = new ChangeMethodAccessLevelVisitor<>( new MethodMatcher(method), J.Modifier.Type.Private ); md = (J.MethodDeclaration) changeMethodAccessLevelVisitor.visit(md, p, getCursor().getParentOrThrow()); assert md != null; return md; } } /** * Utility class for identifying utility classes. *

* A Class is considered a Utility Class if it meets these rules: * *

    *
  • A utility class contains only static methods and fields in its API
  • *
  • A utility class does not have any public constructors
  • *
  • A utility class does not have any {@code implements} or {@code extends} keywords.
  • *
* *

* What's an actual example of a utility class, you ask? Well, here's one. *

* We are keeping this class private for the moment in order to keep the API surface area small. However, * there are other cleanup rules which could benefit from these, such as "make utility class final", etc. * Until then, however, we'll keep this private and unexposed. */ private final class UtilityClassMatcher { private final Collection ignorableAnnotations; private UtilityClassMatcher(Collection ignorableAnnotations) { this.ignorableAnnotations = new ArrayList<>(ignorableAnnotations.size()); for (String ignorableAnnotation : ignorableAnnotations) { this.ignorableAnnotations.add(new AnnotationMatcher(ignorableAnnotation)); } } boolean hasIgnorableAnnotation(Cursor cursor) { AnnotationService service = service(AnnotationService.class); for (AnnotationMatcher ignorableAnn : ignorableAnnotations) { if (service.matches(cursor, ignorableAnn)) { return true; } } return false; } boolean hasMainMethod(J.ClassDeclaration c) { if (c.getType() == null) { return false; } for (Statement statement : c.getBody().getStatements()) { if (statement instanceof J.MethodDeclaration) { J.MethodDeclaration md = (J.MethodDeclaration) statement; if (!md.isConstructor() && md.hasModifier(J.Modifier.Type.Public) && md.hasModifier(J.Modifier.Type.Static) && md.getReturnTypeExpression() != null && JavaType.Primitive.Void.equals(md.getReturnTypeExpression().getType()) && // note that the matcher for "main(String)" will match on "main(String[]) as expected. new MethodMatcher(c.getType().getFullyQualifiedName() + " main(String[])") .matches(md, c)) { return true; } } } return false; } /** * If a Class Declaration has zero constructors, it uses the Implicit Default Constructor. *

* Keep in mind, a Class Declaration can have Explicit Default Constructors if the Constructor is declared * without any Access Modifier such as Public, Private, or Protected. * * @return true if there are zero explicit constructors, meaning the Class has an implicit default constructor. */ boolean hasImplicitDefaultConstructor(J.ClassDeclaration c) { for (Statement statement : c.getBody().getStatements()) { if (statement instanceof J.MethodDeclaration) { J.MethodDeclaration methodDeclaration = (J.MethodDeclaration) statement; if (methodDeclaration.isConstructor()) { return false; } } } return true; } boolean isRefactorableUtilityClass(Cursor cursor) { J.ClassDeclaration c = cursor.getValue(); return isUtilityClass(c) && !hasIgnorableAnnotation(cursor) && !hasMainMethod(c); } boolean isUtilityClass(J.ClassDeclaration c) { if (c.getImplements() != null || c.getExtends() != null) { return false; } int staticMethodCount = countStaticMethods(c); if (staticMethodCount < 0) { return false; } int staticFieldCount = countStaticFields(c); if (staticFieldCount < 0) { return false; } return staticMethodCount != 0 || staticFieldCount != 0; } /** * @return -1 if a non-static field is found, else the count of non-private static fields. */ private int countStaticFields(J.ClassDeclaration classDeclaration) { int count = 0; for (Statement statement : classDeclaration.getBody().getStatements()) { if (!(statement instanceof J.VariableDeclarations)) { continue; } J.VariableDeclarations field = (J.VariableDeclarations) statement; if (!field.hasModifier(J.Modifier.Type.Static)) { return -1; } if (field.hasModifier(J.Modifier.Type.Private)) { continue; } count++; } return count; } /** * @return -1 if a non-static method is found, else the count of non-private static methods. */ private int countStaticMethods(J.ClassDeclaration classDeclaration) { int count = 0; for (Statement statement : classDeclaration.getBody().getStatements()) { if (!(statement instanceof J.MethodDeclaration)) { continue; } J.MethodDeclaration method = (J.MethodDeclaration) statement; if (method.isConstructor()) { continue; } if (!method.hasModifier(J.Modifier.Type.Static)) { return -1; } if (method.hasModifier(J.Modifier.Type.Private)) { continue; } count++; } return count; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy