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.ArrayDeque;
11  import java.util.Deque;
12  import java.util.HashSet;
13  import java.util.Set;
14  
15  /**
16   * Checks that static members are not accessed through an instance reference.
17   *
18   * <p>A static method or field must be accessed through the declaring class
19   * (for example {@code MyClass.staticMethod()}), not through
20   * {@code this.staticMethod()}. Accessing a static member through an instance
21   * reference is misleading because it looks like a polymorphic call while the
22   * dispatch is actually resolved statically at compile time.
23   *
24   * <p>This check scans every class, enum and interface in the file, collects
25   * the names of all declared static methods and fields, and reports every
26   * {@code this.name} expression where {@code name} is in that set.
27   *
28   * @since 0.24
29   */
30  public final class StaticAccessViaInstanceCheck extends AbstractCheck {
31  
32      /**
33       * Stack of static member name sets, one per enclosing class-like scope.
34       */
35      private final Deque<Set<String>> scopes = new ArrayDeque<>();
36  
37      @Override
38      public int[] getDefaultTokens() {
39          return new int[] {
40              TokenTypes.CLASS_DEF,
41              TokenTypes.ENUM_DEF,
42              TokenTypes.INTERFACE_DEF,
43              TokenTypes.DOT,
44          };
45      }
46  
47      @Override
48      public int[] getAcceptableTokens() {
49          return this.getDefaultTokens();
50      }
51  
52      @Override
53      public int[] getRequiredTokens() {
54          return this.getDefaultTokens();
55      }
56  
57      @Override
58      public void beginTree(final DetailAST root) {
59          this.scopes.clear();
60      }
61  
62      @Override
63      public void visitToken(final DetailAST ast) {
64          final int type = ast.getType();
65          if (type == TokenTypes.DOT) {
66              this.checkDot(ast);
67          } else {
68              this.scopes.push(collectStatic(ast));
69          }
70      }
71  
72      @Override
73      public void leaveToken(final DetailAST ast) {
74          if (ast.getType() != TokenTypes.DOT) {
75              this.scopes.pop();
76          }
77      }
78  
79      /**
80       * Reports the dot expression if it accesses a static member via
81       * {@code this}.
82       * @param dot DOT node
83       */
84      private void checkDot(final DetailAST dot) {
85          final DetailAST left = dot.getFirstChild();
86          if (!this.scopes.isEmpty()
87              && left != null
88              && left.getType() == TokenTypes.LITERAL_THIS
89              && isStaticIdent(left.getNextSibling(), this.scopes.peek())) {
90              this.log(
91                  dot,
92                  "Static member must be accessed via class name, not via instance"
93              );
94          }
95      }
96  
97      /**
98       * Tells whether the node is an IDENT whose text is in the set of known
99       * static member names.
100      * @param node Node to check
101      * @param names Known static names
102      * @return True if the node matches
103      */
104     private static boolean isStaticIdent(
105         final DetailAST node, final Set<String> names) {
106         return node != null
107             && node.getType() == TokenTypes.IDENT
108             && names.contains(node.getText());
109     }
110 
111     /**
112      * Collects the names of all static methods and fields directly declared
113      * in the given class-like node.
114      * @param clazz CLASS_DEF, ENUM_DEF or INTERFACE_DEF node
115      * @return Set of static member names
116      */
117     private static Set<String> collectStatic(final DetailAST clazz) {
118         final Set<String> names = new HashSet<>(0);
119         final DetailAST body = clazz.findFirstToken(TokenTypes.OBJBLOCK);
120         for (DetailAST child = body.getFirstChild();
121             child != null; child = child.getNextSibling()) {
122             if (isStaticMember(child)) {
123                 names.add(child.findFirstToken(TokenTypes.IDENT).getText());
124             }
125         }
126         return names;
127     }
128 
129     /**
130      * Tells whether the node is a static method or a static field
131      * declaration.
132      * @param node Node to check
133      * @return True if it is a static method or field
134      */
135     private static boolean isStaticMember(final DetailAST node) {
136         final int type = node.getType();
137         final boolean member = type == TokenTypes.METHOD_DEF
138             || type == TokenTypes.VARIABLE_DEF;
139         boolean result = false;
140         if (member) {
141             final DetailAST modifiers =
142                 node.findFirstToken(TokenTypes.MODIFIERS);
143             result = modifiers != null
144                 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
145         }
146         return result;
147     }
148 }