View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.pmd.rules;
6   
7   import java.util.Map;
8   import java.util.Set;
9   import java.util.stream.IntStream;
10  import net.sourceforge.pmd.lang.ast.Node;
11  import net.sourceforge.pmd.lang.java.ast.ASTAmbiguousName;
12  import net.sourceforge.pmd.lang.java.ast.ASTBlock;
13  import net.sourceforge.pmd.lang.java.ast.ASTClassType;
14  import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
15  import net.sourceforge.pmd.lang.java.ast.ASTExpression;
16  import net.sourceforge.pmd.lang.java.ast.ASTFieldAccess;
17  import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
18  import net.sourceforge.pmd.lang.java.ast.ASTTypeExpression;
19  import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess;
20  import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
21  
22  /**
23   * Heuristics that veto an {@link UnnecessaryLocalRule} report when the local
24   * is semantically required (closure capture, clock snapshot, or capture
25   * before a destructive call). See issue #1607.
26   *
27   * @since 0.27.7
28   */
29  final class UnnecessaryLocalSkips {
30  
31      /**
32       * Simple class names from {@code java.time} whose {@code now()} factory
33       * returns a fresh snapshot of the system clock.
34       */
35      private static final Set<String> TIME_TYPES = Set.of(
36          "Instant", "LocalDate", "LocalDateTime", "LocalTime",
37          "ZonedDateTime", "OffsetDateTime", "OffsetTime",
38          "Year", "YearMonth", "MonthDay", "Clock"
39      );
40  
41      /**
42       * Other (non-{@code now()}) calls that return fresh state on each
43       * invocation, keyed by the qualifier's simple name.
44       */
45      private static final Map<String, Set<String>> FRESH_STATE_CALLS = Map.of(
46          "System", Set.of("currentTimeMillis", "nanoTime"),
47          "UUID", Set.of("randomUUID"),
48          "Math", Set.of("random")
49      );
50  
51      private UnnecessaryLocalSkips() {
52      }
53  
54      /**
55       * The single use of the local is reachable only by crossing a lambda or
56       * anonymous-class boundary, so the local exists to carry the value into
57       * a different exception scope.
58       *
59       * @param block The block enclosing the declaration
60       * @param name The variable name
61       * @param crossings Number of usages found when crossing find boundaries
62       * @return True when at least one usage sits behind a find boundary
63       */
64      static boolean acrossBoundary(
65          final ASTBlock block,
66          final String name,
67          final int crossings
68      ) {
69          return crossings != block
70              .descendants(ASTVariableAccess.class)
71              .filter(ref -> name.equals(ref.getName()))
72              .count();
73      }
74  
75      /**
76       * The initialiser snapshots mutable global state - a clock, a randomness
77       * source, or {@code new Date()} - so inlining would change <em>when</em>
78       * the value is taken.
79       *
80       * @param init The initialiser expression
81       * @return True if the initialiser captures fresh state
82       */
83      static boolean freshState(final ASTExpression init) {
84          final boolean result;
85          if (init instanceof ASTMethodCall call) {
86              result = UnnecessaryLocalSkips.freshStateCall(call);
87          } else if (init instanceof ASTConstructorCall call) {
88              final ASTClassType type = call.getTypeNode();
89              result = type != null && "Date".equals(type.getSimpleName());
90          } else {
91              result = false;
92          }
93          return result;
94      }
95  
96      /**
97       * A statement intervenes between the local's declaration and its single
98       * use, so the local is pinning evaluation order and must stay. When the
99       * initialiser is a method call or a constructor call, any intervening
100      * statement is enough - the call's result cannot be read out of order
101      * without reordering side effects. When the initialiser is a field access
102      * (e.g. {@code System.out}), only an intervening call on the <em>same</em>
103      * qualifier (e.g. {@code System.setOut(...)}) counts, since such a call may
104      * reassign the field before it is read; an unrelated statement leaves the
105      * read inlinable. See issues #1607, #1699, #1700 and #1710.
106      *
107      * @param variable The variable declarator
108      * @param use The single use of the variable
109      * @return True if a statement intervenes between init and its use
110      */
111     static boolean interveningCall(
112         final ASTVariableDeclarator variable,
113         final ASTVariableAccess use
114     ) {
115         final ASTExpression init = variable.getInitializer();
116         final boolean found;
117         if (init instanceof ASTMethodCall
118             || init instanceof ASTConstructorCall) {
119             found = UnnecessaryLocalSkips.intervenes(variable, use);
120         } else if (init instanceof ASTFieldAccess access) {
121             found = UnnecessaryLocalSkips.callsQualifier(variable, use, access);
122         } else {
123             found = false;
124         }
125         return found;
126     }
127 
128     private static boolean intervenes(
129         final ASTVariableDeclarator variable,
130         final ASTVariableAccess use
131     ) {
132         boolean found = false;
133         final ASTBlock block = variable.ancestors(ASTBlock.class).first();
134         if (block != null) {
135             final Node decl = UnnecessaryLocalSkips.childOf(block, variable);
136             final Node consumer = UnnecessaryLocalSkips.childOf(block, use);
137             found = decl != null && consumer != null
138                 && consumer.getIndexInParent() > decl.getIndexInParent() + 1;
139         }
140         return found;
141     }
142 
143     private static boolean callsQualifier(
144         final ASTVariableDeclarator variable,
145         final ASTVariableAccess use,
146         final ASTFieldAccess access
147     ) {
148         boolean found = false;
149         final ASTBlock block = variable.ancestors(ASTBlock.class).first();
150         final String qualifier = UnnecessaryLocalSkips.qualifierImage(
151             access.getQualifier()
152         );
153         if (block != null && qualifier != null) {
154             final Node decl = UnnecessaryLocalSkips.childOf(block, variable);
155             final Node consumer = UnnecessaryLocalSkips.childOf(block, use);
156             if (decl != null && consumer != null) {
157                 found = IntStream.range(
158                     decl.getIndexInParent() + 1, consumer.getIndexInParent()
159                 ).anyMatch(
160                     idx -> UnnecessaryLocalSkips.calls(
161                         block.getChild(idx), qualifier
162                     )
163                 );
164             }
165         }
166         return found;
167     }
168 
169     private static boolean calls(final Node stmt, final String qualifier) {
170         return stmt.descendants(ASTMethodCall.class).toStream().anyMatch(
171             call -> qualifier.equals(
172                 UnnecessaryLocalSkips.qualifierImage(call.getQualifier())
173             )
174         );
175     }
176 
177     private static Node childOf(final ASTBlock block, final Node inner) {
178         Node current = inner;
179         while (current != null && !block.equals(current.getParent())) {
180             current = current.getParent();
181         }
182         return current;
183     }
184 
185     private static boolean freshStateCall(final ASTMethodCall call) {
186         final String name = call.getMethodName();
187         final String qualifier = UnnecessaryLocalSkips.qualifierImage(
188             call.getQualifier()
189         );
190         boolean fresh = false;
191         if (qualifier != null) {
192             final boolean known = UnnecessaryLocalSkips.FRESH_STATE_CALLS
193                 .getOrDefault(qualifier, Set.of()).contains(name);
194             final boolean clock = "now".equals(name)
195                 && UnnecessaryLocalSkips.TIME_TYPES.contains(qualifier);
196             fresh = known || clock;
197         }
198         return fresh;
199     }
200 
201     private static String qualifierImage(final ASTExpression expr) {
202         final String result;
203         if (expr instanceof ASTAmbiguousName name) {
204             result = name.getName();
205         } else if (expr instanceof ASTVariableAccess access) {
206             result = access.getName();
207         } else if (expr instanceof ASTTypeExpression type
208             && type.getTypeNode() instanceof ASTClassType klass) {
209             result = klass.getSimpleName();
210         } else {
211             result = null;
212         }
213         return result;
214     }
215 }