EmptyPublicCtorInClassCheck.java

  1. ////////////////////////////////////////////////////////////////////////////////
  2. // checkstyle: Checks Java source code for adherence to a set of rules.
  3. // Copyright (C) 2001-2019 the original author or authors.
  4. //
  5. // This library is free software; you can redistribute it and/or
  6. // modify it under the terms of the GNU Lesser General Public
  7. // License as published by the Free Software Foundation; either
  8. // version 2.1 of the License, or (at your option) any later version.
  9. //
  10. // This library is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  13. // Lesser General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Lesser General Public
  16. // License along with this library; if not, write to the Free Software
  17. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  18. ////////////////////////////////////////////////////////////////////////////////

  19. package com.github.sevntu.checkstyle.checks.coding;

  20. import java.util.ArrayList;
  21. import java.util.List;
  22. import java.util.regex.Pattern;

  23. import com.github.sevntu.checkstyle.SevntuUtil;
  24. import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
  25. import com.puppycrawl.tools.checkstyle.api.DetailAST;
  26. import com.puppycrawl.tools.checkstyle.api.TokenTypes;

  27. /**
  28.  * <p>
  29.  * This Check looks for useless empty public constructors. Class constructor is considered useless
  30.  * by this Check if and only if class has exactly one ctor, which is public, empty(one that has no
  31.  * statements) and <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.8.9">
  32.  * default</a>.
  33.  * </p>
  34.  * <p>
  35.  * Example 1. Check will generate violation for this code:
  36.  * </p>
  37.  *
  38.  * <pre>
  39.  * class Dummy {
  40.  *     public Dummy() {
  41.  *     }
  42.  * }
  43.  * </pre>
  44.  *
  45.  * <p>
  46.  * Example 2. Check will not generate violation for this code:
  47.  * </p>
  48.  *
  49.  * <pre>
  50.  * class Dummy {
  51.  *     private Dummy() {
  52.  *     }
  53.  * }
  54.  * </pre>
  55.  *
  56.  * <p>
  57.  * class Dummy has only one ctor, which is not public.
  58.  * </p>
  59.  * <p>
  60.  * Example 3. Check will not generate violation for this code:
  61.  * </p>
  62.  *
  63.  * <pre>
  64.  * class Dummy {
  65.  *     public Dummy() {
  66.  *     }
  67.  *     public Dummy(int i) {
  68.  *     }
  69.  * }
  70.  * </pre>
  71.  *
  72.  * <p>
  73.  * class Dummy has multiple ctors.
  74.  * </p>
  75.  * <p>
  76.  * Check has two properties:
  77.  * </p>
  78.  * <p>
  79.  * "classAnnotationNames" - This property contains regex for canonical names of class annotations,
  80.  * which require class to have empty public ctor. Check will not log violations for classes marked
  81.  * with annotations that match this regex. Default option value is "javax\.persistence\.Entity".
  82.  * </p>
  83.  * <p>
  84.  * "ctorAnnotationNames" - This property contains regex for canonical names of ctor annotations,
  85.  * which make empty public ctor essential. Check will not log violations for ctors marked with
  86.  * annotations that match this regex. Default option value is "com\.google\.inject\.Inject".
  87.  * </p>
  88.  * Following configuration will adjust Check to skip classes which annotated with
  89.  * "javax.persistence.Entity" and classes which has empty public ctor with
  90.  * "com\.google\.inject\.Inject".
  91.  *
  92.  * <pre>
  93.  *   &lt;module name="EmptyPublicCtorInClassCheck"&gt;
  94.  *     &lt;property name="classAnnotationNames" value="javax\.persistence\.Entity"/&gt;
  95.  *     &lt;property name="ctorAnnotationNames" value="com\.google\.inject\.Inject"/&gt;
  96.  *   &lt;/module&gt;
  97.  * </pre>
  98.  *
  99.  * @author <a href="mailto:zuy_alexey@mail.ru">Zuy Alexey</a>
  100.  * @since 1.13.0
  101.  */
  102. public class EmptyPublicCtorInClassCheck extends AbstractCheck {

  103.     /**
  104.      * Violation message key.
  105.      */
  106.     public static final String MSG_KEY = "empty.public.ctor";

  107.     /**
  108.      * List of single-type-imports for current AST.
  109.      */
  110.     private final List<String> singleTypeImports = new ArrayList<>();

  111.     /**
  112.      * List of on-demand-imports for current AST.
  113.      */
  114.     private final List<String> onDemandImports = new ArrayList<>();

  115.     /**
  116.      * Package name for current AST or empty string if AST does not contain package name.
  117.      */
  118.     private String filePackageName;

  119.     /**
  120.      * Regex which matches names of class annotations which require class to have public no-argument
  121.      * ctor. Default value is "javax\.persistence\.Entity".
  122.      */
  123.     private Pattern classAnnotationNames = Pattern.compile("javax\\.persistence\\.Entity");

  124.     /**
  125.      * Regex which matches names of ctor annotations which make empty public ctor essential. Default
  126.      * value is "com\.google\.inject\.Inject".
  127.      */
  128.     private Pattern ctorAnnotationNames = Pattern.compile("com\\.google\\.inject\\.Inject");

  129.     /**
  130.      * Sets regex which matches names of class annotations which require class to have public
  131.      * no-argument ctor.
  132.      * @param regex
  133.      *        regex to match annotation names.
  134.      */
  135.     public void setClassAnnotationNames(String regex) {
  136.         if (regex == null || regex.isEmpty()) {
  137.             classAnnotationNames = null;
  138.         }
  139.         else {
  140.             classAnnotationNames = Pattern.compile(regex);
  141.         }
  142.     }

  143.     /**
  144.      * Sets regex which matches names of ctor annotations which make empty public ctor essential.
  145.      * @param regex
  146.      *        regex to match annotation names.
  147.      */
  148.     public void setCtorAnnotationNames(String regex) {
  149.         if (regex == null || regex.isEmpty()) {
  150.             ctorAnnotationNames = null;
  151.         }
  152.         else {
  153.             ctorAnnotationNames = Pattern.compile(regex);
  154.         }
  155.     }

  156.     @Override
  157.     public int[] getDefaultTokens() {
  158.         return new int[] {
  159.             TokenTypes.CLASS_DEF,
  160.             TokenTypes.PACKAGE_DEF,
  161.             TokenTypes.IMPORT,
  162.         };
  163.     }

  164.     @Override
  165.     public int[] getAcceptableTokens() {
  166.         return getDefaultTokens();
  167.     }

  168.     @Override
  169.     public int[] getRequiredTokens() {
  170.         return getDefaultTokens();
  171.     }

  172.     @Override
  173.     public void beginTree(DetailAST aRootNode) {
  174.         singleTypeImports.clear();
  175.         onDemandImports.clear();
  176.         filePackageName = "";
  177.     }

  178.     @Override
  179.     public void visitToken(DetailAST node) {
  180.         switch (node.getType()) {
  181.             case TokenTypes.IMPORT:
  182.                 final String packageMemberName = getIdentifierName(node);

  183.                 if (isOnDemandImport(packageMemberName)) {
  184.                     onDemandImports.add(packageMemberName);
  185.                 }
  186.                 else {
  187.                     singleTypeImports.add(packageMemberName);
  188.                 }
  189.                 break;

  190.             case TokenTypes.CLASS_DEF:
  191.                 if (getClassCtorCount(node) == 1) {
  192.                     final DetailAST ctorDef = getFirstCtorDefinition(node);

  193.                     if (isCtorPublic(ctorDef)
  194.                             && isCtorHasNoParameters(ctorDef)
  195.                             && isCtorHasNoStatements(ctorDef)
  196.                             && !isClassHasRegisteredAnnotation(node)
  197.                             && !isCtorHasRegisteredAnnotation(ctorDef)) {
  198.                         log(ctorDef, MSG_KEY);
  199.                     }
  200.                 }
  201.                 break;

  202.             case TokenTypes.PACKAGE_DEF:
  203.                 filePackageName = getIdentifierName(node);
  204.                 break;
  205.             default:
  206.                 SevntuUtil.reportInvalidToken(node.getType());
  207.                 break;
  208.         }
  209.     }

  210.     /**
  211.      * Calculates constructor count for class.
  212.      * @param classDefNode
  213.      *        a class definition node.
  214.      * @return ctor count for given class definition.
  215.      */
  216.     private static int getClassCtorCount(DetailAST classDefNode) {
  217.         return classDefNode.findFirstToken(TokenTypes.OBJBLOCK).getChildCount(TokenTypes.CTOR_DEF);
  218.     }

  219.     /**
  220.      * Gets first constructor definition for class.
  221.      * @param classDefNode
  222.      *        a class definition node.
  223.      * @return first ctor definition node for class or null if class has no ctor.
  224.      */
  225.     private static DetailAST getFirstCtorDefinition(DetailAST classDefNode) {
  226.         return classDefNode
  227.                 .findFirstToken(TokenTypes.OBJBLOCK)
  228.                 .findFirstToken(TokenTypes.CTOR_DEF);
  229.     }

  230.     /**
  231.      * Checks whether constructor is public.
  232.      * @param ctorDefNode
  233.      *        a ctor definition node(TokenTypes.CTOR_DEF).
  234.      * @return true, if given ctor is public.
  235.      */
  236.     private static boolean isCtorPublic(DetailAST ctorDefNode) {
  237.         return ctorDefNode
  238.                 .findFirstToken(TokenTypes.MODIFIERS)
  239.                 .findFirstToken(TokenTypes.LITERAL_PUBLIC) != null;
  240.     }

  241.     /**
  242.      * Checks whether ctor has no parameters.
  243.      * @param ctorDefNode
  244.      *        a ctor definition node(TokenTypes.CTOR_DEF).
  245.      * @return true, if ctor has no parameters.
  246.      */
  247.     private static boolean isCtorHasNoParameters(DetailAST ctorDefNode) {
  248.         return ctorDefNode.findFirstToken(TokenTypes.PARAMETERS).getChildCount() == 0;
  249.     }

  250.     /**
  251.      * Checks whether ctor body has no statements.
  252.      * @param ctorDefNode
  253.      *        a ctor definition node(TokenTypes.CTOR_DEF).
  254.      * @return true if ctor body has no statements.
  255.      */
  256.     private static boolean isCtorHasNoStatements(DetailAST ctorDefNode) {
  257.         return ctorDefNode.findFirstToken(TokenTypes.SLIST).getChildCount() == 1;
  258.     }

  259.     /**
  260.      * Checks whether class definition has annotation with name specified in
  261.      * {@link #classAnnotationNames} regexp.
  262.      * @param classDefNode
  263.      *        the node of type TokenTypes.CLASS_DEF.
  264.      * @return true, if class definition has annotation with name specified in regexp.
  265.      */
  266.     private boolean isClassHasRegisteredAnnotation(DetailAST classDefNode) {
  267.         final List<String> annotationNames = getAnnotationCanonicalNames(classDefNode);
  268.         return isAnyOfNamesMatches(annotationNames, classAnnotationNames);
  269.     }

  270.     /**
  271.      * Checks whether ctor definition has annotation with name specified in
  272.      * {@link #ctorAnnotationNames} regexp.
  273.      * @param ctorDefNode
  274.      *        the node of type TokenTypes.CTOR_DEF.
  275.      * @return true, if ctor definition has annotation with name specified in regexp.
  276.      */
  277.     private boolean isCtorHasRegisteredAnnotation(DetailAST ctorDefNode) {
  278.         final List<String> annotationNames = getAnnotationCanonicalNames(ctorDefNode);
  279.         return isAnyOfNamesMatches(annotationNames, ctorAnnotationNames);
  280.     }

  281.     /**
  282.      * Checks whether any name from the list matches regex.
  283.      * @param annotationNames
  284.      *        annotation names to match against regex.
  285.      * @param pattern
  286.      *        regex to match names. may be null.
  287.      * @return false, if pattern object is null, otherwise true, if any name from the list matches
  288.      *         regex.
  289.      */
  290.     private static boolean isAnyOfNamesMatches(List<String> annotationNames, Pattern pattern) {
  291.         boolean result = false;

  292.         if (pattern != null) {
  293.             for (String annotationName : annotationNames) {
  294.                 if (pattern.matcher(annotationName).matches()) {
  295.                     result = true;
  296.                     break;
  297.                 }
  298.             }
  299.         }

  300.         return result;
  301.     }

  302.     /**
  303.      * Returns canonical names of annotations for given node.
  304.      * @param node
  305.      *        annotated node.
  306.      * @return list of canonical annotation names for given node.
  307.      */
  308.     private List<String> getAnnotationCanonicalNames(DetailAST node) {
  309.         final List<String> annotationNames = new ArrayList<>();

  310.         DetailAST modifierNode =
  311.                 node.findFirstToken(TokenTypes.MODIFIERS).getFirstChild();

  312.         while (modifierNode != null) {
  313.             if (modifierNode.getType() == TokenTypes.ANNOTATION) {
  314.                 final String annotationName = getIdentifierName(modifierNode);

  315.                 final List<String> annotationPossibleCanonicalNames =
  316.                         generateAnnotationPossibleCanonicalNames(annotationName);

  317.                 annotationNames.add(annotationName);
  318.                 annotationNames.addAll(annotationPossibleCanonicalNames);
  319.             }

  320.             modifierNode = modifierNode.getNextSibling();
  321.         }

  322.         return annotationNames;
  323.     }

  324.     /**
  325.      * Checks whether import is on demand import(one that imports entire package).
  326.      * @param importTargetName
  327.      *        target of import statement.
  328.      * @return true, if import is on demand import import.
  329.      */
  330.     private static boolean isOnDemandImport(String importTargetName) {
  331.         return importTargetName.endsWith(".*");
  332.     }

  333.     /**
  334.      * <p>
  335.      * Generates possible canonical annotation names.
  336.      * </p>
  337.      * @param annotationName
  338.      *        simple annotation name.
  339.      * @return list of possible canonical annotation names.
  340.      */
  341.     private List<String>
  342.             generateAnnotationPossibleCanonicalNames(String annotationName) {
  343.         final List<String> annotationPossibleCanonicalNames = new ArrayList<>();

  344.         for (String importEntry : singleTypeImports) {
  345.             final String annotationCanonicalName =
  346.                     joinSingleTypeImportWithIdentifier(importEntry, annotationName);

  347.             if (annotationCanonicalName != null) {
  348.                 annotationPossibleCanonicalNames.add(annotationCanonicalName);
  349.                 break;
  350.             }
  351.         }

  352.         for (String importEntry : onDemandImports) {
  353.             final String annotationCanonicalName =
  354.                     joinOnDemandImportWithIdentifier(importEntry, annotationName);

  355.             annotationPossibleCanonicalNames.add(annotationCanonicalName);
  356.         }

  357.         final String annotationCanonicalName =
  358.                 joinFilePackageNameWithIdentifier(filePackageName, annotationName);

  359.         annotationPossibleCanonicalNames.add(annotationCanonicalName);

  360.         return annotationPossibleCanonicalNames;
  361.     }

  362.     /**
  363.      * <p>
  364.      * Joins single type import entry and identifier name into fully qualified name.
  365.      * </p>
  366.      * <p>
  367.      * For example: joinMemberImportWithIdentifier("package.Person","Person") returns
  368.      * "package.Person", joinMemberImportWithIdentifier("package.Person","Person.Name") returns
  369.      * "package.Person.Name".
  370.      * </p>
  371.      * @param importEntry
  372.      *        single type import entry for join.
  373.      * @param identifierName
  374.      *        identifier name to join to import.
  375.      * @return fully qualified identifier name if given import corresponds to identifier, otherwise
  376.      *         null.
  377.      */
  378.     private static String
  379.             joinSingleTypeImportWithIdentifier(String importEntry, String identifierName) {
  380.         final String result;
  381.         final String importEntryLastPart = getSimpleIdentifierNameFromQualifiedName(importEntry);
  382.         final String annotationNameFirstPart = getQualifiedNameFirstPart(identifierName);

  383.         if (importEntryLastPart.equals(annotationNameFirstPart)) {
  384.             result = importEntry + identifierName.substring(annotationNameFirstPart.length());
  385.         }
  386.         else {
  387.             result = null;
  388.         }

  389.         return result;
  390.     }

  391.     /**
  392.      * <p>
  393.      * Joins on demand import entry and identifier name into fully qualified name.
  394.      * </p>
  395.      * <p>
  396.      * For example: joinWildcardImportWithIdentifier("package.*","Person") returns "package.Person",
  397.      * joinWildcardImportWithIdentifier("package.*","Person.Name") returns "package.Person.Name".
  398.      * </p>
  399.      * @param importEntry
  400.      *        on demand import entry for join.
  401.      * @param identifierName
  402.      *        identifier name to join to import.
  403.      * @return fully qualified identifier name.
  404.      */
  405.     private static String
  406.             joinOnDemandImportWithIdentifier(String importEntry, String identifierName) {
  407.         return importEntry.substring(0, importEntry.length() - 1) + identifierName;
  408.     }

  409.     /**
  410.      * <p>
  411.      * Joins package name with identifier name into fully qualified name.
  412.      * </p>
  413.      * <p>
  414.      * For example: joinFilePackageNameWithIdentifier("com.example","Person") returns
  415.      * "com.example.Person".
  416.      * </p>
  417.      * @param packageName
  418.      *        package name to use for join.
  419.      * @param identifierName
  420.      *        identifier name to join to package name.
  421.      * @return fully qualified identifier name.
  422.      */
  423.     private static String
  424.             joinFilePackageNameWithIdentifier(String packageName, String identifierName) {
  425.         return packageName + "." + identifierName;
  426.     }

  427.     /**
  428.      * Returns first part of identifier name.
  429.      * @param canonicalName
  430.      *        identifier name.
  431.      * @return first part of identifier name if name is qualified, otherwise returns identifier name
  432.      *         argument.
  433.      */
  434.     private static String getQualifiedNameFirstPart(String canonicalName) {
  435.         final String result;
  436.         final int firstDotIndex = canonicalName.indexOf('.');

  437.         if (firstDotIndex == -1) {
  438.             result = canonicalName;
  439.         }
  440.         else {
  441.             result = canonicalName.substring(0, firstDotIndex);
  442.         }

  443.         return result;
  444.     }

  445.     /**
  446.      * <p>
  447.      * Returns simple identifier name from its qualified name.
  448.      * </p>
  449.      * <p>
  450.      * For example: If method called for name "com.example.company.Person" it will return "Person".
  451.      * </p>
  452.      * @param qualifiedName
  453.      *        qualified identifier name.
  454.      * @return simple identifier name.
  455.      */
  456.     private static String getSimpleIdentifierNameFromQualifiedName(String qualifiedName) {
  457.         return qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
  458.     }

  459.     /**
  460.      * Returns name of identifier contained in specified node.
  461.      * @param identifierNode
  462.      *        a node containing identifier or qualified identifier.
  463.      * @return identifier name for specified node. If node contains qualified name then method
  464.      *         returns its text representation.
  465.      */
  466.     private static String getIdentifierName(DetailAST identifierNode) {
  467.         final DetailAST identNode = identifierNode.findFirstToken(TokenTypes.IDENT);
  468.         final String result;

  469.         if (identNode == null) {
  470.             final StringBuilder builder = new StringBuilder(40);
  471.             DetailAST node = identifierNode.findFirstToken(TokenTypes.DOT);

  472.             while (node.getType() == TokenTypes.DOT) {
  473.                 builder.insert(0, '.').insert(1, node.getLastChild().getText());

  474.                 node = node.getFirstChild();
  475.             }

  476.             builder.insert(0, node.getText());

  477.             result = builder.toString();
  478.         }
  479.         else {
  480.             result = identNode.getText();
  481.         }

  482.         return result;
  483.     }

  484. }