View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2025 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   *
17   * @since 0.18
18   * @todo #738:30min Static inner classes should be qualified with outer class
19   *  Implement QualifyInnerClassCheck so it follows what defined in
20   *  QualifyInnerClassCheck test and add this check to checks.xml and CheckTest.
21   */
22  public final class QualifyInnerClassCheck extends AbstractCheck {
23      // FIXME: do we need to clear these fields in the end?
24      /**
25       * Set of all nested classes.
26       */
27      private final Set<String> nested = new HashSet<>();
28  
29      /**
30       * Whether we already visited root class of the .java file.
31       */
32      private boolean root;
33  
34      @Override
35      public int[] getDefaultTokens() {
36          return new int[]{
37              TokenTypes.CLASS_DEF,
38              TokenTypes.ENUM_DEF,
39              TokenTypes.INTERFACE_DEF,
40              TokenTypes.LITERAL_NEW,
41          };
42      }
43  
44      @Override
45      public int[] getAcceptableTokens() {
46          return this.getDefaultTokens();
47      }
48  
49      @Override
50      public int[] getRequiredTokens() {
51          return this.getDefaultTokens();
52      }
53  
54      @Override
55      public void visitToken(final DetailAST ast) {
56          if (ast.getType() == TokenTypes.CLASS_DEF
57              || ast.getType() == TokenTypes.ENUM_DEF
58              || ast.getType() == TokenTypes.INTERFACE_DEF) {
59              this.scanForNestedClassesIfNecessary(ast);
60          }
61          if (ast.getType() == TokenTypes.LITERAL_NEW) {
62              this.visitNewExpression(ast);
63          }
64      }
65  
66      /**
67       * Checks if class to be instantiated is nested and unqualified.
68       *
69       * FIXME: currently only simple paths are detected
70       * (i.e. `new Foo`, but not `new Foo.Bar`)
71       * @param expr EXPR LITERAL_NEW node that needs to be checked
72       */
73      private void visitNewExpression(final DetailAST expr) {
74          final DetailAST child = expr.getFirstChild();
75          if (child.getType() == TokenTypes.IDENT) {
76              if (this.nested.contains(child.getText())) {
77                  this.log(child, "Static inner class should be qualified with outer class");
78              }
79          } else if (child.getType() != TokenTypes.DOT) {
80              final String message = String.format("unsupported input %d", child.getType());
81              throw new IllegalStateException(message);
82          }
83      }
84  
85      /**
86       * If provided class is top-level, scans it for nested classes.
87       * FIXME: currently it assumes there can be only one top-level class
88       *
89       * @param node Class-like AST node
90       */
91      private void scanForNestedClassesIfNecessary(final DetailAST node) {
92          if (!this.root) {
93              this.root = true;
94              this.scanClass(node);
95          }
96      }
97  
98      /**
99       * Scans class for all nested sub-classes.
100      *
101      * FIXME: checkstyle discourages manual traversing of AST,
102      * but exactly this is happening here.
103      * @param node Class-like AST node that needs to be checked
104      */
105     private void scanClass(final DetailAST node) {
106         this.nested.add(getClassName(node));
107         final DetailAST content = node.findFirstToken(TokenTypes.OBJBLOCK);
108         if (content == null) {
109             return;
110         }
111         for (
112             DetailAST child = content.getFirstChild();
113             child != null;
114             child  = child.getNextSibling()
115         ) {
116             if (child.getType() == TokenTypes.CLASS_DEF
117                 || child.getType() == TokenTypes.ENUM_DEF
118                 || child.getType() == TokenTypes.INTERFACE_DEF) {
119                 this.scanClass(child);
120             }
121         }
122     }
123 
124     /**
125      * Returns class name.
126      * @param clazz Class-like AST node
127      * @return Class name
128      */
129     private static String getClassName(final DetailAST clazz) {
130         for (
131             DetailAST child = clazz.getFirstChild();
132             child != null;
133             child = child.getNextSibling()
134         ) {
135             if (child.getType() == TokenTypes.IDENT) {
136                 return child.getText();
137             }
138         }
139         throw new IllegalStateException("unexpected input: can not find class name");
140     }
141 }