001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.design; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.FullIdent; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 031 032/** 033 * <p> 034 * Checks that a class which has only private constructors 035 * is declared as final. Doesn't check for classes nested in interfaces 036 * or annotations, as they are always {@code final} there. 037 * </p> 038 * <p> 039 * To configure the check: 040 * </p> 041 * <pre> 042 * <module name="FinalClass"/> 043 * </pre> 044 * <p> 045 * Example: 046 * </p> 047 * <pre> 048 * final class MyClass { // OK 049 * private MyClass() { } 050 * } 051 * 052 * class MyClass { // violation, class should be declared final 053 * private MyClass() { } 054 * } 055 * 056 * class MyClass { // OK, since it has a public constructor 057 * int field1; 058 * String field2; 059 * private MyClass(int value) { 060 * this.field1 = value; 061 * this.field2 = " "; 062 * } 063 * public MyClass(String value) { 064 * this.field2 = value; 065 * this.field1 = 0; 066 * } 067 * } 068 * 069 * interface CheckInterface 070 * { 071 * class MyClass { // OK, nested class in interface is always final 072 * private MyClass() {} 073 * } 074 * } 075 * 076 * public @interface Test { 077 * public boolean enabled() 078 * default true; 079 * class MyClass { // OK, class nested in an annotation is always final 080 * private MyClass() { } 081 * } 082 * } 083 * 084 * class TestAnonymousInnerClasses { // OK, class has an anonymous inner class. 085 * public static final TestAnonymousInnerClasses ONE = new TestAnonymousInnerClasses() { 086 * 087 * }; 088 * 089 * private TestAnonymousInnerClasses() { 090 * } 091 * } 092 * </pre> 093 * <p> 094 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 095 * </p> 096 * <p> 097 * Violation Message Keys: 098 * </p> 099 * <ul> 100 * <li> 101 * {@code final.class} 102 * </li> 103 * </ul> 104 * 105 * @since 3.1 106 */ 107@FileStatefulCheck 108public class FinalClassCheck 109 extends AbstractCheck { 110 111 /** 112 * A key is pointing to the warning message text in "messages.properties" 113 * file. 114 */ 115 public static final String MSG_KEY = "final.class"; 116 117 /** 118 * Character separate package names in qualified name of java class. 119 */ 120 private static final String PACKAGE_SEPARATOR = "."; 121 122 /** Keeps ClassDesc objects for stack of declared classes. */ 123 private Deque<ClassDesc> classes; 124 125 /** Full qualified name of the package. */ 126 private String packageName; 127 128 @Override 129 public int[] getDefaultTokens() { 130 return getRequiredTokens(); 131 } 132 133 @Override 134 public int[] getAcceptableTokens() { 135 return getRequiredTokens(); 136 } 137 138 @Override 139 public int[] getRequiredTokens() { 140 return new int[] { 141 TokenTypes.CLASS_DEF, 142 TokenTypes.CTOR_DEF, 143 TokenTypes.PACKAGE_DEF, 144 TokenTypes.LITERAL_NEW, 145 }; 146 } 147 148 @Override 149 public void beginTree(DetailAST rootAST) { 150 classes = new ArrayDeque<>(); 151 packageName = ""; 152 } 153 154 @Override 155 public void visitToken(DetailAST ast) { 156 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 157 158 switch (ast.getType()) { 159 case TokenTypes.PACKAGE_DEF: 160 packageName = extractQualifiedName(ast.getFirstChild().getNextSibling()); 161 break; 162 163 case TokenTypes.CLASS_DEF: 164 registerNestedSubclassToOuterSuperClasses(ast); 165 166 final boolean isFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null; 167 final boolean isAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null; 168 169 final String qualifiedClassName = getQualifiedClassName(ast); 170 classes.push(new ClassDesc(qualifiedClassName, isFinal, isAbstract)); 171 break; 172 173 case TokenTypes.CTOR_DEF: 174 if (!ScopeUtil.isInEnumBlock(ast) && !ScopeUtil.isInRecordBlock(ast)) { 175 final ClassDesc desc = classes.peek(); 176 if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) { 177 desc.registerNonPrivateCtor(); 178 } 179 else { 180 desc.registerPrivateCtor(); 181 } 182 } 183 break; 184 185 case TokenTypes.LITERAL_NEW: 186 if (ast.getFirstChild() != null 187 && ast.getLastChild().getType() == TokenTypes.OBJBLOCK) { 188 for (ClassDesc classDesc : classes) { 189 if (doesNameOfClassMatchAnonymousInnerClassName(ast, classDesc)) { 190 classDesc.registerAnonymousInnerClass(); 191 } 192 } 193 } 194 break; 195 196 default: 197 throw new IllegalStateException(ast.toString()); 198 } 199 } 200 201 @Override 202 public void leaveToken(DetailAST ast) { 203 if (ast.getType() == TokenTypes.CLASS_DEF) { 204 final ClassDesc desc = classes.pop(); 205 if (desc.isWithPrivateCtor() 206 && !(desc.isDeclaredAsAbstract() 207 || desc.isWithAnonymousInnerClass()) 208 && !desc.isDeclaredAsFinal() 209 && !desc.isWithNonPrivateCtor() 210 && !desc.isWithNestedSubclass() 211 && !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) { 212 final String qualifiedName = desc.getQualifiedName(); 213 final String className = getClassNameFromQualifiedName(qualifiedName); 214 log(ast, MSG_KEY, className); 215 } 216 } 217 } 218 219 /** 220 * Get name of class (with qualified package if specified) in {@code ast}. 221 * 222 * @param ast ast to extract class name from 223 * @return qualified name 224 */ 225 private static String extractQualifiedName(DetailAST ast) { 226 return FullIdent.createFullIdent(ast).getText(); 227 } 228 229 /** 230 * Register to outer super classes of given classAst that 231 * given classAst is extending them. 232 * 233 * @param classAst class which outer super classes will be 234 * informed about nesting subclass 235 */ 236 private void registerNestedSubclassToOuterSuperClasses(DetailAST classAst) { 237 final String currentAstSuperClassName = getSuperClassName(classAst); 238 if (currentAstSuperClassName != null) { 239 for (ClassDesc classDesc : classes) { 240 final String classDescQualifiedName = classDesc.getQualifiedName(); 241 if (doesNameInExtendMatchSuperClassName(classDescQualifiedName, 242 currentAstSuperClassName)) { 243 classDesc.registerNestedSubclass(); 244 } 245 } 246 } 247 } 248 249 /** 250 * Check if class name matches with anonymous inner class name. 251 * 252 * @param ast current ast. 253 * @param classDesc class to match. 254 * @return true if current class name matches anonymous inner 255 * class name. 256 */ 257 private static boolean doesNameOfClassMatchAnonymousInnerClassName(DetailAST ast, 258 ClassDesc classDesc) { 259 final String[] className = classDesc.getQualifiedName().split("\\."); 260 return ast.getFirstChild().getText().equals(className[className.length - 1]); 261 } 262 263 /** 264 * Get qualified class name from given class Ast. 265 * 266 * @param classAst class to get qualified class name 267 * @return qualified class name of a class 268 */ 269 private String getQualifiedClassName(DetailAST classAst) { 270 final String className = classAst.findFirstToken(TokenTypes.IDENT).getText(); 271 String outerClassQualifiedName = null; 272 if (!classes.isEmpty()) { 273 outerClassQualifiedName = classes.peek().getQualifiedName(); 274 } 275 return getQualifiedClassName(packageName, outerClassQualifiedName, className); 276 } 277 278 /** 279 * Calculate qualified class name(package + class name) laying inside given 280 * outer class. 281 * 282 * @param packageName package name, empty string on default package 283 * @param outerClassQualifiedName qualified name(package + class) of outer class, 284 * null if doesn't exist 285 * @param className class name 286 * @return qualified class name(package + class name) 287 */ 288 private static String getQualifiedClassName(String packageName, String outerClassQualifiedName, 289 String className) { 290 final String qualifiedClassName; 291 292 if (outerClassQualifiedName == null) { 293 if (packageName.isEmpty()) { 294 qualifiedClassName = className; 295 } 296 else { 297 qualifiedClassName = packageName + PACKAGE_SEPARATOR + className; 298 } 299 } 300 else { 301 qualifiedClassName = outerClassQualifiedName + PACKAGE_SEPARATOR + className; 302 } 303 return qualifiedClassName; 304 } 305 306 /** 307 * Get super class name of given class. 308 * 309 * @param classAst class 310 * @return super class name or null if super class is not specified 311 */ 312 private static String getSuperClassName(DetailAST classAst) { 313 String superClassName = null; 314 final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE); 315 if (classExtend != null) { 316 superClassName = extractQualifiedName(classExtend.getFirstChild()); 317 } 318 return superClassName; 319 } 320 321 /** 322 * Checks if given super class name in extend clause match super class qualified name. 323 * 324 * @param superClassQualifiedName super class qualified name (with package) 325 * @param superClassInExtendClause name in extend clause 326 * @return true if given super class name in extend clause match super class qualified name, 327 * false otherwise 328 */ 329 private static boolean doesNameInExtendMatchSuperClassName(String superClassQualifiedName, 330 String superClassInExtendClause) { 331 String superClassNormalizedName = superClassQualifiedName; 332 if (!superClassInExtendClause.contains(PACKAGE_SEPARATOR)) { 333 superClassNormalizedName = getClassNameFromQualifiedName(superClassQualifiedName); 334 } 335 return superClassNormalizedName.equals(superClassInExtendClause); 336 } 337 338 /** 339 * Get class name from qualified name. 340 * 341 * @param qualifiedName qualified class name 342 * @return class name 343 */ 344 private static String getClassNameFromQualifiedName(String qualifiedName) { 345 return qualifiedName.substring(qualifiedName.lastIndexOf(PACKAGE_SEPARATOR) + 1); 346 } 347 348 /** Maintains information about class' ctors. */ 349 private static final class ClassDesc { 350 351 /** Qualified class name(with package). */ 352 private final String qualifiedName; 353 354 /** Is class declared as final. */ 355 private final boolean declaredAsFinal; 356 357 /** Is class declared as abstract. */ 358 private final boolean declaredAsAbstract; 359 360 /** Does class have non-private ctors. */ 361 private boolean withNonPrivateCtor; 362 363 /** Does class have private ctors. */ 364 private boolean withPrivateCtor; 365 366 /** Does class have nested subclass. */ 367 private boolean withNestedSubclass; 368 369 /** Does class have anonymous inner class. */ 370 private boolean withAnonymousInnerClass; 371 372 /** 373 * Create a new ClassDesc instance. 374 * 375 * @param qualifiedName qualified class name(with package) 376 * @param declaredAsFinal indicates if the 377 * class declared as final 378 * @param declaredAsAbstract indicates if the 379 * class declared as abstract 380 */ 381 /* package */ ClassDesc(String qualifiedName, boolean declaredAsFinal, 382 boolean declaredAsAbstract) { 383 this.qualifiedName = qualifiedName; 384 this.declaredAsFinal = declaredAsFinal; 385 this.declaredAsAbstract = declaredAsAbstract; 386 } 387 388 /** 389 * Get qualified class name. 390 * 391 * @return qualified class name 392 */ 393 private String getQualifiedName() { 394 return qualifiedName; 395 } 396 397 /** Adds private ctor. */ 398 private void registerPrivateCtor() { 399 withPrivateCtor = true; 400 } 401 402 /** Adds non-private ctor. */ 403 private void registerNonPrivateCtor() { 404 withNonPrivateCtor = true; 405 } 406 407 /** Adds nested subclass. */ 408 private void registerNestedSubclass() { 409 withNestedSubclass = true; 410 } 411 412 /** Adds anonymous inner class. */ 413 private void registerAnonymousInnerClass() { 414 withAnonymousInnerClass = true; 415 } 416 417 /** 418 * Does class have private ctors. 419 * 420 * @return true if class has private ctors 421 */ 422 private boolean isWithPrivateCtor() { 423 return withPrivateCtor; 424 } 425 426 /** 427 * Does class have non-private ctors. 428 * 429 * @return true if class has non-private ctors 430 */ 431 private boolean isWithNonPrivateCtor() { 432 return withNonPrivateCtor; 433 } 434 435 /** 436 * Does class have nested subclass. 437 * 438 * @return true if class has nested subclass 439 */ 440 private boolean isWithNestedSubclass() { 441 return withNestedSubclass; 442 } 443 444 /** 445 * Is class declared as final. 446 * 447 * @return true if class is declared as final 448 */ 449 private boolean isDeclaredAsFinal() { 450 return declaredAsFinal; 451 } 452 453 /** 454 * Is class declared as abstract. 455 * 456 * @return true if class is declared as final 457 */ 458 private boolean isDeclaredAsAbstract() { 459 return declaredAsAbstract; 460 } 461 462 /** 463 * Does class have an anonymous inner class. 464 * 465 * @return true if class has anonymous inner class 466 */ 467 private boolean isWithAnonymousInnerClass() { 468 return withAnonymousInnerClass; 469 } 470 471 } 472 473}