Skip to content

Commit ce8880c

Browse files
authored
Merge pull request #1085 from forcedotcom/rm/fixNestedLoopMMSLR
FIX (GraphEngine): @W-13363157@: Handles loop exclusions more effectively
2 parents a20b2b6 + 32ee53e commit ce8880c

File tree

10 files changed

+234
-60
lines changed

10 files changed

+234
-60
lines changed

sfge/src/main/java/com/salesforce/collections/CollectionUtil.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
import com.google.common.collect.ImmutableList;
44
import com.salesforce.exception.UnexpectedException;
55
import com.salesforce.graph.ops.TypeableUtil;
6-
import java.util.Arrays;
7-
import java.util.Collection;
8-
import java.util.List;
9-
import java.util.Map;
10-
import java.util.TreeMap;
11-
import java.util.TreeSet;
6+
import java.util.*;
127
import java.util.concurrent.ConcurrentMap;
138
import java.util.concurrent.ConcurrentSkipListMap;
149
import java.util.function.BiFunction;
@@ -186,5 +181,20 @@ public static <T, U> List<U> newImmutableListOf(
186181
.collect(Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf));
187182
}
188183

184+
/**
185+
* Peek into the stack to get the last element. Converts value into an Optional instead of
186+
* throwing an {@link EmptyStackException} when stack is empty.
187+
*
188+
* @param stack to look at
189+
* @param <T> Stack's generic
190+
* @return last element added to the stack
191+
*/
192+
public static <T> Optional<T> peek(Stack<T> stack) {
193+
if (stack.isEmpty()) {
194+
return Optional.empty();
195+
}
196+
return Optional.of(stack.peek());
197+
}
198+
189199
private CollectionUtil() {}
190200
}

sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.salesforce.collections.CollectionUtil;
66
import com.salesforce.exception.ProgrammingException;
77
import com.salesforce.graph.Schema;
8+
import com.salesforce.graph.vertex.MethodCallExpressionVertex;
89
import java.util.HashMap;
910
import java.util.List;
1011
import java.util.Map;
@@ -68,7 +69,9 @@
6869
public final class StaticBlockUtil {
6970
private static final Logger LOGGER = LogManager.getLogger(StaticBlockUtil.class);
7071

71-
static final String SYNTHETIC_STATIC_BLOCK_METHOD_NAME = "SyntheticStaticBlock_%d";
72+
private static final String SYNTHETIC_STATIC_BLOCK_METHOD_BASE = "SyntheticStaticBlock__";
73+
static final String SYNTHETIC_STATIC_BLOCK_METHOD_NAME =
74+
SYNTHETIC_STATIC_BLOCK_METHOD_BASE + "%d";
7275
static final String STATIC_BLOCK_INVOKER_METHOD = "StaticBlockInvoker";
7376

7477
private StaticBlockUtil() {}
@@ -374,4 +377,9 @@ private static void verifyType(Vertex vertex, String expectedType) {
374377
throw new ProgrammingException("Incorrect vertex type: " + vertex.label());
375378
}
376379
}
380+
381+
public static boolean isStaticBlockMethodCall(MethodCallExpressionVertex vertex) {
382+
final String methodName = vertex.getMethodName();
383+
return methodName.startsWith(SYNTHETIC_STATIC_BLOCK_METHOD_BASE);
384+
}
377385
}

sfge/src/main/java/com/salesforce/rules/ops/boundary/BoundaryDetector.java

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.salesforce.rules.ops.boundary;
22

3+
import com.salesforce.collections.CollectionUtil;
34
import com.salesforce.exception.ProgrammingException;
45
import com.salesforce.exception.UnexpectedException;
5-
import com.salesforce.rules.ops.visitor.LoopDetectionVisitor;
6+
import java.util.List;
67
import java.util.Optional;
78
import java.util.Stack;
9+
import java.util.stream.Collectors;
810
import org.apache.logging.log4j.LogManager;
911
import org.apache.logging.log4j.Logger;
1012

