diff --git a/sfge/src/main/java/com/salesforce/Main.java b/sfge/src/main/java/com/salesforce/Main.java index d7f4e0c31..a4ea7e1f7 100644 --- a/sfge/src/main/java/com/salesforce/Main.java +++ b/sfge/src/main/java/com/salesforce/Main.java @@ -4,6 +4,7 @@ import com.salesforce.cli.OutputFormatter; import com.salesforce.exception.SfgeException; import com.salesforce.exception.SfgeRuntimeException; +import com.salesforce.exception.UnexpectedException; import com.salesforce.graph.ops.GraphUtil; import com.salesforce.messaging.CliMessager; import com.salesforce.metainfo.MetaInfoCollector; @@ -129,6 +130,11 @@ private int execute(String... args) { LOGGER.error("Error while loading graph", ex); System.err.println(formatError(ex)); return INTERNAL_ERROR; + } catch (UnexpectedException ex) { + LOGGER.error("Unexpected exception while loading graph", ex); + System.err.println( + "Unexpected exception while loading graph. See logs for more information."); + return INTERNAL_ERROR; } // Run all of the rules. diff --git a/sfge/src/main/java/com/salesforce/graph/ApexPath.java b/sfge/src/main/java/com/salesforce/graph/ApexPath.java index 340c19c53..08dc08eb7 100644 --- a/sfge/src/main/java/com/salesforce/graph/ApexPath.java +++ b/sfge/src/main/java/com/salesforce/graph/ApexPath.java @@ -269,7 +269,10 @@ public void addVertices(List vertices) { !(vertices.get(0) instanceof BlockStatementVertex) && // Class Instantiation Path - !(vertices.get(0) instanceof FieldVertex)) { + !(vertices.get(0) instanceof FieldVertex) + && + // Static blocks + !(vertices.get(0) instanceof MethodCallExpressionVertex)) { throw new UnexpectedException(vertices); } this.vertices.addAll(vertices); diff --git a/sfge/src/main/java/com/salesforce/graph/Schema.java b/sfge/src/main/java/com/salesforce/graph/Schema.java index b38a1d413..26cd02b95 100644 --- a/sfge/src/main/java/com/salesforce/graph/Schema.java +++ b/sfge/src/main/java/com/salesforce/graph/Schema.java @@ -69,4 +69,16 @@ public static final class JorjeNodeType { public static final String CHILD = "Child"; public static final String PARENT = "Parent"; public static final String NEXT_SIBLING = "NextSibling"; + + /** Mark a vertex as synthetic */ + public static final String IS_SYNTHETIC = "IsSynthetic"; + /** Indicates if a method is a synthetic static block method */ + public static final String IS_STATIC_BLOCK_METHOD = "IsStaticBlockMethod"; + /** Indicates if a method is a synthetic static block invoker method */ + public static final String IS_STATIC_BLOCK_INVOKER_METHOD = "IsStaticBlockInvokerMethod"; + /** + * Indicates if a MethodCallExpression is a synthetic invocation of static block from invoker + * method + */ + public static final String IS_STATIC_BLOCK_INVOCATION = "IsStaticBlockInvocation"; } diff --git a/sfge/src/main/java/com/salesforce/graph/build/AbstractApexVertexBuilder.java b/sfge/src/main/java/com/salesforce/graph/build/AbstractApexVertexBuilder.java index e9c02ffe1..2cd405346 100644 --- a/sfge/src/main/java/com/salesforce/graph/build/AbstractApexVertexBuilder.java +++ b/sfge/src/main/java/com/salesforce/graph/build/AbstractApexVertexBuilder.java @@ -8,6 +8,7 @@ import com.salesforce.graph.Schema; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -72,23 +73,39 @@ private void buildVertices(JorjeNode node, Vertex vNodeParam, String fileName) { } Vertex vPreviousSibling = null; - for (JorjeNode child : node.getChildren()) { - Vertex vChild = g.addV(child.getLabel()).next(); + final List children = node.getChildren(); + final Set verticesAddressed = new HashSet<>(); + verticesAddressed.add(vNode); + + for (int i = 0; i < children.size(); i++) { + final JorjeNode child = children.get(i); + final Vertex vChild = g.addV(child.getLabel()).next(); addProperties(g, child, vChild); - // We are currently adding PARENT and CHILD, in theory the same edge could be navigated - // both ways, however - // the code looks messy when that is done. - // TODO: Determine if this causes performance or resource issues. Consider a single edge - g.addE(Schema.PARENT).from(vChild).to(vNode).iterate(); - g.addE(Schema.CHILD).from(vNode).to(vChild).iterate(); + + /** Handle static block if we are looking at a method that has a block statement. + * See {@linkplain StaticBlockUtil} on why this is needed + * and how we handle it. */ + if (StaticBlockUtil.isStaticBlockStatement(node, child)) { + final Vertex parentVertexForChild = + StaticBlockUtil.createSyntheticStaticBlockMethod(g, vNode, i); + GremlinVertexUtil.addParentChildRelationship(g, parentVertexForChild, vChild); + verticesAddressed.add(parentVertexForChild); + } else { + GremlinVertexUtil.addParentChildRelationship(g, vNode, vChild); + } + if (vPreviousSibling != null) { g.addE(Schema.NEXT_SIBLING).from(vPreviousSibling).to(vChild).iterate(); } vPreviousSibling = vChild; + // To save memory in the graph, don't pass the source name into recursive calls. buildVertices(child, vChild, null); } - afterInsert(g, node, vNode); + // Execute afterInsert() on each vertex we addressed + for (Vertex vertex : verticesAddressed) { + afterInsert(g, node, vertex); + } if (rootVNode != null) { // Only call this for the root node afterFileInsert(g, rootVNode); @@ -101,6 +118,8 @@ private void buildVertices(JorjeNode node, Vertex vNodeParam, String fileName) { */ private final void afterInsert(GraphTraversalSource g, JorjeNode node, Vertex vNode) { if (node.getLabel().equals(ASTConstants.NodeType.METHOD)) { + // If we just added a method, create forward and + // backward code flow for the contents of the method MethodPathBuilderVisitor.apply(g, vNode); } } @@ -111,7 +130,9 @@ private final void afterInsert(GraphTraversalSource g, JorjeNode node, Vertex vN * @param vNode root node that corresponds to the file */ protected void afterFileInsert(GraphTraversalSource g, Vertex vNode) { - // Intentionally left blank + // If the root (class/trigger/etc) contained any static blocks, + // create an invoker method to invoke the static blocks + StaticBlockUtil.createSyntheticStaticBlockInvocation(g, vNode); } protected void addProperties(GraphTraversalSource g, JorjeNode node, Vertex vNode) { @@ -122,11 +143,12 @@ protected void addProperties(GraphTraversalSource g, JorjeNode node, Vertex vNod String key = entry.getKey(); Object value = entry.getValue(); value = adjustPropertyValue(node, key, value); - addProperty(previouslyInsertedKeys, traversal, key, value); + GremlinVertexUtil.addProperty(previouslyInsertedKeys, traversal, key, value); } for (Map.Entry entry : getAdditionalProperties(node).entrySet()) { - addProperty(previouslyInsertedKeys, traversal, entry.getKey(), entry.getValue()); + GremlinVertexUtil.addProperty( + previouslyInsertedKeys, traversal, entry.getKey(), entry.getValue()); } // Commit the changes. @@ -142,49 +164,4 @@ protected Object adjustPropertyValue(JorjeNode node, String key, Object value) { protected Map getAdditionalProperties(JorjeNode node) { return new HashMap<>(); } - - /** Add a property to the traversal, throwing an exception if any keys are duplicated. */ - protected void addProperty( - TreeSet previouslyInsertedKeys, - GraphTraversal traversal, - String keyParam, - Object value) { - final String key = keyParam.intern(); - - if (!previouslyInsertedKeys.add(key)) { - throw new UnexpectedException(key); - } - - if (value instanceof List) { - List list = (List) value; - // Convert ArrayList to an Array. There seems to be a Tinkerpop bug where a singleton - // ArrayList - // isn't properly stored. Remote graphs also have issues with empty lists, so don't add - // an empty list. - if (!list.isEmpty()) { - traversal.property(key, list.toArray()); - } else { - // return so that we don't store a case insensitive version - return; - } - } else if (value instanceof Boolean - || value instanceof Double - || value instanceof Integer - || value instanceof Long - || value instanceof String) { - traversal.property(key, value); - } else { - if (value != null) { - if (!(value instanceof Enum)) { - if (LOGGER.isWarnEnabled()) { - LOGGER.warn( - "Using string for value. type=" + value.getClass().getSimpleName()); - } - } - final String strValue = String.valueOf(value).intern(); - traversal.property(key, strValue); - } - } - CaseSafePropertyUtil.addCaseSafeProperty(traversal, key, value); - } } diff --git a/sfge/src/main/java/com/salesforce/graph/build/CustomerApexVertexBuilder.java b/sfge/src/main/java/com/salesforce/graph/build/CustomerApexVertexBuilder.java index b7419fbcf..073695448 100644 --- a/sfge/src/main/java/com/salesforce/graph/build/CustomerApexVertexBuilder.java +++ b/sfge/src/main/java/com/salesforce/graph/build/CustomerApexVertexBuilder.java @@ -24,6 +24,7 @@ public void build() { @Override protected void afterFileInsert(GraphTraversalSource g, Vertex vNode) { + super.afterFileInsert(g, vNode); ApexPropertyAnnotator.apply(g, vNode); } } diff --git a/sfge/src/main/java/com/salesforce/graph/build/GremlinUtil.java b/sfge/src/main/java/com/salesforce/graph/build/GremlinUtil.java index 72c3ac09b..a34fc84eb 100644 --- a/sfge/src/main/java/com/salesforce/graph/build/GremlinUtil.java +++ b/sfge/src/main/java/com/salesforce/graph/build/GremlinUtil.java @@ -32,7 +32,11 @@ public static Optional getOnlyChild( if (children.isEmpty()) { return Optional.empty(); } else if (children.size() > 1) { - throw new UnexpectedException(children); + throw new UnexpectedException( + "Did not expect more than one child node of type " + + childLabel + + ". Actual count: " + + children.size()); } else { return Optional.of(children.get(0)); } @@ -56,6 +60,11 @@ public static List getChildren(GraphTraversalSource g, Vertex vertex) { .toList(); } + public static List getChildren( + GraphTraversalSource g, Vertex vertex, String childLabel) { + return g.V(vertex).out(Schema.CHILD).hasLabel(childLabel).toList(); + } + public static Optional getPreviousSibling(GraphTraversalSource g, Vertex vertex) { Iterator it = g.V(vertex).in(Schema.NEXT_SIBLING); if (it.hasNext()) { @@ -87,5 +96,14 @@ public static Optional getFirstChild(GraphTraversalSource g, Vertex vert } } + public static Optional getParent(GraphTraversalSource g, Vertex vertex) { + Iterator it = g.V(vertex).out(Schema.PARENT); + if (it.hasNext()) { + return Optional.of(it.next()); + } else { + return Optional.empty(); + } + } + private GremlinUtil() {} } diff --git a/sfge/src/main/java/com/salesforce/graph/build/GremlinVertexUtil.java b/sfge/src/main/java/com/salesforce/graph/build/GremlinVertexUtil.java new file mode 100644 index 000000000..3efb7f3c1 --- /dev/null +++ b/sfge/src/main/java/com/salesforce/graph/build/GremlinVertexUtil.java @@ -0,0 +1,92 @@ +package com.salesforce.graph.build; + +import com.salesforce.exception.UnexpectedException; +import com.salesforce.graph.Schema; +import java.util.List; +import java.util.Optional; +import java.util.TreeSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Vertex; + +/** Handles common operations performed while creating vertices on Graph */ +public final class GremlinVertexUtil { + private static final Logger LOGGER = LogManager.getLogger(GremlinVertexUtil.class); + + private GremlinVertexUtil() {} + + /** Create parent-child relationship between vertices on graph */ + static void addParentChildRelationship( + GraphTraversalSource g, Vertex parentVertex, Vertex childVertex) { + // We are currently adding PARENT and CHILD, in theory the same edge could be navigated + // both ways, however + // the code looks messy when that is done. + // TODO: Determine if this causes performance or resource issues. Consider a single edge + g.addE(Schema.PARENT).from(childVertex).to(parentVertex).iterate(); + g.addE(Schema.CHILD).from(parentVertex).to(childVertex).iterate(); + } + + /** Make a synthetic vertex a sibling of an existing vertex on graph */ + static void makeSiblings(GraphTraversalSource g, Vertex vertex, Vertex syntheticVertex) { + final Vertex rootVertex = getParentVertex(g, vertex); + + addParentChildRelationship(g, rootVertex, syntheticVertex); + } + + static Vertex getParentVertex(GraphTraversalSource g, Vertex vertex) { + // Get parent node of vertex + final Optional rootVertex = GremlinUtil.getParent(g, vertex); + if (!rootVertex.isPresent()) { + throw new UnexpectedException( + "Did not expect vertex to not have a parent vertex. vertex=" + vertex); + } + return rootVertex.get(); + } + + /** Add a property to the traversal, throwing an exception if any keys are duplicated. */ + protected static void addProperty( + TreeSet previouslyInsertedKeys, + GraphTraversal traversal, + String keyParam, + Object value) { + final String key = keyParam.intern(); + + if (!previouslyInsertedKeys.add(key)) { + throw new UnexpectedException(key); + } + + if (value instanceof List) { + List list = (List) value; + // Convert ArrayList to an Array. There seems to be a Tinkerpop bug where a singleton + // ArrayList + // isn't properly stored. Remote graphs also have issues with empty lists, so don't add + // an empty list. + if (!list.isEmpty()) { + traversal.property(key, list.toArray()); + } else { + // return so that we don't store a case insensitive version + return; + } + } else if (value instanceof Boolean + || value instanceof Double + || value instanceof Integer + || value instanceof Long + || value instanceof String) { + traversal.property(key, value); + } else { + if (value != null) { + if (!(value instanceof Enum)) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn( + "Using string for value. type=" + value.getClass().getSimpleName()); + } + } + final String strValue = String.valueOf(value).intern(); + traversal.property(key, strValue); + } + } + CaseSafePropertyUtil.addCaseSafeProperty(traversal, key, value); + } +} diff --git a/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java b/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java new file mode 100644 index 000000000..3a2a8756f --- /dev/null +++ b/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java @@ -0,0 +1,377 @@ +package com.salesforce.graph.build; + +import com.salesforce.apex.jorje.ASTConstants; +import com.salesforce.apex.jorje.JorjeNode; +import com.salesforce.collections.CollectionUtil; +import com.salesforce.exception.ProgrammingException; +import com.salesforce.graph.Schema; +import com.salesforce.graph.ops.MethodUtil; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Vertex; + +/** + * Handles creation of synthetic methods and vertices to gracefully invoke static code blocks. + * + *

