View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.HashSet;
11  import java.util.Set;
12  
13  /**
14   * Checks if inner classes are properly accessed using their qualified name
15   * with the outer class.
16   * @since 0.18
17   */
18  public final class QualifyInnerClassCheck extends AbstractCheck {
19  
20      /**
21       * Set of all nested classes.
22       */
23      private final Set<String> nested = new HashSet<>();
24  
25      /**
26       * Whether we already visited root class of the .java file.
27       */
28      private boolean root;
29  
30      @Override
31      public int[] getDefaultTokens() {
32          return new int[]{
33              TokenTypes.CLASS_DEF,
34              TokenTypes.ENUM_DEF,
35              TokenTypes.INTERFACE_DEF,
36              TokenTypes.LITERAL_NEW,
37          };
38      }
39  
40      @Override
41      public int[] getAcceptableTokens() {
42          return this.getDefaultTokens();
43      }
44  
45      @Override
46      public int[] getRequiredTokens() {
47          return this.getDefaultTokens();
48      }
49  
50      @Override
51      public void beginTree(final DetailAST ast) {
52          this.nested.clear();
53          this.root = false;
54      }
55  
56      @Override
57      public void visitToken(final DetailAST ast) {
58          if (ast.getType() == TokenTypes.CLASS_DEF
59              || ast.getType() == TokenTypes.ENUM_DEF
60              || ast.getType() == TokenTypes.INTERFACE_DEF) {
61              this.scanForNestedClassesIfNecessary(ast);
62          }
63          if (ast.getType() == TokenTypes.LITERAL_NEW) {
64              this.visitNewExpression(ast);
65          }
66      }
67  
68      /**
69       * Checks if class to be instantiated is nested and unqualified.
70       * @param expr EXPR LITERAL_NEW node that needs to be checked
71       */
72      private void visitNewExpression(final DetailAST expr) {
73          final DetailAST child = expr.getFirstChild();
74          if (child != null
75              && child.getType() == TokenTypes.IDENT
76              && this.nested.contains(child.getText())) {
77              this.log(child, "Static inner class should be qualified with outer class");
78          }
79      }
80  
81      /**
82       * If provided class is top-level, scans it for nested classes.
83       * @param node Class-like AST node
84       */
85      private void scanForNestedClassesIfNecessary(final DetailAST node) {
86          if (!this.root) {
87              this.root = true;
88              this.scanClass(node);
89          }
90      }
91  
92      /**
93       * Scans class for all nested sub-classes.
94       * @param node Class-like AST node that needs to be checked
95       */
96      private void scanClass(final DetailAST node) {
97          final DetailAST content = node.findFirstToken(TokenTypes.OBJBLOCK);
98          if (content == null) {
99              return;
100         }
101         for (
102             DetailAST child = content.getFirstChild();
103             child != null;
104             child = child.getNextSibling()
105         ) {
106             if (child.getType() == TokenTypes.CLASS_DEF
107                 || child.getType() == TokenTypes.ENUM_DEF
108                 || child.getType() == TokenTypes.INTERFACE_DEF) {
109                 this.nested.add(getClassName(child));
110                 this.scanClass(child);
111             }
112         }
113     }
114 
115     /**
116      * Returns class name.
117      * @param clazz Class-like AST node
118      * @return Class name
119      */
120     private static String getClassName(final DetailAST clazz) {
121         for (
122             DetailAST child = clazz.getFirstChild();
123             child != null;
124             child = child.getNextSibling()
125         ) {
126             if (child.getType() == TokenTypes.IDENT) {
127                 return child.getText();
128             }
129         }
130         throw new IllegalStateException("Unable to find class name");
131     }
132 }