@@ -16,26 +18,15 @@
1618
* @param <T> Boundary type associated with the implementation.
1719
*/
1820
public abstract class BoundaryDetector<T extends Boundary<R>, R> {
19-
private static final Logger LOGGER = LogManager.getLogger(LoopDetectionVisitor.class);
20-
private final Stack<T> boundaries;
21+
private static final Logger LOGGER = LogManager.getLogger(BoundaryDetector.class);
22+
protected final Stack<T> boundaries;
2123

2224
protected BoundaryDetector() {
2325
this.boundaries = new Stack<>();
2426
}
2527

26-
private void push(T boundary) {
27-
this.boundaries.push(boundary);
28-
}
29-
30-
private T pop() {
31-
return this.boundaries.pop();
32-
}
33-
3428
public Optional<T> peek() {
35-
if (boundaries.isEmpty()) {
36-
return Optional.empty();
37-
}
38-
return Optional.of(this.boundaries.peek());
29+
return CollectionUtil.peek(boundaries);
3930
}
4031

4132
/**
@@ -47,7 +38,7 @@ public void pushBoundary(T boundary) {
4738
if (LOGGER.isDebugEnabled()) {
4839
LOGGER.debug("Entering loop boundary with boundaryItem=" + boundary.getBoundaryItem());
4940
}
50-
push(boundary);
41+
this.boundaries.push(boundary);
5142
}
5243

5344
/**
@@ -78,6 +69,13 @@ public void popBoundary(R boundaryItem) {
7869
}
7970

8071
// Remove the boundary
81-
pop();
72+
this.boundaries.pop();
73+
}
74+
75+
/**
76+
* @return items in the boundary stack. Items are ordered TODO ?
77+
*/
78+
public List<T> getBoundaryItems() {
79+
return boundaries.stream().collect(Collectors.toList());
8280
}
8381
}

sfge/src/main/java/com/salesforce/rules/ops/boundary/LoopBoundaryDetector.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,47 @@
11
package com.salesforce.rules.ops.boundary;
22

3+
import com.salesforce.collections.CollectionUtil;
34
import com.salesforce.graph.vertex.SFVertex;
45
import java.util.Optional;
6+
import java.util.Stack;
57

