diff --git a/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java b/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java index 0c45737c0..37c1d3eed 100644 --- a/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java +++ b/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java @@ -9,11 +9,13 @@ public final class UserFacingMessages { public static final class RuleDescriptions { public static final String APEX_NULL_POINTER_EXCEPTION_RULE = - "Identfies Apex operations that dereference null objects and throw NullPointerExceptions."; + "Identifies Apex operations that dereference null objects and throw NullPointerExceptions."; public static final String UNIMPLEMENTED_TYPE_RULE = "Identifies abstract classes and interfaces that are non-global and don't have implementations or extensions."; public static final String UNUSED_METHOD_RULE = "Identifies methods that aren't invoked from recognized entry points."; + public static final String MULTIPLE_MASS_SCHEMA_LOOKUP_RULE = + "Detects mass schema lookups that can cause performance degradation if made more than once in a path. These methods are: Schema.getGlobalDescribe() and Schema.describeSObjects(...). Flagged lookups include those within a loop or multiple invocations in a path."; } public static final class RuleViolationTemplates { @@ -75,4 +77,10 @@ public static final class CompilationErrors { "Graph engine encountered compilation errors. Fix the errors in %s and retry."; public static final String EXCEPTION_FORMAT_TEMPLATE = "%s, Caused by:\n%s"; } + + public static final class MultipleMassSchemaLookupRuleTemplates { + public static final String MESSAGE_TEMPLATE = "%s was %s at %s:%d."; + public static final String OCCURRENCE_LOOP_TEMPLATE = "called inside a %s"; + public static final String OCCURRENCE_MULTIPLE_TEMPLATE = "preceded by a call to %s"; + } } diff --git a/sfge/src/main/java/com/salesforce/graph/visitor/DefaultNoOpPathVertexVisitor.java b/sfge/src/main/java/com/salesforce/graph/visitor/DefaultNoOpPathVertexVisitor.java index 66ac55361..1fcf25aa2 100644 --- a/sfge/src/main/java/com/salesforce/graph/visitor/DefaultNoOpPathVertexVisitor.java +++ b/sfge/src/main/java/com/salesforce/graph/visitor/DefaultNoOpPathVertexVisitor.java @@ -3,47 +3,7 @@ import com.salesforce.graph.ApexPath; import com.salesforce.graph.symbols.DefaultNoOpScopeVisitor; import com.salesforce.graph.symbols.SymbolProvider; -import com.salesforce.graph.vertex.AssignmentExpressionVertex; -import com.salesforce.graph.vertex.BaseSFVertex; -import com.salesforce.graph.vertex.BlockStatementVertex; -import com.salesforce.graph.vertex.CatchBlockStatementVertex; -import com.salesforce.graph.vertex.DmlDeleteStatementVertex; -import com.salesforce.graph.vertex.DmlInsertStatementVertex; -import com.salesforce.graph.vertex.DmlMergeStatementVertex; -import com.salesforce.graph.vertex.DmlUndeleteStatementVertex; -import com.salesforce.graph.vertex.DmlUpdateStatementVertex; -import com.salesforce.graph.vertex.DmlUpsertStatementVertex; -import com.salesforce.graph.vertex.ElseWhenBlockVertex; -import com.salesforce.graph.vertex.EmptyReferenceExpressionVertex; -import com.salesforce.graph.vertex.ExpressionStatementVertex; -import com.salesforce.graph.vertex.FieldDeclarationStatementsVertex; -import com.salesforce.graph.vertex.FieldDeclarationVertex; -import com.salesforce.graph.vertex.FieldVertex; -import com.salesforce.graph.vertex.ForEachStatementVertex; -import com.salesforce.graph.vertex.ForLoopStatementVertex; -import com.salesforce.graph.vertex.IfBlockStatementVertex; -import com.salesforce.graph.vertex.IfElseBlockStatementVertex; -import com.salesforce.graph.vertex.LiteralExpressionVertex; -import com.salesforce.graph.vertex.MethodCallExpressionVertex; -import com.salesforce.graph.vertex.MethodVertex; -import com.salesforce.graph.vertex.ModifierNodeVertex; -import com.salesforce.graph.vertex.NewKeyValueObjectExpressionVertex; -import com.salesforce.graph.vertex.NewListLiteralExpressionVertex; -import com.salesforce.graph.vertex.NewObjectExpressionVertex; -import com.salesforce.graph.vertex.ParameterVertex; -import com.salesforce.graph.vertex.PrefixExpressionVertex; -import com.salesforce.graph.vertex.ReferenceExpressionVertex; -import com.salesforce.graph.vertex.ReturnStatementVertex; -import com.salesforce.graph.vertex.SoqlExpressionVertex; -import com.salesforce.graph.vertex.StandardConditionVertex; -import com.salesforce.graph.vertex.SuperMethodCallExpressionVertex; -import com.salesforce.graph.vertex.SwitchStatementVertex; -import com.salesforce.graph.vertex.ThrowStatementVertex; -import com.salesforce.graph.vertex.TryCatchFinallyBlockStatementVertex; -import com.salesforce.graph.vertex.ValueWhenBlockVertex; -import com.salesforce.graph.vertex.VariableDeclarationStatementsVertex; -import com.salesforce.graph.vertex.VariableDeclarationVertex; -import com.salesforce.graph.vertex.VariableExpressionVertex; +import com.salesforce.graph.vertex.*; /** * A visitor that does nothing, useful for deriving visitors. Delegates to {@link @@ -284,6 +244,9 @@ public boolean visit(VariableExpressionVertex.Single vertex, SymbolProvider symb @Override public void afterVisit(BaseSFVertex vertex, SymbolProvider symbols) {} + @Override + public void afterVisit(DoLoopStatementVertex vertex, SymbolProvider symbols) {} + @Override public void afterVisit(DmlDeleteStatementVertex vertex, SymbolProvider symbols) {} @@ -302,6 +265,12 @@ public void afterVisit(DmlUpdateStatementVertex vertex, SymbolProvider symbols) @Override public void afterVisit(DmlUpsertStatementVertex vertex, SymbolProvider symbols) {} + @Override + public void afterVisit(ForEachStatementVertex vertex, SymbolProvider symbols) {} + + @Override + public void afterVisit(ForLoopStatementVertex vertex, SymbolProvider symbols) {} + @Override public void afterVisit(FieldDeclarationVertex vertex, SymbolProvider symbols) {} @@ -322,4 +291,7 @@ public void afterVisit(StandardConditionVertex.Positive vertex, SymbolProvider s @Override public void afterVisit(ThrowStatementVertex vertex, SymbolProvider symbols) {} + + @Override + public void afterVisit(WhileLoopStatementVertex vertex, SymbolProvider symbols) {} } diff --git a/sfge/src/main/java/com/salesforce/graph/visitor/PathVertexVisitor.java b/sfge/src/main/java/com/salesforce/graph/visitor/PathVertexVisitor.java index c79390204..1be9cea7b 100644 --- a/sfge/src/main/java/com/salesforce/graph/visitor/PathVertexVisitor.java +++ b/sfge/src/main/java/com/salesforce/graph/visitor/PathVertexVisitor.java @@ -2,47 +2,7 @@ import com.salesforce.graph.ApexPath; import com.salesforce.graph.symbols.SymbolProvider; -import com.salesforce.graph.vertex.AssignmentExpressionVertex; -import com.salesforce.graph.vertex.BaseSFVertex; -import com.salesforce.graph.vertex.BlockStatementVertex; -import com.salesforce.graph.vertex.CatchBlockStatementVertex; -import com.salesforce.graph.vertex.DmlDeleteStatementVertex; -import com.salesforce.graph.vertex.DmlInsertStatementVertex; -import com.salesforce.graph.vertex.DmlMergeStatementVertex; -import com.salesforce.graph.vertex.DmlUndeleteStatementVertex; -import com.salesforce.graph.vertex.DmlUpdateStatementVertex; -import com.salesforce.graph.vertex.DmlUpsertStatementVertex; -import com.salesforce.graph.vertex.ElseWhenBlockVertex; -import com.salesforce.graph.vertex.EmptyReferenceExpressionVertex; -import com.salesforce.graph.vertex.ExpressionStatementVertex; -import com.salesforce.graph.vertex.FieldDeclarationStatementsVertex; -import com.salesforce.graph.vertex.FieldDeclarationVertex; -import com.salesforce.graph.vertex.FieldVertex; -import com.salesforce.graph.vertex.ForEachStatementVertex; -import com.salesforce.graph.vertex.ForLoopStatementVertex; -import com.salesforce.graph.vertex.IfBlockStatementVertex; -import com.salesforce.graph.vertex.IfElseBlockStatementVertex; -import com.salesforce.graph.vertex.LiteralExpressionVertex; -import com.salesforce.graph.vertex.MethodCallExpressionVertex; -import com.salesforce.graph.vertex.MethodVertex; -import com.salesforce.graph.vertex.ModifierNodeVertex; -import com.salesforce.graph.vertex.NewKeyValueObjectExpressionVertex; -import com.salesforce.graph.vertex.NewListLiteralExpressionVertex; -import com.salesforce.graph.vertex.NewObjectExpressionVertex; -import com.salesforce.graph.vertex.ParameterVertex; -import com.salesforce.graph.vertex.PrefixExpressionVertex; -import com.salesforce.graph.vertex.ReferenceExpressionVertex; -import com.salesforce.graph.vertex.ReturnStatementVertex; -import com.salesforce.graph.vertex.SoqlExpressionVertex; -import com.salesforce.graph.vertex.StandardConditionVertex; -import com.salesforce.graph.vertex.SuperMethodCallExpressionVertex; -import com.salesforce.graph.vertex.SwitchStatementVertex; -import com.salesforce.graph.vertex.ThrowStatementVertex; -import com.salesforce.graph.vertex.TryCatchFinallyBlockStatementVertex; -import com.salesforce.graph.vertex.ValueWhenBlockVertex; -import com.salesforce.graph.vertex.VariableDeclarationStatementsVertex; -import com.salesforce.graph.vertex.VariableDeclarationVertex; -import com.salesforce.graph.vertex.VariableExpressionVertex; +import com.salesforce.graph.vertex.*; /** * Visits vertices in a particular path. Return true if the visitor does not want to visit the @@ -147,6 +107,8 @@ void recursionDetected( void afterVisit(BaseSFVertex vertex, SymbolProvider symbols); + void afterVisit(DoLoopStatementVertex vertex, SymbolProvider symbols); + void afterVisit(DmlDeleteStatementVertex vertex, SymbolProvider symbols); void afterVisit(DmlInsertStatementVertex vertex, SymbolProvider symbols); @@ -159,6 +121,10 @@ void recursionDetected( void afterVisit(DmlUpsertStatementVertex vertex, SymbolProvider symbols); + void afterVisit(ForEachStatementVertex vertex, SymbolProvider symbols); + + void afterVisit(ForLoopStatementVertex vertex, SymbolProvider symbols); + void afterVisit(FieldDeclarationVertex vertex, SymbolProvider symbols); void afterVisit(MethodCallExpressionVertex vertex, SymbolProvider symbols); @@ -172,4 +138,6 @@ void recursionDetected( void afterVisit(StandardConditionVertex.Positive vertex, SymbolProvider symbols); void afterVisit(ThrowStatementVertex vertex, SymbolProvider symbols); + + void afterVisit(WhileLoopStatementVertex vertex, SymbolProvider symbols); } diff --git a/sfge/src/main/java/com/salesforce/rules/MultipleMassSchemaLookupRule.java b/sfge/src/main/java/com/salesforce/rules/MultipleMassSchemaLookupRule.java new file mode 100644 index 000000000..9f0a6ddcd --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/MultipleMassSchemaLookupRule.java @@ -0,0 +1,63 @@ +package com.salesforce.rules; + +import com.salesforce.config.UserFacingMessages; +import com.salesforce.graph.ApexPath; +import com.salesforce.graph.vertex.BaseSFVertex; +import com.salesforce.rules.multiplemassschemalookup.MultipleMassSchemaLookupRuleHandler; +import java.util.ArrayList; +import java.util.List; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; + +/** Rule to detect possible performance degradations while invoking Schema.getGlobalDescribe(). */ +public class MultipleMassSchemaLookupRule extends AbstractPathTraversalRule { + + private final MultipleMassSchemaLookupRuleHandler ruleHandler; + + private MultipleMassSchemaLookupRule() { + ruleHandler = MultipleMassSchemaLookupRuleHandler.getInstance(); + } + + @Override + public boolean test(BaseSFVertex vertex) { + return ruleHandler.test(vertex); + } + + @Override + protected List _run(GraphTraversalSource g, ApexPath path, BaseSFVertex vertex) { + List violations = new ArrayList<>(); + + violations.addAll(ruleHandler.detectViolations(g, path, vertex)); + + return violations; + } + + @Override + protected int getSeverity() { + return SEVERITY.HIGH.code; + } + + @Override + protected String getDescription() { + return UserFacingMessages.RuleDescriptions.MULTIPLE_MASS_SCHEMA_LOOKUP_RULE; + } + + @Override + protected String getCategory() { + return CATEGORY.PERFORMANCE.name; + } + + @Override + protected boolean isEnabled() { + return false; + } + + public static MultipleMassSchemaLookupRule getInstance() { + return MultipleMassSchemaLookupRule.LazyHolder.INSTANCE; + } + + private static final class LazyHolder { + // Postpone initialization until first use + private static final MultipleMassSchemaLookupRule INSTANCE = + new MultipleMassSchemaLookupRule(); + } +} diff --git a/sfge/src/main/java/com/salesforce/rules/PathBasedRuleRunner.java b/sfge/src/main/java/com/salesforce/rules/PathBasedRuleRunner.java index f3f7c8fa8..fd3c0eb70 100644 --- a/sfge/src/main/java/com/salesforce/rules/PathBasedRuleRunner.java +++ b/sfge/src/main/java/com/salesforce/rules/PathBasedRuleRunner.java @@ -13,6 +13,7 @@ import com.salesforce.graph.vertex.SFVertex; import com.salesforce.rules.fls.apex.operations.FlsViolationInfo; import com.salesforce.rules.fls.apex.operations.FlsViolationMessageUtil; +import com.salesforce.rules.multiplemassschemalookup.MultipleMassSchemaLookupInfo; import com.salesforce.rules.ops.ProgressListener; import com.salesforce.rules.ops.ProgressListenerProvider; import java.util.*; @@ -119,7 +120,8 @@ private void executeRulesOnPaths(List paths) { // This set holds the violations whose information couldn't be fully processed at creation // time, and // require post-processing. - final HashSet incompleteThrowables = new HashSet<>(); + final HashSet flsViolationInfos = new HashSet<>(); + final HashSet mmsLookupInfos = new HashSet<>(); // For each path... for (ApexPath path : paths) { // If the path's metadata is present... @@ -142,7 +144,10 @@ private void executeRulesOnPaths(List paths) { // to the list of such // objects. if (ruleThrowable instanceof FlsViolationInfo) { - incompleteThrowables.add((FlsViolationInfo) ruleThrowable); + flsViolationInfos.add((FlsViolationInfo) ruleThrowable); + } else if (ruleThrowable instanceof MultipleMassSchemaLookupInfo) { + // FIXME: PR incoming with refactors to this portion + mmsLookupInfos.add((MultipleMassSchemaLookupInfo) ruleThrowable); } else if (ruleThrowable instanceof Violation) { // If the violation is done, it can just go directly into the results // set. @@ -155,7 +160,8 @@ private void executeRulesOnPaths(List paths) { } } - convertToViolations(incompleteThrowables); + convertFlsInfoToViolations(flsViolationInfos); + convertMmsInfoToViolations(mmsLookupInfos); if (!foundVertex) { // If no vertices were found, we should log something so that information isn't lost, @@ -167,7 +173,16 @@ private void executeRulesOnPaths(List paths) { } } - private void convertToViolations(HashSet flsViolationInfos) { + private void convertMmsInfoToViolations(HashSet mmsLookupInfos) { + for (MultipleMassSchemaLookupInfo mmsLookupInfo : mmsLookupInfos) { + Violation.RuleViolation violation = mmsLookupInfo.convert(); + violation.setPropertiesFromRule(MultipleMassSchemaLookupRule.getInstance()); + violations.add(violation); + } + } + + // TODO: Restructure to make this logic work on other types of violation info too + private void convertFlsInfoToViolations(HashSet flsViolationInfos) { // Consolidate/regroup FLS violations across paths so that there are no // duplicates with different field sets for the same source/sink/dmlOperation final HashSet consolidatedFlsViolationInfos = diff --git a/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MassSchemaLookupInfoUtil.java b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MassSchemaLookupInfoUtil.java new file mode 100644 index 000000000..9bd2261fb --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MassSchemaLookupInfoUtil.java @@ -0,0 +1,51 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.salesforce.config.UserFacingMessages; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; +import com.salesforce.graph.vertex.SFVertex; + +/** Utility to help with violation message creation on AvoidMassSchemaLookupRule */ +public final class MassSchemaLookupInfoUtil { + private MassSchemaLookupInfoUtil() {} + + public static String getMessage(MultipleMassSchemaLookupInfo info) { + return getMessage( + info.getSinkVertex().getFullMethodName(), + info.getRepetitionType(), + getOccurrenceInfoValue(info.getRepetitionType(), info.getRepetitionVertex()), + info.getRepetitionVertex().getDefiningType(), + info.getRepetitionVertex().getBeginLine()); + } + + public static String getMessage( + String sinkMethodName, + RuleConstants.RepetitionType repetitionType, + String occurrenceInfoValue, + String occurrenceClassName, + int occurrenceLine) { + final String occurrenceMessage = getOccurrenceMessage(repetitionType, occurrenceInfoValue); + + return String.format( + UserFacingMessages.MultipleMassSchemaLookupRuleTemplates.MESSAGE_TEMPLATE, + sinkMethodName, + occurrenceMessage, + occurrenceClassName, + occurrenceLine); + } + + private static String getOccurrenceInfoValue( + RuleConstants.RepetitionType repetitionType, SFVertex repetitionVertex) { + if (RuleConstants.RepetitionType.MULTIPLE.equals(repetitionType)) { + // Use method name on template message + return ((MethodCallExpressionVertex) repetitionVertex).getFullMethodName(); + } else { + // Use Loop type on template message + return repetitionVertex.getLabel(); + } + } + + private static String getOccurrenceMessage( + RuleConstants.RepetitionType repetitionType, String value) { + return repetitionType.getMessage(value); + } +} diff --git a/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupInfo.java b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupInfo.java new file mode 100644 index 000000000..f02ed4357 --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupInfo.java @@ -0,0 +1,81 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.salesforce.exception.ProgrammingException; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; +import com.salesforce.graph.vertex.SFVertex; +import com.salesforce.rules.RuleThrowable; +import com.salesforce.rules.Violation; + +/** + * Represents information required to create a violation from {@link + * com.salesforce.rules.MultipleMassSchemaLookupRule}. + */ +public class MultipleMassSchemaLookupInfo implements RuleThrowable { + + /** + * vertex where the expensive Schema lookup happens TODO: Is this always only a + * MethodCallExpression? + */ + private final MethodCallExpressionVertex sinkVertex; + + /** origin of the path leading to the GGD operation * */ + private final SFVertex sourceVertex; + + /** Type of repetition that happens between source and sink * */ + private final RuleConstants.RepetitionType repetitionType; + + /** Vertex where the repetition occurs */ + private final SFVertex repetitionVertex; + + public MultipleMassSchemaLookupInfo( + SFVertex sourceVertex, + MethodCallExpressionVertex sinkVertex, + RuleConstants.RepetitionType repetitionType, + SFVertex repetitionVertex) { + validateInput(repetitionType, repetitionVertex); + this.sourceVertex = sourceVertex; + this.sinkVertex = sinkVertex; + this.repetitionType = repetitionType; + this.repetitionVertex = repetitionVertex; + } + + private void validateInput( + RuleConstants.RepetitionType repetitionType, SFVertex repetitionVertex) { + if (repetitionType == null) { + throw new ProgrammingException("repetitionType cannot be null."); + } + + if (repetitionVertex == null) { + throw new ProgrammingException("repetitionVertex cannot be null."); + } + + if (RuleConstants.RepetitionType.MULTIPLE.equals(repetitionType)) { + if (!(repetitionVertex instanceof MethodCallExpressionVertex)) { + throw new ProgrammingException( + "Repetition of type MULTIPLE can only happen on MethodCallExpressions. repetitionVertex=" + + repetitionVertex); + } + } + } + + public Violation.PathBasedRuleViolation convert() { + return new Violation.PathBasedRuleViolation( + MassSchemaLookupInfoUtil.getMessage(this), sourceVertex, sinkVertex); + } + + public MethodCallExpressionVertex getSinkVertex() { + return sinkVertex; + } + + public SFVertex getSourceVertex() { + return sourceVertex; + } + + public RuleConstants.RepetitionType getRepetitionType() { + return repetitionType; + } + + public SFVertex getRepetitionVertex() { + return repetitionVertex; + } +} diff --git a/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleHandler.java b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleHandler.java new file mode 100644 index 000000000..d0eb306b9 --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleHandler.java @@ -0,0 +1,58 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.salesforce.exception.ProgrammingException; +import com.salesforce.graph.ApexPath; +import com.salesforce.graph.symbols.DefaultSymbolProviderVertexVisitor; +import com.salesforce.graph.vertex.BaseSFVertex; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; +import com.salesforce.graph.vertex.SFVertex; +import com.salesforce.graph.visitor.ApexPathWalker; +import com.salesforce.rules.MultipleMassSchemaLookupRule; +import java.util.Set; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; + +/** Executes internals of {@link MultipleMassSchemaLookupRule} */ +public class MultipleMassSchemaLookupRuleHandler { + + /** + * @param vertex to consider for analysis + * @return true if the vertex parameter requires to be treated as a target vertex for {@link + * MultipleMassSchemaLookupRule}. + */ + public boolean test(BaseSFVertex vertex) { + return vertex instanceof MethodCallExpressionVertex + && RuleConstants.isSchemaExpensiveMethod((MethodCallExpressionVertex) vertex); + } + + public Set detectViolations( + GraphTraversalSource g, ApexPath path, BaseSFVertex vertex) { + if (!(vertex instanceof MethodCallExpressionVertex)) { + throw new ProgrammingException( + "GetGlobalDescribeViolationRule unexpected invoked on an instance that's not MethodCallExpressionVertex. vertex=" + + vertex); + } + + final SFVertex sourceVertex = path.getMethodVertex().orElse(null); + + final MultipleMassSchemaLookupVisitor ruleVisitor = + new MultipleMassSchemaLookupVisitor( + sourceVertex, (MethodCallExpressionVertex) vertex); + // TODO: I'm expecting to add other visitors depending on the other factors we will analyze + // to decide if a GGD call is not performant. + DefaultSymbolProviderVertexVisitor symbols = new DefaultSymbolProviderVertexVisitor(g); + ApexPathWalker.walkPath(g, path, ruleVisitor, symbols); + + // Once it finishes walking, collect any violations thrown + return ruleVisitor.getViolations(); + } + + public static MultipleMassSchemaLookupRuleHandler getInstance() { + return MultipleMassSchemaLookupRuleHandler.LazyHolder.INSTANCE; + } + + private static final class LazyHolder { + // Postpone initialization until first use + private static final MultipleMassSchemaLookupRuleHandler INSTANCE = + new MultipleMassSchemaLookupRuleHandler(); + } +} diff --git a/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupVisitor.java b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupVisitor.java new file mode 100644 index 000000000..fcc190999 --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupVisitor.java @@ -0,0 +1,74 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.salesforce.graph.symbols.SymbolProvider; +import com.salesforce.graph.vertex.BaseSFVertex; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; +import com.salesforce.graph.vertex.SFVertex; +import com.salesforce.rules.ops.LoopDetectionVisitor; +import java.util.HashSet; +import java.util.Set; + +/** + * Visitor detects when more than one invocation of Schema.getGlobalDescribe() is made in a path. + */ +class MultipleMassSchemaLookupVisitor extends LoopDetectionVisitor { + /** Represents the path entry point that this visitor is walking */ + private final SFVertex sourceVertex; + + /** Represents the mass schema lookup vertex that we're looking earlier calls for. */ + private final MethodCallExpressionVertex sinkVertex; + + /** Collects violation information */ + private final HashSet violations; + + /** Indicates if the sink vertex has been visited or not. */ + private boolean isSinkVisited = false; + + MultipleMassSchemaLookupVisitor(SFVertex sourceVertex, MethodCallExpressionVertex sinkVertex) { + this.sourceVertex = sourceVertex; + this.sinkVertex = sinkVertex; + this.violations = new HashSet<>(); + } + + @Override + protected void execAfterLoopVertexVisit(BaseSFVertex vertex, SymbolProvider symbols) { + if (shouldContinue()) { + violations.add( + new MultipleMassSchemaLookupInfo( + sourceVertex, sinkVertex, RuleConstants.RepetitionType.LOOP, vertex)); + } + } + + @Override + public void afterVisit(MethodCallExpressionVertex vertex, SymbolProvider symbols) { + if (vertex.equals(sinkVertex)) { + // Mark sink as visited. From this point on, we don't need to + // look for anymore loops or additional calls. + // TODO: A more performant approach would be to stop walking path from this point + isSinkVisited = true; + } else if (RuleConstants.isSchemaExpensiveMethod(vertex) && shouldContinue()) { + violations.add( + new MultipleMassSchemaLookupInfo( + sourceVertex, + sinkVertex, + RuleConstants.RepetitionType.MULTIPLE, + vertex)); + } + } + + /** + * Decides whether the rule should continue collecting violations + * + * @return true if the rule visitor should continue. + */ + private boolean shouldContinue() { + return !isSinkVisited; + } + + /** + * @return Violations collected by the rule. + */ + Set getViolations() { + return violations; + } +} diff --git a/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/RuleConstants.java b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/RuleConstants.java new file mode 100644 index 000000000..1571ef530 --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/multiplemassschemalookup/RuleConstants.java @@ -0,0 +1,41 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.google.common.collect.ImmutableSet; +import com.salesforce.config.UserFacingMessages; +import com.salesforce.graph.vertex.MethodCallExpressionVertex; + +public class RuleConstants { + + static final String METHOD_SCHEMA_GET_GLOBAL_DESCRIBE = "schema.getglobaldescribe"; + static final String METHOD_SCHEMA_DESCRIBE_SOBJECTS = "schema.describesobjects"; + + private static final ImmutableSet EXPENSIVE_METHODS = + ImmutableSet.of(METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, METHOD_SCHEMA_DESCRIBE_SOBJECTS); + + /** + * @param vertex to check + * @return true if the method call is a known expensive schema call. + */ + static boolean isSchemaExpensiveMethod(MethodCallExpressionVertex vertex) { + final String fullMethodName = vertex.getFullMethodName(); + return EXPENSIVE_METHODS.contains(fullMethodName.toLowerCase()); + } + + /** Enum to indicate the type of repetition the method call was subjected. */ + public enum RepetitionType { + LOOP(UserFacingMessages.MultipleMassSchemaLookupRuleTemplates.OCCURRENCE_LOOP_TEMPLATE), + MULTIPLE( + UserFacingMessages.MultipleMassSchemaLookupRuleTemplates + .OCCURRENCE_MULTIPLE_TEMPLATE); + + String messageTemplate; + + RepetitionType(String messageTemplate) { + this.messageTemplate = messageTemplate; + } + + public String getMessage(String... params) { + return String.format(messageTemplate, params); + } + } +} diff --git a/sfge/src/main/java/com/salesforce/rules/ops/LoopDetectionVisitor.java b/sfge/src/main/java/com/salesforce/rules/ops/LoopDetectionVisitor.java new file mode 100644 index 000000000..02c1f830e --- /dev/null +++ b/sfge/src/main/java/com/salesforce/rules/ops/LoopDetectionVisitor.java @@ -0,0 +1,31 @@ +package com.salesforce.rules.ops; + +import com.salesforce.graph.symbols.SymbolProvider; +import com.salesforce.graph.vertex.*; +import com.salesforce.graph.visitor.DefaultNoOpPathVertexVisitor; + +/** Visitor that gets notified when a loop vertex is invoked in the path. */ +public abstract class LoopDetectionVisitor extends DefaultNoOpPathVertexVisitor { + + protected abstract void execAfterLoopVertexVisit(BaseSFVertex vertex, SymbolProvider symbols); + + @Override + public void afterVisit(DoLoopStatementVertex vertex, SymbolProvider symbols) { + execAfterLoopVertexVisit(vertex, symbols); + } + + @Override + public void afterVisit(ForEachStatementVertex vertex, SymbolProvider symbols) { + execAfterLoopVertexVisit(vertex, symbols); + } + + @Override + public void afterVisit(ForLoopStatementVertex vertex, SymbolProvider symbols) { + execAfterLoopVertexVisit(vertex, symbols); + } + + @Override + public void afterVisit(WhileLoopStatementVertex vertex, SymbolProvider symbols) { + execAfterLoopVertexVisit(vertex, symbols); + } +} diff --git a/sfge/src/test/java/com/salesforce/TestUtil.java b/sfge/src/test/java/com/salesforce/TestUtil.java index c07e868de..7118e9525 100644 --- a/sfge/src/test/java/com/salesforce/TestUtil.java +++ b/sfge/src/test/java/com/salesforce/TestUtil.java @@ -5,7 +5,6 @@ import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.fail; -import com.google.common.collect.Lists; import com.google.gson.Gson; import com.salesforce.apex.jorje.ASTConstants; import com.salesforce.apex.jorje.AstNodeWrapper; @@ -45,10 +44,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -300,6 +296,17 @@ public static List getViolations( String definingType, String methodName, Boolean renderXml) { + + return getViolations(g, sourceCode, definingType, methodName, renderXml, rule); + } + + public static List getViolations( + GraphTraversalSource g, + String[] sourceCode, + String definingType, + String methodName, + Boolean renderXml, + AbstractPathBasedRule... rules) { if (USE_EXISTING_GRAPH != sourceCode) { Config.Builder builder = Config.Builder.get(g, sourceCode); if (renderXml != null) { @@ -310,8 +317,9 @@ public static List getViolations( } final MethodVertex methodVertex = getMethodVertex(g, definingType, methodName); + final PathBasedRuleRunner ruleRunner = - new PathBasedRuleRunner(g, Lists.newArrayList(rule), methodVertex); + new PathBasedRuleRunner(g, Arrays.asList(rules), methodVertex); final List violations = new ArrayList<>(ruleRunner.runRules()); return violations; diff --git a/sfge/src/test/java/com/salesforce/rules/MultipleRuleViolationsScenarioTest.java b/sfge/src/test/java/com/salesforce/rules/MultipleRuleViolationsScenarioTest.java new file mode 100644 index 000000000..6422ba50c --- /dev/null +++ b/sfge/src/test/java/com/salesforce/rules/MultipleRuleViolationsScenarioTest.java @@ -0,0 +1,45 @@ +package com.salesforce.rules; + +import com.salesforce.TestUtil; +import com.salesforce.testutils.BasePathBasedRuleTest; +import java.util.List; +import java.util.TreeSet; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +public class MultipleRuleViolationsScenarioTest extends BasePathBasedRuleTest { + @Test + public void testFlsAndMmsRules() { + String sourceCode = + "public class MyClass {\n" + + " void foo(String[] objectNames) {\n" + + " for (String objectName: objectNames) {\n" + + " Map schemaDescribe = Schema.getGlobalDescribe();\n" + + " SObjectType sot = schemaDescribe.get(objectName);\n" + + " if (sot.getDescribe().isAccessible()){\n" + + " SObject so = sot.newSObject();\n" + + " insert so;\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + + List violations = + TestUtil.getViolations( + g, + new String[] {sourceCode}, + "MyClass", + "foo", + false, + ApexFlsViolationRule.getInstance(), + MultipleMassSchemaLookupRule.getInstance()); + + // Including an additional step that happens in the actual process. + // Any missed field in the violation would show up here as a NPE on comparator. + final TreeSet sortedViolations = new TreeSet<>(); + sortedViolations.addAll(violations); + + MatcherAssert.assertThat(sortedViolations, Matchers.hasSize(2)); + } +} diff --git a/sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/BaseAvoidMultipleMassSchemaLookupTest.java b/sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/BaseAvoidMultipleMassSchemaLookupTest.java new file mode 100644 index 000000000..da79065cc --- /dev/null +++ b/sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/BaseAvoidMultipleMassSchemaLookupTest.java @@ -0,0 +1,20 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.salesforce.rules.MultipleMassSchemaLookupRule; +import com.salesforce.testutils.BasePathBasedRuleTest; +import com.salesforce.testutils.ViolationWrapper; + +/** Base class to tests for {@link MultipleMassSchemaLookupRule} */ +public abstract class BaseAvoidMultipleMassSchemaLookupTest extends BasePathBasedRuleTest { + + protected ViolationWrapper.MassSchemaLookupInfoBuilder expect( + int sinkLine, + String sinkMethodName, + int occurrenceLine, + String occurrenceClassName, + RuleConstants.RepetitionType type, + String typeInfo) { + return ViolationWrapper.MassSchemaLookupInfoBuilder.get( + sinkLine, sinkMethodName, occurrenceLine, occurrenceClassName, type, typeInfo); + } +} diff --git a/sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleTest.java b/sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleTest.java new file mode 100644 index 000000000..718b65956 --- /dev/null +++ b/sfge/src/test/java/com/salesforce/rules/multiplemassschemalookup/MultipleMassSchemaLookupRuleTest.java @@ -0,0 +1,370 @@ +package com.salesforce.rules.multiplemassschemalookup; + +import com.salesforce.apex.jorje.ASTConstants; +import com.salesforce.rules.MultipleMassSchemaLookupRule; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +// TODO: Breakdown to more test suites +public class MultipleMassSchemaLookupRuleTest extends BaseAvoidMultipleMassSchemaLookupTest { + + private static final MultipleMassSchemaLookupRule RULE = + MultipleMassSchemaLookupRule.getInstance(); + + @CsvSource({ + "ForEachStatement, for (String s : myList)", + "ForLoopStatement, for (Integer i; i < s.size; s++)", + "WhileLoopStatement, while(true)" + }) + @ParameterizedTest(name = "{displayName}: {0}") + public void testSimpleGgdInLoop(String loopAstLabel, String loopStructure) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo() {\n" + + " List myList = new String[] {'Account','Contact'};\n" + + " " + loopStructure + " {\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + " }\n" + + "}\n"; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 5, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 4, + "MyClass", + RuleConstants.RepetitionType.LOOP, + loopAstLabel)); + } + + @CsvSource({ + "ForEachStatement, for (String s : myList)", + "ForLoopStatement, for (Integer i; i < s.size; s++)", + "WhileLoopStatement, while(true)" + }) + @ParameterizedTest(name = "{displayName}: {0}") + @Disabled // TODO: Only surrounding loop should be counted as a violation. + public void testSimpleGgdOutsideLoop(String loopAstLabel, String loopStructure) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo() {\n" + + " List myList = new String[] {'Account','Contact'};\n" + + " " + loopStructure + " {\n" + + " String s = 'abc';\n" + + " }\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + "}\n"; + // spotless:on + + assertNoViolation(RULE, sourceCode); + } + + @CsvSource({ + "ForEachStatement, for (String s : myList)", + "ForLoopStatement, for (Integer i; i < s.size; s++)", + "WhileLoopStatement, while(true)" + }) + @ParameterizedTest(name = "{displayName}: {0}") + public void testGgdInLoopInMethodCall(String loopAstLabel, String loopStructure) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo() {\n" + + " List myList = new String[] {'Account','Contact'};\n" + + " " + loopStructure + " {\n" + + " getMoreInfo();\n" + + " }\n" + + " }\n" + + " void getMoreInfo() {\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + "}\n"; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 9, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 4, + "MyClass", + RuleConstants.RepetitionType.LOOP, + loopAstLabel)); + } + + @CsvSource({ + "ForEachStatement, for (String s1 : myList), for (String s2 : myList)", + "ForLoopStatement, for (Integer i; i < myList.size; s++), for (Integer j; j < myList.size; s++)", + "WhileLoopStatement, while(true), while(true)" + }) + @ParameterizedTest(name = "{displayName}: {0}") + public void testLoopWithinLoop( + String loopAstLabel, String loopStructure1, String loopStructure2) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo(String[] myList) {\n" + + " " + loopStructure1 + " {\n" + + " " + loopStructure2 + " {\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + " }\n" + + "}\n" + + "}\n"; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 5, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 3, + "MyClass", + RuleConstants.RepetitionType.LOOP, + loopAstLabel), + expect( + 5, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 4, + "MyClass", + RuleConstants.RepetitionType.LOOP, + loopAstLabel)); + } + + @CsvSource({ + "GGD followed by GGD, Schema.getGlobalDescribe, Schema.getGlobalDescribe", + "GGD followed by DSO, Schema.getGlobalDescribe, Schema.describeSObjects", + "DSO followed by DSO, Schema.describeSObjects, Schema.describeSObjects", + "DSO followed by GGD, Schema.describeSObjects, Schema.getGlobalDescribe" + }) + @ParameterizedTest(name = "{displayName}: {0}") + public void testMultipleInvocations(String testName, String firstCall, String secondCall) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo() {\n" + + " " + firstCall + "();\n" + + " " + secondCall + "();\n" + + " }\n" + + "}\n"; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 4, + secondCall, + 3, + "MyClass", + RuleConstants.RepetitionType.MULTIPLE, + firstCall)); + } + + @Test + public void testForEachLoopInClassInstance() { + // spotless:off + String sourceCode[] = { + "public class MyClass {\n" + + " void foo() {\n" + + " Another[] anotherList = new Another[] {new Another()};\n" + + " for (Another an : anotherList) {\n" + + " an.exec();\n" + + " }\n" + + " }\n" + + "}\n", + "public class Another {\n" + + "void exec() {\n" + + " Schema.getGlobalDescribe();\n" + + "}\n" + + "}\n" + }; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 3, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 4, + "MyClass", + RuleConstants.RepetitionType.LOOP, + ASTConstants.NodeType.FOR_EACH_STATEMENT)); + } + + /** TODO: Handle path expansion from array[index].methodCall() */ + @Test + @Disabled + public void testForLoopInClassInstance() { + // spotless:off + String sourceCode[] = { + "public class MyClass {\n" + + " void foo() {\n" + + " Another[] anotherList = new Another[] {new Another()};\n" + + " for (Integer i = 0 ; i < anotherList.size; i++) {\n" + + " anotherList[i].exec();\n" + + " }\n" + + " }\n" + + "}\n", + "public class Another {\n" + + "void exec() {\n" + + " Schema.getGlobalDescribe();\n" + + "}\n" + + "}\n" + }; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 3, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 4, + "MyClass", + RuleConstants.RepetitionType.LOOP, + ASTConstants.NodeType.FOR_LOOP_STATEMENT)); + } + + @Test + public void testForLoopConditionalOnClassInstance() { + // spotless:off + String sourceCode[] = { + "public class MyClass {\n" + + " void foo() {\n" + + " Another[] anotherList = new Another[] {new Another(false), new Another(false)};\n" + + " for (Another an : anotherList) {\n" + + " an.exec();\n" + + " }\n" + + " }\n" + + "}\n", + "public class Another {\n" + + "boolean shouldCheck;\n" + + "Another(boolean b) {\n" + + " shouldCheck = b;\n" + + "}\n" + + "void exec() {\n" + + " if (shouldCheck) {\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + "}\n" + + "}\n" + }; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 8, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 4, + "MyClass", + RuleConstants.RepetitionType.LOOP, + ASTConstants.NodeType.FOR_EACH_STATEMENT)); + } + + @Test // TODO: Check if this is a false positive. Static block should get invoked only once + public void testLoopFromStaticBlock() { + // spotless:off + String[] sourceCode = { + "public class MyClass {\n" + + " void foo(String[] objectNames) {\n" + + " for (Integer i = 0; i < objectNames.size; i++) {\n" + + " AnotherClass.doNothing();\n" + + " }\n" + + " }\n" + + "}\n", + "public class AnotherClass {\n" + + " static {\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + " static void doNothing() {\n" + + " // do nothing \n" + + " }\n" + + "}\n" + }; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 3, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 3, + "MyClass", + RuleConstants.RepetitionType.LOOP, + ASTConstants.NodeType.FOR_LOOP_STATEMENT)); + } + + @CsvSource({ + "ForEachStatement, for (String s : myList)", + "ForLoopStatement, for (Integer i; i < s.size; s++)", + "WhileLoopStatement, while(true)" + }) + @ParameterizedTest(name = "{displayName}: {0}") + public void testLoopAfterGgdShouldNotGetViolation(String loopAstLabel, String loopStructure) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo() {\n" + + " Schema.getGlobalDescribe();\n" + + " " + loopStructure + " {\n" + + " List myList = new String[] {'Account','Contact'};\n" + + " System.debug('hi');\n" + + " }\n" + + " }\n" + + "}\n"; + // spotless:on + + assertNoViolation(RULE, sourceCode); + } + + @CsvSource({ + "ForEachStatement, for (String s : myList)", + "ForLoopStatement, for (Integer i; i < s.size; s++)", + "WhileLoopStatement, while(true)" + }) + @ParameterizedTest(name = "{displayName}: {0}") + @Disabled // TODO: Only surrounding loop should be counted as a violation. + public void testLoopBeforeAndAroundGgd(String loopAstLabel, String loopStructure) { + // spotless:off + String sourceCode = + "public class MyClass {\n" + + " void foo() {\n" + + " List myList = new String[] {'Account','Contact'};\n" + + " " + loopStructure + " {\n" + + " System.debug('hi');\n" + + " }\n" + + " " + loopStructure + " {\n" + + " Schema.getGlobalDescribe();\n" + + " }\n" + + " }\n" + + "}\n"; + // spotless:on + + assertViolations( + RULE, + sourceCode, + expect( + 8, + RuleConstants.METHOD_SCHEMA_GET_GLOBAL_DESCRIBE, + 7, + "MyClass", + RuleConstants.RepetitionType.LOOP, + loopAstLabel)); + } +} diff --git a/sfge/src/test/java/com/salesforce/testutils/ViolationWrapper.java b/sfge/src/test/java/com/salesforce/testutils/ViolationWrapper.java index addd5c93a..969316f58 100644 --- a/sfge/src/test/java/com/salesforce/testutils/ViolationWrapper.java +++ b/sfge/src/test/java/com/salesforce/testutils/ViolationWrapper.java @@ -4,11 +4,14 @@ import com.salesforce.collections.CollectionUtil; import com.salesforce.config.UserFacingMessages; import com.salesforce.graph.ops.SoqlParserUtil; +import com.salesforce.rules.MultipleMassSchemaLookupRule; import com.salesforce.rules.fls.apex.operations.FlsConstants; import com.salesforce.rules.fls.apex.operations.FlsStripInaccessibleWarningInfo; import com.salesforce.rules.fls.apex.operations.FlsViolationInfo; import com.salesforce.rules.fls.apex.operations.FlsViolationMessageUtil; import com.salesforce.rules.fls.apex.operations.UnresolvedCrudFlsViolationInfo; +import com.salesforce.rules.multiplemassschemalookup.MassSchemaLookupInfoUtil; +import com.salesforce.rules.multiplemassschemalookup.RuleConstants; import java.util.Arrays; import java.util.TreeSet; import java.util.function.Function; @@ -197,6 +200,47 @@ public String getMessage() { } } + /** Message builder to help with testing {@link MultipleMassSchemaLookupRule}. */ + public static class MassSchemaLookupInfoBuilder extends ViolationBuilder { + private final String sinkMethodName; + private final RuleConstants.RepetitionType repetitionType; + private final String typeInfo; + private final String occurrenceClassName; + private final int occurrenceLine; + + private MassSchemaLookupInfoBuilder( + int sinkLine, + String sinkMethodName, + int occurrenceLine, + String occurrenceClassName, + RuleConstants.RepetitionType type, + String typeInfo) { + super(sinkLine); + this.sinkMethodName = sinkMethodName; + this.occurrenceLine = occurrenceLine; + this.occurrenceClassName = occurrenceClassName; + this.repetitionType = type; + this.typeInfo = typeInfo; + } + + public static MassSchemaLookupInfoBuilder get( + int sinkLine, + String sinkMethodName, + int occurrenceLine, + String occurrenceClassName, + RuleConstants.RepetitionType type, + String typeInfo) { + return new MassSchemaLookupInfoBuilder( + sinkLine, sinkMethodName, occurrenceLine, occurrenceClassName, type, typeInfo); + } + + @Override + public String getMessage() { + return MassSchemaLookupInfoUtil.getMessage( + sinkMethodName, repetitionType, typeInfo, occurrenceClassName, occurrenceLine); + } + } + public static class MessageBuilder { private final int line; private final String violationMsg;