Consider this example: + * + * + * class StaticBlockClass { + * static { + * System.debug("inside static block 1"); + * } + * static { + * System.debug("inside static block 2"); + * } + * } + * + * + * In Jorje's compilation structure, static blocks are represented like this: + * + * class StaticBlockClass { + * private static void () { + * { + * System.debug("inside static block 1"); + * } + * { + * System.debug("inside static block 2"); + * } + * } + * } + * + * + *

Having multiple block statements inside a method breaks SFGE's normal code flow logic. + * This makes handling code blocks in () impossible. + *

As an alternative, we are creating synthetic vertices in the Graph to + * represent the static blocks as individual methods. + *

We also create one top-level synthetic method ("StaticBlockInvoker") that invokes + * individual static block methods. While creating static scope + * for this class, we should invoke the method call expressions inside the top-level synthetic + * method. + * + *

New structure looks like this: + * + * class StaticBlockClass { + * private static void SyntheticStaticBlock_1() { + * System.debug("inside static block 1"); + * } + * private static void SyntheticStaticBlock_2() { + * System.debug("inside static block 2"); + * } + * private static void StaticBlockInvoker() { + * SyntheticStaticBlock_1(); + * SyntheticStaticBlock_2(); + * } + * } + * + */ +public final class StaticBlockUtil { + private static final Logger LOGGER = LogManager.getLogger(StaticBlockUtil.class); + + static final String SYNTHETIC_STATIC_BLOCK_METHOD_NAME = "SyntheticStaticBlock_%d"; + static final String STATIC_BLOCK_INVOKER_METHOD = "StaticBlockInvoker"; + + private StaticBlockUtil() {} + + /** + * Creates a synthetic method vertex to represent a static code block + * + * @param g traversal graph + * @param clinitVertex () method's vertex + * @param staticBlockIndex index to use for name uniqueness - TODO: this index is currently just + * the child count index and does not necessarily follow sequence + * @return new synthetic method vertex with name SyntheticStaticBlock_%d, which will be the + * parent for blockStatementVertex, and a sibling of () + */ + public static Vertex createSyntheticStaticBlockMethod( + GraphTraversalSource g, + Vertex clinitVertex, + int staticBlockIndex) { + final Vertex syntheticMethodVertex = g.addV(ASTConstants.NodeType.METHOD).next(); + final String definingType = clinitVertex.value(Schema.DEFINING_TYPE); + final List siblings = GremlinUtil.getChildren(g, GremlinVertexUtil.getParentVertex(g, clinitVertex)); + final int nextSiblingIndex = siblings.size(); + + addSyntheticStaticBlockMethodProperties( + g, definingType, syntheticMethodVertex, staticBlockIndex, nextSiblingIndex); + + final Vertex modifierNodeVertex = g.addV(ASTConstants.NodeType.MODIFIER_NODE).next(); + addStaticModifierProperties(g, definingType, modifierNodeVertex); + GremlinVertexUtil.addParentChildRelationship(g, syntheticMethodVertex, modifierNodeVertex); + + GremlinVertexUtil.makeSiblings(g, clinitVertex, syntheticMethodVertex); + + return syntheticMethodVertex; + } + + /** + * If rootNode contains synthetic static block methods (created through {@link + * #createSyntheticStaticBlockMethod}), adds a top-level synthetic method that invokes each + * static block method. + */ + public static void createSyntheticStaticBlockInvocation( + GraphTraversalSource g, Vertex rootNode) { + // Check if root node contains any static block methods + final List staticBlockMethods = getStaticBlockMethods(g, rootNode); + + // Create static block invocation method + if (!staticBlockMethods.isEmpty()) { + final String definingType = rootNode.value(Schema.DEFINING_TYPE); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Creating synthetic invocation method for {} to invoke {} static block(s)", + definingType, + staticBlockMethods.size()); + } + + final Vertex invokerMethodVertex = + createSyntheticInvocationsMethod(g, rootNode, staticBlockMethods, definingType); + MethodPathBuilderVisitor.apply(g, invokerMethodVertex); + } + } + + static boolean isStaticBlockStatement(JorjeNode node, JorjeNode child) { + return isClinitMethod(node) && containsStaticBlock(node) && isBlockStatement(child); + } + + private static Vertex createSyntheticInvocationsMethod( + GraphTraversalSource g, + Vertex rootNode, + List staticBlockMethods, + String definingType) { + // Create new synthetic method StaticBlockInvoker to invoke each synthetic static block + // method + final List siblings = GremlinUtil.getChildren(g, rootNode); + final Vertex invokerMethodVertex = g.addV(ASTConstants.NodeType.METHOD).next(); + addStaticBlockInvokerProperties(g, definingType, invokerMethodVertex, siblings.size()); + GremlinVertexUtil.addParentChildRelationship(g, rootNode, invokerMethodVertex); + + final Vertex modifierNodeVertex = g.addV(ASTConstants.NodeType.MODIFIER_NODE).next(); + addStaticModifierProperties(g, definingType, modifierNodeVertex); + GremlinVertexUtil.addParentChildRelationship(g, invokerMethodVertex, modifierNodeVertex); + + // Create synthetic BlockStatement inside StaticBlockInvoker to hold the method invocations + final Vertex blockStatementInInvoker = g.addV(ASTConstants.NodeType.BLOCK_STATEMENT).next(); + addBlockStatementProperties(g, definingType, blockStatementInInvoker); + GremlinVertexUtil.addParentChildRelationship( + g, invokerMethodVertex, blockStatementInInvoker); + + for (int i = 0; i < staticBlockMethods.size(); i++) { + final Vertex staticBlockMethod = staticBlockMethods.get(i); + boolean isLastMethod = i == staticBlockMethods.size() - 1; + // Create a method call invocation to the synthetic static block method + final Vertex exprStaticBlockMethodCall = + createMethodCallExpression(g, definingType, staticBlockMethod, isLastMethod); + + // Add the expression statement containing the method call inside StaticBlockInvoker's + // block statement + GremlinVertexUtil.addParentChildRelationship( + g, blockStatementInInvoker, exprStaticBlockMethodCall); + } + + return invokerMethodVertex; + } + + private static Vertex createMethodCallExpression( + GraphTraversalSource g, + String definingType, + Vertex staticBlockMethod, + boolean isLastMethod) { + final Vertex expressionStmt = g.addV(ASTConstants.NodeType.EXPRESSION_STATEMENT).next(); + addExpressionStmtProperties(g, definingType, expressionStmt, isLastMethod); + + // Create new MethodCallExpression for the synthetic static block method + final Vertex staticBlockMethodCall = + g.addV(ASTConstants.NodeType.METHOD_CALL_EXPRESSION).next(); + addMethodCallExpressionProperties( + g, definingType, staticBlockMethod, staticBlockMethodCall); + GremlinVertexUtil.addParentChildRelationship(g, expressionStmt, staticBlockMethodCall); + + // Create EmptyReferenceExpression inside MethodCallExpression + final Vertex emptyMethodReference = + g.addV(ASTConstants.NodeType.EMPTY_REFERENCE_EXPRESSION).next(); + addEmptyMethodReferenceProperties(g, definingType, emptyMethodReference); + GremlinVertexUtil.addParentChildRelationship( + g, staticBlockMethodCall, emptyMethodReference); + return expressionStmt; + } + + private static void addExpressionStmtProperties( + GraphTraversalSource g, + String definingType, + Vertex expressionStmt, + boolean isLastMethod) { + verifyType(expressionStmt, ASTConstants.NodeType.EXPRESSION_STATEMENT); + + final Map properties = new HashMap<>(); + properties.put(Schema.IS_SYNTHETIC, true); + properties.put(Schema.DEFINING_TYPE, definingType); + properties.put(Schema.FIRST_CHILD, true); + properties.put(Schema.LAST_CHILD, true); + properties.put(Schema.CHILD_INDEX, 0); + + addProperties(g, expressionStmt, properties); + } + + private static void addEmptyMethodReferenceProperties( + GraphTraversalSource g, String definingType, Vertex emptyMethodReference) { + verifyType(emptyMethodReference, ASTConstants.NodeType.EMPTY_REFERENCE_EXPRESSION); + + final Map properties = new HashMap<>(); + properties.put(Schema.IS_SYNTHETIC, true); + properties.put(Schema.DEFINING_TYPE, definingType); + properties.put(Schema.FIRST_CHILD, true); + properties.put(Schema.LAST_CHILD, true); + properties.put(Schema.CHILD_INDEX, 0); + + addProperties(g, emptyMethodReference, properties); + } + + private static void addMethodCallExpressionProperties( + GraphTraversalSource g, + String definingType, + Vertex staticBlockMethod, + Vertex staticBlockMethodCall) { + verifyType(staticBlockMethodCall, ASTConstants.NodeType.METHOD_CALL_EXPRESSION); + + final Map properties = new HashMap<>(); + properties.put(Schema.IS_SYNTHETIC, true); + properties.put(Schema.DEFINING_TYPE, definingType); + properties.put(Schema.METHOD_NAME, staticBlockMethod.value(Schema.NAME)); + properties.put(Schema.FULL_METHOD_NAME, staticBlockMethod.value(Schema.NAME)); + properties.put(Schema.IS_STATIC_BLOCK_INVOCATION, true); + properties.put(Schema.FIRST_CHILD, true); + properties.put(Schema.LAST_CHILD, true); + properties.put(Schema.CHILD_INDEX, 0); + + addProperties(g, staticBlockMethodCall, properties); + } + + private static void addBlockStatementProperties( + GraphTraversalSource g, String definingType, Vertex blockStatementInInvoker) { + verifyType(blockStatementInInvoker, ASTConstants.NodeType.BLOCK_STATEMENT); + + final Map properties = new HashMap<>(); + properties.put(Schema.IS_SYNTHETIC, true); + properties.put(Schema.DEFINING_TYPE, definingType); + properties.put(Schema.CHILD_INDEX, 1); + properties.put(Schema.FIRST_CHILD, false); + properties.put(Schema.LAST_CHILD, true); + + addProperties(g, blockStatementInInvoker, properties); + } + + private static void addStaticModifierProperties( + GraphTraversalSource g, String definingType, Vertex modifier) { + verifyType(modifier, ASTConstants.NodeType.MODIFIER_NODE); + + final Map properties = new HashMap<>(); + properties.put(Schema.IS_SYNTHETIC, true); + properties.put(Schema.DEFINING_TYPE, definingType); + properties.put(Schema.STATIC, true); + properties.put(Schema.ABSTRACT, false); + properties.put(Schema.GLOBAL, false); + properties.put(Schema.MODIFIERS, 8); // Apparently, static methods have modifiers 8 + properties.put(Schema.FIRST_CHILD, true); + properties.put(Schema.LAST_CHILD, false); + properties.put(Schema.CHILD_INDEX, 0); + + addProperties(g, modifier, properties); + } + + private static void addStaticBlockInvokerProperties( + GraphTraversalSource g, String definingType, Vertex staticBlockInvokerVertex, int childIndex) { + verifyType(staticBlockInvokerVertex, ASTConstants.NodeType.METHOD); + final Map properties = new HashMap<>(); + properties.put(Schema.NAME, STATIC_BLOCK_INVOKER_METHOD); + properties.put(Schema.IS_STATIC_BLOCK_INVOKER_METHOD, true); + properties.put(Schema.CHILD_INDEX, childIndex); + addCommonSynthMethodProperties(g, definingType, staticBlockInvokerVertex, properties); + } + + private static void addSyntheticStaticBlockMethodProperties( + GraphTraversalSource g, + String definingType, + Vertex syntheticMethodVertex, + int staticBlockIndex, + int childIndex) { + verifyType(syntheticMethodVertex, ASTConstants.NodeType.METHOD); + final Map properties = new HashMap<>(); + properties.put( + Schema.NAME, String.format(SYNTHETIC_STATIC_BLOCK_METHOD_NAME, staticBlockIndex)); + properties.put(Schema.IS_STATIC_BLOCK_METHOD, true); + properties.put(Schema.CHILD_INDEX, childIndex); + + addCommonSynthMethodProperties(g, definingType, syntheticMethodVertex, properties); + } + + private static void addCommonSynthMethodProperties( + GraphTraversalSource g, + String definingType, + Vertex staticBlockInvokerVertex, + Map properties) { + properties.put(Schema.ARITY, 0); + properties.put(Schema.CONSTRUCTOR, false); + properties.put(Schema.DEFINING_TYPE, definingType); + properties.put(Schema.IS_SYNTHETIC, true); + properties.put(Schema.RETURN_TYPE, ASTConstants.TYPE_VOID); + + addProperties(g, staticBlockInvokerVertex, properties); + } + + private static void addProperties( + GraphTraversalSource g, Vertex vertex, Map properties) { + final TreeSet previouslyInsertedKeys = CollectionUtil.newTreeSet(); + final GraphTraversal traversal = g.V(vertex.id()); + + for (Map.Entry entry : properties.entrySet()) { + GremlinVertexUtil.addProperty( + previouslyInsertedKeys, traversal, entry.getKey(), entry.getValue()); + previouslyInsertedKeys.add(entry.getKey()); + } + + // Commit the changes. + traversal.next(); + } + + /** @return true if given node represents () */ + private static boolean isClinitMethod(JorjeNode node) { + return ASTConstants.NodeType.METHOD.equals(node.getLabel()) + && MethodUtil.STATIC_CONSTRUCTOR_CANONICAL_NAME.equals( + node.getProperties().get(Schema.NAME)); + } + + /** @return true if () node contains any static block definitions */ + private static boolean containsStaticBlock(JorjeNode node) { + for (JorjeNode childNode : node.getChildren()) { + if (ASTConstants.NodeType.BLOCK_STATEMENT.equals(childNode.getLabel())) { + return true; + } + } + return false; + } + + private static List getStaticBlockMethods(GraphTraversalSource g, Vertex root) { + return g.V(root) + .out(Schema.CHILD) + .hasLabel(ASTConstants.NodeType.METHOD) + .has(Schema.IS_STATIC_BLOCK_METHOD, true) + .toList(); + } + + private static boolean isBlockStatement(JorjeNode child) { + return ASTConstants.NodeType.BLOCK_STATEMENT.equals(child.getLabel()); + } + + private static void verifyType(Vertex vertex, String expectedType) { + if (!expectedType.equals(vertex.label())) { + throw new ProgrammingException("Incorrect vertex type: " + vertex.label()); + } + } +} diff --git a/sfge/src/main/java/com/salesforce/graph/ops/expander/ApexPathExpander.java b/sfge/src/main/java/com/salesforce/graph/ops/expander/ApexPathExpander.java index af23a3009..f1dbb9665 100644 --- a/sfge/src/main/java/com/salesforce/graph/ops/expander/ApexPathExpander.java +++ b/sfge/src/main/java/com/salesforce/graph/ops/expander/ApexPathExpander.java @@ -150,6 +150,12 @@ final class ApexPathExpander implements ClassStaticScopeProvider, EngineDirectiv */ private final TreeSet currentlyInitializingStaticClasses; + /** + * Track classes whose static scopes have been initialized. This would help double visiting some + * nodes. + */ + private final TreeSet alreadyInitializedStaticClasses; + /** The symbol provider that corresponds to the {@link #topMostPath} */ private /*finalTODO Add clear method to SymbolProviderVertexVisitor*/ SymbolProviderVertexVisitor symbolProviderVisitor; @@ -195,6 +201,7 @@ final class ApexPathExpander implements ClassStaticScopeProvider, EngineDirectiv this.classStaticScopes = CollectionUtil.newTreeMap(); this.engineDirectiveContext = new EngineDirectiveContext(); this.currentlyInitializingStaticClasses = CollectionUtil.newTreeSet(); + this.alreadyInitializedStaticClasses = CollectionUtil.newTreeSet(); } /** @@ -272,6 +279,8 @@ final class ApexPathExpander implements ClassStaticScopeProvider, EngineDirectiv this.startScope = (SymbolProvider) CloneUtil.clone((DeepCloneable) other.startScope); this.currentlyInitializingStaticClasses = CloneUtil.cloneTreeSet(other.currentlyInitializingStaticClasses); + this.alreadyInitializedStaticClasses = + CloneUtil.cloneTreeSet(other.alreadyInitializedStaticClasses); } /** @@ -370,15 +379,18 @@ void initializeClassStaticScope(String className) apexPath = getTopMostPath().getStaticInitializationPath(className).get(); } - if (!classStaticScope.getState().equals(AbstractClassScope.State.INITIALIZED)) { + if (!classStaticScope.getState().equals(AbstractClassScope.State.INITIALIZED) + && !alreadyInitializedStaticClasses.contains(fullClassName)) { if (apexPath != null && apexPath.getCollectible() != null) { visit(apexPath.getCollectible()); } symbolProviderVisitor.popScope(classStaticScope); classStaticScope.setState(AbstractClassScope.State.INITIALIZED); + alreadyInitializedStaticClasses.add(fullClassName); } } finally { - if (!currentlyInitializingStaticClasses.remove(fullClassName)) { + if (!currentlyInitializingStaticClasses.remove(fullClassName) + && alreadyInitializedStaticClasses.contains(fullClassName)) { throw new ProgrammingException( "Set did not contain class. values=" + currentlyInitializingStaticClasses); diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java b/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java index 940f23704..df046f8bf 100644 --- a/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java +++ b/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java @@ -2,6 +2,10 @@ import static com.salesforce.apex.jorje.ASTConstants.NodeType; +import com.salesforce.Collectible; +import com.salesforce.apex.jorje.ASTConstants; +import com.salesforce.collections.CollectionUtil; +import com.salesforce.exception.ProgrammingException; import com.salesforce.exception.UnexpectedException; import com.salesforce.graph.ApexPath; import com.salesforce.graph.DeepCloneable; @@ -10,15 +14,20 @@ import com.salesforce.graph.ops.MethodUtil; import com.salesforce.graph.symbols.apex.ApexValue; import com.salesforce.graph.vertex.BaseSFVertex; +import com.salesforce.graph.vertex.BlockStatementVertex; import com.salesforce.graph.vertex.FieldDeclarationVertex; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; +import com.salesforce.graph.vertex.MethodVertex; import com.salesforce.graph.vertex.SFVertexFactory; import com.salesforce.graph.vertex.UserClassVertex; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Optional; import org.apache.tinkerpop.gremlin.process.traversal.Order; import org.apache.tinkerpop.gremlin.process.traversal.Scope; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.T; /** Used when invoking a static method on a class. */ public final class ClassStaticScope extends AbstractClassScope @@ -105,7 +114,37 @@ private static List getFieldDeclarations( return results; } - /** + private static List getStaticBlocks( + GraphTraversalSource g, UserClassVertex userClass) { + List results = new ArrayList<>(); + + String superClassName = userClass.getSuperClassName().orElse(null); + if (superClassName != null) { + UserClassVertex superClass = ClassUtil.getUserClass(g, superClassName).orElse(null); + if (superClass != null) { + results.addAll(getStaticBlocks(g, superClass)); + } + } + + results.addAll(SFVertexFactory.loadVertices( + g, + g.V(userClass.getId()) + .out(Schema.CHILD) + .hasLabel(NodeType.METHOD) + .has(Schema.IS_STATIC_BLOCK_INVOKER_METHOD, true) + .out(Schema.CHILD) + .hasLabel(ASTConstants.NodeType.BLOCK_STATEMENT) + .order(Scope.global) + .by(Schema.CHILD_INDEX, Order.asc) + .out(Schema.CHILD) + .hasLabel(NodeType.EXPRESSION_STATEMENT) + .out(Schema.CHILD) + .hasLabel(NodeType.METHOD_CALL_EXPRESSION))); + + return results; + } + + /** * Returns a path that represents the static properties defined by the class. The following * example would contain a path for 'MyClass' that contains the Field and FieldDeclarations for * 's'. {@code @@ -121,9 +160,13 @@ private static List getFieldDeclarations( public static Optional getInitializationPath( GraphTraversalSource g, String classname) { ClassStaticScope classStaticScope = ClassStaticScope.get(g, classname); + if (classStaticScope.getState().equals(State.INITIALIZED)) { + throw new ProgrammingException("Initialization path does not need to be invoked on a class that's already initialized: " + classStaticScope.getClassName()); + } List vertices = new ArrayList<>(); vertices.addAll(classStaticScope.getFields()); vertices.addAll(getFieldDeclarations(g, classStaticScope.userClass)); + vertices.addAll(getStaticBlocks(g, classStaticScope.userClass)); if (vertices.isEmpty()) { return Optional.empty(); } else { @@ -132,4 +175,5 @@ public static Optional getInitializationPath( return Optional.of(apexPath); } } + } diff --git a/sfge/src/test/java/com/salesforce/graph/build/GraphBuildTestUtil.java b/sfge/src/test/java/com/salesforce/graph/build/GraphBuildTestUtil.java new file mode 100644 index 000000000..07cc97edf --- /dev/null +++ b/sfge/src/test/java/com/salesforce/graph/build/GraphBuildTestUtil.java @@ -0,0 +1,61 @@ +package com.salesforce.graph.build; + +import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.has; + +import com.salesforce.apex.jorje.ASTConstants; +import com.salesforce.apex.jorje.JorjeUtil; +import com.salesforce.graph.ApexPath; +import com.salesforce.graph.Schema; +import com.salesforce.graph.cache.VertexCacheProvider; +import com.salesforce.graph.ops.ApexPathUtil; +import com.salesforce.graph.symbols.DefaultSymbolProviderVertexVisitor; +import com.salesforce.graph.vertex.MethodVertex; +import com.salesforce.graph.vertex.SFVertexFactory; +import com.salesforce.graph.visitor.ApexPathWalker; +import com.salesforce.graph.visitor.DefaultNoOpPathVertexVisitor; +import com.salesforce.graph.visitor.PathVertexVisitor; +import java.util.ArrayList; +import java.util.List; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; + +public class GraphBuildTestUtil { + static void buildGraph(GraphTraversalSource g, String sourceCode) { + buildGraph(g, new String[] {sourceCode}); + } + + static void buildGraph(GraphTraversalSource g, String[] sourceCodes) { + List compilations = new ArrayList(); + for (int i = 0; i < sourceCodes.length; i++) { + compilations.add( + new Util.CompilationDescriptor( + "TestCode" + i, JorjeUtil.compileApexFromString(sourceCodes[i]))); + } + + VertexCacheProvider.get().initialize(g); + CustomerApexVertexBuilder customerApexVertexBuilder = + new CustomerApexVertexBuilder(g, compilations); + + for (GraphBuilder graphBuilder : new GraphBuilder[] {customerApexVertexBuilder}) { + graphBuilder.build(); + } + } + + /** Sanity method to walk all paths. Helps to ensure all of the push/pops are correct */ + static List walkAllPaths(GraphTraversalSource g, String methodName) { + MethodVertex methodVertex = + SFVertexFactory.load( + g, + g.V().hasLabel(ASTConstants.NodeType.METHOD) + .has(Schema.NAME, methodName) + .not(has(Schema.IS_STANDARD, true))); + List paths = ApexPathUtil.getForwardPaths(g, methodVertex); + + for (ApexPath path : paths) { + DefaultSymbolProviderVertexVisitor symbols = new DefaultSymbolProviderVertexVisitor(g); + PathVertexVisitor visitor = new DefaultNoOpPathVertexVisitor(); + ApexPathWalker.walkPath(g, path, visitor, symbols); + } + + return paths; + } +} diff --git a/sfge/src/test/java/com/salesforce/graph/build/MethodPathBuilderTest.java b/sfge/src/test/java/com/salesforce/graph/build/MethodPathBuilderTest.java index 49698f5da..ddef8934d 100644 --- a/sfge/src/test/java/com/salesforce/graph/build/MethodPathBuilderTest.java +++ b/sfge/src/test/java/com/salesforce/graph/build/MethodPathBuilderTest.java @@ -13,12 +13,9 @@ import com.salesforce.TestUtil; import com.salesforce.apex.jorje.ASTConstants.NodeType; -import com.salesforce.apex.jorje.JorjeUtil; import com.salesforce.graph.ApexPath; import com.salesforce.graph.Schema; -import com.salesforce.graph.cache.VertexCacheProvider; import com.salesforce.graph.ops.ApexPathUtil; -import com.salesforce.graph.symbols.DefaultSymbolProviderVertexVisitor; import com.salesforce.graph.vertex.BaseSFVertex; import com.salesforce.graph.vertex.BlockStatementVertex; import com.salesforce.graph.vertex.CatchBlockStatementVertex; @@ -39,9 +36,6 @@ import com.salesforce.graph.vertex.ValueWhenBlockVertex; import com.salesforce.graph.vertex.VariableDeclarationStatementsVertex; import com.salesforce.graph.vertex.VariableExpressionVertex; -import com.salesforce.graph.visitor.ApexPathWalker; -import com.salesforce.graph.visitor.DefaultNoOpPathVertexVisitor; -import com.salesforce.graph.visitor.PathVertexVisitor; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -57,7 +51,7 @@ import org.junit.jupiter.api.Test; public class MethodPathBuilderTest { - private GraphTraversalSource g; + protected GraphTraversalSource g; private static final String[] BLOCK = new String[] {NodeType.BLOCK_STATEMENT}; @@ -129,7 +123,7 @@ public void testMethodWithSingleExpression() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -159,7 +153,7 @@ public void testMethodWithSingleExpression() { MatcherAssert.assertThat(getVerticesWithEndScope(), hasSize(equalTo(1))); assertEndScopes(BLOCK, ExpressionStatementVertex.class, 3); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -175,7 +169,7 @@ public void testMethodWithNestedIfs() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // // // @@ -437,7 +431,7 @@ public void testMethodWithIfStatement() { // Implicit else assertEndScopes(BLOCK_IF_BLOCK, BlockStatementVertex.class, 3, 3); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -453,7 +447,7 @@ public void testMethodWithSingleIfElseStatement() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -542,7 +536,7 @@ public void testMethodWithSingleIfElseStatement() { // System.debug('GoodBye'); assertEndScopes(BLOCK_IF_BLOCK, ExpressionStatementVertex.class, 6); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -560,7 +554,7 @@ public void testMethodWithSingleIf_ElseIf_ElseStatement() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -692,7 +686,7 @@ public void testMethodWithSingleIf_ElseIf_ElseStatement() { // System.debug('GoodBye'); assertEndScopes(BLOCK_IF_BLOCK, ExpressionStatementVertex.class, 8); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -712,7 +706,7 @@ public void testMethodWithNestedIfElses() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -819,7 +813,7 @@ public void testMethodWithNestedIfElses() { // System.debug('Not Logged'); assertEndScopes(BLOCK_IF_BLOCK, ExpressionStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -835,7 +829,7 @@ public void testMethodWithExpressionBeforeAndAfterIf() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -946,7 +940,7 @@ public void testMethodWithExpressionBeforeAndAfterIf() { // System.debug('After'); assertEndScopes(BLOCK, ExpressionStatementVertex.class, 7); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -965,7 +959,7 @@ public void testMethodWithInnerIfExpressionBeforeAndAfterIf() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -1114,7 +1108,7 @@ public void testMethodWithInnerIfExpressionBeforeAndAfterIf() { // System.debug('After'); assertEndScopes(BLOCK, ExpressionStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -1133,7 +1127,7 @@ public void testMethodWithForEach() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (9 edges) BlockStatement->VariableDeclarationStatements->ForEachStatement // @@ -1185,7 +1179,7 @@ public void testMethodWithForEach() { // System.debug('Not Logged'); assertEndScopes(BLOCK_IF_BLOCK_FOREACH_BLOCK, ExpressionStatementVertex.class, 8); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -1205,7 +1199,7 @@ public void testMethodWithForLoop() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (10 edges) BlockStatement->VariableDeclarationStatements->ForLoopStatement // @@ -1258,7 +1252,7 @@ public void testMethodWithForLoop() { // System.debug('Not Logged'); assertEndScopes(BLOCK_IF_BLOCK_FORLOOP_BLOCK, ExpressionStatementVertex.class, 9); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } /** @@ -1283,7 +1277,7 @@ public void testMethodWithForLoopNoInitializerOrStandardConditionOrIncrementer() + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (8 edges) BlockStatement->VariableDeclarationStatements->ForLoopStatement // ->BlockStatement @@ -1335,7 +1329,7 @@ public void testMethodWithForLoopNoInitializerOrStandardConditionOrIncrementer() // System.debug('Not Logged'); assertEndScopes(BLOCK_IF_BLOCK_FORLOOP_BLOCK, ExpressionStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } /** Tests the case where the for loop doesn't contain an initializer. */ @@ -1357,7 +1351,7 @@ public void testMethodWithForLoopNoInitializer() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (10 edges) // BlockStatement->VariableDeclarationStatements->ForLoopStatement->StandardCondition->PostfixExpression @@ -1410,7 +1404,7 @@ public void testMethodWithForLoopNoInitializer() { // System.debug('Not Logged'); assertEndScopes(BLOCK_IF_BLOCK_FORLOOP_BLOCK, ExpressionStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } /** @@ -1435,7 +1429,7 @@ public void testMethodWithForLoopNoInitializerOrStandardCondition() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (9 edges) // BlockStatement->VariableDeclarationStatements->ForLoopStatement->PostfixExpression @@ -1488,7 +1482,7 @@ public void testMethodWithForLoopNoInitializerOrStandardCondition() { // System.debug('Not Logged'); assertEndScopes(BLOCK_IF_BLOCK_FORLOOP_BLOCK, ExpressionStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -1509,7 +1503,7 @@ public void testMethodWithForLoopExpressionBeforeAndAfter() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (10 edges) BlockStatement->VariableDeclarationStatements->ForLoopStatement // @@ -1591,7 +1585,7 @@ public void testMethodWithForLoopExpressionBeforeAndAfter() { // System.debug('After'); assertEndScopes(BLOCK, ExpressionStatementVertex.class, 12); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -1613,7 +1607,7 @@ public void testMethodWithForLoopExpressionBeforeAndAfterEndsForScope() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); MatcherAssert.assertThat(getVerticesWithEndScope(), hasSize(equalTo(4))); @@ -1629,7 +1623,7 @@ public void testMethodWithForLoopExpressionBeforeAndAfterEndsForScope() { // System.debug('After'); assertEndScopes(BLOCK, ExpressionStatementVertex.class, 13); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -1644,7 +1638,7 @@ public void testMethodWithEarlyReturn() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // // @@ -2046,7 +2040,7 @@ public void testMultipleEarlyReturns() { // insert assertEndScopes(BLOCK, DmlInsertStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -2066,7 +2060,7 @@ public void testInsertGuardedByExceptionInOtherMethodWithReturn() { + "}\n" }; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); List paths; MethodVertex methodVertex = TestUtil.getVertexOnLine(g, MethodVertex.class, 2); @@ -2092,7 +2086,7 @@ public void testInsertGuardedByExceptionInOtherMethodWithReturn() { // insert assertEndScopes(BLOCK, ThrowStatementVertex.class, 10); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -2108,7 +2102,7 @@ public void testMethodWithSingleTryCatch() { + " }\n" + "}"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // // @@ -2322,7 +2316,7 @@ public void testMethodWithSingleTryCatchAndExpressionBeforeAndAfter() { // System.debug('After'); assertEndScopes(BLOCK, ExpressionStatementVertex.class, 9); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -2339,7 +2333,7 @@ public void testIfWithMethodInStandardCondition() { + " }\n" + "}\n"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // doSomething // @@ -2397,7 +2391,7 @@ public void testIfWithMethodInStandardCondition() { List edges = g.V().outE(Schema.CFG_PATH).toList(); MatcherAssert.assertThat(edges, hasSize(7)); - walkAllPaths("doSomething"); + GraphBuildTestUtil.walkAllPaths(g, "doSomething"); } @Test @@ -2413,7 +2407,7 @@ public void testWhileStatement() { + " }\n" + "}\n"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); // (6 edges) // BlockStatement->VariableDeclarationStatements->WhileLoopStatement->StandardCondition->BlockStatement->ExpressionStatement->ExpressionStatement @@ -2424,7 +2418,7 @@ public void testWhileStatement() { ExpressionStatementVertex.class, 6); - List paths = walkAllPaths("doSomething"); + List paths = GraphBuildTestUtil.walkAllPaths(g, "doSomething"); MatcherAssert.assertThat(paths, hasSize(equalTo(1))); } @@ -2448,9 +2442,9 @@ public void testWhileStatementInOtherMethod() { + " }\n" + "}\n"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); - List paths = walkAllPaths("doSomething"); + List paths = GraphBuildTestUtil.walkAllPaths(g, "doSomething"); MatcherAssert.assertThat(paths, hasSize(equalTo(1))); } @@ -2535,7 +2529,7 @@ public void testEnumSwitchStatement() { + " }\n" + "}\n"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); BlockStatementVertex blockStatementLine2 = TestUtil.getVertexOnLine(g, BlockStatementVertex.class, 2); @@ -2611,7 +2605,7 @@ public void testEnumSwitchStatement() { Pair.of(elseWhenBlock, elseWhenBlockBlockStatement), Pair.of(elseWhenBlockBlockStatement, elseWhenBlockExpressionStatement)); - List paths = walkAllPaths("doSomething"); + List paths = GraphBuildTestUtil.walkAllPaths(g, "doSomething"); MatcherAssert.assertThat(paths, hasSize(equalTo(3))); } @@ -2635,7 +2629,7 @@ public void testIntegerSwitchStatement() { + " }\n" + "}\n"; - buildGraph(g, sourceCode); + GraphBuildTestUtil.buildGraph(g, sourceCode); BlockStatementVertex blockStatementLine2 = TestUtil.getVertexOnLine(g, BlockStatementVertex.class, 2); @@ -2709,7 +2703,7 @@ public void testIntegerSwitchStatement() { Pair.of(elseWhenBlock, elseWhenBlockBlockStatement), Pair.of(elseWhenBlockBlockStatement, elseWhenBlockExpressionStatement)); - List paths = walkAllPaths("doSomething"); + List paths = GraphBuildTestUtil.walkAllPaths(g, "doSomething"); MatcherAssert.assertThat(paths, hasSize(equalTo(3))); } @@ -2736,8 +2730,8 @@ public void testMethodSwitchStatement() { + " }\n" + "}\n"; - buildGraph(g, sourceCode); - List paths = walkAllPaths("doSomething"); + GraphBuildTestUtil.buildGraph(g, sourceCode); + List paths = GraphBuildTestUtil.walkAllPaths(g, "doSomething"); // There are 3 paths since #walkPaths does not use any excluders MatcherAssert.assertThat(paths, hasSize(equalTo(3))); } @@ -2809,44 +2803,4 @@ private void assertEndScopes( hasSize(equalTo(endScopes.length))); MatcherAssert.assertThat(vertex.getEndScopes(), contains(endScopes)); } - - /** Sanity method to walk all paths. Helps to ensure all of the push/pops are correct */ - private List walkAllPaths(String methodName) { - MethodVertex methodVertex = - SFVertexFactory.load( - g, - g.V().hasLabel(NodeType.METHOD) - .has(Schema.NAME, methodName) - .not(has(Schema.IS_STANDARD, true))); - List paths = ApexPathUtil.getForwardPaths(g, methodVertex); - - for (ApexPath path : paths) { - DefaultSymbolProviderVertexVisitor symbols = new DefaultSymbolProviderVertexVisitor(g); - PathVertexVisitor visitor = new DefaultNoOpPathVertexVisitor(); - ApexPathWalker.walkPath(g, path, visitor, symbols); - } - - return paths; - } - - private static void buildGraph(GraphTraversalSource g, String sourceCode) { - buildGraph(g, new String[] {sourceCode}); - } - - private static void buildGraph(GraphTraversalSource g, String[] sourceCodes) { - List compilations = new ArrayList<>(); - for (int i = 0; i < sourceCodes.length; i++) { - compilations.add( - new Util.CompilationDescriptor( - "TestCode" + i, JorjeUtil.compileApexFromString(sourceCodes[i]))); - } - - VertexCacheProvider.get().initialize(g); - CustomerApexVertexBuilder customerApexVertexBuilder = - new CustomerApexVertexBuilder(g, compilations); - - for (GraphBuilder graphBuilder : new GraphBuilder[] {customerApexVertexBuilder}) { - graphBuilder.build(); - } - } } diff --git a/sfge/src/test/java/com/salesforce/graph/build/StaticBlockUtilTest.java b/sfge/src/test/java/com/salesforce/graph/build/StaticBlockUtilTest.java new file mode 100644 index 000000000..075bb92b5 --- /dev/null +++ b/sfge/src/test/java/com/salesforce/graph/build/StaticBlockUtilTest.java @@ -0,0 +1,117 @@ +package com.salesforce.graph.build; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +import com.salesforce.TestUtil; +import com.salesforce.graph.ApexPath; +import com.salesforce.graph.vertex.BaseSFVertex; +import com.salesforce.graph.vertex.BlockStatementVertex; +import com.salesforce.graph.vertex.ExpressionStatementVertex; +import com.salesforce.graph.vertex.LiteralExpressionVertex; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; +import java.util.List; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class StaticBlockUtilTest { + protected GraphTraversalSource g; + private static final String SYNTHETIC_STATIC_BLOCK_METHOD_1 = + String.format(StaticBlockUtil.SYNTHETIC_STATIC_BLOCK_METHOD_NAME, 1); + + @BeforeEach + public void setup() { + this.g = TestUtil.getGraph(); + } + + // does each static block have a corresponding synthetic method block? + @Test + public void testMethodForStaticBlock() { + String[] sourceCode = { + "public class MyClass {\n" + + " static {\n" + + " System.debug('inside static block 1');\n" + + "}\n" + + " static {\n" + + " System.debug('inside static block 2');\n" + + " }\n" + + "void doSomething() {\n" + + " System.debug('inside doSomething');\n" + + "}\n" + + "}" + }; + + GraphBuildTestUtil.buildGraph(g, sourceCode); + + verifyStaticBlockMethod( + String.format(StaticBlockUtil.SYNTHETIC_STATIC_BLOCK_METHOD_NAME, 1), + "inside static block 1"); + verifyStaticBlockMethod( + String.format(StaticBlockUtil.SYNTHETIC_STATIC_BLOCK_METHOD_NAME, 2), + "inside static block 2"); + } + + private void verifyStaticBlockMethod(String methodName, String printedString) { + final List staticBlockPaths = GraphBuildTestUtil.walkAllPaths(g, methodName); + assertThat(staticBlockPaths, hasSize(1)); + final ApexPath path = staticBlockPaths.get(0); + + // Make sure synthetic method is linked to the static block and its contents + final BlockStatementVertex blockStatementVertex = (BlockStatementVertex) path.firstVertex(); + final List blockStmtChildren = blockStatementVertex.getChildren(); + assertThat(blockStmtChildren, hasSize(1)); + + final ExpressionStatementVertex expressionStatementVertex = + (ExpressionStatementVertex) blockStmtChildren.get(0); + final List exprStmtChildren = expressionStatementVertex.getChildren(); + assertThat(exprStmtChildren, hasSize(1)); + + final MethodCallExpressionVertex methodCallExpressionVertex = + (MethodCallExpressionVertex) exprStmtChildren.get(0); + assertThat(methodCallExpressionVertex.getFullMethodName(), equalTo("System.debug")); + final LiteralExpressionVertex literalExpressionVertex = + (LiteralExpressionVertex) methodCallExpressionVertex.getParameters().get(0); + + assertThat(literalExpressionVertex.getLiteralAsString(), equalTo(printedString)); + } + + // does synthetic invoker method exist and contain the necessary parts and relationship? + @Test + public void testMethodInvokerForStaticBlock() { + String[] sourceCode = { + "public class MyClass {\n" + + " static {\n" + + " System.debug('inside static block');\n" + + "}\n" + + "void doSomething() {\n" + + " System.debug('inside doSomething');\n" + + "}\n" + + "}" + }; + + GraphBuildTestUtil.buildGraph(g, sourceCode); + + final List staticBlockPaths = + GraphBuildTestUtil.walkAllPaths(g, StaticBlockUtil.STATIC_BLOCK_INVOKER_METHOD); + assertThat(staticBlockPaths, hasSize(1)); + final ApexPath path = staticBlockPaths.get(0); + + // Make sure synthetic method is linked to the static block and its contents + final BlockStatementVertex blockStatementVertex = (BlockStatementVertex) path.firstVertex(); + final List blockStmtChildren = blockStatementVertex.getChildren(); + assertThat(blockStmtChildren, hasSize(1)); + + final ExpressionStatementVertex expressionStatementVertex = + (ExpressionStatementVertex) blockStmtChildren.get(0); + final List exprStmtChildren = expressionStatementVertex.getChildren(); + assertThat(exprStmtChildren, hasSize(1)); + + final MethodCallExpressionVertex methodCallExpressionVertex = + (MethodCallExpressionVertex) exprStmtChildren.get(0); + assertThat( + methodCallExpressionVertex.getFullMethodName(), + equalTo(SYNTHETIC_STATIC_BLOCK_METHOD_1)); + } +} diff --git a/sfge/src/test/java/com/salesforce/graph/symbols/StaticCodeBlockInvocationTest.java b/sfge/src/test/java/com/salesforce/graph/symbols/StaticCodeBlockInvocationTest.java new file mode 100644 index 000000000..914be6b5b --- /dev/null +++ b/sfge/src/test/java/com/salesforce/graph/symbols/StaticCodeBlockInvocationTest.java @@ -0,0 +1,300 @@ +package com.salesforce.graph.symbols; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.salesforce.TestRunner; +import com.salesforce.TestUtil; +import com.salesforce.graph.symbols.apex.ApexStringValue; +import com.salesforce.graph.symbols.apex.ApexValue; +import com.salesforce.graph.visitor.SystemDebugAccumulator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class StaticCodeBlockInvocationTest { + protected GraphTraversalSource g; + + @BeforeEach + public void setup() { + this.g = TestUtil.getGraph(); + } + + @Test + public void testSingleStaticBlockFromStaticMethod() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + + " static {\n" + + " System.debug('static block');\n" + + " }\n" + + " public static String foo() {\n" + + " return 'hello';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat(allResults, hasSize(2)); + + // verify that static block is invoked first + ApexStringValue stringValue = (ApexStringValue) allResults.get(0).get(); + assertThat(stringValue.getValue().get(), equalTo("static block")); + } + + @Test + public void testStaticBlockFromConstructor() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + + " static {\n" + + " System.debug('static block');\n" + + " }\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " StaticBlockClass sb = new StaticBlockClass();\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat(allResults, hasSize(1)); + + // verify that static block is invoked first + ApexStringValue stringValue = (ApexStringValue) allResults.get(0).get(); + assertThat(stringValue.getValue().get(), equalTo("static block")); + } + + @Test + public void testNoStaticBlock() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + " public static String foo = 'hello';\n" + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo);\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat(allResults, hasSize(1)); + + // verify non-static-block case is handled correctly + ApexStringValue stringValue = (ApexStringValue) allResults.get(0).get(); + assertThat(stringValue.getValue().get(), equalTo("hello")); + } + + @Test + @Disabled // TODO: static field defined along with static block should not be double-invoked + public void testSingleStaticBlockAndField() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + + " static {\n" + + " System.debug('static block');\n" + + " }\n" + + " static String myStr = 'hello';\n" + + " public static String foo() {\n" + + " return 'hello';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat(allResults, hasSize(2)); + + // verify that static block is invoked first + ApexStringValue stringValue = (ApexStringValue) allResults.get(0).get(); + assertThat(stringValue.getValue().get(), equalTo("static block")); + } + + @Test + @Disabled // TODO: static field defined along with static block should not be double-invoked + public void testSingleStaticBlockAndFieldInvokingAMethod() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + + " static {\n" + + " System.debug('static block');\n" + + " }\n" + + " static String myStr = foo();\n" + + " public static String bar() {\n" + + " return 'bar';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.bar());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat(allResults, hasSize(2)); + + // verify that static block is invoked first + ApexStringValue stringValue = (ApexStringValue) allResults.get(0).get(); + assertThat(stringValue.getValue().get(), equalTo("static block")); + } + + @Test + public void testMultipleStaticBlocks() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + + " static {\n" + + " System.debug('static block 1');\n" + + " }\n" + + " static {\n" + + " System.debug('static block 2');\n" + + " }\n" + + " public static String foo() {\n" + + " return 'hello';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + final List resultStrings = getResultStrings(allResults); + assertTrue(resultStrings.contains("static block 1")); + assertTrue(resultStrings.contains("static block 2")); + assertTrue(resultStrings.contains("hello")); + } + + @Test + @Disabled // TODO: fix issue where static block 2 is invoked twice instead of once + public void testEachStaticBlockIsInvokedOnlyOnce() { + String[] sourceCode = { + "public class StaticBlockClass {\n" + + " static {\n" + + " System.debug('static block 1');\n" + + " }\n" + + " static {\n" + + " System.debug('static block 2');\n" + + " }\n" + + " public static String foo() {\n" + + " return 'hello';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat( + allResults, + hasSize(3)); // TODO: this is currently returning 4 where static block 2 gets + // invoked twice + } + + @Test + public void testSuperClassStaticBlocks() { + String[] sourceCode = { + "public class SuperStaticBlockClass {\n" + + " static {\n" + + " System.debug('super static block');\n" + + " }\n" + + "}", + "public class StaticBlockClass extends SuperStaticBlockClass {\n" + + " static {\n" + + " System.debug('static block');\n" + + " }\n" + + " public static String foo() {\n" + + " return 'hello';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + final List resultStrings = getResultStrings(allResults); + assertTrue(resultStrings.contains("super static block")); + assertTrue(resultStrings.contains("static block")); + assertTrue(resultStrings.contains("hello")); + } + + @Test + @Disabled // TODO: super class's static block should be invoked only once + public void testSuperClassStaticBlocksInvokedOnceEach() { + String[] sourceCode = { + "public class SuperStaticBlockClass {\n" + + " static {\n" + + " System.debug('super static block');\n" + + " }\n" + + "}", + "public class StaticBlockClass extends SuperStaticBlockClass {\n" + + " static {\n" + + " System.debug('static block');\n" + + " }\n" + + " public static String foo() {\n" + + " return 'hello';\n" + + "}\n" + + "}", + "public class MyClass {\n" + + " public void doSomething() {\n" + + " System.debug(StaticBlockClass.foo());\n" + + " }\n" + + "}" + }; + + TestRunner.Result result = TestRunner.walkPath(g, sourceCode); + SystemDebugAccumulator visitor = result.getVisitor(); + final List>> allResults = visitor.getAllResults(); + + assertThat(allResults, hasSize(3)); + } + + private List getResultStrings(List>> allResults) { + return allResults.stream() + .map(r -> ((ApexStringValue) r.get()).getValue().get()) + .collect(Collectors.toList()); + } +}