68
/** Implementation of boundary detection for loops. */
79
public class LoopBoundaryDetector extends BoundaryDetector<LoopBoundary, SFVertex> {
10+
11+
/**
12+
* Check if the visitor is currently inside a loop. Takes exclusion boundary into account.
13+
*
14+
* @return Optional of the last effective boundary vertex. If none exist, return empty to
15+
* indicate that exclusion is effective.
16+
*/
817
public Optional<? extends SFVertex> isInsideLoop() {
9-
final Optional<LoopBoundary> loopBoundaryOpt = peek();
18+
// TODO: This can be done without the copy stack as well, but it adds more complexity
19+
// than I'm willing to handle at the moment. Add more tests if revisiting.
20+
final Stack<LoopBoundary> copyStack = new Stack<>();
21+
copyStack.addAll(super.boundaries);
22+
return this._isInsideLoop(copyStack);
23+
}
24+
25+
private Optional<? extends SFVertex> _isInsideLoop(
26+
Stack<? extends LoopBoundary> boundaryStack) {
27+
final Optional<? extends LoopBoundary> loopBoundaryOpt = CollectionUtil.peek(boundaryStack);
1028
if (loopBoundaryOpt.isPresent()) {
29+
final LoopBoundary loopBoundary = loopBoundaryOpt.get();
1130
// Check if we are inside a loop exclusion.
12-
if (loopBoundaryOpt.get() instanceof LoopExclusionBoundary) {
13-
// This is exclusion zone. We consider this not a loop.
31+
32+
if (loopBoundary instanceof PermanentLoopExclusionBoundary) {
33+
// All the previous boundaries don't matter
1434
return Optional.empty();
35+
} else if (loopBoundary instanceof OverridableLoopExclusionBoundary) {
36+
// Pop out the OverridableLoopExclusionBoundary.
37+
boundaryStack.pop();
38+
// Pop out the corresponding ForEach Loop Boundary.
39+
boundaryStack.pop();
40+
return _isInsideLoop(boundaryStack);
41+
} else {
42+
// Inside a loop.
43+
return Optional.of(loopBoundary.getBoundaryItem());
1544
}
16-
17-
// Inside a loop.
18-
return Optional.of(loopBoundaryOpt.get().getBoundaryItem());
1945
}
2046

2147
// Not inside a loop

sfge/src/main/java/com/salesforce/rules/ops/boundary/LoopExclusionBoundary.java

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.salesforce.rules.ops.boundary;
2+
3+
import com.salesforce.graph.vertex.SFVertex;
4+
5+
/**
6+
* Indicates parts of the loop that get run only once. However, a nested loop scenario can still
7+
* execute this portion multiple times. For example:
8+
*
9+
* <ul>
10+
* <li>getValues() method in: <code>for (String s: getValues())</code>
11+
* </ul>
12+
*/
13+
public class OverridableLoopExclusionBoundary extends LoopBoundary {
14+
public OverridableLoopExclusionBoundary(SFVertex loopVertex) {
15+
super(loopVertex);
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.salesforce.rules.ops.boundary;
2+
3+
import com.salesforce.graph.vertex.SFVertex;
4+
5+
/**
6+
* Indicates portions of code that will be executed only once no matter how many loops this is
7+
* nested under.
8+
*
9+
* <p>For example: Static initialization of a class
10+
*/
11+
public class PermanentLoopExclusionBoundary extends LoopBoundary {
12+
public PermanentLoopExclusionBoundary(SFVertex loopVertex) {
13+
super(loopVertex);
14+
}
15+
}

sfge/src/main/java/com/salesforce/rules/ops/visitor/LoopDetectionVisitor.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.salesforce.rules.ops.visitor;
22

3+
import com.salesforce.graph.build.StaticBlockUtil;
34
import com.salesforce.graph.symbols.SymbolProvider;
45
import com.salesforce.graph.vertex.*;
56
import com.salesforce.graph.visitor.DefaultNoOpPathVertexVisitor;
67
import com.salesforce.rules.ops.boundary.LoopBoundary;
78
import com.salesforce.rules.ops.boundary.LoopBoundaryDetector;
8-
import com.salesforce.rules.ops.boundary.LoopExclusionBoundary;
9+
import com.salesforce.rules.ops.boundary.OverridableLoopExclusionBoundary;
10+
import com.salesforce.rules.ops.boundary.PermanentLoopExclusionBoundary;
911
import java.util.Optional;
1012

1113
/** Visitor that gets notified when a loop vertex is invoked in the path. */
@@ -78,28 +80,49 @@ public boolean visit(MethodCallExpressionVertex vertex, SymbolProvider symbols)
7880
final Optional<LoopBoundary> currentLoopBoundaryOpt = loopBoundaryDetector.peek();
7981
if (currentLoopBoundaryOpt.isPresent()) {
8082
final SFVertex loopBoundaryItem = currentLoopBoundaryOpt.get().getBoundaryItem();
83+
84+
createPermanentLoopExclusionIfApplicable(vertex, loopBoundaryItem);
85+
86+
// If permanent loop exclusion was added, overridable loop exclusion will never get
87+
// added.
88+
// Leaving the conditional check here to be more obvious.
8189
if (loopBoundaryItem instanceof ForEachStatementVertex) {
82-
// We are within a ForEach statement.
83-
// Check if the method calls parent is the same as this ForEach statement.
84-
// If they are the same, this method would get invoked only once.
85-
BaseSFVertex parentVertex = vertex.getParent();
86-
if (parentVertex instanceof ForEachStatementVertex
87-
&& parentVertex.equals(loopBoundaryItem)) {
88-
// This method would get invoked only once.
89-
loopBoundaryDetector.pushBoundary(new LoopExclusionBoundary(vertex));
90-
}
90+
createOverridableLoopExclusion(vertex, loopBoundaryItem);
9191
}
9292
}
9393
return true;
9494
}
9595

96+
private void createPermanentLoopExclusionIfApplicable(
97+
MethodCallExpressionVertex vertex, SFVertex loopBoundaryItem) {
98+
if (StaticBlockUtil.isStaticBlockMethodCall(vertex)) {
99+
// All nested loops before this don't get counted as a loop context.
100+
loopBoundaryDetector.pushBoundary(new PermanentLoopExclusionBoundary(vertex));
101+
}
102+
}
103+
104+
private void createOverridableLoopExclusion(
105+
MethodCallExpressionVertex vertex, SFVertex loopBoundaryItem) {
106+
// We are within a ForEach statement.
107+
// Check if the method calls parent is the same as this ForEach statement.
108+
// If they are the same, this method would get invoked only once.
109+
BaseSFVertex parentVertex = vertex.getParent();
110+
if (parentVertex instanceof ForEachStatementVertex
111+
&& parentVertex.equals(loopBoundaryItem)) {
112+
// This method would get invoked only once within the immediate surrounding loop
113+
// context
114+
loopBoundaryDetector.pushBoundary(new OverridableLoopExclusionBoundary(vertex));
115+
}
116+
}
117+
96118
@Override
97119
public void afterVisit(MethodCallExpressionVertex vertex, SymbolProvider symbols) {
98120
// If within a method call loop exclusion, pop boundary here.
99121
final Optional<LoopBoundary> currentLoopBoundaryOpt = loopBoundaryDetector.peek();
100122
if (currentLoopBoundaryOpt.isPresent()) {
101123
final LoopBoundary loopBoundary = currentLoopBoundaryOpt.get();
102-
if (loopBoundary instanceof LoopExclusionBoundary) {
124+
if (loopBoundary instanceof OverridableLoopExclusionBoundary
125+
|| loopBoundary instanceof PermanentLoopExclusionBoundary) {
103126
// We are in exclusion zone. Check if the method call on the loop exclusion boundary
104127
// is the same as the current method call.
105128
if (vertex.equals(loopBoundary.getBoundaryItem())) {

sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleTest.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,7 @@ public void testForLoopConditionalOnClassInstance() {
283283
ASTConstants.NodeType.FOR_EACH_STATEMENT));
284284
}
285285

286-
@Test // TODO: Check if this is a false positive. Static block should get invoked only once
287-
@Disabled
286+
@Test
288287
public void testLoopFromStaticBlock() {
289288
// spotless:off
290289
String[] sourceCode = {
@@ -309,6 +308,39 @@ public void testLoopFromStaticBlock() {
309308
assertNoViolation(RULE, sourceCode);
310309
}
311310

311+
@Test
312+
public void testLoopFromStaticBlock_nestedLoop() {
313+
// spotless:off
314+
String[] sourceCode = {
315+
"public class MyClass {\n"
316+
+ " void foo(String[] objectNames) {\n"
317+
+ " for (Integer i = 0; i < objectNames.size; i++) {\n"
318+
+ " FirstClass fc = new FirstClass();\n"
319+
+ " fc.redirect(i);\n"
320+
+ " }\n"
321+
+ " }\n"
322+
+ "}\n",
323+
"public class FirstClass {\n" +
324+
" void redirect(Integer lim) {\n" +
325+
" for (Integer i = 0; i < lim; i++) {\n" +
326+
" AnotherClass.doNothing();\n" +
327+
" }\n" +
328+
" }\n" +
329+
"}\n",
330+
"public class AnotherClass {\n"
331+
+ " static {\n"
332+
+ " Schema.getGlobalDescribe();\n"
333+
+ " }\n"
334+
+ " static void doNothing() {\n"
335+
+ " // do nothing \n"
336+
+ " }\n"
337+
+ "}\n"
338+
};
339+
// spotless:on
340+
341+
assertNoViolation(RULE, sourceCode);
342+
}
343+
312344
@CsvSource({
313345
"ForEachStatement, for (String s : myList)",
314346
"ForLoopStatement, for (Integer i; i < s.size; s++)",

0 commit comments

Comments
 (0)