diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/build.gradle.kts index 98c3667a0ed..aba7303b3d7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/build.gradle.kts @@ -3,6 +3,8 @@ dependencies { implementation(project(":azure-intellij-plugin-lib-java")) implementation("com.microsoft.azure:azure-toolkit-common-lib") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") + testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") + testImplementation("org.mockito:mockito-core:3.9.0") intellijPlatform { // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. @@ -11,4 +13,8 @@ dependencies { bundledPlugin("org.jetbrains.idea.maven.model") bundledPlugin("com.intellij.gradle") } + + tasks.named("test", Test::class) { + useJUnitPlatform() + } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/readme.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/readme.md new file mode 100644 index 00000000000..a8753d0a67b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/readme.md @@ -0,0 +1,95 @@ +# **Azure Toolkit for IntelliJ: Java SDK Integration** + +The **Azure Toolkit for IntelliJ** is a project designed to empower Java developers by simplifying the creation, configuration, and usage of Azure services directly within IntelliJ IDEA. This plugin enhances productivity by providing seamless access to Azure SDKs and integrates a **Code Quality Analyzer Tool Window** that offers continuous analysis, real-time code suggestions, to improve Java code quality. + +## **Features** +- **Imported Rule Sets**: The plugin integrates with Azure SDKs to provide real-time code suggestions and best practices. +- **Code Quality Analyzer**: The tool window offers continuous analysis and recommendations to improve Java code quality. + +## **Integrated Rule Sets** + +### **Messaging** + +#### **1. Prefer ServiceBusProcessorClient over ServiceBusReceiverAsyncClient** +- **Anti-pattern**: Using the low-level `ServiceBusReceiverAsyncClient` API, which requires advanced Reactive programming skills. +- **Issue**: Increased complexity and potential misuse by non-experts in Reactive paradigms. +- **Severity**: WARNING +- **Recommendation**: Use the higher-level `ServiceBusProcessorClient` for simplified and efficient message handling. + [Learn more](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/servicebus/azure-messaging-servicebus/README.md#when-to-use-servicebusprocessorclient). + +#### **2. Explicitly Disable Auto-Complete in ServiceBus Clients** +- **Anti-pattern**: Relying on default auto-completion without explicit verification. +- **Issue**: Messages may be incorrectly marked as completed even after processing failures. +- **Severity**: WARNING +- **Recommendation**: Use `disableAutoComplete()` to control message completion explicitly. See the [Azure ServiceBus documentation](https://learn.microsoft.com/java/api/com.azure.messaging.servicebus.servicebusclientbuilder.servicebusreceiverclientbuilder-disableautocomplete) for guidance. + +#### **3. Prefer EventProcessorClient over EventHubConsumerAsyncClient** +- **Anti-pattern**: Use of low level “EventHubConsumerAsyncClient” useful only when building a Reactive library or + an end-to-end Reactive application. +- **Issue**: Increased complexity and potential misuse by non-experts in Reactive paradigms. +- **Severity**: WARNING +- **Recommendation**: Use the higher-level `EventProcessorClient` for efficient and reliable event processing. + [Learn more](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-prefetch?tabs=dotnet#why-is-prefetch-not-the-default-option). + +#### **4. Use EventProcessorClient for Checkpoint Management** +- **Anti-pattern**: Calling `updateCheckpointAsync()` without proper blocking (`block()`). +- **Issue**: Results in ineffective checkpoint updates. +- **Severity**: WARNING +- **Recommendation**: Ensure the `block()` operator is used with an appropriate timeout for reliable checkpoint updates. + +--- + +### **Identity** + +#### **5. Avoid Hardcoded API Keys and Tokens** +- **Anti-pattern**: Storing sensitive credentials in source code. +- **Issue**: Exposes credentials to security breaches. +- **Severity**: WARNING +- **Recommendation**: `DefaultAzureCredential` is recommended for authentication. If not, then use environment variables when using key based authentication. + [Learn more](https://learn.microsoft.com/java/api/com.azure.identity.defaultazurecredential?view=azure-java-stable). + +--- + +### **Async** + +#### **6. Use SyncPoller Instead of PollerFlux#getSyncPoller()** +- **Anti-pattern**: Converting asynchronous polling to synchronous with `getSyncPoller()`. +- **Issue**: Adds unnecessary complexity and blocking on asynchronous operation. +- **Severity**: WARNING +- **Recommendation**: Use `SyncPoller` directly for synchronous operations. + [Learn more](https://learn.microsoft.com/java/api/com.azure.core.util.polling.syncpoller?view=azure-java-stable). + +--- + +### **Storage** + +#### **7. Storage Upload without Length Check** +- **Anti-pattern**: Using Azure Storage upload APIs without a length parameter, causing the entire data payload to buffer in memory. +- **Issue**: Risks `OutOfMemoryErrors` for large files or high-volume uploads. +- **Severity**: INFO +- **Recommendation**: Use APIs that accept a length parameter. Refer to the [Azure SDK for Java documentation](https://learn.microsoft.com/azure/storage/blobs/storage-blob-upload-java) for details. + +--- + +### **Performance Optimization** + +#### **8. Avoid Dynamic Client Creation** +- **Anti-pattern**: Creating new client instances for each operation instead of reusing them. +- **Issue**: Leads to resource overhead, reduced performance, and increased latency. +- **Severity**: WARNING +- **Recommendation**: Reuse client instances throughout the application's lifecycle. + [Learn more](https://learn.microsoft.com/azure/developer/java/sdk/overview#connect-to-and-use-azure-resources-with-client-libraries). + +#### **9. Batch Operations Instead of Single Operations in Loops** +- **Anti-pattern**: Performing repetitive single operations instead of batch processing. +- **Issue**: Inefficient resource use and slower execution. +- **Severity**: WARNING +- **Recommendation**: Utilize batch APIs for optimized resource usage. + +--- + +#### **10. Recommended Alternatives for Common APIs** +- **Authentication**: Use `DefaultAzureCredential` over connection strings. +- **Azure OpenAI**: Prefer `getChatCompletions` for conversational AI instead of `getCompletions`. + [Learn more](https://learn.microsoft.com/java/api/overview/azure/ai-openai-readme?view=azure-java-preview). + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheck.java new file mode 100644 index 00000000000..caa0ce5d4e8 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheck.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection tool to check discouraged Connection String usage. + */ +public class ConnectionStringCheck extends LocalInspectionTool { + + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new ConnectionStringCheckVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + static class ConnectionStringCheckVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + ConnectionStringCheckVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "ConnectionStringCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + // Check if the rule should be skipped + if (SKIP_WHOLE_RULE) { + return; + } + + PsiMethod method = HelperUtils.getResolvedMethod(expression); + if (HelperUtils.isItDiscouragedAPI(method, RULE_CONFIG.getUsagesToCheck(), RULE_CONFIG.getScopeToCheck())) { + + PsiElement problemElement = expression.getMethodExpression().getReferenceNameElement(); + if (problemElement != null) { + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheck.java new file mode 100644 index 00000000000..60ff5635ba4 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheck.java @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * This class extends the LocalInspectionTool and is used to inspect the usage of Azure SDK ServiceBusReceiver & + * ServiceBusProcessor clients in the code. It checks if the auto-complete feature is disabled for the clients. If the + * auto-complete feature is not disabled, a problem is registered with the ProblemsHolder. + */ +public class DisableAutoCompleteCheck extends LocalInspectionTool { + + /** + * Build the visitor for the inspection. This visitor will be used to traverse the PSI tree. + * + * @param holder The holder for the problems found + * + * @return The visitor for the inspection. This is not used anywhere else in the code. + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new DisableAutoCompleteVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * This class extends the JavaElementVisitor and is used to visit the Java elements in the code. It checks for the + * usage of Azure SDK ServiceBusReceiver & ServiceBusProcessor clients and whether the auto-complete feature is + * disabled. If the auto-complete feature is not disabled, a problem is registered with the ProblemsHolder. + */ + static class DisableAutoCompleteVisitor extends JavaElementVisitor { + + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + private final ProblemsHolder holder; + + DisableAutoCompleteVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "DisableAutoCompleteCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + /** + * This method is used to visit the declaration statements in the code. It checks for the declaration of Azure + * SDK ServiceBusReceiver & ServiceBusProcessor clients and whether the auto-complete feature is disabled. If + * the auto-complete feature is not disabled, a problem is registered with the ProblemsHolder. + * + * @param statement The declaration statement to visit + */ + @Override + public void visitDeclarationStatement(PsiDeclarationStatement statement) { + + if (SKIP_WHOLE_RULE) { + return; + } + super.visitDeclarationStatement(statement); + + // Get the declared elements + PsiElement[] elements = statement.getDeclaredElements(); + + // Get the variable declaration + if (elements.length > 0 && elements[0] instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) elements[0]; + + // Process the variable declaration + processVariableDeclaration(variable); + } + } + + /** + * This method is used to process the variable declaration. It checks for the declaration of Azure SDK + * ServiceBusReceiver & ServiceBusProcessor clients and whether the auto-complete feature is disabled. If the + * auto-complete feature is not disabled, a problem is registered with the ProblemsHolder. + * + * @param variable The variable to process + */ + private void processVariableDeclaration(PsiVariable variable) { + + // Retrieve the client name (left side of the declaration) + PsiType clientType = variable.getType(); + + // Check the assignment part (right side) + PsiExpression initializer = variable.getInitializer(); + + // Check if the client type is an Azure SDK client + if (HelperUtils.isAzurePackage(clientType.getCanonicalText())) { + if (HelperUtils.checkIfInScope(RULE_CONFIG.getScopeToCheck(), clientType.getPresentableText())) { + if (initializer instanceof PsiMethodCallExpression) { + if (!isAutoCompleteDisabled((PsiMethodCallExpression) initializer)) { + holder.registerProblem(initializer, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } + } + + /** + * This method is used to check if the auto-complete feature is disabled. It iterates up the chain of method + * calls to check if the auto-complete feature is disabled. + * + * @param methodCallExpression The method call expression to check + * + * @return true if the auto-complete feature is disabled, false otherwise + */ + private static boolean isAutoCompleteDisabled(PsiMethodCallExpression methodCallExpression) { + + // Iterating up the chain of method calls + PsiExpression qualifier = methodCallExpression.getMethodExpression().getQualifierExpression(); + + // Check if the method call chain has the method to check + while (qualifier instanceof PsiMethodCallExpression) { + + if (qualifier instanceof PsiMethodCallExpression) { + + // Get the method expression + PsiReferenceExpression methodExpression = + ((PsiMethodCallExpression) qualifier).getMethodExpression(); + + // Get the method name + String methodName = methodExpression.getReferenceName(); + + // Check if the method name is the method to check + if (RULE_CONFIG.getUsagesToCheck().contains(methodName)) { + return true; + } + } + qualifier = ((PsiMethodCallExpression) qualifier).getMethodExpression().getQualifierExpression(); + } + // When the chain has been traversed and the method to check is not found + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheck.java new file mode 100644 index 00000000000..71b21373ef3 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheck.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection to detect dynamic client creation in loops. + */ +public class DynamicClientCreationCheck extends LocalInspectionTool { + + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new DynamicClientCreationVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + static class DynamicClientCreationVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + public DynamicClientCreationVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "DynamicClientCreationCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + @Override + public void visitForStatement(@NotNull PsiForStatement statement) { + if (SKIP_WHOLE_RULE) return; + + PsiStatement body = statement.getBody(); + if (!(body instanceof PsiBlockStatement)) return; + + PsiCodeBlock codeBlock = ((PsiBlockStatement) body).getCodeBlock(); + for (PsiStatement blockChild : codeBlock.getStatements()) { + if (blockChild instanceof PsiExpressionStatement) { + checkExpression(((PsiExpressionStatement) blockChild).getExpression()); + } else if (blockChild instanceof PsiDeclarationStatement) { + checkDeclaration((PsiDeclarationStatement) blockChild); + } + } + } + + private void checkExpression(PsiExpression expression) { + if (expression instanceof PsiAssignmentExpression) { + PsiExpression rhs = ((PsiAssignmentExpression) expression).getRExpression(); + if (rhs instanceof PsiMethodCallExpression && isClientCreationMethod((PsiMethodCallExpression) rhs)) { + holder.registerProblem(rhs, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + + private void checkDeclaration(PsiDeclarationStatement declaration) { + for (PsiElement declaredElement : declaration.getDeclaredElements()) { + if (declaredElement instanceof PsiLocalVariable) { + PsiExpression initializer = ((PsiLocalVariable) declaredElement).getInitializer(); + if (initializer instanceof PsiMethodCallExpression && isClientCreationMethod((PsiMethodCallExpression) initializer)) { + holder.registerProblem(initializer, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } + + private boolean isClientCreationMethod(PsiMethodCallExpression methodCallExpression) { + PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); + String methodName = methodExpression.getReferenceName(); + + if (methodName == null || !RULE_CONFIG.getUsagesToCheck().contains(methodName)) { + return false; + } + + PsiExpression qualifier = methodExpression.getQualifierExpression(); + if (qualifier == null || qualifier.getType() == null) { + return false; + } + + return HelperUtils.isAzurePackage(qualifier.getType().getCanonicalText()); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheck.java new file mode 100644 index 00000000000..9051eac3daf --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheck.java @@ -0,0 +1,72 @@ +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiTypeElement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * This class is used to check if the EventHubConsumerAsyncClient is being used in the code. + */ +public class EventHubConsumerAsyncClientCheck extends LocalInspectionTool { + /** + * Method to build the visitor for the inspection tool. + * + * @param holder Holder for the problems found by the inspection + * + * @return JavaElementVisitor a visitor to visit the method call expressions + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new EventHubConsumerAsyncClientCheck.EventHubConsumerAsyncClientVisitor(holder, + RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * Visitor class to visit the method call expressions and check for the use of getSyncPoller() on a PollerFlux. The + * visitor will check if the method call is on a PollerFlux and if the method call is on an Azure SDK client. + */ + static class EventHubConsumerAsyncClientVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + /** + * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * + * @param holder Holder for the problems found by the inspection + * @param ruleConfigs Rule configurations for the inspection + */ + public EventHubConsumerAsyncClientVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "EventHubConsumerAsyncClientCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + @Override + public void visitTypeElement(PsiTypeElement element) { + super.visitTypeElement(element); + if (SKIP_WHOLE_RULE) { + return; + } + if (HelperUtils.isItDiscouragedClient(element, RULE_CONFIG.getUsagesToCheck())) { + holder.registerProblem(element, RULE_CONFIG.getAntiPatternMessage()); + } + } + + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheck.java new file mode 100644 index 00000000000..8ee07be789f --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheck.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection tool to check discouraged GetCompletions API usage in openai package context. + */ +public class GetCompletionsCheck extends LocalInspectionTool { + + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new GetCompletionsCheck.GetCompletionsVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + static class GetCompletionsVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + GetCompletionsVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "GetCompletionsCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + // visit all methodcalls + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + // Check if the rule should be skipped + if (SKIP_WHOLE_RULE) { + return; + } + PsiMethod method = HelperUtils.getResolvedMethod(expression); + if (HelperUtils.isItDiscouragedAPI(method, RULE_CONFIG.getUsagesToCheck(), + RULE_CONFIG.getScopeToCheck())) { + + PsiElement problemElement = expression.getMethodExpression().getReferenceNameElement(); + if (problemElement != null) { + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheck.java new file mode 100644 index 00000000000..d8d04abf5f8 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheck.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection tool to detect the use of getSyncPoller() on a PollerFlux. The inspection will check if the method call is + * on a PollerFlux and if the method call is on an Azure SDK client. If both conditions are met, the inspection will + * register a problem with the suggestion to use SyncPoller instead. + * + * This is an example of an anti-pattern that would be detected by the inspection tool. + * public void exampleUsage() { + * PollerFlux pollerFlux = createPollerFlux(); + * + * // Anti-pattern: Using getSyncPoller() on PollerFlux + * SyncPoller syncPoller = pollerFlux.getSyncPoller(); + * } + */ +public class GetSyncPollerOnPollerFluxCheck extends LocalInspectionTool { + + /** + * Method to build the visitor for the inspection tool. + * + * @param holder Holder for the problems found by the inspection + * + * @return JavaElementVisitor a visitor to visit the method call expressions + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new GetSyncPollerOnPollerFluxVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * Visitor class to visit the method call expressions and check for the use of getSyncPoller() on a PollerFlux. The + * visitor will check if the method call is on a PollerFlux and if the method call is on an Azure SDK client. + */ + class GetSyncPollerOnPollerFluxVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + /** + * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * + * @param holder Holder for the problems found by the inspection + * @param ruleConfigs Rule configurations for the inspection + */ + public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "GetSyncPollerOnPollerFluxCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + /** + * Method to visit the method call expressions and check for the use of getSyncPoller() on a PollerFlux. + * + * @param expression Method call expression to visit + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + // Check if the whole rule should be skipped + if (SKIP_WHOLE_RULE) { + return; + } + + PsiMethod method = expression.resolveMethod(); + if (method != null) { + PsiClass containingClass = method.getContainingClass(); + if (containingClass != null) { + String qualifiedName = containingClass.getQualifiedName(); + if (qualifiedName != null && HelperUtils.isAzurePackage(qualifiedName)) { + if (isGetSyncPollerCall(expression)) { + holder.registerProblem(expression.getMethodExpression().getReferenceNameElement(), + RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } + } + + /** + * Helper method to check if the method call is on a PollerFlux type. + * + * @param expression Method call expression to check + * + * @return true if the method call is a getSyncPoller() call, false otherwise + */ + private boolean isGetSyncPollerCall(@NotNull PsiMethodCallExpression expression) { + for (String usage : RULE_CONFIG.getUsagesToCheck()) { + if (expression.getMethodExpression().getReferenceName().startsWith(usage)) { + return true; + } + } + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheck.java new file mode 100644 index 00000000000..a631468fbbb --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheck.java @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiBinaryExpression; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiLiteralExpression; +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 com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import org.jetbrains.annotations.NotNull; + +/** + * Custom inspection class to detect hardcoded API keys and tokens in Java code. The inspection tool will flag and + * suggest to use environment variables or DefaultAzureCredential instead other credential types. + */ +public class HardcodedAPIKeysAndTokensCheck extends LocalInspectionTool { + private static final Pattern pattern = Pattern.compile("clientId|tenantId|clientSecret|username|password", + Pattern.CASE_INSENSITIVE); + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new APIKeysAndTokensVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * Visitor to inspect Java elements for hardcoded API keys and tokens. + */ + static class APIKeysAndTokensVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + APIKeysAndTokensVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "HardcodedAPIKeysAndTokensCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + @Override + public void visitNewExpression(@NotNull PsiNewExpression newExpression) { + PsiJavaCodeReferenceElement classReference = newExpression.getClassReference(); + if (classReference == null) { + return; + } + + String qualifiedName = classReference.getQualifiedName(); + String referenceName = classReference.getReferenceName(); + + if (HelperUtils.isAzurePackage(qualifiedName) && HelperUtils.checkIfInUsages(RULE_CONFIG.getUsagesToCheck(), referenceName)){ + checkForHardcodedStrings(newExpression); + } + } + + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression methodCall) { + // Check all arguments passed to the method call and if the method call is from the rule config + PsiType qualifierType = methodCall.getType(); + if (qualifierType != null && RULE_CONFIG.getUsagesToCheck().contains(qualifierType.getCanonicalText())) { + for (PsiExpression argument : methodCall.getArgumentList().getExpressions()) { + if (isProblematicString(argument)) { + holder.registerProblem( + argument, + RULE_CONFIG.getAntiPatternMessage() + ); + } + } + } + + // check for chained method calls + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (qualifier != null) { + if (isProblematicChainedMethodCall(methodCall)) { + holder.registerProblem( + methodCall, + RULE_CONFIG.getAntiPatternMessage() + ); + } + } + } + + /** + * Checks for problematic chained method calls. + */ + private boolean isProblematicChainedMethodCall(@NotNull PsiMethodCallExpression methodCall) { + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (!(qualifier != null && qualifier.getType() instanceof PsiClassType)) { + return false; + } + + PsiClass containingClass = ((PsiClassType) qualifier.getType()).resolve(); + if (containingClass == null || containingClass.getQualifiedName() == null) { + return false; + } + + if (RULE_CONFIG.getUsagesToCheck().stream() + .anyMatch(scope -> containingClass.getName().startsWith(scope))) { + // Check if the method name is "clientId" + String methodName = methodCall.getMethodExpression().getReferenceName(); + if (checkForPattern(methodCall, methodName)) { + return true; + } + } + return false; + } + + private boolean checkForPattern(@Nonnull PsiMethodCallExpression methodCall, String methodName) { + if (pattern.matcher(methodName).find()) { + // Check if the argument to clientId() is a problematic string + PsiExpression[] arguments = methodCall.getArgumentList().getExpressions(); + if (arguments.length == 1 && isProblematicString(arguments[0])) { + return true; + } + } + return false; + } + + private void checkForHardcodedStrings(@NotNull PsiNewExpression newExpression) { + for (PsiElement child : newExpression.getChildren()) { + if (child instanceof PsiExpressionList expression) { + for (PsiExpression argument : expression.getExpressions()) { + if (isProblematicString(argument)) { + holder.registerProblem(newExpression, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } + } + + /** + * Recursively checks if an expression is a hardcoded string or derived from one. + */ + private boolean isProblematicString(@NotNull PsiExpression expression) { + if (expression instanceof PsiLiteralExpression literal && literal.getValue() instanceof String value) { + // Direct string literal. + return isASCII(value); + } else if (expression instanceof PsiReferenceExpression reference) { + // Check if the reference points to a string variable initialized with a literal. + PsiElement resolved = reference.resolve(); + if (resolved instanceof PsiVariable variable) { + PsiExpression initializer = variable.getInitializer(); + return initializer != null && isProblematicString(initializer); + } + } else if (expression instanceof PsiBinaryExpression binaryExpression) { + // Check if concatenated strings involve literals. + PsiExpression left = binaryExpression.getLOperand(); + PsiExpression right = binaryExpression.getROperand(); + return isProblematicString(left) || (right != null && isProblematicString(right)); + } + return false; + } + + private static boolean isASCII(String text) { + return text.length() == 32 && text.chars().allMatch(ch -> (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f')); + } + } +} + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheck.java new file mode 100644 index 00000000000..6e9bfbaa98a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheck.java @@ -0,0 +1,62 @@ +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiTypeElement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * This class is used to check if the ServiceBusReceiverAsyncClient is being used in the code. + */ +public class ServiceBusReceiverAsyncClientCheck extends LocalInspectionTool { + + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new ServiceBusReceiverAsyncClientCheck.ServiceBusReceiverAsyncClientCheckVisitor(holder, + RuleConfigLoader.getInstance().getRuleConfigs()); + } + + + static class ServiceBusReceiverAsyncClientCheckVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + /** + * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * + * @param holder Holder for the problems found by the inspection + * @param ruleConfigs Rule configurations for the inspection + */ + public ServiceBusReceiverAsyncClientCheckVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "ServiceBusReceiverAsyncClientCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + @Override + public void visitTypeElement(PsiTypeElement element) { + super.visitTypeElement(element); + if (SKIP_WHOLE_RULE) { + return; + } + if (HelperUtils.isItDiscouragedClient(element, RULE_CONFIG.getUsagesToCheck())) { + holder.registerProblem(element, RULE_CONFIG.getAntiPatternMessage()); + } + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopTextAnalyticsCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopTextAnalyticsCheck.java new file mode 100644 index 00000000000..f679667c3c6 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopTextAnalyticsCheck.java @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiDoWhileStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiForeachStatement; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.PsiWhileStatement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection to check if there is a Text Analytics client operation inside a loop. If a Text Analytics client operation + * is found inside a loop, and the API has a batch alternative, a problem will be registered. + *

+ * This is an example of a situation where the inspection should register a problem: + *

+ * // Loop through the list of texts and detect the language for each text 1. for (String text : texts) { + * DetectedLanguage detectedLanguage = textAnalyticsClient.detectLanguage(text); System.out.println("Text: " + text + " + * | Detected Language: " + detectedLanguage.getName() + " | Confidence Score: " + + * detectedLanguage.getConfidenceScore()); } + *

+ * // Traditional for-loop to recognize entities for each text for (int i = 0; i < texts.size(); i++) { String text = + * texts.get(i); textAnalyticsClient.recognizeEntities(text); // Process recognized entities if needed } + */ +public class SingleOperationInLoopTextAnalyticsCheck extends LocalInspectionTool { + + /** + * Build the visitor for the inspection. This visitor will be used to traverse the PSI tree. + * + * @param holder The holder for the problems found + * + * @return The visitor for the inspection + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new SingleOperationInLoopVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * Visitor class to traverse the PSI tree and check for single Azure client operation inside a loop. The visitor + * will check for loops of type for, foreach, while, and do-while. The visitor will check for a Text Analytics + * client operation inside the loop. If a Text Analytics client operation is found inside the loop, and the API has + * a batch alternative, a problem will be registered. + */ + static class SingleOperationInLoopVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + public SingleOperationInLoopVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "SingleOperationInLoopTextAnalyticsCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + @Override + public void visitForStatement(@NotNull PsiForStatement statement) { + if (SKIP_WHOLE_RULE) return; + checkLoopStatement(statement); + } + + @Override + public void visitForeachStatement(@NotNull PsiForeachStatement statement) { + if (SKIP_WHOLE_RULE) return; + checkLoopStatement(statement); + } + + @Override + public void visitWhileStatement(@NotNull PsiWhileStatement statement) { + if (SKIP_WHOLE_RULE) return; + checkLoopStatement(statement); + } + + @Override + public void visitDoWhileStatement(@NotNull PsiDoWhileStatement statement) { + if (SKIP_WHOLE_RULE) return; + checkLoopStatement(statement); + } + + private void checkLoopStatement(PsiStatement loopStatement) { + PsiStatement loopBody = getLoopBody(loopStatement); + if (!(loopBody instanceof PsiBlockStatement)) return; + + PsiCodeBlock codeBlock = ((PsiBlockStatement) loopBody).getCodeBlock(); + for (PsiStatement statement : codeBlock.getStatements()) { + if (statement instanceof PsiExpressionStatement) { + checkExpressionStatement((PsiExpressionStatement) statement); + } else if (statement instanceof PsiDeclarationStatement) { + checkDeclarationStatement((PsiDeclarationStatement) statement); + } + } + } + + private void checkExpressionStatement(PsiExpressionStatement statement) { + PsiExpression expression = statement.getExpression(); + if (expression instanceof PsiMethodCallExpression) { + checkMethodCall((PsiMethodCallExpression) expression); + } + } + + private void checkDeclarationStatement(PsiDeclarationStatement statement) { + for (PsiElement element : statement.getDeclaredElements()) { + if (element instanceof PsiVariable) { + PsiExpression initializer = ((PsiVariable) element).getInitializer(); + if (initializer instanceof PsiMethodCallExpression) { + checkMethodCall((PsiMethodCallExpression) initializer); + } + } + } + } + + private void checkMethodCall(PsiMethodCallExpression methodCall) { + if (isAzureTextAnalyticsClientOperation(methodCall)) { + String methodName = methodCall.getMethodExpression().getReferenceName(); + holder.registerProblem( + methodCall, + RULE_CONFIG.getAntiPatternMessage() + " Consider using " + methodName + "Batch instead." + ); + } + } + + private static PsiStatement getLoopBody(PsiStatement loopStatement) { + if (loopStatement instanceof PsiForStatement) { + return ((PsiForStatement) loopStatement).getBody(); + } + if (loopStatement instanceof PsiForeachStatement) { + return ((PsiForeachStatement) loopStatement).getBody(); + } + if (loopStatement instanceof PsiWhileStatement) { + return ((PsiWhileStatement) loopStatement).getBody(); + } + if (loopStatement instanceof PsiDoWhileStatement) { + return ((PsiDoWhileStatement) loopStatement).getBody(); + } + return null; + } + + private static boolean isAzureTextAnalyticsClientOperation(PsiMethodCallExpression methodCall) { + // Check the qualifier expression (e.g., the object or variable the method is called on) + PsiExpression qualifierExpression = methodCall.getMethodExpression().getQualifierExpression(); + + if (qualifierExpression != null) { + PsiType qualifierType = qualifierExpression.getType(); + if (qualifierType != null) { + String qualifiedTypeName = qualifierType.getCanonicalText(); + if (qualifiedTypeName != null && RULE_CONFIG.getScopeToCheck().stream().anyMatch(qualifiedTypeName::startsWith)) { + String methodName = methodCall.getMethodExpression().getReferenceName(); + return RULE_CONFIG.getUsagesToCheck().contains(methodName + "Batch"); + } + } + } + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheck.java new file mode 100644 index 00000000000..ab6d07bd099 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheck.java @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaRecursiveElementWalkingVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +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 com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection tool to enforce usage of Azure Storage upload APIs with a 'length' parameter. + */ +public class StorageUploadWithoutLengthCheck extends LocalInspectionTool { + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new StorageUploadVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * Visitor class to check for Azure Storage upload APIs without a 'length' parameter. + */ + static class StorageUploadVisitor extends JavaRecursiveElementWalkingVisitor { + private static final String LENGTH_TYPE = "long"; + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + + StorageUploadVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "StorageUploadWithoutLengthCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + @Override + public void visitMethodCallExpression(PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + if (SKIP_WHOLE_RULE || !shouldAnalyze(expression)) { + return; + } + + if (!hasLengthArgument(expression)) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); + } + } + + private boolean shouldAnalyze(PsiMethodCallExpression expression) { + String methodName = expression.getMethodExpression().getReferenceName(); + return RULE_CONFIG.getUsagesToCheck().contains(methodName) && isInScope(expression); + } + + private boolean isInScope(PsiMethodCallExpression expression) { + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + if (!(qualifier != null && qualifier.getType() instanceof PsiClassType)) { + return false; + } + + PsiClass containingClass = ((PsiClassType) qualifier.getType()).resolve(); + if (containingClass == null || containingClass.getQualifiedName() == null) { + return false; + } + + return RULE_CONFIG.getScopeToCheck().stream() + .anyMatch(scope -> containingClass.getQualifiedName().startsWith(scope)); + } + + /** + * Checks if the given method call expression has a 'length' argument. + * + * @param expression the method call expression to check + * + * @return true if the method call expression has a 'length' argument, false otherwise + */ + private boolean hasLengthArgument(PsiMethodCallExpression expression) { + return Arrays.stream(expression.getArgumentList().getExpressions()) + .anyMatch(arg -> isLongType(arg) || isLengthMethodCall(arg) || isLongTypeInCallChain(arg) + || (arg instanceof PsiNewExpression && hasLongConstructorArgument((PsiNewExpression) arg))); + } + + /** + * Checks if the given expression is a method call that returns a long value. + * + * @param expression the expression to check + * + * @return true if the expression is a method call that returns a long value, false otherwise + */ + private boolean isLongTypeInCallChain(PsiExpression expression) { + while (expression instanceof PsiMethodCallExpression) { + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) expression; + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + + if (qualifier instanceof PsiReferenceExpression && + resolvesToVariableWithLength((PsiReferenceExpression) qualifier)) { + return false; + } + + if (qualifier instanceof PsiNewExpression && + hasLengthArgumentInConstructor(((PsiNewExpression) qualifier).resolveConstructor())) { + return false; + } + + expression = qualifier; + } + return false; + } + + /** + * Checks if the given qualifier resolves to a variable with a 'length' field or method. + * + * @param qualifier the qualifier to check + * + * @return true if the qualifier resolves to a variable with a 'length' field or method, false otherwise + */ + private boolean resolvesToVariableWithLength(PsiReferenceExpression qualifier) { + PsiElement resolved = qualifier.resolve(); + if (resolved instanceof PsiVariable) { + PsiVariable variable = (PsiVariable) resolved; + return hasLengthFieldOrMethod(variable.getType()); + } + return false; + } + + private boolean isLongType(PsiExpression expression) { + return expression.getType() != null && LENGTH_TYPE.equals(expression.getType().getCanonicalText()); + } + + private boolean hasLongConstructorArgument(PsiNewExpression newExpression) { + return Arrays.stream(newExpression.getArgumentList().getExpressions()) + .anyMatch(this::isLongType) + || isLengthRelatedConstructor(newExpression.resolveConstructor()); + } + + private boolean hasLengthArgumentInConstructor(PsiMethod constructor) { + if (constructor == null) { + return false; + } + return Arrays.stream(constructor.getParameterList().getParameters()) + .anyMatch(param -> LENGTH_TYPE.equals(param.getType().getCanonicalText())); + } + + private boolean hasLengthFieldOrMethod(PsiType type) { + if (!(type instanceof PsiClassType)) { + return false; + } + PsiClass resolvedClass = ((PsiClassType) type).resolve(); + if (resolvedClass == null) { + return false; + } + + return Arrays.stream(resolvedClass.getAllMethods()) + .anyMatch(method -> "length".equals(method.getName()) + && method.getParameterList().getParametersCount() == 0 + && LENGTH_TYPE.equals(method.getReturnType().getCanonicalText())) + || Arrays.stream(resolvedClass.getAllFields()) + .anyMatch(field -> "length".equals(field.getName()) + && LENGTH_TYPE.equals(field.getType().getCanonicalText())); + } + + private boolean isLengthMethodCall(PsiExpression expression) { + if (!(expression instanceof PsiMethodCallExpression)) { + return false; + } + + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) expression; + String methodName = methodCall.getMethodExpression().getReferenceName(); + if (!"length".equals(methodName)) { + return false; + } + + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (!(qualifier instanceof PsiReferenceExpression)) { + return false; + } + + PsiElement resolved = ((PsiReferenceExpression) qualifier).resolve(); + return resolved instanceof PsiVariable + && "java.lang.String".equals(((PsiVariable) resolved).getType().getCanonicalText()); + } + + private boolean isLengthRelatedConstructor(PsiMethod constructor) { + if (constructor == null || constructor.getContainingClass() == null) { + return false; + } + + String className = constructor.getContainingClass().getQualifiedName(); + if (className == null || !className.endsWith("UploadOptions")) { + return false; + } + + return Arrays.stream(constructor.getParameterList().getParameters()) + .anyMatch(param -> LENGTH_TYPE.equals(param.getType().getCanonicalText())); + } + } +} + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheck.java new file mode 100644 index 00000000000..6d6a04b8e8f --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheck.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * This class extends the AbstractUpdateCheckpointChecker class to check for the usage of the updateCheckpointAsync() + * method call in the code. The visitor inspects the method call expressions and checks if the method call is + * updateCheckpointAsync(). If the method call is updateCheckpointAsync() and the following method is subscribe, a + * problem is registered. + */ +public class UpdateCheckpointAsyncSubscribeCheck extends LocalInspectionTool { + + /** + * Build the visitor for the inspection. This visitor will be used to traverse the PSI tree. + * + * @param holder The holder for the problems found + * + * @return The visitor for the inspection. This is not used anywhere else in the code. + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new UpdateCheckpointAsyncVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * This class extends the JavaElementVisitor to visit the elements in the code. It checks if the method call is + * updateCheckpointAsync() and if the following method is `subscribe`. If both conditions are met, a problem is + * registered with the suggestion message. + */ + static class UpdateCheckpointAsyncVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + /** + * Constructor to initialize the visitor with the holder. + * + * @param holder ProblemsHolder to register problems + */ + UpdateCheckpointAsyncVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "UpdateCheckpointAsyncSubscribeCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + /** + * This method visits the method call expressions in the code. It checks if the method call is + * updateCheckpointAsync() and if the following method is `subscribe`. If both conditions are met, a problem is + * registered with the suggestion message. + * + * @param expression The method call expression to inspect + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + // Check if the rule should be skipped + if (SKIP_WHOLE_RULE) { + return; + } + + if (expression.getMethodExpression().getReferenceName() == null) { + return; + } + + // Check if the updateCheckpointAsync() method call is called on an provided context + // (EventBatchContext) object + PsiMethod method = HelperUtils.getResolvedMethod(expression); + if (HelperUtils.isItDiscouragedAPI(method, RULE_CONFIG.getUsagesToCheck(), RULE_CONFIG.getScopeToCheck())) { + + // Check if the following method is `subscribe` and + if (HelperUtils.isFollowedBySubscribe(expression)) { + PsiElement problemElement = expression.getMethodExpression().getReferenceNameElement(); + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheck.java new file mode 100644 index 00000000000..9922feb975f --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheck.java @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.util.NlsSafe; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Inspection tool to check for the use of blocking method calls on async clients in Azure SDK. + */ +public class UseOfBlockOnAsyncClientsCheck extends LocalInspectionTool { + + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new UseOfBlockOnAsyncClientsVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); + } + + /** + * Visitor to check for the use of blocking methods on async clients in Azure SDK. + */ + static class UseOfBlockOnAsyncClientsVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + UseOfBlockOnAsyncClientsVisitor(ProblemsHolder holder, Map ruleConfigs) { + this.holder = holder; + initializeRuleConfig(ruleConfigs); + } + + private void initializeRuleConfig(Map ruleConfigs) { + if (RULE_CONFIG == null) { + final String ruleName = "UseOfBlockOnAsyncClientsCheck"; + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); + } + } + + /** + * This method is used to visit the method call expression and check for blocking calls on async clients. + * + * @param expression PsiMethodCallExpression - the method call expression to visit + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + if (SKIP_WHOLE_RULE) { + return; + } + + if (!isBlockingMethodCall(expression)) { + return; + } + + if (isAsyncClientBlockingCall(expression)) { + PsiElement problemElement = expression.getMethodExpression().getReferenceNameElement(); + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessage()); + } + } + + /** + * Checks if the method call is a blocking call on an async client. + * + * @param expression PsiMethodCallExpression - the method call expression + * + * @return true if it's a blocking call on an async client, false otherwise + */ + private boolean isAsyncClientBlockingCall(@NotNull PsiMethodCallExpression expression) { + PsiExpression qualifierExpression = expression.getMethodExpression().getQualifierExpression(); + if (qualifierExpression instanceof PsiMethodCallExpression) { + PsiMethodCallExpression qualifierMethodCall = (PsiMethodCallExpression) qualifierExpression; + PsiType qualifierReturnType = qualifierMethodCall.getType(); + + if (qualifierReturnType instanceof PsiClassType) { + PsiClass qualifierReturnTypeClass = ((PsiClassType) qualifierReturnType).resolve(); + if (qualifierReturnTypeClass != null && isReactiveType(qualifierReturnTypeClass)) { + return isAzureAsyncClient(qualifierMethodCall); + } + } + } + return false; + } + + /** + * Checks if the method call is a blocking method call on a reactive type. + * + * @param expression PsiMethodCallExpression - the method call expression + * + * @return true if it's a blocking method call on a reactive type, false otherwise + */ + private boolean isBlockingMethodCall(@NotNull PsiMethodCallExpression expression) { + return RULE_CONFIG.getUsagesToCheck().stream().anyMatch(expression.getMethodExpression().getReferenceName()::equals); + } + + /** + * Checks if the class is an async client in Azure SDK. + * + * @param qualifierMethodCall PsiMethodCallExpression - the method call expression + * + * @return true if the class is an async client in Azure SDK, false otherwise + */ + private boolean isAzureAsyncClient(PsiMethodCallExpression qualifierMethodCall) { + PsiExpression clientExpression = getClientExpression(qualifierMethodCall); + if (clientExpression instanceof PsiReferenceExpression) { + PsiType clientType = clientExpression.getType(); + if (clientType instanceof PsiClassType) { + PsiClass clientClass = ((PsiClassType) clientType).resolve(); + String qualifiedName = clientClass.getQualifiedName(); + return qualifiedName != null && HelperUtils.isAzurePackage(clientClass.getQualifiedName()) && + clientClass.getQualifiedName().endsWith("AsyncClient"); + } + } + return false; + } + + /** + * Extracts the client expression by traveling up the method call chain. + * + * @param methodCall PsiMethodCallExpression - the method call expression + * + * @return the client expression at the end of the chain + */ + private PsiExpression getClientExpression(PsiMethodCallExpression methodCall) { + PsiExpression clientExpression = methodCall.getMethodExpression().getQualifierExpression(); + while (clientExpression instanceof PsiMethodCallExpression) { + clientExpression = + ((PsiMethodCallExpression) clientExpression).getMethodExpression().getQualifierExpression(); + } + return clientExpression; + } + + /** + * Checks if the class is a reactive type (Mono, Flux, etc.). + * + * @param psiClass PsiClass - the class to check + * + * @return true if the class is reactive, false otherwise + */ + private boolean isReactiveType(PsiClass psiClass) { + return RULE_CONFIG.getScopeToCheck().contains(psiClass.getQualifiedName()); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/RuleConfig.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/RuleConfig.java new file mode 100644 index 00000000000..2f1ded692fa --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/RuleConfig.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.models; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.Getter; + +/** + * This class contains configuration options for code style rules. It contains the methods to check, the client name, and + * the antipattern message. + */ +@Getter +public class RuleConfig { + private final List usagesToCheck; + private final List scopeToCheck; + private final String antiPatternMessage; + private final Map regexPatternsToCheck; + private final boolean skipRuleCheck; + + /** + * Constructor for RuleConfig. + * + * @param usagesToCheck List of methods to check. Defaults to "all" if null or empty. + * @param scopeToCheck List of clients to check. Defaults to "all" if null or empty. + * @param antiPatternMessage Antipattern messages to display. + * @param regexPatternsToCheck Map of regex patterns to check. + * @param skipRuleCheck Whether to skip the rule check. + */ + public RuleConfig(List usagesToCheck, List scopeToCheck, String antiPatternMessage, + Map regexPatternsToCheck, boolean skipRuleCheck) { + this.usagesToCheck = usagesToCheck == null || usagesToCheck.isEmpty() + ? Collections.emptyList() + : Collections.unmodifiableList(usagesToCheck); + this.scopeToCheck = scopeToCheck == null || scopeToCheck.isEmpty() + ? Collections.emptyList() + : Collections.unmodifiableList(scopeToCheck); + this.antiPatternMessage = antiPatternMessage; + this.regexPatternsToCheck = regexPatternsToCheck == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(regexPatternsToCheck); + this.skipRuleCheck = skipRuleCheck; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/HelperUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/HelperUtils.java new file mode 100644 index 00000000000..6f99f1f9293 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/HelperUtils.java @@ -0,0 +1,242 @@ +package com.microsoft.azure.toolkit.intellij.java.sdk.utils; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiTypeElement; +import com.intellij.psi.PsiWhiteSpace; +import java.util.List; + +/* + * Utility class to provide helper methods for the Azure SDK rules. + */ +public class HelperUtils { + private static final String AZURE_PACKAGE_NAME = "com.azure"; + + /** + * Checks if the method call is following a specific method call like `subscribe`. + * + * @param expression The method call expression to analyze. + * + * @return True if the method call is following a `subscribe` method call, false otherwise. + */ + public static boolean isFollowedBySubscribe(PsiMethodCallExpression expression) { + if (expression == null) { + return false; + } + + // Case 1: Chained Call -> Check if any method in the chain is subscribe() + if (isAnyMethodInChainSubscribe(expression)) { + return true; + } + + // Case 2: Sequential Call -> Check if the method call result is stored in a variable and later subscribed + return isVariableStoredAndSubscribed(expression); + } + + // Case 1: Checks if any method in a chain is "subscribe()" + private static boolean isAnyMethodInChainSubscribe(PsiMethodCallExpression expression) { + PsiElement current = expression; + + while (current instanceof PsiMethodCallExpression methodCall) { + PsiReferenceExpression methodExpression = methodCall.getMethodExpression(); + if ("subscribe".equals(methodExpression.getReferenceName())) { + return true; // Found subscribe() + } + + // Move up the PSI tree + PsiElement parent = current.getParent(); + + // Handle cases where subscribe is a reference and not yet a method call + if (parent instanceof PsiReferenceExpression referenceExpression) { + PsiElement grandParent = referenceExpression.getParent(); + if (grandParent instanceof PsiMethodCallExpression grandMethodCall && + "subscribe".equals(referenceExpression.getReferenceName())) { + return true; // Reference becomes a method call + } + } + + // Continue traversal + current = parent; + } + + return false; + } + + + // Case 2: Checks if result is assigned to a variable and later subscribed + private static boolean isVariableStoredAndSubscribed(PsiMethodCallExpression expression) { + PsiElement parent = expression.getParent(); + + // Ensure the method call is assigned to a variable + if (!(parent instanceof PsiLocalVariable variable)) { + return false; + } + + // Get the variable name + String variableName = variable.getName(); + if (variableName == null) { + return false; + } + + // Find if "subscribe" is later called on this variable + PsiElement current = variable.getParent(); + while (current != null) { + PsiElement nextSibling = current.getNextSibling(); + while (nextSibling instanceof PsiWhiteSpace || nextSibling instanceof PsiComment) { + nextSibling = nextSibling.getNextSibling(); + } + + if (nextSibling instanceof PsiExpressionStatement exprStmt) { + PsiExpression expr = exprStmt.getExpression(); + if (expr instanceof PsiMethodCallExpression methodCall) { + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (qualifier instanceof PsiReferenceExpression refExpr && variableName.equals(refExpr.getReferenceName()) + && "subscribe".equals(methodCall.getMethodExpression().getReferenceName())) { + return true; + } + } + } + + current = current.getParent(); + } + + return false; + } + + /** + * Checks if the class qualified name is an Azure package. + * @param classQualifiedName The qualified name of the class. + * @return True if the class is in an Azure package, false otherwise. + */ + public static boolean isAzurePackage(String classQualifiedName) { + return classQualifiedName.startsWith(AZURE_PACKAGE_NAME); + } + + /** + * Get the resolved method from a method call expression. + * @param element The method call expression. + * @return The resolved method, or null if the method is not resolved. + */ + public static PsiMethod getResolvedMethod(PsiElement element) { + // Ensure the element is a method call + if (!(element instanceof PsiMethodCallExpression methodCallExpression)) { + return null; + } + + // Resolve the method being called + PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); + PsiElement resolvedMethod = methodExpression.resolve(); + if (!(resolvedMethod instanceof PsiMethod method)) { + return null; + } + return method; + } + + /** + * Checks if the method is in the list of usages. + * @param usages The list of usages. + * @param methodName The name of the method to check. + * @return True if the method is in the list of usages, false otherwise. + */ + public static boolean checkIfInUsages(List usages, String methodName) { + if (usages.isEmpty()) { + return true; + } + return usages.stream().anyMatch(usage -> usage.equals(methodName)); + } + + /** + * Checks if the class qualified name is in the list of scopes. + * @param scope The list of scopes. + * @param classQualifiedName The qualified name of the class to check. + * @return True if the class is in the list of scopes, false otherwise. + */ + public static boolean checkIfInScope(List scope, String classQualifiedName) { + if (scope.isEmpty()) { + return true; + } + return scope.stream().anyMatch(classQualifiedName::contains); + } + + /** + * Checks if the method is a discouraged API. + * @param method The method to check. + * @param usages The list of usages. + * @param scopes The list of scopes. + * @return True if the method is a discouraged API, false otherwise. + */ + public static boolean isItDiscouragedAPI(PsiMethod method, List usages, List scopes) { + if (method == null) { + return false; + } + + // Check if the method is a discouraged API + String methodName = method.getName(); + if (!checkIfInUsages(usages, methodName)) { + return false; + } + + // Get qualified name of the containing class + String classQualifiedName = HelperUtils.getContainingClassOfMethod(method); + if (classQualifiedName == null) { + return false; + } + + // Verify package name and scope + if (!HelperUtils.isAzurePackage(classQualifiedName)) { + return false; + } + + // Verify scope + return checkIfInScope(scopes, classQualifiedName); + } + + /** + * Checks if the element is a discouraged client. + * @param element The element to check. + * @param usages The list of usages. + * @return True if the element is a discouraged client, false otherwise. + */ + public static boolean isItDiscouragedClient(PsiTypeElement element, List usages) { + PsiType psiType = element.getType(); + if (psiType instanceof PsiClassType) { + PsiClass psiClass = ((PsiClassType) psiType).resolve(); + if (psiClass != null) { + String qualifiedName = psiClass.getQualifiedName(); + if (qualifiedName != null) { + return isAzurePackage(qualifiedName) && usages.contains(psiClass.getName()); + } + } + } + return false; + } + + private static String getContainingClassOfMethod(PsiMethod method) { + if (method == null) { + return null; + } + + // Get the containing class of the method + PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) { + return null; + } + + // Get qualified name of the containing class + String classQualifiedName = containingClass.getQualifiedName(); + if (classQualifiedName == null) { + return null; + } + return classQualifiedName; + + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/MavenUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/MavenUtils.java index 5aa8e784690..4e565c78e70 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/MavenUtils.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/MavenUtils.java @@ -1,16 +1,6 @@ package com.microsoft.azure.toolkit.intellij.java.sdk.utils; import com.microsoft.azure.toolkit.intellij.java.sdk.models.MavenArtifactDetails; -import lombok.extern.slf4j.Slf4j; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import javax.annotation.Nullable; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -18,6 +8,15 @@ import java.time.OffsetDateTime; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import lombok.extern.slf4j.Slf4j; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; @Slf4j public final class MavenUtils { @@ -29,7 +28,7 @@ private MavenUtils() { /** * Gets the latest released version of the given artifact from Maven repository. * - * @param groupId The group id of the artifact. + * @param groupId The group id of the artifact. * @param artifactId The artifact id of the artifact. * @return The latest version or {@code null} if an error occurred while retrieving the latest * version. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoader.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoader.java new file mode 100644 index 00000000000..d89134a28b0 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoader.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RuleConfigLoader implements ProjectActivity { + private static final String CONFIG_FILE_PATH = "./ruleConfigs.json"; + private static RuleConfigLoader INSTANCE; + private Map ruleConfigs; + + private RuleConfigLoader() { + this.ruleConfigs = new HashMap<>(); + } + + /** + * Gets the singleton instance of RuleConfigLoader. + * + * @return The singleton instance of RuleConfigLoader. + */ + public static RuleConfigLoader getInstance() { + return INSTANCE; + } + + @Nullable + @Override + public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { + initialize(); + return null; + } + + /** + * Get the rule configurations. + * + * @return the rule configurations + */ + public Map getRuleConfigs() { + return Collections.unmodifiableMap(ruleConfigs); + } + + private void initialize(){ + try { + this.ruleConfigs.putAll(this.loadRuleConfigurations()); + INSTANCE = this; + } catch (IOException e) { + log.warn("Failed to initialize RuleConfigLoader: " + e.getMessage(), e); + } + } + + private Map loadRuleConfigurations() throws IOException { + InputStream configStream = getClass().getClassLoader().getResourceAsStream(RuleConfigLoader.CONFIG_FILE_PATH); + if (configStream == null) { + log.info("Rule configuration file not found: " + RuleConfigLoader.CONFIG_FILE_PATH); + return new HashMap<>(); + } + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(configStream); + Map configs = new HashMap<>(); + + rootNode.fields().forEachRemaining(entry -> { + String ruleName = entry.getKey(); + JsonNode ruleNode = entry.getValue(); + + if (ruleNode.path("hasDerivedRules").asBoolean(false)) { + // Parse derived rules + ruleNode.fields().forEachRemaining(derivedEntry -> { + if (!derivedEntry.getKey().equals("hasDerivedRules")) { + String derivedRuleName = derivedEntry.getKey(); + JsonNode derivedRuleNode = derivedEntry.getValue(); + RuleConfig derivedRuleConfig = parseRuleConfig(derivedRuleNode); + configs.put(derivedRuleName, derivedRuleConfig); + } + }); + } else { + RuleConfig ruleConfig = parseRuleConfig(ruleNode); + configs.put(ruleName, ruleConfig); + } + }); + + return configs; + } + + private RuleConfig parseRuleConfig(JsonNode ruleNode) { + List usages = parseStringOrArray(ruleNode.path("usages")); + List scope = parseStringOrArray(ruleNode.path("scope")); + String antiPatternMessage = ruleNode.path("antiPatternMessage").asText(null); + Map regexPatterns = parseRegexPatterns(ruleNode.path("regexPatterns")); + boolean skipRuleCheck = ruleNode.path("skip").asBoolean(false); + + return new RuleConfig(usages, scope, antiPatternMessage, regexPatterns, skipRuleCheck); + } + + private List parseStringOrArray(JsonNode node) { + List values = new ArrayList<>(); + if (node.isTextual()) { + values.add(node.asText()); + } else if (node.isArray()) { + node.forEach(element -> values.add(element.asText())); + } + return values; + } + + private Map parseRegexPatterns(JsonNode regexPatternsNode) { + Map regexPatterns = new HashMap<>(); + if (regexPatternsNode != null && regexPatternsNode.isObject()) { + regexPatternsNode.fields().forEachRemaining(entry -> regexPatterns.put(entry.getKey(), entry.getValue().asText())); + } + return regexPatterns; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/azure-intellij-plugin-java-sdk.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/azure-intellij-plugin-java-sdk.xml index 03ad76c31e6..c24f920d612 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/azure-intellij-plugin-java-sdk.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/azure-intellij-plugin-java-sdk.xml @@ -4,6 +4,116 @@ com.intellij.modules.java - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/ruleConfigs.json b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/ruleConfigs.json new file mode 100644 index 00000000000..6cc1242feed --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/ruleConfigs.json @@ -0,0 +1,103 @@ +{ + "UpdateCheckpointAsyncSubscribeCheck": { + "usages": "updateCheckpointAsync", + "scope": "EventBatchContext", + "antiPatternMessage": "Consider replacing `subscribe()` with `block()` or `block(timeout)`, or use the synchronous version `updateCheckpoint()` for better reliability." + }, + "DynamicClientCreationCheck": { + "usages": [ + "buildClient", + "buildAsyncClient" + ], + "antiPatternMessage": "A new client instance is being created dynamically. For better performance and resource management, consider creating a single instance and reusing it." + }, + "DetectDiscouragedAPIUsageCheck": { + "hasDerivedRules": true, + "ConnectionStringCheck": { + "usages": "connectionString", + "antiPatternMessage": "Connection String API usage detected. Consider using `DefaultAzureCredential` for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication).", + "solution": "`DefaultAzureCredential` is recommended if the service client supports Token Credential (Entra ID Authentication). If not, then use Connection Strings based authentication." + }, + "GetCompletionsCheck": { + "usages": "getCompletions", + "scope": "com.azure.ai.openai", + "antiPatternMessage": "`getCompletions` API usage detected. Consider using the `getChatCompletions` API instead." + } + }, + "DetectDiscouragedClientCheck": { + "hasDerivedRules": true, + "ServiceBusReceiverAsyncClientCheck": { + "usages": "ServiceBusReceiverAsyncClient", + "antiPatternMessage": "Use of `ServiceBusReceiverAsyncClient` detected. Consider using `ServiceBusProcessorClient` instead." + }, + "EventHubConsumerAsyncClientCheck": { + "usages": "EventHubConsumerAsyncClient", + "antiPatternMessage": "Use of `EventHubConsumerAsyncClient` detected. Consider using `EventProcessorClient` as it simplifies event processing and provides a higher-level abstraction." + } + }, + "DisableAutoCompleteCheck": { + "usages": "disableAutoComplete", + "scope": [ + "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient" + ], + "antiPatternMessage": "Auto-completion enabled by default. Consider using the `disableAutoComplete()` API call to prevent automatic message completion." + }, + "GetSyncPollerOnPollerFluxCheck": { + "usages": "getSyncPoller", + "antiPatternMessage": "`getSyncPoller()` API usage on a `PollerFlux` detected. Consider using a `SyncPoller` directly to handle synchronous polling tasks." + }, + "HardcodedAPIKeysAndTokensCheck": { + "usages": [ + "AzureKeyCredential", + "AccessToken", + "KeyCredential", + "AzureNamedKeyCredential", + "AzureSasCredential", + "AzureNamedKey", + "ClientSecretCredentialBuilder", + "UsernamePasswordCredentialBuilder", + "BasicAuthenticationCredential" + ], + "antiPatternMessage": "`DefaultAzureCredential` is recommended for authentication if the service client supports Token Credential (Entra ID Authentication). If not, then use environment variables when using key based authentication." + }, + "SingleOperationInLoopTextAnalyticsCheck": { + "usages": [ + "detectLanguageBatch", + "recognizeEntitiesBatch", + "recognizePiiEntitiesBatch", + "recognizeLinkedEntitiesBatch", + "extractKeyPhrasesBatch", + "analyzeSentimentBatch" + ], + "scope": "com.azure.ai.textanalytics", + "antiPatternMessage": "Individual operations performed in a loop detected. This SDK provides a batch operation API, use it to perform multiple actions in a single request." + }, + "StorageUploadWithoutLengthCheck": { + "usages": [ + "upload", + "uploadWithResponse" + ], + "scope": "com.azure.storage.", + "antiPatternMessage": "Azure Storage upload API without length parameter detected. Consider using upload API with length parameter instead." + }, + "UseOfBlockOnAsyncClientsCheck": { + "usages": [ + "block", + "blockOptional", + "blockFirst", + "blockLast", + "toIterable", + "toStream", + "toFuture", + "blockFirstOptional", + "blockLastOptional" + ], + "scope": [ + "reactor.core.publisher.Flux", + "reactor.core.publisher.Mono" + ], + "antiPatternMessage": "Blocking calls detected on asynchronous clients. Consider using fully synchronous APIs instead." + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheckTest.java new file mode 100644 index 00000000000..ba9687c7fdb --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheckTest.java @@ -0,0 +1,111 @@ +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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; + +/** + * Class for testing {@link ConnectionStringCheck}. + */ +public class ConnectionStringCheckTest { + + private static final String SUGGESTION_MESSAGE = + "`DefaultAzureCredential` is recommended if the service client supports Token Credential (Entra ID Authentication). If not, then use Connection Strings based authentication."; + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiDeclarationStatement mockDeclarationStatement; + + @Mock + private PsiMethodCallExpression methodCallExpression; + @Mock + private RuleConfigLoader mockRuleConfigLoader; + @Mock + private RuleConfig mockRuleConfig; + @Mock + private PsiElement problemElement; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("connectionString")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure.")); + // Inject mock rules + Map mockRules = Map.of("ConnectionStringCheck", mockRuleConfig); + mockVisitor = new ConnectionStringCheck.ConnectionStringCheckVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void detectsDiscouragedAPIUsage(TestCase testCase) { + setupMockAPI(testCase.methodToCheck, testCase.numOfInvocations, testCase.packageName); + mockVisitor.visitMethodCallExpression(methodCallExpression); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(problemElement), + eq(SUGGESTION_MESSAGE)); + } + + private void setupMockAPI(String methodToCheck, int numOfInvocations, String packageName) { + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiMethod resolvedMethod = mock(PsiMethod.class); + PsiClass containingClass = mock(PsiClass.class); + problemElement = mock(PsiElement.class); + + when(methodCallExpression.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.resolve()).thenReturn(resolvedMethod); + when(resolvedMethod.getContainingClass()).thenReturn(containingClass); + when(resolvedMethod.getName()).thenReturn(methodToCheck); + when(containingClass.getQualifiedName()).thenReturn(packageName); + when(methodExpression.getReferenceNameElement()).thenReturn(problemElement); + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("connectionString", "com.azure.", + 1), + new TestCase("allowedMethod", "com.azure.", 0), + new TestCase("connectionString", "com.microsoft.azure.", 0) + ); + } + + private static class TestCase { + String methodToCheck; + String packageName; + int numOfInvocations; + + TestCase(String methodToCheck, String packageName, int numOfInvocations) { + this.methodToCheck = methodToCheck; + this.packageName = packageName; + this.numOfInvocations = numOfInvocations; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheckTest.java new file mode 100644 index 00000000000..ab0cc991d9a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheckTest.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DisableAutoCompleteCheck.DisableAutoCompleteVisitor; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 is used to test the DisableAutoCompleteCheck class. It tests the visitDeclarationStatement method of the + * DisableAutoCompleteVisitor class. Use of AC refers to the auto-complete feature. + */ +public class DisableAutoCompleteCheckTest { + + private static final String SUGGESTION_MESSAGE = + "Auto-completion enabled by default. Consider using the `disableAutoComplete()` API call to prevent automatic message completion."; + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiDeclarationStatement mockDeclarationStatement; + + @Mock + private PsiMethodCallExpression initializer; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("disableAutoComplete")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Arrays.asList( "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient")); + mockDeclarationStatement = mock(PsiDeclarationStatement.class); + Map mockRules = Map.of("DisableAutoCompleteCheck", mockRuleConfig); + mockVisitor = new DisableAutoCompleteVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testDisableAutoCompleteCheck(TestCase testCase) { + setupMockMethodCall(testCase.methodFound, testCase.numOfInvocations, testCase.packageName, testCase.className); + mockVisitor.visitDeclarationStatement(mockDeclarationStatement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(initializer), eq(SUGGESTION_MESSAGE)); + } + + private void setupMockMethodCall(String methodToCheck, int numOfInvocations, String packageName, String className) { + PsiVariable declaredElement = mock(PsiVariable.class); + PsiElement[] declaredElements = new PsiElement[] {declaredElement}; + + PsiType clientType = mock(PsiType.class); + initializer = mock(PsiMethodCallExpression.class); + + PsiReferenceExpression expression = mock(PsiReferenceExpression.class); + PsiMethodCallExpression qualifier = mock(PsiMethodCallExpression.class); + PsiMethodCallExpression finalExpression = mock(PsiMethodCallExpression.class); + + when(mockDeclarationStatement.getDeclaredElements()).thenReturn(declaredElements); + when(declaredElement.getType()).thenReturn(clientType); + when(declaredElement.getInitializer()).thenReturn(initializer); + when(clientType.getCanonicalText()).thenReturn(packageName); + when(clientType.getPresentableText()).thenReturn(packageName + className); + + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiMethod resolvedMethod = mock(PsiMethod.class); + PsiClass containingClass = mock(PsiClass.class); + + when(initializer.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.resolve()).thenReturn(resolvedMethod); + when(resolvedMethod.getContainingClass()).thenReturn(containingClass); + when(resolvedMethod.getName()).thenReturn(methodToCheck); + when(containingClass.getQualifiedName()).thenReturn(packageName); + + // Ensure the method call chain is properly set up + when(methodExpression.getQualifierExpression()).thenReturn(qualifier); + when(qualifier.getMethodExpression()).thenReturn(expression); + when(expression.getQualifierExpression()).thenReturn(finalExpression); + when(finalExpression.getMethodExpression()).thenReturn(expression); + when(expression.getReferenceName()).thenReturn(methodToCheck); + + if (!"disableAutoComplete".equals(methodToCheck)) { + when(expression.getQualifierExpression()).thenReturn(null); + } + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("com.azure.", "ServiceBusReceiverClient", 1, "notDisableAutoComplete"), + new TestCase("com.azure.", "ServiceBusProcessorClient", 1, "notDisableAutoComplete"), + new TestCase("com.azure.", "ServiceBusReceiverAsyncClient", 1, "notDisableAutoComplete"), + new TestCase("com.azure.", "ServiceBusRuleManagerClient", 0, "notDisableAutoComplete"), + new TestCase("com.azure.", "ServiceBusReceiverClient", 0, "disableAutoComplete"), + new TestCase("com.microsoft.azure.", "ServiceBusReceiverClient", 0, "disableAutoComplete") + ); + } + + private static class TestCase { + String packageName; + String className; + int numOfInvocations; + String methodFound; + + TestCase(String packageName, String className, int numOfInvocations, String methodFound) { + this.packageName = packageName; + this.className = className; + this.numOfInvocations = numOfInvocations; + this.methodFound = methodFound; + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheckTest.java new file mode 100644 index 00000000000..1f98c31da25 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheckTest.java @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiType; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DynamicClientCreationCheck.DynamicClientCreationVisitor; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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; + +/*** + * Tests for {@link DynamicClientCreationCheck} + */ +public class DynamicClientCreationCheckTest { + private static final String SUGGESTION_MESSAGE = + "A new client instance is being created dynamically. For better performance and resource management, consider" + + " creating a single instance and reusing it."; + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiForStatement mockElement; + + @Mock + private PsiMethodCallExpression mockMethodCallExpression; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("buildClient", "buildAsyncClient")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + mockElement = mock(PsiForStatement.class); + Map mockRules = Map.of("DynamicClientCreationCheck", mockRuleConfig); + mockVisitor = new DynamicClientCreationVisitor(mockHolder, mockRules); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testDynamicClientCreation(TestCase testCase) { + if (testCase.isAssignment) { + setupAssignmentExpression(testCase.methodName, testCase.packageName, testCase.numOfInvocations); + mockVisitor.visitForStatement(mockElement); + + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockMethodCallExpression), + eq(SUGGESTION_MESSAGE)); + } else { + setupWithDeclarationStatement(testCase.methodName, testCase.packageName, testCase.numOfInvocations); + mockVisitor.visitForStatement(mockElement); + + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockMethodCallExpression), eq(SUGGESTION_MESSAGE)); + } + } + + private void setupAssignmentExpression(String methodName, String packageName, int numOfInvocations) { + PsiStatement statement = mock(PsiStatement.class); + PsiBlockStatement body = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + PsiExpressionStatement blockChild = mock(PsiExpressionStatement.class); + PsiStatement[] blockStatements = new PsiStatement[] {blockChild}; + + PsiAssignmentExpression expression = mock(PsiAssignmentExpression.class); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiExpression qualifierExpression = mock(PsiExpression.class); + PsiType type = mock(PsiType.class); + + when(mockElement.getBody()).thenReturn(body); + when(body.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(blockStatements); + + when(blockChild.getExpression()).thenReturn(expression); + when(expression.getRExpression()).thenReturn(mockMethodCallExpression); + + when(mockMethodCallExpression.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.getReferenceName()).thenReturn(methodName); + when(methodExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(qualifierExpression.getType()).thenReturn(type); + when(qualifierExpression.getType().getCanonicalText()).thenReturn(packageName); + } + + private void setupWithDeclarationStatement(String methodName, String packageName, int numOfInvocations) { + PsiStatement statement = mock(PsiStatement.class); + PsiBlockStatement body = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + PsiDeclarationStatement blockChild = mock(PsiDeclarationStatement.class); + PsiStatement[] blockStatements = new PsiStatement[] {blockChild}; + + PsiLocalVariable declaredElement = mock(PsiLocalVariable.class); + PsiElement[] declaredElements = new PsiElement[] {declaredElement}; + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiExpression qualifierExpression = mock(PsiExpression.class); + PsiType type = mock(PsiType.class); + + when(mockElement.getBody()).thenReturn(body); + when(body.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(blockStatements); + + when(blockChild.getDeclaredElements()).thenReturn(declaredElements); + when(declaredElement.getInitializer()).thenReturn(mockMethodCallExpression); + + when(mockMethodCallExpression.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.getReferenceName()).thenReturn(methodName); + when(methodExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(qualifierExpression.getType()).thenReturn(type); + when(qualifierExpression.getType().getCanonicalText()).thenReturn(packageName); + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("buildClient", "com.azure.", 1, true), + new TestCase("buildClient", "com.azure.", 1, false), + new TestCase("buildClient", "com.Notazure.", 0, true), + new TestCase("buildClient", "com.Notazure.", 0, false), + new TestCase("NotbuildClient", "com.azure.", 0, true), + new TestCase("NotbuildClient", "com.azure.", 0, false) + ); + } + + private static class TestCase { + String methodName; + String packageName; + int numOfInvocations; + boolean isAssignment; + + TestCase(String methodName, String packageName, int numOfInvocations, boolean isAssignment) { + this.methodName = methodName; + this.packageName = packageName; + this.numOfInvocations = numOfInvocations; + this.isAssignment = isAssignment; + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheckTest.java new file mode 100644 index 00000000000..8b8529d0c4f --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheckTest.java @@ -0,0 +1,95 @@ +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiTypeElement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 is used to check if the EventHubConsumerAsyncClient is being used in the code. + */ +public class EventHubConsumerAsyncClientCheckTest { + + private static final String SUGGESTION_MESSAGE = "Use of `EventHubConsumerAsyncClient` detected. Consider using `EventProcessorClient` as it simplifies event processing and provides a higher-level abstraction."; + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiTypeElement mockTypeElement; + + @Mock + private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("EventHubConsumerAsyncClient")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + Map mockRules = Map.of("EventHubConsumerAsyncClientCheck", mockRuleConfig); + mockVisitor = + new EventHubConsumerAsyncClientCheck.EventHubConsumerAsyncClientVisitor(mockHolder, mockRules); + + mockTypeElement = mock(PsiTypeElement.class); + } + + @ParameterizedTest + @MethodSource("provideEventHubConsumerAsyncClientTestCases") + void detectsEventHubConsumerAsyncClientUsage(TestCase testCase) { + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); + mockVisitor.visitTypeElement(mockTypeElement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), + eq(SUGGESTION_MESSAGE)); + } + + private static Stream provideEventHubConsumerAsyncClientTestCases() { + return Stream.of( + new TestCase("EventHubConsumerAsyncClient", 1), + new TestCase("EventProcessorClient", 0), + new TestCase("", 0) + ); + } + + private void setupMockElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck) { + PsiClassType mockType = mock(PsiClassType.class); + PsiClass mockClass = mock(PsiClass.class); + when(typeElement.getType()).thenReturn(mockType); + when(mockType.resolve()).thenReturn(mockClass); + when(mockClass.getQualifiedName()).thenReturn("com.azure."); + when(mockClass.getName()).thenReturn(clientToCheck); + } + + private static class TestCase { + String usagesToCheck; + int numOfInvocations; + + TestCase(String usagesToCheck, int numOfInvocations) { + this.usagesToCheck = usagesToCheck; + this.numOfInvocations = numOfInvocations; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheckTest.java new file mode 100644 index 00000000000..c83494c8e1a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheckTest.java @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.Mockito.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; + +/** + * Tests for {@link GetCompletionsCheck}. + */ +public class GetCompletionsCheckTest { + private static final String SUGGESTION_MESSAGE = + "`getCompletions` API usage detected. Consider using the `getChatCompletions` API instead."; + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression methodCallExpression; + + @Mock + private PsiElement problemElement; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + methodCallExpression = mock(PsiMethodCallExpression.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getCompletions")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + Map mockRules = Map.of("GetCompletionsCheck", mockRuleConfig); + mockVisitor = new GetCompletionsCheck.GetCompletionsVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void detectsDiscouragedAPIUsage(TestCase testCase) { + setupMockAPI(testCase.methodToCheck, testCase.numOfInvocations, testCase.packageName); + mockVisitor.visitMethodCallExpression(methodCallExpression); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(problemElement), + eq(SUGGESTION_MESSAGE)); + } + + private void setupMockAPI(String methodToCheck, int numOfInvocations, String packageName) { + methodCallExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiMethod resolvedMethod = mock(PsiMethod.class); + PsiClass containingClass = mock(PsiClass.class); + problemElement = mock(PsiElement.class); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Arrays.asList(packageName)); + + when(methodCallExpression.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.resolve()).thenReturn(resolvedMethod); + when(resolvedMethod.getContainingClass()).thenReturn(containingClass); + when(resolvedMethod.getName()).thenReturn(methodToCheck); + when(containingClass.getQualifiedName()).thenReturn(packageName); + when(methodExpression.getReferenceNameElement()).thenReturn(problemElement); + } + + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("getCompletions", "com.azure.ai.openai", 1), + new TestCase("getCompletions", "com.azure.other", 0) + ); + } + + private static class TestCase { + String methodToCheck; + String packageName; + int numOfInvocations; + + TestCase(String methodToCheck, String packageName, int numOfInvocations) { + this.methodToCheck = methodToCheck; + this.packageName = packageName; + this.numOfInvocations = numOfInvocations; + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheckTest.java new file mode 100644 index 00000000000..855b2e84078 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheckTest.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.util.PsiTreeUtil; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for GetSyncPollerOnPollerFluxCheck. + */ +public class GetSyncPollerOnPollerFluxCheckTest { + private static final String SUGGESTION_MESSAGE = + "`getSyncPoller()` API usage on a `PollerFlux` detected. Consider using a `SyncPoller` directly to handle synchronous polling tasks."; + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression mockMethodCallExpression; + + @Mock + private PsiElement mockElement; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getSyncPoller")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + mockElement = mock(PsiElement.class); + Map mockRules = Map.of("GetSyncPollerOnPollerFluxCheck", mockRuleConfig); + mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testGetSyncPollerOnPollerFluxCheck(TestCase testCase) { + mockMethodExpression(testCase.methodName, testCase.className, testCase.numberOfInvocations); + mockVisitor.visitMethodCallExpression(mockMethodCallExpression); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(mockElement, SUGGESTION_MESSAGE); + } + + private static Stream testCases() { + return Stream.of( + new TestCase("getSyncPoller", "com.azure.core.util.polling.PollerFlux", 1), + new TestCase("getAnotherMethod", "com.azure.core.util.polling.PollerFlux", 0), + new TestCase("getSyncPoller", "com.not.azure..util.polling.DifferentClassName", 0), + new TestCase("getSyncPoller", null, 0) + ); + } + + private void mockMethodExpression(String methodName, String className, int numberOfInvocations) { + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiExpression expression = mock(PsiExpression.class); + PsiType type = mock(PsiType.class); + PsiClass containingClass = mock(PsiClass.class); + PsiMethod method = mock(PsiMethod.class); + + when(mockMethodCallExpression.resolveMethod()).thenReturn(method); + when(method.getContainingClass()).thenReturn(containingClass); + when(containingClass.getQualifiedName()).thenReturn(className); + when(mockMethodCallExpression.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getReferenceName()).thenReturn(methodName); + when(referenceExpression.getReferenceNameElement()).thenReturn(mockElement); + } + + private static class TestCase { + String methodName; + String className; + int numberOfInvocations; + + TestCase(String methodName, String className, int numberOfInvocations) { + this.methodName = methodName; + this.className = className; + this.numberOfInvocations = numberOfInvocations; + } + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheckTest.java new file mode 100644 index 00000000000..2e7d54b4633 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheckTest.java @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiNewExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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; + +/** + * Test the HardcodedAPIKeysAndTokensCheck class for hardcoded API keys and tokens. + * When a client is authenticated with AzurekeyCredentials and AccessToken, a problem is registered. + * These are some instances that a flag would be raised. + * 1. TextAnalyticsClient client = new TextAnalyticsClientBuilder() + * .endpoint(endpoint) + * .credential(new AzureKeyCredential(apiKey)) + * .buildClient(); + *

+ * 2. TokenCredential credential = request -> { + * AccessToken token = new AccessToken("", OffsetDateTime.now().plusHours(1)); + * } + */ +public class HardcodedAPIKeysAndTokensCheckTest { + + private static final String SUGGESTION_MESSAGE = + "`DefaultAzureCredential` is recommended for authentication if the service client supports Token Credential (Entra ID Authentication). If not, then use environment variables when using key based authentication."; + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private RuleConfig mockRuleConfig; + private PsiElement problemElement; + @Mock + private RuleConfigLoader mockRuleConfigLoader; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("AzureKeyCredential", + "AccessToken", + "KeyCredential", + "AzureNamedKeyCredential", + "AzureSasCredential", + "AzureNamedKey", + "ClientSecretCredentialBuilder", + "UsernamePasswordCredentialBuilder", + "BasicAuthenticationCredential")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + Map mockRules = Map.of("HardcodedAPIKeysAndTokensCheck", mockRuleConfig); + + mockVisitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testHardcodedAPIKeysAndTokensCheck(TestCase testCase) { + PsiNewExpression mockExpression = mockMethodExpression(testCase.apiName, testCase.credentialString, + testCase.numOfInvocations); + mockVisitor.visitNewExpression(mockExpression); + + verify(mockHolder, times(testCase.numOfInvocations)) + .registerProblem(eq(mockExpression),eq(SUGGESTION_MESSAGE)); + } + + @Test + public void testNoFlagIfNoHardcodedAPIKeysAndTokens() { + PsiNewExpression newExpression = mock(PsiNewExpression.class); + PsiJavaCodeReferenceElement javaCodeReferenceElement = mock(PsiJavaCodeReferenceElement.class); + PsiLiteralExpression literalExpression = mock(PsiLiteralExpression.class); + + when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); + when(javaCodeReferenceElement.getReferenceName()).thenReturn("AzureKeyCredential"); + when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure."); + when(newExpression.getChildren()).thenReturn(new PsiElement[]{literalExpression}); + when(literalExpression.getValue()).thenReturn(System.getenv()); + + mockVisitor.visitNewExpression(newExpression); + + verify(mockHolder, times(0)).registerProblem(eq(newExpression), eq(SUGGESTION_MESSAGE)); + } + + private static Stream testCases() { + return Stream.of( + new TestCase("AzureKeyCredential", "340c6ea27d214f88b7a759fee63cbfa1", 1), + new TestCase("AccessToken", "access-token", 0), + new TestCase("KeyCredential", "340c6ea27d214f88b7a759fee63cbfa1", 1), + new TestCase("AzureNamedKeyCredential", "340c6ea27d214f88b7a759fee63cbfa1", 1), + new TestCase("AzureSasCredential", "", 0), + new TestCase("AzureNamedKey", "", 0), + new TestCase("SomeOtherClient", "340c6ea27d214f88b7a759fee63cbfa1", 0), + new TestCase("", "340c6ea27d214f88b7a759fee63cbfa1", 0) + ); + } + + private PsiNewExpression mockMethodExpression(String authServiceToCheck, String credentialString, int numOfInvocations) { + PsiNewExpression newExpression = mock(PsiNewExpression.class); + PsiJavaCodeReferenceElement javaCodeReferenceElement = mock(PsiJavaCodeReferenceElement.class); + PsiLiteralExpression literalExpression = mock(PsiLiteralExpression.class); + PsiExpressionList expressionList = mock(PsiExpressionList.class); + + when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); + when(javaCodeReferenceElement.getReferenceName()).thenReturn(authServiceToCheck); + when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure."); + when(newExpression.getChildren()).thenReturn(new PsiElement[]{expressionList}); + when(expressionList.getExpressions()).thenReturn(new PsiExpression[]{literalExpression}); + when(literalExpression.getValue()).thenReturn(credentialString); + + return newExpression; + } + + static class TestCase { + String apiName; + String credentialString; + int numOfInvocations; + + TestCase(String apiName, String credentialString, int numOfInvocations) { + this.apiName = apiName; + this.credentialString = credentialString; + this.numOfInvocations = numOfInvocations; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheckTest.java new file mode 100644 index 00000000000..30b50b1f883 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheckTest.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiTypeElement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 ServiceBusReceiverAsyncClientCheck class by mocking the ProblemsHolder and PsiElementVisitor + * and verifying that a problem is registered when the ServiceBusReceiverAsyncClient is used. + * The test also verifies that a problem is not registered when the PsiElement is null. + * + * Here are some examples of test data where registerProblem should be called: + * 1. ServiceBusReceiverAsyncClient client = new ServiceBusReceiverAsyncClient(); + * 2. private ServiceBusReceiverAsyncClient receiver; + * 3. final ServiceBusReceiverAsyncClient autoCompleteReceiver = + * toClose(getReceiverBuilder(false, entityType, index, false) + * .buildAsyncClient()); + */ +public class ServiceBusReceiverAsyncClientCheckTest { + + private static final String SUGGESTION_MESSAGE = "Use of `ServiceBusReceiverAsyncClient` detected. Consider using `ServiceBusProcessorClient` instead."; + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiTypeElement mockTypeElement; + + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("ServiceBusReceiverAsyncClient")); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + mockTypeElement = mock(PsiTypeElement.class); + Map mockRules = Map.of("ServiceBusReceiverAsyncClientCheck", mockRuleConfig); + mockVisitor = new ServiceBusReceiverAsyncClientCheck.ServiceBusReceiverAsyncClientCheckVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideServiceBusReceiverAsyncClientTestCases") + void detectsServiceBusReceiverAsyncClientUsage(TestCase testCase) { + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); + mockVisitor.visitTypeElement(mockTypeElement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), eq(SUGGESTION_MESSAGE)); + } + + private static Stream provideServiceBusReceiverAsyncClientTestCases() { + return Stream.of( + new TestCase("ServiceBusReceiverAsyncClient", 1), + new TestCase("ServiceBusProcessorClient", 0), + new TestCase("", 0) + ); + } + + private void setupMockElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck) { + PsiClassType mockType = mock(PsiClassType.class); + PsiClass mockClass = mock(PsiClass.class); + when(typeElement.getType()).thenReturn(mockType); + when(mockType.resolve()).thenReturn(mockClass); + when(mockClass.getQualifiedName()).thenReturn("com.azure."); + when(mockClass.getName()).thenReturn(clientToCheck); + } + + private static class TestCase { + String usagesToCheck; + int numOfInvocations; + + TestCase(String usagesToCheck, int numOfInvocations) { + this.usagesToCheck = usagesToCheck; + this.numOfInvocations = numOfInvocations; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheckTest.java new file mode 100644 index 00000000000..6ee29ab051a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheckTest.java @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiDoWhileStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiForeachStatement; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.PsiWhileStatement; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +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 is used to test the SingleOperationInLoopCheck class. The SingleOperationInLoopCheck is an inspection to + * check if there is a single Azure client operation inside a loop. A single Azure client operation is defined as a + * method call on a class that is part of the Azure SDK. If a single Azure client operation is found inside a loop, a + * problem will be registered. + *

+ * THis is an example of a situation where the inspection should register a problem: + *

+ * 1. With a single PsiDeclarationStatement inside a while loop // While loop int i = 0; while (i < 10) { + *

+ * BlobAsyncClient blobAsyncClient = new BlobClientBuilder() + * .endpoint("https://.blob.core.windows.net") .sasToken("") + * .containerName("") .blobName("") .buildAsyncClient(); + *

+ * i++; } + *

+ * 2. With a single PsiExpressionStatement inside a for loop for (String documentPath : documentPaths) { + *

+ * blobAsyncClient.uploadFromFile(documentPath) .doOnSuccess(response -> System.out.println("Blob uploaded successfully + * in enhanced for loop.")) .subscribe(); } + */ +public class SingleOperationInLoopCheckTest { + private static final String SUGGESTION_MESSAGE = "Individual operations performed in a loop detected. This SDK provides a batch operation API, use it to perform multiple actions in a single request."; + @Mock + private ProblemsHolder mockHolder; + private JavaElementVisitor mockVisitor; + private PsiMethodCallExpression initializer; + private PsiMethodCallExpression expression; + + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("detectLanguageBatch", + "recognizeEntitiesBatch, recognizePiiEntitiesBatch, recognizeLinkedEntitiesBatch", + "extractKeyPhrasesBatch", "analyzeSentimentBatch")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure.ai.textanalytics")); + initializer = mock(PsiMethodCallExpression.class); + expression = mock(PsiMethodCallExpression.class); + Map mockRules = Map.of("SingleOperationInLoopTextAnalyticsCheck", mockRuleConfig); + mockVisitor = new SingleOperationInLoopTextAnalyticsCheck.SingleOperationInLoopVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testSingleOperationInLoop(TestCase testCase) { + String expectedMessage = SUGGESTION_MESSAGE + " Consider using " + testCase.methodName + "Batch instead."; + if (testCase.isDeclaration) { + setupWithSinglePsiDeclarationStatement(testCase.loopStatement, testCase.packageName, + testCase.numberOfInvocations, testCase.methodName); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(initializer), + Mockito.eq(expectedMessage)); + } else { + setupWithSinglePsiExpressionStatement(testCase.loopStatement, testCase.packageName, + testCase.numberOfInvocations, testCase.methodName); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(expression), + Mockito.eq(expectedMessage)); + } + } + + private void setupWithSinglePsiExpressionStatement(PsiStatement loopStatement, String packageName, + int numberOfInvocations, String methodName) { + PsiBlockStatement loopBody = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + PsiClass containingClass = mock(PsiClass.class); + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiExpressionStatement mockStatement = mock(PsiExpressionStatement.class); + PsiStatement[] statements = new PsiStatement[] {mockStatement}; + PsiExpression qualifierExpression = mock(PsiExpression.class); + + when(mockStatement.getExpression()).thenReturn(expression); + when(loopBody.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(statements); + when(expression.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(qualifierExpression.getType()).then(invocation -> { + PsiType psiType = mock(PsiType.class); + when(psiType.getCanonicalText()).thenReturn(packageName); + return psiType; + }); + when(referenceExpression.getReferenceName()).thenReturn(methodName); + if (loopStatement instanceof PsiForStatement) { + when(((PsiForStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitForStatement((PsiForStatement) loopStatement); + } else if (loopStatement instanceof PsiForeachStatement) { + when(((PsiForeachStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitForeachStatement((PsiForeachStatement) loopStatement); + } else if (loopStatement instanceof PsiWhileStatement) { + when(((PsiWhileStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitWhileStatement((PsiWhileStatement) loopStatement); + } else if (loopStatement instanceof PsiDoWhileStatement) { + when(((PsiDoWhileStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitDoWhileStatement((PsiDoWhileStatement) loopStatement); + } + } + + private void setupWithSinglePsiDeclarationStatement(PsiStatement loopStatement, String packageName, + int numberOfInvocations, String methodName) { + PsiBlockStatement loopBody = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + PsiVariable element = mock(PsiVariable.class); + PsiElement[] elements = new PsiElement[] {element}; + PsiClass containingClass = mock(PsiClass.class); + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiDeclarationStatement mockStatement = mock(PsiDeclarationStatement.class); + PsiStatement[] statements = new PsiStatement[] {mockStatement}; + PsiExpression qualifierExpression = mock(PsiExpression.class); + + when(mockStatement.getDeclaredElements()).thenReturn(elements); + when(loopBody.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(statements); + when(element.getInitializer()).thenReturn(initializer); + when(initializer.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(qualifierExpression.getType()).then(invocation -> { + PsiType psiType = mock(PsiType.class); + when(psiType.getCanonicalText()).thenReturn(packageName); + return psiType; + }); + when(referenceExpression.getReferenceName()).thenReturn(methodName); + + if (loopStatement instanceof PsiForStatement) { + when(((PsiForStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitForStatement((PsiForStatement) loopStatement); + } else if (loopStatement instanceof PsiForeachStatement) { + when(((PsiForeachStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitForeachStatement((PsiForeachStatement) loopStatement); + } else if (loopStatement instanceof PsiWhileStatement) { + when(((PsiWhileStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitWhileStatement((PsiWhileStatement) loopStatement); + } else if (loopStatement instanceof PsiDoWhileStatement) { + when(((PsiDoWhileStatement) loopStatement).getBody()).thenReturn(loopBody); + mockVisitor.visitDoWhileStatement((PsiDoWhileStatement) loopStatement); + } + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase(mock(PsiForStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", false), + new TestCase(mock(PsiForeachStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", false), + new TestCase(mock(PsiWhileStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", false), + new TestCase(mock(PsiDoWhileStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", false), + new TestCase(mock(PsiForStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", true), + new TestCase(mock(PsiForeachStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", true), + new TestCase(mock(PsiWhileStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", true), + new TestCase(mock(PsiDoWhileStatement.class), "com.azure.ai.textanalytics", 1, "detectLanguage", true), + new TestCase(mock(PsiForStatement.class), "com.microsoft.azure.storage.blob", 0, "detectLanguage", true), + new TestCase(mock(PsiForStatement.class), "com.azure.ai.textanalytics", 0, "differentMethodName", true) + ); + } + + private static class TestCase { + PsiStatement loopStatement; + String packageName; + int numberOfInvocations; + String methodName; + boolean isDeclaration; + + TestCase(PsiStatement loopStatement, String packageName, int numberOfInvocations, String methodName, + boolean isDeclaration) { + this.loopStatement = loopStatement; + this.packageName = packageName; + this.numberOfInvocations = numberOfInvocations; + this.methodName = methodName; + this.isDeclaration = isDeclaration; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheckTest.java new file mode 100644 index 00000000000..ba45c40615a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheckTest.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaRecursiveElementWalkingVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.util.PsiTypesUtil; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +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 is used to test the StorageUploadWithoutLengthCheck class. + * It tests the visitor method to check if the upload methods are being called without a 'length' parameter of type 'long'. + * + * These are examples of situations where the visitor method should register a problem: + * 1. @ServiceMethod(returns = ReturnType.SINGLE) + * public void upload(InputStream data) { + * uploadWithResponse(new BlobParallelUploadOptions(data), null, null); + * } + * + * 2. uploadWithResponse(new BlobParallelUploadOptions(data).setRequestConditions(blobRequestConditions), null, Context.NONE); + * + * 3. upload(data, false); + */ +public class StorageUploadWithoutLengthCheckTest { + private static final String SUGGESTION_MESSAGE = "Azure Storage upload API without length parameter detected. Consider using upload API with length parameter instead."; + + @Mock + private ProblemsHolder mockHolder; + @Mock + private JavaRecursiveElementWalkingVisitor mockVisitor; + @Mock + private PsiMethodCallExpression mockExpression; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("upload", + "uploadWithResponse")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure.storage.")); + mockExpression = mock(PsiMethodCallExpression.class); + Map mockRules = Map.of("StorageUploadWithoutLengthCheck", mockRuleConfig); + mockVisitor = new StorageUploadWithoutLengthCheck.StorageUploadVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testStorageUploadWithoutLengthCheck(TestCase testCase) { + setupStorageCall(testCase.methodName, testCase.numberOfInvocations); + mockVisitor.visitMethodCallExpression(mockExpression); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(eq(mockExpression), contains(SUGGESTION_MESSAGE)); + } + + private void setupStorageCall(String methodName, int numberOfInvocations) { + mockExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression mockReferenceExpression = mock(PsiReferenceExpression.class); + PsiExpression mockQualifierExpression = mock(PsiExpression.class); + PsiType mockPsiType = mock(PsiType.class); + PsiClassType qualifierType = mock(PsiClassType.class); + PsiExpressionList mockArgumentList = mock(PsiExpressionList.class); + PsiClass mockContainingClass = mock(PsiClass.class); + + when(mockExpression.getMethodExpression()).thenReturn(mockReferenceExpression); + + when(mockReferenceExpression.getQualifierExpression()).thenReturn(mockQualifierExpression); + when(mockQualifierExpression.getType()).thenReturn(qualifierType); + when(qualifierType.resolve()).thenReturn(mockContainingClass); + + when(mockContainingClass.getQualifiedName()).thenReturn("com.azure.storage.blob.BlobAsyncClient"); + + when(mockReferenceExpression.getReferenceName()).thenReturn(methodName); + + when(mockExpression.getArgumentList()).thenReturn(mockArgumentList); + PsiExpression[] mockArguments = new PsiExpression[numberOfInvocations]; + for (int i = 0; i < numberOfInvocations; i++) { + mockArguments[i] = mock(PsiExpression.class); + } + when(mockArgumentList.getExpressions()).thenReturn(mockArguments); + } + + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("upload", 1), + new TestCase("notInList", 0) + ); + } + + private static class TestCase { + String methodName; + int numberOfInvocations; + + TestCase(String methodName, int numberOfInvocations) { + this.methodName = methodName; + this.numberOfInvocations = numberOfInvocations; + } + } +} + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheckTest.java new file mode 100644 index 00000000000..8d993d64c43 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheckTest.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiReferenceExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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; + +/** + * Tests for {@link UpdateCheckpointAsyncSubscribeCheck} and derived classes. + */ +public class UpdateCheckpointAsyncSubscribeCheckTest { + private static final String SUGGESTION_MESSAGE = "Consider replacing `subscribe()` with `block()` or `block(timeout)`, or use the synchronous version `updateCheckpoint()` for better reliability."; + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private PsiMethodCallExpression mockMethodCallExpression; + @Mock + private PsiElement mockPsiElement; + + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + @Mock + private JavaElementVisitor mockVisitor; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("updateCheckpointAsync")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.singletonList("EventBatchContext")); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + mockPsiElement = mock(PsiElement.class); + + Map mockRules = Map.of("UpdateCheckpointAsyncSubscribeCheck", mockRuleConfig); + mockVisitor = new UpdateCheckpointAsyncSubscribeCheck.UpdateCheckpointAsyncVisitor(mockHolder, + mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testWithParameterizedCases(String packageName, String mainMethodFound, + int numOfInvocations, String followingMethod, String objectType) { + setupMockMethodCall(packageName, mainMethodFound, numOfInvocations, followingMethod, + objectType, + SUGGESTION_MESSAGE); + mockVisitor.visitMethodCallExpression(mockMethodCallExpression); + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(mockPsiElement), + eq(SUGGESTION_MESSAGE)); + } + + private static Stream provideTestCases() { + return Stream.of( + new Object[] {"com.azure.", "updateCheckpointAsync", 1, + "subscribe", "EventBatchContext"}, + new Object[] {"com.azure.", "updateCheckpointAsync", 0, "notSubscribe", "EventBatchContext"} + ); + } + + private void setupMockMethodCall(String packageName, String mainMethodFound, + int numOfInvocations, String followingMethod, String objectType, String expectedMessage) { + PsiReferenceExpression mockReferenceExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression parentReferenceExpression = mock(PsiReferenceExpression.class); + PsiMethodCallExpression grandParentMethodCalLExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression mockQualifier = mock(PsiReferenceExpression.class); + PsiParameter mockParameter = mock(PsiParameter.class); + PsiClassType parameterType = mock(PsiClassType.class); + PsiClass psiClass = mock(PsiClass.class); + PsiMethod resolvedMethod = mock(PsiMethod.class); + PsiClass containingClass = mock(PsiClass.class); + PsiReferenceExpression followingMethodRefExpression = mock(PsiReferenceExpression.class); + + when(mockMethodCallExpression.getMethodExpression()).thenReturn(mockReferenceExpression); + when(mockReferenceExpression.getReferenceName()).thenReturn(mainMethodFound); + when(mockReferenceExpression.resolve()).thenReturn(resolvedMethod); + when(resolvedMethod.getContainingClass()).thenReturn(containingClass); + when(resolvedMethod.getName()).thenReturn(mainMethodFound); + when(containingClass.getQualifiedName()).thenReturn(packageName + objectType); + + when(mockMethodCallExpression.getParent()).thenReturn(followingMethodRefExpression); + when(followingMethodRefExpression.getParent()).thenReturn(grandParentMethodCalLExpression); + when(grandParentMethodCalLExpression.getMethodExpression()).thenReturn(parentReferenceExpression); + when(followingMethodRefExpression.getReferenceName()).thenReturn(followingMethod); + when(mockReferenceExpression.getQualifierExpression()).thenReturn(mockQualifier); + when(mockQualifier.resolve()).thenReturn(mockParameter); + when(mockParameter.getType()).thenReturn(parameterType); + when(parameterType.getCanonicalText()).thenReturn(objectType); + when(parameterType.resolve()).thenReturn(psiClass); + when(psiClass.getQualifiedName()).thenReturn(packageName + objectType); + when(mockReferenceExpression.getReferenceNameElement()).thenReturn(mockPsiElement); + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheckTest.java new file mode 100644 index 00000000000..f5d1bcff423 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheckTest.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +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 is used to test the UseOfBlockOnAsyncClientsCheck class. The UseOfBlockOnAsyncClientsCheck class is an + * inspection tool that checks for the use of blocking method on async clients in Azure SDK. This inspection will check + * for the use of blocking method on reactive types like Mono, Flux, etc. This is an example of what should be flagged: + *

+ * private ServiceBusReceiverAsyncClient receiver; receiver.complete(received).block(Duration.ofSeconds(15)); + *

+ * private final ServiceBusReceiverAsyncClient client; try { if (isComplete) { client.complete(message) + * .doOnSuccess(success -> System.out.println("Message completed successfully")) .doOnError(error -> + * System.err.println("Error completing message: " + error.getMessage())) .log() .timeout(Duration.ofSeconds(30)) + * .retry(3) .block(); + *

+ * } else { client.abandon(message).block(); } + */ +public class UseOfBlockOnAsyncClientsCheckTest { + private static final String SUGGESTION_MESSAGE = "Blocking calls detected on asynchronous clients. Consider using" + + " fully synchronous APIs instead."; + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression mockElement; + + @Mock + private PsiElement problemElement; + + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("block", + "blockOptional", + "blockFirst", + "blockLast", + "toIterable", + "toStream", + "toFuture", + "blockFirstOptional", + "blockLastOptional")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Arrays.asList("reactor.core.publisher.Flux", + "reactor.core.publisher.Mono")); + mockElement = mock(PsiMethodCallExpression.class); + problemElement = mock(PsiElement.class); + Map mockRules = Map.of("UseOfBlockOnAsyncClientsCheck", mockRuleConfig); + mockVisitor = new UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor(mockHolder, mockRules); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testUseOfBlockOnAsyncClient(TestCase testCase) { + setupMockMethodCallExpression(testCase.methodName, testCase.clientPackageName, testCase.numberOfInvocations, + testCase.reactivePackageName); + mockVisitor.visitMethodCallExpression(mockElement); + + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(problemElement), + Mockito.eq(SUGGESTION_MESSAGE)); + } + + private void setupMockMethodCallExpression(String methodName, String clientPackageName, int numberOfInvocations, + String reactivePackageName) { + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiMethodCallExpression expression = mock(PsiMethodCallExpression.class); + PsiClassType type = mock(PsiClassType.class); + PsiClass qualifierReturnTypeClass = mock(PsiClass.class); + + PsiReferenceExpression clientReferenceExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression clientQualifierExpression = mock(PsiReferenceExpression.class); + PsiClassType clientType = mock(PsiClassType.class); + PsiClass clientReturnTypeClass = mock(PsiClass.class); + + when(mockElement.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getReferenceName()).thenReturn(methodName); + + when(referenceExpression.getQualifierExpression()).thenReturn(expression); + when(expression.getType()).thenReturn(type); + when(type.resolve()).thenReturn(qualifierReturnTypeClass); + + when(qualifierReturnTypeClass.getQualifiedName()).thenReturn(reactivePackageName); + + when(expression.getMethodExpression()).thenReturn(clientReferenceExpression); + when(clientReferenceExpression.getQualifierExpression()).thenReturn(clientQualifierExpression); + when(clientQualifierExpression.getType()).thenReturn(clientType); + when(clientType.resolve()).thenReturn(clientReturnTypeClass); + when(clientReturnTypeClass.getQualifiedName()).thenReturn(clientPackageName); + when(referenceExpression.getReferenceNameElement()).thenReturn(problemElement); + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("block", "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient", 1, + "reactor.core.publisher.Flux"), + new TestCase("blockOptional", "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient", 1, + "reactor.core.publisher.Mono"), + new TestCase("blockFirst", "com.notAzure.", 0, "reactor.core.publisher.Flux"), + new TestCase("nonBlockingMethod", "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient", 0, + "reactor.core.publisher.Flux"), + new TestCase("block", "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient", 0, "java.util.List"), + new TestCase("block", "com.azure.messaging.servicebus.ServiceBusReceiverClient", 0, + "reactor.core.publisher.Mono") + ); + } + + private static class TestCase { + String methodName; + String clientPackageName; + int numberOfInvocations; + String reactivePackageName; + + TestCase(String methodName, String clientPackageName, int numberOfInvocations, String reactivePackageName) { + this.methodName = methodName; + this.clientPackageName = clientPackageName; + this.numberOfInvocations = numberOfInvocations; + this.reactivePackageName = reactivePackageName; + } + } +} \ No newline at end of file