Skip to content

Commit 2a3369f

Browse files
authored
Merge pull request #1020 from forcedotcom/d/W-12696959
FIX (GraphEngine): @W-12696959@: UnusedMethodRule now fully supports constructors.
2 parents 586a507 + 40b78fb commit 2a3369f

File tree

5 files changed

+197
-63
lines changed

5 files changed

+197
-63
lines changed

sfge/src/main/java/com/salesforce/rules/UnusedMethodRule.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,9 @@ private List<Violation> convertMethodsToViolations() {
144144
* for filtering the list of all possible candidates into just the eligible ones.
145145
*/
146146
private boolean methodIsIneligible(MethodVertex vertex) {
147-
// TODO: At this time, only static methods, private instance methods, and private/protected
148-
// constructors are supported. This limit will be loosened over time, and eventually
149-
// removed entirely.
150-
if (!vertex.isStatic()
151-
&& !(vertex.isPrivate() || (vertex.isProtected() && vertex.isConstructor()))) {
147+
// TODO: At this time, only static methods, constructors, and private instance methods are
148+
// supported. This limit will be loosened over time, and eventually removed entirely.
149+
if (!vertex.isStatic() && !vertex.isConstructor() && !vertex.isPrivate()) {
152150
return true;
153151
}
154152

sfge/src/main/java/com/salesforce/rules/unusedmethod/ConstructorMethodCallValidator.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,16 @@ protected boolean internalUsageDetected() {
6868
*/
6969
@Override
7070
protected boolean externalUsageDetected() {
71-
// TODO: IMPLEMENT THIS METHOD
71+
// Get all `new Whatever()` calls for the defining type.
72+
List<NewObjectExpressionVertex> potentialExternalCalls =
73+
ruleStateTracker.getConstructionsOfType(targetMethod.getDefiningType());
74+
// Test each one to see whether its parameters match.
75+
for (NewObjectExpressionVertex potentialExternalCall : potentialExternalCalls) {
76+
if (parametersAreValid(potentialExternalCall)) {
77+
return true;
78+
}
79+
}
80+
// If we're here, it's because we couldn't find any matches.
7281
return false;
7382
}
7483
}

sfge/src/main/java/com/salesforce/rules/unusedmethod/RuleStateTracker.java

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import com.salesforce.graph.vertex.*;
1414
import java.util.*;
1515
import java.util.stream.Collectors;
16+
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
1617
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
1718
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
19+
import org.apache.tinkerpop.gremlin.structure.Vertex;
1820

1921
/**
2022
* A helper class for {@link com.salesforce.rules.UnusedMethodRule}, which tracks various elements
@@ -238,6 +240,48 @@ boolean classInheritsMatchingMethod(String definingType, String signature) {
238240
return MethodUtil.getMethodWithSignature(g, definingType, signature, true).isPresent();
239241
}
240242

243+
/**
244+
* Get all {@link NewObjectExpressionVertex} instances representing instantiations of an object
245+
* whose type is {@code constructedType}.
246+
*/
247+
List<NewObjectExpressionVertex> getConstructionsOfType(String constructedType) {
248+
// Start off with all NewObjectExpressionVertex instances whose declared type references the
249+
// desired type.
250+
List<NewObjectExpressionVertex> results =
251+
SFVertexFactory.loadVertices(
252+
g,
253+
g.V()
254+
.where(
255+
H.has(
256+
NodeType.NEW_OBJECT_EXPRESSION,
257+
Schema.TYPE,
258+
constructedType)));
259+
260+
// Inner types can be referenced by outer/sibling types by just the inner name rather than
261+
// the full name.
262+
// This means we need to do more, but what that "more" is depends on whether we're looking
263+
// at an inner or outer type.
264+
boolean typeIsInner = constructedType.contains(".");
265+
if (typeIsInner) {
266+
// If the type is inner, then we need to add inner-name-only references occurring in
267+
// other classes.
268+
String[] portions = constructedType.split("\\.");
269+
String outerType = portions[0];
270+
String innerType = portions[1];
271+
GraphTraversal<Vertex, Vertex> traversal =
272+
g.V().where(H.has(NodeType.NEW_OBJECT_EXPRESSION, Schema.TYPE, innerType));
273+
List<NewObjectExpressionVertex> additionalResults =
274+
getAliasedInnerTypeUsages(outerType, NodeType.NEW_OBJECT_EXPRESSION, traversal);
275+
results.addAll(additionalResults);
276+
} else {
277+
// If the type isn't an inner type, then it's possible that some of the references we
278+
// found are actually
279+
// inner-name-only references to inner types. So we need to remove those.
280+
results = removeInnerTypeCollisions(results, constructedType);
281+
}
282+
return results;
283+
}
284+
241285
/**
242286
* Get all {@link MethodCallExpressionVertex} instances representing invocations of a method
243287
* named {@code methodName} on a thing called {@code referencedType}.
@@ -262,45 +306,17 @@ List<MethodCallExpressionVertex> getInvocationsOnType(
262306
String[] portions = referencedType.split("\\.");
263307
String outerType = portions[0];
264308
String innerType = portions[1];
309+
GraphTraversal<Vertex, Vertex> traversal =
310+
TraversalUtil.traverseInvocationsOf(
311+
g, new ArrayList<>(), innerType, methodName);
265312
List<MethodCallExpressionVertex> additionalResults =
266-
SFVertexFactory.loadVertices(
267-
g,
268-
TraversalUtil.traverseInvocationsOf(
269-
g, new ArrayList<>(), innerType, methodName)
270-
.where(
271-
__.or(
272-
H.has(
273-
NodeType.METHOD_CALL_EXPRESSION,
274-
Schema.DEFINING_TYPE,
275-
outerType),
276-
H.hasStartingWith(
277-
NodeType.METHOD_CALL_EXPRESSION,
278-
Schema.DEFINING_TYPE,
279-
outerType + "."))));
313+
getAliasedInnerTypeUsages(
314+
outerType, NodeType.METHOD_CALL_EXPRESSION, traversal);
280315
results.addAll(additionalResults);
281316
} else {
282317
// If the type isn't an inner type, then it's possible some of the references we found
283318
// are actually inner-name-only references to inner types. So we need to remove those.
284-
results =
285-
results.stream()
286-
.filter(
287-
v -> {
288-
// Convert the result's definingType into an outer type by
289-
// getting everything before the first period.
290-
String outerType = v.getDefiningType().split("\\.")[0];
291-
// Get any inner classes for the outer type and cast their
292-
// names to lowercase.
293-
Set<String> innerClassNames =
294-
getInnerClasses(outerType).stream()
295-
.map(i -> i.getName().toLowerCase())
296-
.collect(Collectors.toSet());
297-
// If the set lacks an entry for our referenced type, then
298-
// there's no conflicting inner class and we can keep this
299-
// result.
300-
return !innerClassNames.contains(
301-
referencedType.toLowerCase());
302-
})
303-
.collect(Collectors.toList());
319+
results = removeInnerTypeCollisions(results, referencedType);
304320
}
305321
return results;
306322
}
@@ -371,4 +387,59 @@ Optional<BaseSFVertex> getDeclarationOfReferencedValue(MethodCallExpressionVerte
371387
// Optional and let the caller figure out what to do with it.
372388
return Optional.empty();
373389
}
390+
391+
/**
392+
* An inner class's sibling/outer classes can reference it by its inner type alone. This method
393+
* accepts a traversal containing all potential references to the inner name, and filters for
394+
* those that occur in the sibling/outer classes and therefore refer to the inner type.
395+
*
396+
* @param outerType - The name of the outer class.
397+
* @param nodeType - The node type being targeted.
398+
* @param initialTraversal - A traversal containing all nodes of {@code nodeType} that
399+
* potentially reference the inner type.
400+
* @return - A list of {@link BaseSFVertex} instances representing the subset of {@code
401+
* initialTraversal} that are aliased inner type references.
402+
* @param <T> - An extension of {@link BaseSFVertex}.
403+
*/
404+
private <T extends BaseSFVertex> List<T> getAliasedInnerTypeUsages(
405+
String outerType, String nodeType, GraphTraversal<Vertex, Vertex> initialTraversal) {
406+
return SFVertexFactory.loadVertices(
407+
g,
408+
initialTraversal.where(
409+
__.or(
410+
H.has(nodeType, Schema.DEFINING_TYPE, outerType),
411+
H.hasStartingWith(
412+
nodeType, Schema.DEFINING_TYPE, outerType + "."))));
413+
}
414+
415+
/**
416+
* If an inner class shares the name of an outer class, then references to the inner class that
417+
* occur in its outer/sibling classes can resemble references to the outer class. This class
418+
* filters such false positives from the results of a query.
419+
*
420+
* @param unfilteredResults - The results, pre-filtering
421+
* @param referencedType - The outer type with which inner types may collide
422+
* @return - {@code unfilteredResults}, with all colliding references removed
423+
*/
424+
private <T extends BaseSFVertex> List<T> removeInnerTypeCollisions(
425+
List<T> unfilteredResults, String referencedType) {
426+
return unfilteredResults.stream()
427+
.filter(
428+
v -> {
429+
// Convert the result's definingType into an outer type by getting
430+
// everything before the first period.
431+
String outerType = v.getDefiningType().split("\\.")[0];
432+
// Get any inner classes belonging to this outer type in a
433+
// case-insensitive set.
434+
Set<String> innerClassNames =
435+
CollectionUtil.newTreeSetOf(
436+
getInnerClasses(outerType).stream()
437+
.map(UserClassVertex::getName)
438+
.collect(Collectors.toList()));
439+
// If the set lacks an entry for our referenced type, then there's no
440+
// conflicting inner class, and we can keep this result.
441+
return !innerClassNames.contains(referencedType);
442+
})
443+
.collect(Collectors.toList());
444+
}
374445
}

sfge/src/test/java/com/salesforce/rules/unusedmethod/ConstructorsTest.java

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import com.salesforce.rules.Violation;
77
import java.util.function.Consumer;
88
import org.apache.commons.lang3.StringUtils;
9-
import org.junit.jupiter.api.Disabled;
109
import org.junit.jupiter.params.ParameterizedTest;
1110
import org.junit.jupiter.params.provider.CsvSource;
1211
import org.junit.jupiter.params.provider.ValueSource;
@@ -23,13 +22,12 @@ public class ConstructorsTest extends BaseUnusedMethodTest {
2322
* @param declaration - The declaration of the tested constructor
2423
* @param arity - The constructor's arity
2524
*/
26-
// TODO: ENABLE MORE TESTS AS WE ADD MORE FUNCTIONALITY
2725
@CsvSource({
2826
// One test per constructor, per visibility scope.
2927
// EXCEPTION: No test for private 0-arity, since such methods are ineligible.
30-
// "public MyClass(), 0",
28+
"public MyClass(), 0",
3129
"protected MyClass(), 0",
32-
// "public MyClass(boolean b), 1",
30+
"public MyClass(boolean b), 1",
3331
"protected MyClass(boolean b), 1",
3432
"private MyClass(boolean b), 1"
3533
})
@@ -54,12 +52,7 @@ public void constructorWithoutInvocation_expectViolation(String declaration, int
5452
*
5553
* @param visibility - The visibility of the target constructor.
5654
*/
57-
@ValueSource(
58-
strings = {
59-
// "public",
60-
"protected",
61-
"private"
62-
})
55+
@ValueSource(strings = {"public", "protected", "private"})
6356
@ParameterizedTest(name = "{displayName}: {0} constructor")
6457
public void constructorCalledViaThis_expectNoViolation(String visibility) {
6558
// spotless:off
@@ -85,12 +78,7 @@ public void constructorCalledViaThis_expectNoViolation(String visibility) {
8578
* @param visibility - Visibility of the parent constructor
8679
* @param arity - Arity of the parent constructor
8780
*/
88-
@CsvSource({
89-
// "public, 0",
90-
// "public, 1",
91-
"protected, 0",
92-
"protected, 1"
93-
})
81+
@CsvSource({"public, 0", "public, 1", "protected, 0", "protected, 1"})
9482
@ParameterizedTest(name = "{displayName}: {0} constructor with arity {1}")
9583
public void constructorCalledViaSuper_expectNoViolation(String visibility, Integer arity) {
9684
// spotless:off
@@ -149,6 +137,35 @@ public void constructorUncalledByGrandchildSuper_expectViolation(Integer arity)
149137
assertViolations(sourceCodes, assertion);
150138
}
151139

140+
/**
141+
* Simple tests for cases where one class's constructor is called somewhere in another class.
142+
*
143+
* @param arity - The arity of the tested constructor.
144+
*/
145+
@ValueSource(ints = {0, 1})
146+
@ParameterizedTest(name = "{displayName}: Arity {0}")
147+
public void constructorCalledViaNew_expectNoViolation(Integer arity) {
148+
// spotless:off
149+
String[] sourceCodes = new String[]{
150+
// Declare our tested class.
151+
"global class TestedClass {\n"
152+
// Declare a constructor with the expected arity that does nothing in particular.
153+
+ " public TestedClass(" + StringUtils.repeat("boolean b", arity) + ") {}\n"
154+
+ "}",
155+
// Declare a class to invoke the tested constructor.
156+
"global class InvokerClass {\n"
157+
// Annotate the invoker method to avoid tripping the rule.
158+
+ " /* sfge-disable-stack UnusedMethodRule */\n"
159+
+ " public static TestedClass invokeTestedConstructor() {\n"
160+
// Invoke the tested constructor with however many parameters it expects.
161+
+ " return new TestedClass(" + StringUtils.repeat("true", arity) + ");\n"
162+
+ " }\n"
163+
+ "}"
164+
};
165+
// spotless:on
166+
assertNoViolations(sourceCodes, 1);
167+
}
168+
152169
/**
153170
* Tests for cases where an inner class's constructor is called in the outer class.
154171
*
@@ -162,7 +179,6 @@ public void constructorUncalledByGrandchildSuper_expectViolation(Integer arity)
162179
"InnerClass, InnerClass"
163180
})
164181
@ParameterizedTest(name = "{displayName}: Declared as {0}, constructed as new {1}()")
165-
@Disabled
166182
public void innerConstructorUsedByOuter_expectNoViolation(String variable, String constructor) {
167183
// spotless:off
168184
String sourceCode =
@@ -194,7 +210,6 @@ public void innerConstructorUsedByOuter_expectNoViolation(String variable, Strin
194210
"InnerClass, InnerClass"
195211
})
196212
@ParameterizedTest(name = "{displayName}: Declared as {0}, constructed as new {1}()")
197-
@Disabled
198213
public void innerConstructorUsedBySiblingInner_expectNoViolation(
199214
String variable, String constructor) {
200215
// spotless:off
@@ -216,4 +231,47 @@ public void innerConstructorUsedBySiblingInner_expectNoViolation(
216231
// spotless:on
217232
assertNoViolations(sourceCode, 1);
218233
}
234+
235+
/**
236+
* These tests cover variants of a weird edge case: If an inner class and an outer class share
237+
* the same name (e.g., {@code Whatever} and {@code Outer.Whatever}), then calling {@code new
238+
* Whatever()} in the inner class's outer/sibling classes should count as an invocation for the
239+
* inner class, not the outer class.
240+
*
241+
* @param arity - The arity of the tested constructor.
242+
*/
243+
@ValueSource(ints = {0, 1})
244+
@ParameterizedTest(name = "{displayName}: Constructor of arity {0}")
245+
public void externalReferenceSyntaxCollision_expectViolation(Integer arity) {
246+
// spotless:off
247+
String[] sourceCodes = new String[]{
248+
// Declare an outer class with a constructor.
249+
"global class CollidingName {\n"
250+
+ " public CollidingName(" + StringUtils.repeat("boolean b", arity) + ") {}\n"
251+
+ "}",
252+
// Declare another outer class.
253+
"global class OuterClass {\n"
254+
// Give the outer class an inner class whose name collides with the tested outer class.
255+
+ " global class CollidingName {\n"
256+
// Give the inner class a constructor with the same params as the outer class.
257+
+ " public CollidingName(" + StringUtils.repeat("boolean b", arity) + ") {}\n"
258+
+ " }\n"
259+
+ " \n"
260+
// Give the outer class a static method that instantiates the inner class.
261+
+ " /* sfge-disable-stack UnusedMethodRule */\n"
262+
+ " public static boolean callConstructor() {\n"
263+
// IMPORTANT: This is where the constructor is called.
264+
+ " CollidingName obj = new CollidingName(" + StringUtils.repeat("true", arity) +");\n"
265+
+ " return true;\n"
266+
+ " }\n"
267+
+ "}"
268+
};
269+
// spotless:on
270+
assertViolations(
271+
sourceCodes,
272+
v -> {
273+
assertEquals("<init>", v.getSourceVertexName());
274+
assertEquals("CollidingName", v.getSourceDefiningType());
275+
});
276+
}
219277
}

0 commit comments

Comments
 (0)