diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md index 78f2c3415fe..5c5c5d137d6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md @@ -301,3 +301,14 @@ integration, telemetry connectivity, and Azure Toolkit integration. to the [Troubleshoot dependency version conflicts documentation](https://learn.microsoft.com/en-us/azure/developer/java/sdk/troubleshooting-dependency-version-conflict) for additional information on resolving dependency version conflicts. + + +17. #### Calling 'stop' then 'start' APIs on a ServiceBusProcessor Client + +- **Anti-pattern**: Calling `stop()` followed by `start()` on a `ServiceBusProcessorClient` instance. +- **Issue**: Calling `stop()` followed by `start()` on a `ServiceBusProcessorClient` involves significant complexity and + may + be deprecated in future versions of the SDK. +- **Severity: WARNING** +- **Recommendation**: Please close this processor instance and create a new one to restart processing. Refer to + the [GitHub issue](https://github.com/Azure/azure-sdk-for-java/issues/34464) for more details \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StopThenStartOnServiceBusProcessorCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StopThenStartOnServiceBusProcessorCheck.java new file mode 100644 index 00000000000..8774b5a4df5 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StopThenStartOnServiceBusProcessorCheck.java @@ -0,0 +1,345 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class is a LocalInspectionTool that checks if a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object. + * If this is the case, a problem is registered with the ProblemsHolder. + */ +public class StopThenStartOnServiceBusProcessorCheck extends LocalInspectionTool { + + /** + * This method builds a visitor that visits the PsiMethodCallExpression and checks if a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object. + * If this is the case, a problem is registered with the ProblemsHolder. + * + * @param holder The ProblemsHolder to register the problem with + * @param isOnTheFly A boolean that indicates if the inspection is being run on the fly - not used in this implementation but required by the method signature + * @return A JavaElementVisitor that visits the PsiMethodCallExpression and checks if a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new StopThenStartOnServiceBusProcessorVisitor(holder); + } + + /** + * This class is a JavaElementVisitor that visits the PsiMethodCallExpression and checks if a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object. + * If this is the case, a problem is registered with the ProblemsHolder. + */ + static class StopThenStartOnServiceBusProcessorVisitor extends JavaElementVisitor { + + // Create a ProblemsHolder to register the problem with + private final ProblemsHolder holder; + + // Create a map to store a boolean indicating if stop was called on the variable + private final Map variableStateMap = new HashMap<>(); + + // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + // Create a variable to store the associated variable -- the variable that the method is called on + private static PsiVariable associatedVariable = null; + + // Create a map to store the problems and their line numbers -- to avoid duplicate problems + private static final Map problemsMap = new HashMap<>(); + + // Load the rule configuration + static { + final String ruleName = "StopThenStartOnServiceBusProcessorCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || RULE_CONFIG.getClientsToCheck().isEmpty(); + } + + /** + * This constructor initializes the Visitor with the ProblemsHolder + * + * @param holder The ProblemsHolder to register the problem with when found + */ + StopThenStartOnServiceBusProcessorVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** + * This method visits the PsiElement and checks if it's a declaration of a ServiceBusProcessorClient object. + * If it is, the associated variable is stored in the variableStateMap. + * The method also checks the body of the method for method calls and their definitions. + * If a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object, a problem is registered with the ProblemsHolder. + * The method also resets the associated variable to false after visiting the method body once the recursive calls are done. + * + * @param element The PsiElement to visit + */ + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + + // check if there's been a new expression + if (SKIP_WHOLE_RULE) { + return; + } + + // Check if the element is a declaration of a ServiceBusProcessorClient object + if (element instanceof PsiNewExpression) { + PsiNewExpression newExpression = (PsiNewExpression) element; + + PsiVariable tempVariable = findAssociatedVariable(newExpression); + + if (tempVariable != null && isServiceBusProcessorClient(tempVariable)) { + associatedVariable = tempVariable; + variableStateMap.put(associatedVariable.hashCode(), false); + } + } + + + // Check the body of the method for method calls and their definitions + if (element instanceof PsiMethod && associatedVariable != null) { + PsiMethod method = (PsiMethod) element; + PsiCodeBlock body = method.getBody(); + + if (body != null) { + + // Start visiting the body elements + visitMethodBody(body); + } + + // Reset the associated variable to false after visiting the method body + if (associatedVariable != null) { + variableStateMap.put(associatedVariable.hashCode(), false); + } + } + } + + /** + * This method recursively visits all method calls and their definitions in the PsiCodeBlock. + * It checks if a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object. + * If this is the case, a problem is registered with the ProblemsHolder. + * + * @param body The PsiCodeBlock to visit + */ + private void visitMethodBody(@NotNull PsiCodeBlock body) { + + // Iterate over the children of the method body to find method calls + for (PsiElement child : body.getChildren()) { + + // Only process expression statements - they are Java statements that end in a semicolon + if (!(child instanceof PsiExpressionStatement)) { + continue; + } + + // Get the expression from the statement + PsiExpression expression = ((PsiExpressionStatement) child).getExpression(); + + // Only process method calls + if (!(expression instanceof PsiMethodCallExpression)) { + continue; + } + + // Handle the method call + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) expression; + + // check the element being called on -- if it's a variable, check if it's the one we're tracking + PsiElement element = methodCall.getMethodExpression().getQualifierExpression(); + + if (element instanceof PsiReferenceExpression) { + + PsiReferenceExpression reference = (PsiReferenceExpression) element; + PsiElement resolvedElement = reference.resolve(); + + // Check if the resolved element is a variable + if ((resolvedElement instanceof PsiVariable)) { + + if (variableStateMap.containsKey(resolvedElement.hashCode())) { + + // Check the API calls on the variable we're tracking for a stop then start + if (checkMethodCall(methodCall)) { + + // Get the containing file + PsiFile psiFile = element.getContainingFile(); + + // Get the project from the PsiFile + Project project = psiFile.getProject(); + + // Get the document corresponding to the PsiFile + Document document = PsiDocumentManager.getInstance(project).getDocument(psiFile); + + if (document != null) { + // Get the offset of the element + int offset = element.getTextOffset(); + + // Get the line number corresponding to the offset + int lineNumber = document.getLineNumber(offset); + + if (!problemsMap.containsKey(lineNumber)) { + holder.registerProblem(methodCall, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + problemsMap.put(lineNumber, methodCall.getText()); + return; + } + } + } + } + } + } + + // Resolve the method call to its method definition + PsiMethod resolvedMethod = methodCall.resolveMethod(); + + if (resolvedMethod == null) { + continue; + } + + PsiFile containingFile = resolvedMethod.getContainingFile(); + if (containingFile == null) { + continue; + } + + // Check if the containing file is within the current project + boolean isInCurrentProject = isFileInCurrentProject(containingFile); + if (isInCurrentProject) { + PsiCodeBlock resolvedBody = resolvedMethod.getBody(); + if (resolvedBody != null) { + visitMethodBody(resolvedBody); // Recursively visit the resolved method's body + } + } + } + } + + + /** + * This method visits the PsiMethodCallExpression and checks if a stop method is called on a ServiceBusProcessorClient object, followed by a start method call on the same object. + * If this is the case, a problem is registered with the ProblemsHolder. + * + * @param expression The PsiMethodCallExpression to visit + */ + private boolean checkMethodCall(PsiMethodCallExpression expression) { + + // Check if the method being called is 'stop' or 'start' + PsiReferenceExpression methodExpression = expression.getMethodExpression(); + String methodName = methodExpression.getReferenceName(); + + if (!(RULE_CONFIG.getMethodsToCheck().contains(methodName))) { + return false; + } + + // Get the qualifier of the method call - the object on which the method is called + PsiExpression qualifier = methodExpression.getQualifierExpression(); + + if (!(qualifier instanceof PsiReferenceExpression)) { + return false; + } + PsiElement reference = ((PsiReferenceExpression) qualifier).resolve(); + + if (!(reference instanceof PsiVariable)) { + return false; + } + + // Get the variable that the method is called on + PsiVariable variable = (PsiVariable) reference; + + // Boolean indicating if stop was called on the variable + Boolean wasStopCalled = variableStateMap.get(variable.hashCode()); + + // If 'stop' is called, mark that 'stop' was called on the variable + if ("stop".equals(methodName)) { + variableStateMap.put(variable.hashCode(), true); // Mark that stop was called + + // If 'start' is called and 'stop' was called on the variable, register a problem + } else if ("start".equals(methodName)) { + return wasStopCalled; + } + return false; + } + + /** + * This method checks if the type of the variable is ServiceBusProcessorClient + * + * @param variable The variable to check + * @return A boolean indicating if the type of the variable is ServiceBusProcessorClient + */ + private static boolean isServiceBusProcessorClient(PsiVariable variable) { + + PsiType type = variable.getType(); + String typeText = type.getCanonicalText(); + return typeText != null && typeText.contains(RULE_CONFIG.getClientsToCheck().get(0)) && typeText.startsWith(RuleConfig.AZURE_PACKAGE_NAME); + } + + /** + * This method finds the variable associated with a PsiNewExpression. + * + * @param newExpression The PsiNewExpression instance. + * @return The associated PsiVariable or null if not found. + */ + public PsiVariable findAssociatedVariable(PsiNewExpression newExpression) { + + // Get the parent of the new expression -- the variable declaration or assignment + PsiElement parent = newExpression.getParent(); + + // Traverse upwards to find a variable declaration or assignment + while (parent != null) { + + //I If the parent is a variable declaration, return the variable + if (parent instanceof PsiVariable) { + return (PsiVariable) parent; // Direct variable initialization + + // Get the AssignmentExpression parent of the variable declaration + } else if (parent instanceof PsiAssignmentExpression) { + PsiAssignmentExpression assignmentExpression = (PsiAssignmentExpression) parent; + + // Get the left expression of the assignment + PsiExpression lhs = assignmentExpression.getLExpression(); + + // If the left expression is a reference expression, resolve it to get the variable associated with the assignment + if (lhs instanceof PsiReferenceExpression) { + PsiReferenceExpression referenceExpression = (PsiReferenceExpression) lhs; + + PsiElement resolvedElement = referenceExpression.resolve(); + + if (resolvedElement instanceof PsiVariable) { + return (PsiVariable) resolvedElement; // Variable in assignment + } + } + } + // Move up the tree + parent = parent.getParent(); + } + return null; // No associated variable found + } + + /** + * Helper method to check if the file is in the current project. + * It's used to check if the method call is in the current project (i.e defined by the user and not a library). + * + * @param file The PsiFile to check + * @return A boolean indicating if the file is in the current project + */ + private boolean isFileInCurrentProject(PsiFile file) { + // Assuming `project` is an instance of com.intellij.openapi.project.Project + Project project = file.getProject(); + return ProjectRootManager.getInstance(project).getFileIndex().isInContent(file.getVirtualFile()); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml index 14668726b33..fb82fbb5102 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml @@ -261,5 +261,16 @@ com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.IncompatibleDependencyCheck + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.StopThenStartOnServiceBusProcessorCheck + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json index ed53fc1cb91..611ac782d16 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json @@ -143,5 +143,14 @@ "IncompatibleDependencyCheck": { "url": "https://raw.githubusercontent.com/Azure/azure-sdk-for-java/main/eng/versioning/supported_external_dependency_versions.json", "antiPatternMessage": "The version of {{fullName}} is not compatible with other dependencies of the same library defined in the pom.xml. Please use versions of the same library release group {{recommendedVersion}}.x to ensure proper functionality." + }, + "StopThenStartOnServiceBusProcessorCheck": { + "methodsToCheck": [ + "stop", + "start", + "close" + ], + "clientsToCheck": "ServiceBusProcessorClient", + "antiPatternMessage": "Starting Processor that was stopped before is not recommended, and this feature may be deprecated in the future. Please close this processor instance and create a new one to restart processing" } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StopThenStartOnServiceBusProcessorCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StopThenStartOnServiceBusProcessorCheckTest.java new file mode 100644 index 00000000000..178dd1d059b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StopThenStartOnServiceBusProcessorCheckTest.java @@ -0,0 +1,356 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.StopThenStartOnServiceBusProcessorCheck.StopThenStartOnServiceBusProcessorVisitor; + +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * This class tests the StopThenStartOnServiceBusProcessorCheck class + */ +public class StopThenStartOnServiceBusProcessorCheckTest { + + // Create a mock ProblemsHolder to be used in the test + @Mock + private ProblemsHolder mockHolder; + + // Create a mock visitor for visiting the PsiMethodCallExpression + @Mock + private StopThenStartOnServiceBusProcessorVisitor mockVisitor; + + @BeforeEach + public void setUp() { + // Set up the test + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when `stop` is called before `start` + * on the same variable in the same method body without recursion. + * The test case should register a problem with the ProblemsHolder. + */ + @Test + public void testSameVariableNonRecursiveStopThenStart() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 1; + + boolean sameVariable = true; + boolean recursiveSecondLevel = false; + boolean isStopThenStart = true; + boolean directVariableInitialization = false; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when 'start' is not called after 'stop' + * on the same variable in a different method body with recursion. + * The test case should not register a problem with the ProblemsHolder. + */ + @Test + public void testSameVariableNonRecursiveStartThenStop() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 0; + + boolean sameVariable = true; + boolean recursiveSecondLevel = true; + boolean isStopThenStart = false; + boolean directVariableInitialization = true; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when `stop` is called before `start` + * on different variables in the same method body without recursion. + * The test case should not register a problem with the ProblemsHolder. + */ + @Test + public void testDifferentVariablesNonRecursiveStopThenStart() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 0; + + boolean sameVariable = false; + boolean recursiveSecondLevel = false; + boolean isStopThenStart = true; + boolean directVariableInitialization = false; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when 'start' is not called after 'stop' + * on different variables in the same method body with recursion. + * The test case should not register a problem with the ProblemsHolder. + */ + @Test + public void testDifferentVariablesNonRecursiveStartThenStop() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 0; + + boolean sameVariable = false; + boolean recursiveSecondLevel = true; + boolean isStopThenStart = false; + boolean directVariableInitialization = true; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when `stop` is called before `start` + * on the same variable in a recursive second level method. + * The test case should register a problem with the ProblemsHolder. + */ + @Test + public void testSameVariableRecursiveStopThenStart() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 1; + + boolean sameVariable = true; + boolean recursiveSecondLevel = true; + boolean isStopThenStart = true; + boolean directVariableInitialization = false; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when 'start' is not called after 'stop' + * on the same variable in a recursive second level method. + * The test case should not register a problem with the ProblemsHolder. + */ + @Test + public void testSameVariableRecursiveStartThenStop() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 0; + + boolean sameVariable = true; + boolean recursiveSecondLevel = true; + boolean isStopThenStart = false; + boolean directVariableInitialization = true; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when `stop` is called before `start` + * on different variables in a recursive second level method. + * The test case should not register a problem with the ProblemsHolder. + */ + @Test + public void testDifferentVariablesRecursiveStopThenStart() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 0; + + boolean sameVariable = false; + boolean recursiveSecondLevel = true; + boolean isStopThenStart = true; + boolean directVariableInitialization = false; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This test case tests the StopThenStartOnServiceBusProcessorCheck class when 'start' is not called after 'stop' + * on different variables in a recursive second level method. + * The test case should not register a problem with the ProblemsHolder. + */ + @Test + public void testDifferentVariablesRecursiveStartThenStop() { + String stopMethod = "stop"; + String startMethod = "start"; + String packageName = "com.azure.messaging.servicebus.ServiceBusProcessorClient"; + int numOfInvocations = 0; + + boolean sameVariable = false; + boolean recursiveSecondLevel = true; + boolean isStopThenStart = false; + boolean directVariableInitialization = true; + + verifyRegisterProblem(stopMethod, startMethod, isStopThenStart, packageName, numOfInvocations, sameVariable, recursiveSecondLevel, directVariableInitialization); + } + + /** + * This helper method creates a new StopThenStartOnServiceBusProcessorVisitor object + * + * @return a new StopThenStartOnServiceBusProcessorVisitor object + */ + private StopThenStartOnServiceBusProcessorVisitor createVisitor() { + return new StopThenStartOnServiceBusProcessorVisitor(mockHolder); + } + + /** + * This helper method verifies that the ProblemsHolder has been registered with the problem + * + * @param stopMethod the stop method to be called + * @param startMethod the start method to be called + * @param packageName the package name of the variable + * @param numOfInvocations the number of times the problem should be registered + * @param sameVariable a boolean indicating if the start method is called on the same variable + */ + private void verifyRegisterProblem(String stopMethod, String startMethod, boolean isStopThenStart, String packageName, int numOfInvocations, boolean sameVariable, boolean recursiveSecondLevel, boolean directVariableInitialization) { + + PsiNewExpression newExpression = mock(PsiNewExpression.class); + PsiMethod method = mock(PsiMethod.class); + PsiCodeBlock mainBody = mock(PsiCodeBlock.class); + PsiCodeBlock secondBody = mock(PsiCodeBlock.class); + + when(newExpression.getParent()).thenReturn(method); + + if (recursiveSecondLevel) { + when(method.getBody()).thenReturn(mainBody); + } else { + when(method.getBody()).thenReturn(secondBody); + } + + // findAssociatedVariable method + PsiVariable findAssociatedVariable = mock(PsiVariable.class); + PsiAssignmentExpression assignmentExpression = mock(PsiAssignmentExpression.class); + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + + // isServiceBusProcessorClient method + PsiType type = mock(PsiType.class); + + // visitMethodBody method + PsiExpressionStatement stopChild = mock(PsiExpressionStatement.class); + PsiExpressionStatement startChild = mock(PsiExpressionStatement.class); + + PsiElement[] stopStartChildren; + if (isStopThenStart) { + stopStartChildren = new PsiElement[]{stopChild, startChild}; + } else { + stopStartChildren = new PsiElement[]{startChild, stopChild}; + } + + PsiMethodCallExpression stopExpression = mock(PsiMethodCallExpression.class); + PsiMethodCallExpression startExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression stopMethodExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression startMethodExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression stopQualifierExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression startQualifierExpression = mock(PsiReferenceExpression.class); + + PsiVariable stopResolvedElement = mock(PsiVariable.class); + PsiVariable startResolvedElement = mock(PsiVariable.class); + + PsiMethodCallExpression helperExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression helperMethodExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression helperQualifierExpression = mock(PsiReferenceExpression.class); + + PsiMethod helperMethod = mock(PsiMethod.class); + PsiFile containingFile = mock(PsiFile.class); + Project project = mock(Project.class); + ProjectRootManager projectRootManager = mock(ProjectRootManager.class); + ProjectFileIndex projectFileIndex = mock(ProjectFileIndex.class); + PsiExpressionStatement otherChild = mock(PsiExpressionStatement.class); + PsiElement[] otherChildren = new PsiElement[]{otherChild}; + + Document document = mock(Document.class); + PsiDocumentManager psiDocumentManager = mock(PsiDocumentManager.class); + + // findAssociatedVariable method + if (directVariableInitialization) { + when(newExpression.getParent()).thenReturn(findAssociatedVariable); + } else { + + when(newExpression.getParent()).thenReturn(assignmentExpression); + when(assignmentExpression.getLExpression()).thenReturn(referenceExpression); + when(referenceExpression.resolve()).thenReturn(findAssociatedVariable); + } + + // isServiceBusProcessorClient method + when(findAssociatedVariable.getType()).thenReturn(type); + when(type.getCanonicalText()).thenReturn(packageName); + + // visitMethodBody method & checkMethodCall method + when(secondBody.getChildren()).thenReturn(stopStartChildren); + + when(stopChild.getExpression()).thenReturn(stopExpression); + when(startChild.getExpression()).thenReturn(startExpression); + + when(stopExpression.getMethodExpression()).thenReturn(stopMethodExpression); + when(startExpression.getMethodExpression()).thenReturn(startMethodExpression); + when(stopMethodExpression.getQualifierExpression()).thenReturn(stopQualifierExpression); + when(startMethodExpression.getQualifierExpression()).thenReturn(startQualifierExpression); + + if (sameVariable) { + when(stopQualifierExpression.resolve()).thenReturn(findAssociatedVariable); + when(startQualifierExpression.resolve()).thenReturn(findAssociatedVariable); + } else { + when(stopQualifierExpression.resolve()).thenReturn(stopResolvedElement); + when(startQualifierExpression.resolve()).thenReturn(startResolvedElement); + } + + when(stopMethodExpression.getReferenceName()).thenReturn(stopMethod); + when(startMethodExpression.getReferenceName()).thenReturn(startMethod); + + when(mainBody.getChildren()).thenReturn(otherChildren); + when(otherChild.getExpression()).thenReturn(helperExpression); + when(helperExpression.getMethodExpression()).thenReturn(helperMethodExpression); + when(helperMethodExpression.getQualifierExpression()).thenReturn(helperQualifierExpression); + when(helperQualifierExpression.resolve()).thenReturn(null); + + when(helperExpression.resolveMethod()).thenReturn(helperMethod); + + // isFileInCurrentProject method + when(helperMethod.getContainingFile()).thenReturn(containingFile); + when(containingFile.getProject()).thenReturn(project); + when(ProjectRootManager.getInstance(project)).thenReturn(projectRootManager); + when(projectRootManager.getFileIndex()).thenReturn(projectFileIndex); + when(projectFileIndex.isInContent(containingFile.getVirtualFile())).thenReturn(true); + + when(helperMethod.getBody()).thenReturn(secondBody); + + // Checking for duplicate registered problems + when(startQualifierExpression.getContainingFile()).thenReturn(containingFile); + when(PsiDocumentManager.getInstance(project)).thenReturn(psiDocumentManager); + when(psiDocumentManager.getDocument(containingFile)).thenReturn(document); + when(startMethodExpression.getTextOffset()).thenReturn(1); + when(document.getLineNumber(startMethodExpression.getTextOffset())).thenReturn(1); + + mockVisitor.visitElement(newExpression); + mockVisitor.visitMethod(method); + + // Verify that the ProblemsHolder has been registered with the problem + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(startExpression), contains("Starting Processor that was stopped before is not recommended, and this feature may be deprecated in the future. Please close this processor instance and create a new one to restart processing")); + } +}