001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.Comparator; 024import java.util.Deque; 025import java.util.HashMap; 026import java.util.LinkedHashMap; 027import java.util.Map; 028import java.util.Optional; 029import java.util.function.Function; 030import java.util.function.ToIntFunction; 031 032import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 033import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 034import com.puppycrawl.tools.checkstyle.api.DetailAST; 035import com.puppycrawl.tools.checkstyle.api.TokenTypes; 036import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 037import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 038import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 039import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 040 041/** 042 * <div> 043 * Ensures that identifies classes that can be effectively declared as final are explicitly 044 * marked as final. The following are different types of classes that can be identified: 045 * </div> 046 * <ol> 047 * <li> 048 * Private classes with no declared constructors. 049 * </li> 050 * <li> 051 * Classes with any modifier, and contains only private constructors. 052 * </li> 053 * </ol> 054 * 055 * <p> 056 * Classes are skipped if: 057 * </p> 058 * <ol> 059 * <li> 060 * Class is Super class of some Anonymous inner class. 061 * </li> 062 * <li> 063 * Class is extended by another class in the same file. 064 * </li> 065 * </ol> 066 * 067 * @since 3.1 068 */ 069@FileStatefulCheck 070public class FinalClassCheck 071 extends AbstractCheck { 072 073 /** 074 * A key is pointing to the warning message text in "messages.properties" 075 * file. 076 */ 077 public static final String MSG_KEY = "final.class"; 078 079 /** 080 * Character separate package names in qualified name of java class. 081 */ 082 private static final String PACKAGE_SEPARATOR = "."; 083 084 /** Keeps ClassDesc objects for all inner classes. */ 085 private Map<String, ClassDesc> innerClasses; 086 087 /** 088 * Maps anonymous inner class's {@link TokenTypes#LITERAL_NEW} node to 089 * the outer type declaration's fully qualified name. 090 */ 091 private Map<DetailAST, String> anonInnerClassToOuterTypeDecl; 092 093 /** Keeps TypeDeclarationDescription object for stack of declared type descriptions. */ 094 private Deque<TypeDeclarationDescription> typeDeclarations; 095 096 /** Full qualified name of the package. */ 097 private String packageName; 098 099 /** 100 * Creates a new {@code FinalClassCheck} instance. 101 */ 102 public FinalClassCheck() { 103 // no code by default 104 } 105 106 @Override 107 public int[] getDefaultTokens() { 108 return getRequiredTokens(); 109 } 110 111 @Override 112 public int[] getAcceptableTokens() { 113 return getRequiredTokens(); 114 } 115 116 @Override 117 public int[] getRequiredTokens() { 118 return new int[] { 119 TokenTypes.ANNOTATION_DEF, 120 TokenTypes.CLASS_DEF, 121 TokenTypes.ENUM_DEF, 122 TokenTypes.INTERFACE_DEF, 123 TokenTypes.RECORD_DEF, 124 TokenTypes.CTOR_DEF, 125 TokenTypes.PACKAGE_DEF, 126 TokenTypes.LITERAL_NEW, 127 }; 128 } 129 130 @Override 131 public void beginTree(DetailAST rootAST) { 132 typeDeclarations = new ArrayDeque<>(); 133 innerClasses = new LinkedHashMap<>(); 134 anonInnerClassToOuterTypeDecl = new HashMap<>(); 135 packageName = ""; 136 } 137 138 @Override 139 public void visitToken(DetailAST ast) { 140 switch (ast.getType()) { 141 case TokenTypes.PACKAGE_DEF -> 142 packageName = CheckUtil.extractQualifiedName(ast.getFirstChild().getNextSibling()); 143 144 case TokenTypes.ANNOTATION_DEF, 145 TokenTypes.ENUM_DEF, 146 TokenTypes.INTERFACE_DEF, 147 TokenTypes.RECORD_DEF -> { 148 final TypeDeclarationDescription description = new TypeDeclarationDescription( 149 extractQualifiedTypeName(ast), 0, ast); 150 typeDeclarations.push(description); 151 } 152 153 case TokenTypes.CLASS_DEF -> visitClass(ast); 154 155 case TokenTypes.CTOR_DEF -> visitCtor(ast); 156 157 case TokenTypes.LITERAL_NEW -> { 158 if (ast.getFirstChild() != null 159 && ast.getLastChild().getType() == TokenTypes.OBJBLOCK) { 160 161 String outerTypeName = packageName; 162 if (!typeDeclarations.isEmpty()) { 163 outerTypeName = typeDeclarations.peek().getQualifiedName(); 164 } 165 anonInnerClassToOuterTypeDecl.put(ast, outerTypeName); 166 } 167 } 168 169 default -> throw new IllegalStateException(ast.toString()); 170 } 171 } 172 173 /** 174 * Called to process a type definition. 175 * 176 * @param ast the token to process 177 */ 178 private void visitClass(DetailAST ast) { 179 final String qualifiedClassName = extractQualifiedTypeName(ast); 180 final ClassDesc currClass = new ClassDesc(qualifiedClassName, typeDeclarations.size(), ast); 181 typeDeclarations.push(currClass); 182 innerClasses.put(qualifiedClassName, currClass); 183 } 184 185 /** 186 * Called to process a constructor definition. 187 * 188 * @param ast the token to process 189 */ 190 private void visitCtor(DetailAST ast) { 191 if (!ScopeUtil.isInEnumBlock(ast) && !ScopeUtil.isInRecordBlock(ast)) { 192 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 193 if (modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) == null) { 194 // Can be only of type ClassDesc, preceding if statements guarantee it. 195 final ClassDesc desc = (ClassDesc) typeDeclarations.getFirst(); 196 desc.registerNonPrivateCtor(); 197 } 198 } 199 } 200 201 @Override 202 public void leaveToken(DetailAST ast) { 203 if (TokenUtil.isTypeDeclaration(ast.getType())) { 204 typeDeclarations.pop(); 205 } 206 } 207 208 @Override 209 public void finishTree(DetailAST rootAST) { 210 anonInnerClassToOuterTypeDecl.forEach(this::registerAnonymousInnerClassToSuperClass); 211 innerClasses.forEach(this::registerExtendedClass); 212 innerClasses.forEach((qualifiedClassName, classDesc) -> { 213 if (shouldBeDeclaredAsFinal(classDesc)) { 214 final String className = CommonUtil.baseClassName(qualifiedClassName); 215 log(classDesc.getTypeDeclarationAst(), MSG_KEY, className); 216 } 217 }); 218 } 219 220 /** 221 * Checks whether a class should be declared as final or not. 222 * 223 * @param classDesc description of the class 224 * @return true if given class should be declared as final otherwise false 225 */ 226 private static boolean shouldBeDeclaredAsFinal(ClassDesc classDesc) { 227 final boolean shouldBeFinal; 228 229 final boolean skipClass = classDesc.isDeclaredAsFinal() 230 || classDesc.isDeclaredAsAbstract() 231 || classDesc.isSuperClassOfAnonymousInnerClass() 232 || classDesc.isWithNestedSubclass(); 233 234 if (skipClass) { 235 shouldBeFinal = false; 236 } 237 else if (classDesc.isHasDeclaredConstructor()) { 238 shouldBeFinal = classDesc.isDeclaredAsPrivate(); 239 } 240 else { 241 shouldBeFinal = !classDesc.isWithNonPrivateCtor(); 242 } 243 return shouldBeFinal; 244 } 245 246 /** 247 * Register to outer super class of given classAst that 248 * given classAst is extending them. 249 * 250 * @param qualifiedClassName qualifies class name(with package) of the current class 251 * @param currentClass class which outer super class will be informed about nesting subclass 252 */ 253 private void registerExtendedClass(String qualifiedClassName, 254 ClassDesc currentClass) { 255 final String superClassName = getSuperClassName(currentClass.getTypeDeclarationAst()); 256 if (superClassName != null) { 257 final ToIntFunction<ClassDesc> nestedClassCountProvider = classDesc -> { 258 return CheckUtil.typeDeclarationNameMatchingCount(qualifiedClassName, 259 classDesc.getQualifiedName()); 260 }; 261 getNearestClassWithSameName(superClassName, nestedClassCountProvider) 262 .or(() -> Optional.ofNullable(innerClasses.get(superClassName))) 263 .ifPresent(ClassDesc::registerNestedSubclass); 264 } 265 } 266 267 /** 268 * Register to the super class of anonymous inner class that the given class is instantiated 269 * by an anonymous inner class. 270 * 271 * @param literalNewAst ast node of {@link TokenTypes#LITERAL_NEW} representing anonymous inner 272 * class 273 * @param outerTypeDeclName Fully qualified name of the outer type declaration of anonymous 274 * inner class 275 */ 276 private void registerAnonymousInnerClassToSuperClass(DetailAST literalNewAst, 277 String outerTypeDeclName) { 278 final String superClassName = CheckUtil.getShortNameOfAnonInnerClass(literalNewAst); 279 280 final ToIntFunction<ClassDesc> anonClassCountProvider = classDesc -> { 281 return getAnonSuperTypeMatchingCount(outerTypeDeclName, classDesc.getQualifiedName()); 282 }; 283 getNearestClassWithSameName(superClassName, anonClassCountProvider) 284 .or(() -> Optional.ofNullable(innerClasses.get(superClassName))) 285 .ifPresent(ClassDesc::registerSuperClassOfAnonymousInnerClass); 286 } 287 288 /** 289 * Get the nearest class with same name. 290 * 291 * <p>The parameter {@code countProvider} exists because if the class being searched is the 292 * super class of anonymous inner class, the rules of evaluation are a bit different, 293 * consider the following example- 294 * <pre> 295 * {@code 296 * public class Main { 297 * static class One { 298 * static class Two { 299 * } 300 * } 301 * 302 * class Three { 303 * One.Two object = new One.Two() { // Object of Main.Three.One.Two 304 * // and not of Main.One.Two 305 * }; 306 * 307 * static class One { 308 * static class Two { 309 * } 310 * } 311 * } 312 * } 313 * } 314 * </pre> 315 * If the {@link Function} {@code countProvider} hadn't used 316 * {@link FinalClassCheck#getAnonSuperTypeMatchingCount} to 317 * calculate the matching count then the logic would have falsely evaluated 318 * {@code Main.One.Two} to be the super class of the anonymous inner class. 319 * 320 * @param className name of the class 321 * @param countProvider the function to apply to calculate the name matching count 322 * @return {@link Optional} of {@link ClassDesc} object of the nearest class with the same name. 323 * @noinspection CallToStringConcatCanBeReplacedByOperator 324 * @noinspectionreason CallToStringConcatCanBeReplacedByOperator - operator causes 325 * pitest to fail 326 */ 327 private Optional<ClassDesc> getNearestClassWithSameName(String className, 328 ToIntFunction<ClassDesc> countProvider) { 329 final String dotAndClassName = PACKAGE_SEPARATOR.concat(className); 330 final Comparator<ClassDesc> longestMatch = Comparator.comparingInt(countProvider); 331 return innerClasses.entrySet().stream() 332 .filter(entry -> entry.getKey().endsWith(dotAndClassName)) 333 .map(Map.Entry::getValue) 334 .min(longestMatch.reversed().thenComparingInt(ClassDesc::getDepth)); 335 } 336 337 /** 338 * Extract the qualified type declaration name from given type declaration Ast. 339 * 340 * @param typeDeclarationAst type declaration for which qualified name is being fetched 341 * @return qualified name of a type declaration 342 */ 343 private String extractQualifiedTypeName(DetailAST typeDeclarationAst) { 344 final String className = typeDeclarationAst.findFirstToken(TokenTypes.IDENT).getText(); 345 String outerTypeDeclarationQualifiedName = null; 346 if (!typeDeclarations.isEmpty()) { 347 outerTypeDeclarationQualifiedName = typeDeclarations.peek().getQualifiedName(); 348 } 349 return CheckUtil.getQualifiedTypeDeclarationName(packageName, 350 outerTypeDeclarationQualifiedName, 351 className); 352 } 353 354 /** 355 * Get super class name of given class. 356 * 357 * @param classAst class 358 * @return super class name or null if super class is not specified 359 */ 360 private static String getSuperClassName(DetailAST classAst) { 361 String superClassName = null; 362 final DetailAST classExtend = classAst.findFirstToken(TokenTypes.EXTENDS_CLAUSE); 363 if (classExtend != null) { 364 superClassName = CheckUtil.extractQualifiedName(classExtend.getFirstChild()); 365 } 366 return superClassName; 367 } 368 369 /** 370 * Calculates and returns the type declaration matching count when {@code classToBeMatched} is 371 * considered to be super class of an anonymous inner class. 372 * 373 * <p> 374 * Suppose our pattern class is {@code Main.ClassOne} and class to be matched is 375 * {@code Main.ClassOne.ClassTwo.ClassThree} then type declaration name matching count would 376 * be calculated by comparing every character, and updating main counter when we hit "." or 377 * when it is the last character of the pattern class and certain conditions are met. This is 378 * done so that matching count is 13 instead of 5. This is due to the fact that pattern class 379 * can contain anonymous inner class object of a nested class which isn't true in case of 380 * extending classes as you can't extend nested classes. 381 * </p> 382 * 383 * @param patternTypeDeclaration type declaration against which the given type declaration has 384 * to be matched 385 * @param typeDeclarationToBeMatched type declaration to be matched 386 * @return type declaration matching count 387 */ 388 private static int getAnonSuperTypeMatchingCount(String patternTypeDeclaration, 389 String typeDeclarationToBeMatched) { 390 final int typeDeclarationToBeMatchedLength = typeDeclarationToBeMatched.length(); 391 final int minLength = Math 392 .min(typeDeclarationToBeMatchedLength, patternTypeDeclaration.length()); 393 final char packageSeparator = PACKAGE_SEPARATOR.charAt(0); 394 final boolean shouldCountBeUpdatedAtLastCharacter = 395 typeDeclarationToBeMatchedLength > minLength 396 && typeDeclarationToBeMatched.charAt(minLength) == packageSeparator; 397 398 int result = 0; 399 for (int idx = 0; 400 idx < minLength 401 && patternTypeDeclaration.charAt(idx) == typeDeclarationToBeMatched.charAt(idx); 402 idx++) { 403 404 if (idx == minLength - 1 && shouldCountBeUpdatedAtLastCharacter 405 || patternTypeDeclaration.charAt(idx) == packageSeparator) { 406 result = idx; 407 } 408 } 409 return result; 410 } 411 412 /** 413 * Maintains information about the type of declaration. 414 * Any ast node of type {@link TokenTypes#CLASS_DEF} or {@link TokenTypes#INTERFACE_DEF} 415 * or {@link TokenTypes#ENUM_DEF} or {@link TokenTypes#ANNOTATION_DEF} 416 * or {@link TokenTypes#RECORD_DEF} is considered as a type declaration. 417 * It does not maintain information about classes, a subclass called {@link ClassDesc} 418 * does that job. 419 */ 420 private static class TypeDeclarationDescription { 421 422 /** 423 * Complete type declaration name with package name and outer type declaration name. 424 */ 425 private final String qualifiedName; 426 427 /** 428 * Depth of nesting of type declaration. 429 */ 430 private final int depth; 431 432 /** 433 * Type declaration ast node. 434 */ 435 private final DetailAST typeDeclarationAst; 436 437 /** 438 * Create an instance of TypeDeclarationDescription. 439 * 440 * @param qualifiedName Complete type declaration name with package name and outer type 441 * declaration name. 442 * @param depth Depth of nesting of type declaration 443 * @param typeDeclarationAst Type declaration ast node 444 */ 445 private TypeDeclarationDescription(String qualifiedName, int depth, 446 DetailAST typeDeclarationAst) { 447 this.qualifiedName = qualifiedName; 448 this.depth = depth; 449 this.typeDeclarationAst = typeDeclarationAst; 450 } 451 452 /** 453 * Get the complete type declaration name i.e. type declaration name with package name 454 * and outer type declaration name. 455 * 456 * @return qualified class name 457 */ 458 /* package */ String getQualifiedName() { 459 return qualifiedName; 460 } 461 462 /** 463 * Get the depth of type declaration. 464 * 465 * @return the depth of nesting of type declaration 466 */ 467 /* package */ int getDepth() { 468 return depth; 469 } 470 471 /** 472 * Get the type declaration ast node. 473 * 474 * @return ast node of the type declaration 475 */ 476 477 /* package */ DetailAST getTypeDeclarationAst() { 478 return typeDeclarationAst; 479 } 480 } 481 482 /** 483 * Maintains information about the class. 484 */ 485 private static final class ClassDesc extends TypeDeclarationDescription { 486 487 /** Is class declared as final. */ 488 private final boolean declaredAsFinal; 489 490 /** Is class declared as abstract. */ 491 private final boolean declaredAsAbstract; 492 493 /** Is class contains private modifier. */ 494 private final boolean declaredAsPrivate; 495 496 /** Does class have implicit constructor. */ 497 private final boolean hasDeclaredConstructor; 498 499 /** Does class have non-private ctors. */ 500 private boolean withNonPrivateCtor; 501 502 /** Does class have nested subclass. */ 503 private boolean withNestedSubclass; 504 505 /** Whether the class is the super class of an anonymous inner class. */ 506 private boolean superClassOfAnonymousInnerClass; 507 508 /** 509 * Create a new ClassDesc instance. 510 * 511 * @param qualifiedName qualified class name(with package) 512 * @param depth class nesting level 513 * @param classAst classAst node 514 */ 515 private ClassDesc(String qualifiedName, int depth, DetailAST classAst) { 516 super(qualifiedName, depth, classAst); 517 final DetailAST modifiers = classAst.findFirstToken(TokenTypes.MODIFIERS); 518 declaredAsFinal = modifiers.findFirstToken(TokenTypes.FINAL) != null; 519 declaredAsAbstract = modifiers.findFirstToken(TokenTypes.ABSTRACT) != null; 520 declaredAsPrivate = modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null; 521 hasDeclaredConstructor = 522 classAst.getLastChild().findFirstToken(TokenTypes.CTOR_DEF) == null; 523 } 524 525 /** Adds non-private ctor. */ 526 private void registerNonPrivateCtor() { 527 withNonPrivateCtor = true; 528 } 529 530 /** Adds nested subclass. */ 531 private void registerNestedSubclass() { 532 withNestedSubclass = true; 533 } 534 535 /** Adds anonymous inner class. */ 536 private void registerSuperClassOfAnonymousInnerClass() { 537 superClassOfAnonymousInnerClass = true; 538 } 539 540 /** 541 * Does class have non-private ctors. 542 * 543 * @return true if class has non-private ctors 544 */ 545 private boolean isWithNonPrivateCtor() { 546 return withNonPrivateCtor; 547 } 548 549 /** 550 * Does class have nested subclass. 551 * 552 * @return true if class has nested subclass 553 */ 554 private boolean isWithNestedSubclass() { 555 return withNestedSubclass; 556 } 557 558 /** 559 * Is class declared as final. 560 * 561 * @return true if class is declared as final 562 */ 563 private boolean isDeclaredAsFinal() { 564 return declaredAsFinal; 565 } 566 567 /** 568 * Is class declared as abstract. 569 * 570 * @return true if class is declared as final 571 */ 572 private boolean isDeclaredAsAbstract() { 573 return declaredAsAbstract; 574 } 575 576 /** 577 * Whether the class is the super class of an anonymous inner class. 578 * 579 * @return {@code true} if the class is the super class of an anonymous inner class. 580 */ 581 private boolean isSuperClassOfAnonymousInnerClass() { 582 return superClassOfAnonymousInnerClass; 583 } 584 585 /** 586 * Does class have implicit constructor. 587 * 588 * @return true if class have implicit constructor 589 */ 590 private boolean isHasDeclaredConstructor() { 591 return hasDeclaredConstructor; 592 } 593 594 /** 595 * Does class is private. 596 * 597 * @return true if class is private 598 */ 599 private boolean isDeclaredAsPrivate() { 600 return declaredAsPrivate; 601 } 602 } 603 604}