EmptyPublicCtorInClassCheck.java
- ////////////////////////////////////////////////////////////////////////////////
- // checkstyle: Checks Java source code for adherence to a set of rules.
- // Copyright (C) 2001-2019 the original author or authors.
- //
- // This library is free software; you can redistribute it and/or
- // modify it under the terms of the GNU Lesser General Public
- // License as published by the Free Software Foundation; either
- // version 2.1 of the License, or (at your option) any later version.
- //
- // This library is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- // Lesser General Public License for more details.
- //
- // You should have received a copy of the GNU Lesser General Public
- // License along with this library; if not, write to the Free Software
- // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- ////////////////////////////////////////////////////////////////////////////////
- package com.github.sevntu.checkstyle.checks.coding;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.regex.Pattern;
- import com.github.sevntu.checkstyle.SevntuUtil;
- import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
- import com.puppycrawl.tools.checkstyle.api.DetailAST;
- import com.puppycrawl.tools.checkstyle.api.TokenTypes;
- /**
- * <p>
- * This Check looks for useless empty public constructors. Class constructor is considered useless
- * by this Check if and only if class has exactly one ctor, which is public, empty(one that has no
- * statements) and <a href="http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.8.9">
- * default</a>.
- * </p>
- * <p>
- * Example 1. Check will generate violation for this code:
- * </p>
- *
- * <pre>
- * class Dummy {
- * public Dummy() {
- * }
- * }
- * </pre>
- *
- * <p>
- * Example 2. Check will not generate violation for this code:
- * </p>
- *
- * <pre>
- * class Dummy {
- * private Dummy() {
- * }
- * }
- * </pre>
- *
- * <p>
- * class Dummy has only one ctor, which is not public.
- * </p>
- * <p>
- * Example 3. Check will not generate violation for this code:
- * </p>
- *
- * <pre>
- * class Dummy {
- * public Dummy() {
- * }
- * public Dummy(int i) {
- * }
- * }
- * </pre>
- *
- * <p>
- * class Dummy has multiple ctors.
- * </p>
- * <p>
- * Check has two properties:
- * </p>
- * <p>
- * "classAnnotationNames" - This property contains regex for canonical names of class annotations,
- * which require class to have empty public ctor. Check will not log violations for classes marked
- * with annotations that match this regex. Default option value is "javax\.persistence\.Entity".
- * </p>
- * <p>
- * "ctorAnnotationNames" - This property contains regex for canonical names of ctor annotations,
- * which make empty public ctor essential. Check will not log violations for ctors marked with
- * annotations that match this regex. Default option value is "com\.google\.inject\.Inject".
- * </p>
- * Following configuration will adjust Check to skip classes which annotated with
- * "javax.persistence.Entity" and classes which has empty public ctor with
- * "com\.google\.inject\.Inject".
- *
- * <pre>
- * <module name="EmptyPublicCtorInClassCheck">
- * <property name="classAnnotationNames" value="javax\.persistence\.Entity"/>
- * <property name="ctorAnnotationNames" value="com\.google\.inject\.Inject"/>
- * </module>
- * </pre>
- *
- * @author <a href="mailto:zuy_alexey@mail.ru">Zuy Alexey</a>
- * @since 1.13.0
- */
- public class EmptyPublicCtorInClassCheck extends AbstractCheck {
- /**
- * Violation message key.
- */
- public static final String MSG_KEY = "empty.public.ctor";
- /**
- * List of single-type-imports for current AST.
- */
- private final List<String> singleTypeImports = new ArrayList<>();
- /**
- * List of on-demand-imports for current AST.
- */
- private final List<String> onDemandImports = new ArrayList<>();
- /**
- * Package name for current AST or empty string if AST does not contain package name.
- */
- private String filePackageName;
- /**
- * Regex which matches names of class annotations which require class to have public no-argument
- * ctor. Default value is "javax\.persistence\.Entity".
- */
- private Pattern classAnnotationNames = Pattern.compile("javax\\.persistence\\.Entity");
- /**
- * Regex which matches names of ctor annotations which make empty public ctor essential. Default
- * value is "com\.google\.inject\.Inject".
- */
- private Pattern ctorAnnotationNames = Pattern.compile("com\\.google\\.inject\\.Inject");
- /**
- * Sets regex which matches names of class annotations which require class to have public
- * no-argument ctor.
- * @param regex
- * regex to match annotation names.
- */
- public void setClassAnnotationNames(String regex) {
- if (regex == null || regex.isEmpty()) {
- classAnnotationNames = null;
- }
- else {
- classAnnotationNames = Pattern.compile(regex);
- }
- }
- /**
- * Sets regex which matches names of ctor annotations which make empty public ctor essential.
- * @param regex
- * regex to match annotation names.
- */
- public void setCtorAnnotationNames(String regex) {
- if (regex == null || regex.isEmpty()) {
- ctorAnnotationNames = null;
- }
- else {
- ctorAnnotationNames = Pattern.compile(regex);
- }
- }
- @Override
- public int[] getDefaultTokens() {
- return new int[] {
- TokenTypes.CLASS_DEF,
- TokenTypes.PACKAGE_DEF,
- TokenTypes.IMPORT,
- };
- }
- @Override
- public int[] getAcceptableTokens() {
- return getDefaultTokens();
- }
- @Override
- public int[] getRequiredTokens() {
- return getDefaultTokens();
- }
- @Override
- public void beginTree(DetailAST aRootNode) {
- singleTypeImports.clear();
- onDemandImports.clear();
- filePackageName = "";
- }
- @Override
- public void visitToken(DetailAST node) {
- switch (node.getType()) {
- case TokenTypes.IMPORT:
- final String packageMemberName = getIdentifierName(node);
- if (isOnDemandImport(packageMemberName)) {
- onDemandImports.add(packageMemberName);
- }
- else {
- singleTypeImports.add(packageMemberName);
- }
- break;
- case TokenTypes.CLASS_DEF:
- if (getClassCtorCount(node) == 1) {
- final DetailAST ctorDef = getFirstCtorDefinition(node);
- if (isCtorPublic(ctorDef)
- && isCtorHasNoParameters(ctorDef)
- && isCtorHasNoStatements(ctorDef)
- && !isClassHasRegisteredAnnotation(node)
- && !isCtorHasRegisteredAnnotation(ctorDef)) {
- log(ctorDef, MSG_KEY);
- }
- }
- break;
- case TokenTypes.PACKAGE_DEF:
- filePackageName = getIdentifierName(node);
- break;
- default:
- SevntuUtil.reportInvalidToken(node.getType());
- break;
- }
- }
- /**
- * Calculates constructor count for class.
- * @param classDefNode
- * a class definition node.
- * @return ctor count for given class definition.
- */
- private static int getClassCtorCount(DetailAST classDefNode) {
- return classDefNode.findFirstToken(TokenTypes.OBJBLOCK).getChildCount(TokenTypes.CTOR_DEF);
- }
- /**
- * Gets first constructor definition for class.
- * @param classDefNode
- * a class definition node.
- * @return first ctor definition node for class or null if class has no ctor.
- */
- private static DetailAST getFirstCtorDefinition(DetailAST classDefNode) {
- return classDefNode
- .findFirstToken(TokenTypes.OBJBLOCK)
- .findFirstToken(TokenTypes.CTOR_DEF);
- }
- /**
- * Checks whether constructor is public.
- * @param ctorDefNode
- * a ctor definition node(TokenTypes.CTOR_DEF).
- * @return true, if given ctor is public.
- */
- private static boolean isCtorPublic(DetailAST ctorDefNode) {
- return ctorDefNode
- .findFirstToken(TokenTypes.MODIFIERS)
- .findFirstToken(TokenTypes.LITERAL_PUBLIC) != null;
- }
- /**
- * Checks whether ctor has no parameters.
- * @param ctorDefNode
- * a ctor definition node(TokenTypes.CTOR_DEF).
- * @return true, if ctor has no parameters.
- */
- private static boolean isCtorHasNoParameters(DetailAST ctorDefNode) {
- return ctorDefNode.findFirstToken(TokenTypes.PARAMETERS).getChildCount() == 0;
- }
- /**
- * Checks whether ctor body has no statements.
- * @param ctorDefNode
- * a ctor definition node(TokenTypes.CTOR_DEF).
- * @return true if ctor body has no statements.
- */
- private static boolean isCtorHasNoStatements(DetailAST ctorDefNode) {
- return ctorDefNode.findFirstToken(TokenTypes.SLIST).getChildCount() == 1;
- }
- /**
- * Checks whether class definition has annotation with name specified in
- * {@link #classAnnotationNames} regexp.
- * @param classDefNode
- * the node of type TokenTypes.CLASS_DEF.
- * @return true, if class definition has annotation with name specified in regexp.
- */
- private boolean isClassHasRegisteredAnnotation(DetailAST classDefNode) {
- final List<String> annotationNames = getAnnotationCanonicalNames(classDefNode);
- return isAnyOfNamesMatches(annotationNames, classAnnotationNames);
- }
- /**
- * Checks whether ctor definition has annotation with name specified in
- * {@link #ctorAnnotationNames} regexp.
- * @param ctorDefNode
- * the node of type TokenTypes.CTOR_DEF.
- * @return true, if ctor definition has annotation with name specified in regexp.
- */
- private boolean isCtorHasRegisteredAnnotation(DetailAST ctorDefNode) {
- final List<String> annotationNames = getAnnotationCanonicalNames(ctorDefNode);
- return isAnyOfNamesMatches(annotationNames, ctorAnnotationNames);
- }
- /**
- * Checks whether any name from the list matches regex.
- * @param annotationNames
- * annotation names to match against regex.
- * @param pattern
- * regex to match names. may be null.
- * @return false, if pattern object is null, otherwise true, if any name from the list matches
- * regex.
- */
- private static boolean isAnyOfNamesMatches(List<String> annotationNames, Pattern pattern) {
- boolean result = false;
- if (pattern != null) {
- for (String annotationName : annotationNames) {
- if (pattern.matcher(annotationName).matches()) {
- result = true;
- break;
- }
- }
- }
- return result;
- }
- /**
- * Returns canonical names of annotations for given node.
- * @param node
- * annotated node.
- * @return list of canonical annotation names for given node.
- */
- private List<String> getAnnotationCanonicalNames(DetailAST node) {
- final List<String> annotationNames = new ArrayList<>();
- DetailAST modifierNode =
- node.findFirstToken(TokenTypes.MODIFIERS).getFirstChild();
- while (modifierNode != null) {
- if (modifierNode.getType() == TokenTypes.ANNOTATION) {
- final String annotationName = getIdentifierName(modifierNode);
- final List<String> annotationPossibleCanonicalNames =
- generateAnnotationPossibleCanonicalNames(annotationName);
- annotationNames.add(annotationName);
- annotationNames.addAll(annotationPossibleCanonicalNames);
- }
- modifierNode = modifierNode.getNextSibling();
- }
- return annotationNames;
- }
- /**
- * Checks whether import is on demand import(one that imports entire package).
- * @param importTargetName
- * target of import statement.
- * @return true, if import is on demand import import.
- */
- private static boolean isOnDemandImport(String importTargetName) {
- return importTargetName.endsWith(".*");
- }
- /**
- * <p>
- * Generates possible canonical annotation names.
- * </p>
- * @param annotationName
- * simple annotation name.
- * @return list of possible canonical annotation names.
- */
- private List<String>
- generateAnnotationPossibleCanonicalNames(String annotationName) {
- final List<String> annotationPossibleCanonicalNames = new ArrayList<>();
- for (String importEntry : singleTypeImports) {
- final String annotationCanonicalName =
- joinSingleTypeImportWithIdentifier(importEntry, annotationName);
- if (annotationCanonicalName != null) {
- annotationPossibleCanonicalNames.add(annotationCanonicalName);
- break;
- }
- }
- for (String importEntry : onDemandImports) {
- final String annotationCanonicalName =
- joinOnDemandImportWithIdentifier(importEntry, annotationName);
- annotationPossibleCanonicalNames.add(annotationCanonicalName);
- }
- final String annotationCanonicalName =
- joinFilePackageNameWithIdentifier(filePackageName, annotationName);
- annotationPossibleCanonicalNames.add(annotationCanonicalName);
- return annotationPossibleCanonicalNames;
- }
- /**
- * <p>
- * Joins single type import entry and identifier name into fully qualified name.
- * </p>
- * <p>
- * For example: joinMemberImportWithIdentifier("package.Person","Person") returns
- * "package.Person", joinMemberImportWithIdentifier("package.Person","Person.Name") returns
- * "package.Person.Name".
- * </p>
- * @param importEntry
- * single type import entry for join.
- * @param identifierName
- * identifier name to join to import.
- * @return fully qualified identifier name if given import corresponds to identifier, otherwise
- * null.
- */
- private static String
- joinSingleTypeImportWithIdentifier(String importEntry, String identifierName) {
- final String result;
- final String importEntryLastPart = getSimpleIdentifierNameFromQualifiedName(importEntry);
- final String annotationNameFirstPart = getQualifiedNameFirstPart(identifierName);
- if (importEntryLastPart.equals(annotationNameFirstPart)) {
- result = importEntry + identifierName.substring(annotationNameFirstPart.length());
- }
- else {
- result = null;
- }
- return result;
- }
- /**
- * <p>
- * Joins on demand import entry and identifier name into fully qualified name.
- * </p>
- * <p>
- * For example: joinWildcardImportWithIdentifier("package.*","Person") returns "package.Person",
- * joinWildcardImportWithIdentifier("package.*","Person.Name") returns "package.Person.Name".
- * </p>
- * @param importEntry
- * on demand import entry for join.
- * @param identifierName
- * identifier name to join to import.
- * @return fully qualified identifier name.
- */
- private static String
- joinOnDemandImportWithIdentifier(String importEntry, String identifierName) {
- return importEntry.substring(0, importEntry.length() - 1) + identifierName;
- }
- /**
- * <p>
- * Joins package name with identifier name into fully qualified name.
- * </p>
- * <p>
- * For example: joinFilePackageNameWithIdentifier("com.example","Person") returns
- * "com.example.Person".
- * </p>
- * @param packageName
- * package name to use for join.
- * @param identifierName
- * identifier name to join to package name.
- * @return fully qualified identifier name.
- */
- private static String
- joinFilePackageNameWithIdentifier(String packageName, String identifierName) {
- return packageName + "." + identifierName;
- }
- /**
- * Returns first part of identifier name.
- * @param canonicalName
- * identifier name.
- * @return first part of identifier name if name is qualified, otherwise returns identifier name
- * argument.
- */
- private static String getQualifiedNameFirstPart(String canonicalName) {
- final String result;
- final int firstDotIndex = canonicalName.indexOf('.');
- if (firstDotIndex == -1) {
- result = canonicalName;
- }
- else {
- result = canonicalName.substring(0, firstDotIndex);
- }
- return result;
- }
- /**
- * <p>
- * Returns simple identifier name from its qualified name.
- * </p>
- * <p>
- * For example: If method called for name "com.example.company.Person" it will return "Person".
- * </p>
- * @param qualifiedName
- * qualified identifier name.
- * @return simple identifier name.
- */
- private static String getSimpleIdentifierNameFromQualifiedName(String qualifiedName) {
- return qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
- }
- /**
- * Returns name of identifier contained in specified node.
- * @param identifierNode
- * a node containing identifier or qualified identifier.
- * @return identifier name for specified node. If node contains qualified name then method
- * returns its text representation.
- */
- private static String getIdentifierName(DetailAST identifierNode) {
- final DetailAST identNode = identifierNode.findFirstToken(TokenTypes.IDENT);
- final String result;
- if (identNode == null) {
- final StringBuilder builder = new StringBuilder(40);
- DetailAST node = identifierNode.findFirstToken(TokenTypes.DOT);
- while (node.getType() == TokenTypes.DOT) {
- builder.insert(0, '.').insert(1, node.getLastChild().getText());
- node = node.getFirstChild();
- }
- builder.insert(0, node.getText());
- result = builder.toString();
- }
- else {
- result = identNode.getText();
- }
- return result;
- }
- }