From d58f560acb279fa4c0df02d8992d29a925f52347 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Sun, 5 Jan 2025 20:39:13 -0800 Subject: [PATCH 1/7] Initial code analyzer refactor --- .../build.gradle.kts | 6 + .../azure-intellij-plugin-java-sdk/readme.md | 74 +++++ .../analyzer/AbstractLibraryVersionCheck.java | 85 +++++ .../sdk/analyzer/AsyncSubscribeChecker.java | 92 ++++++ .../sdk/analyzer/ConnectionStringCheck.java | 18 ++ .../DetectDiscouragedAPIUsageCheck.java | 105 +++++++ .../DetectDiscouragedClientCheck.java | 88 ++++++ .../analyzer/DisableAutoCompleteCheck.java | 160 ++++++++++ .../analyzer/DynamicClientCreationCheck.java | 88 ++++++ .../EndpointOnNonAzureOpenAIAuthCheck.java | 169 ++++++++++ .../sdk/analyzer/GetCompletionsCheck.java | 18 ++ .../GetSyncPollerOnPollerFluxCheck.java | 130 ++++++++ .../HardcodedAPIKeysAndTokensCheck.java | 86 +++++ .../analyzer/IncompatibleDependencyCheck.java | 207 ++++++++++++ ...iesWithTimeIntervalInQueryStringCheck.java | 201 ++++++++++++ .../analyzer/ServiceBusReceiveModeCheck.java | 208 ++++++++++++ .../ServiceBusReceiverAsyncClientCheck.java | 21 ++ .../analyzer/SingleOperationInLoopCheck.java | 295 ++++++++++++++++++ .../StorageUploadWithoutLengthCheck.java | 195 ++++++++++++ ...UpdateCheckpointAsyncSubscribeChecker.java | 88 ++++++ .../analyzer/UpgradeLibraryVersionCheck.java | 105 +++++++ .../UseOfBlockOnAsyncClientsCheck.java | 157 ++++++++++ .../models/DependencyVersionsDataCache.java | 190 +++++++++++ .../intellij/java/sdk/models/RuleConfig.java | 102 ++++++ .../utils/DependencyVersionFileFetcher.java | 184 +++++++++++ .../java/sdk/utils/RuleConfigLoader.java | 247 +++++++++++++++ .../intellij/java/sdk/utils/VersionUtils.java | 90 ++++++ .../AbstractLibraryVersionCheckTest.java | 174 +++++++++++ .../analyzer/AsyncSubscribeCheckerTest.java | 91 ++++++ .../DetectDiscouragedAPIUsageCheckTest.java | 130 ++++++++ .../DetectDiscouragedClientCheckTest.java | 113 +++++++ .../DisableAutoCompleteCheckTest.java | 123 ++++++++ .../DynamicClientCreationCheckTest.java | 161 ++++++++++ ...EndpointOnNonAzureOpenAIAuthCheckTest.java | 112 +++++++ .../GetSyncPollerOnPollerFluxCheckTest.java | 91 ++++++ .../HardcodedAPIKeysAndTokensCheckTest.java | 124 ++++++++ ...ithTimeIntervalInQueryStringCheckTest.java | 210 +++++++++++++ .../ServiceBusReceiveModeCheckTest.java | 153 +++++++++ .../SingleOperationInLoopCheckTest.java | 204 ++++++++++++ .../StorageUploadWithoutLengthCheckTest.java | 115 +++++++ .../UseOfBlockOnAsyncClientsCheckTest.java | 138 ++++++++ 41 files changed, 5348 insertions(+) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/readme.md create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeChecker.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeChecker.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/RuleConfig.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoader.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeCheckerTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DisableAutoCompleteCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DynamicClientCreationCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetSyncPollerOnPollerFluxCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/HardcodedAPIKeysAndTokensCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/StorageUploadWithoutLengthCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UseOfBlockOnAsyncClientsCheckTest.java 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..f815832cd8d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/readme.md @@ -0,0 +1,74 @@ +# **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** + +### **1. 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/en-us/azure/storage/blobs/storage-blob-upload-java) for details. + +### **2. 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 here](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/servicebus/azure-messaging-servicebus/README.md#when-to-use-servicebusprocessorclient). + +### **3. 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/en-us/java/api/com.azure.messaging.servicebus.servicebusclientbuilder.servicebusreceiverclientbuilder-disableautocomplete) for guidance. + +### **4. 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. + [Detailed guidance here](https://learn.microsoft.com/en-us/azure/developer/java/sdk/overview#connect-to-and-use-azure-resources-with-client-libraries). + +### **5. Avoid Hardcoded API Keys and Tokens** +- **Anti-pattern**: Storing sensitive credentials in source code. +- **Issue**: Exposes credentials to security breaches. +- **Severity**: WARNING +- **Recommendation**: Use `DefaultAzureCredential` or `AzureKeyCredential` for authentication. + [Learn more](https://learn.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential?view=azure-java-stable). + +### **6. Use SyncPoller Instead of PollerFlux#getSyncPoller()** +- **Anti-pattern**: Converting asynchronous polling to synchronous with `getSyncPoller()`. +- **Issue**: Adds unnecessary complexity. +- **Severity**: WARNING +- **Recommendation**: Use `SyncPoller` directly for synchronous operations. + [Documentation link](https://learn.microsoft.com/java/api/com.azure.core.util.polling.syncpoller?view=azure-java-stable). + +### **7. Optimize Receive Mode and Prefetch Value** +- **Anti-pattern**: Using `PEEK_LOCK` with a high prefetch value. +- **Issue**: Can lead to performance bottlenecks and message lock expirations. +- **Severity**: WARNING +- **Recommendation**: Balance the prefetch value for efficient and concurrent processing. + [Learn more](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-prefetch?tabs=dotnet#why-is-prefetch-not-the-default-option). + +### **8. Recommended Alternatives for Common APIs** + +#### a. **Authentication**: Use `DefaultAzureCredential` over connection strings. +#### b. **Azure OpenAI**: Prefer `getChatCompletions` for conversational AI instead of `getCompletions`. +[More information here](https://learn.microsoft.com/java/api/overview/azure/ai-openai-readme?view=azure-java-preview). + +### **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. 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. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java new file mode 100644 index 00000000000..0f7aee68948 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java @@ -0,0 +1,85 @@ +// 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.lang.StdLanguages; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import java.io.IOException; +import org.jetbrains.idea.maven.project.MavenProjectsManager; + +/** + * Abstract class for the library version check inspection. The UpgradeLibraryVersionCheck and + * IncompatibleDependencyCheck classes extend this class. + *

+ * The UpgradeLibraryVersionCheck class checks the version of the libraries in the pom.xml file against the recommended + * version. The IncompatibleDependencyCheck class checks the version of the libraries in the pom.xml file against + * compatible versions. + */ +public abstract class AbstractLibraryVersionCheck extends LocalInspectionTool { + + /** + * Method to check the pom.xml file for the libraries and their versions. + * + * @param file The pom.xml file to check for the libraries and their versions + * @param holder The holder for the problems found in the file + * + * @throws IOException If an error occurs while reading the file + */ + protected void checkPomXml(XmlFile file, ProblemsHolder holder) throws IOException { + MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstance(file.getProject()); + if (!mavenProjectsManager.isMavenizedProject()) { + return; + } + + XmlFile xmlFile = (XmlFile) file.getViewProvider().getPsi(StdLanguages.XML); + XmlTag rootTag = xmlFile.getRootTag(); + + if (rootTag != null && "project".equals(rootTag.getName())) { + for (XmlTag dependenciesTag : rootTag.findSubTags("dependencies")) { + for (XmlTag dependencyTag : dependenciesTag.findSubTags("dependency")) { + XmlTag groupIdTag = dependencyTag.findFirstSubTag("groupId"); + XmlTag artifactIdTag = dependencyTag.findFirstSubTag("artifactId"); + XmlTag versionTag = dependencyTag.findFirstSubTag("version"); + + if (groupIdTag != null && artifactIdTag != null && versionTag != null) { + String fullName = groupIdTag.getValue().getText() + ":" + artifactIdTag.getValue().getText(); + String currentVersion = versionTag.getValue().getText(); + this.checkAndFlagVersion(fullName, currentVersion, holder, versionTag); + } + } + } + } + } + + /** + * Method to get the formatted message for the anti-pattern. + * + * @param fullName The full name of the library eg "com.azure:azure-core" + * @param recommendedVersion The recommended version of the library eg "1.0" + * @param RULE_CONFIG The rule configuration object + * + * @return The formatted message for the anti-pattern with the full name and recommended version + */ + protected static String getFormattedMessage(String fullName, String recommendedVersion, RuleConfig RULE_CONFIG) { + return RULE_CONFIG.getAntiPatternMessage() + .replace("{{fullName}}", fullName) + .replace("{{recommendedVersion}}", recommendedVersion); + } + + /** + * Abstract method to check the version of the library and flag it if necessary. + * + * @param fullName The full name of the library eg "com.azure:azure-core" + * @param currentVersion The current version of the library eg "1.0" + * @param holder The holder for the problems found + * @param versionElement The element for the version of the library + */ + protected abstract void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, + PsiElement versionElement) throws IOException; +} \ 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/AsyncSubscribeChecker.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeChecker.java new file mode 100644 index 00000000000..994ec3ede3a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeChecker.java @@ -0,0 +1,92 @@ +// 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.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.PsiParameter; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import java.util.List; + +/** + * Abstract base class for checking the usage of the following method to be ''subscribe'' in provided context. + */ +public abstract class AsyncSubscribeChecker extends LocalInspectionTool { + /** + * 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. + */ + protected static boolean isFollowingMethodSubscribe(PsiMethodCallExpression expression) { + // Check if the parent element is a method call expression + if (!(expression.getParent() instanceof PsiReferenceExpression reference)) { + return false; + } + + // Check if the grandparent element is a method call expression + PsiElement grandParent = reference.getParent(); + if (!(grandParent instanceof PsiMethodCallExpression parentCall)) { + return false; + } + + // Check if the parent method call's name is "subscribe" + return "subscribe".equals(parentCall.getMethodExpression().getReferenceName()); + } + + /** + * This method checks if the method call is made on an object from the provided Azure SDK context. + * + * @param expression The method call expression to analyze. + * @param scopeToCheck The list of class contexts to check. + * + * @return True if the method call is made on an object in the provided Azure SDK context, false otherwise. + */ + protected static boolean isCalledInProvidedContext(PsiMethodCallExpression expression, List scopeToCheck) { + // Get the qualifier expression from the method call expression + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + if (!(qualifier instanceof PsiReferenceExpression)) { + return false; + } + + // Resolve the qualifier element + PsiElement resolvedElement = ((PsiReferenceExpression) qualifier).resolve(); + if (!(resolvedElement instanceof PsiParameter)) { + return false; + } + + // Check the parameter type + PsiParameter parameter = (PsiParameter) resolvedElement; + PsiType parameterType = parameter.getType(); + + // Verify if the parameter type is within the provided context + if (!(parameterType instanceof PsiClassType)) { + return false; + } + + PsiClassType classType = (PsiClassType) parameterType; + PsiClass psiClass = classType.resolve(); + if (psiClass == null) { + return false; + } + + String qualifiedName = psiClass.getQualifiedName(); + if (qualifiedName == null) { + return false; + } + + // Check if the qualified name matches the Azure SDK context + boolean isInProvidedScope = scopeToCheck.contains(parameterType.getCanonicalText()); + boolean isAzureSDKContext = qualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME); + + return isInProvidedScope && isAzureSDKContext; + } +} 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..bbc840120ac --- /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,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; + +/** + * Inspection tool to check discouraged Connection String usage. + */ +public class ConnectionStringCheck extends DetectDiscouragedAPIUsageCheck { + + @Override + protected RuleConfig getRuleConfig() { + return RuleConfigLoader.getInstance().getRuleConfig("ConnectionStringCheck"); + } +} \ 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/DetectDiscouragedAPIUsageCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheck.java new file mode 100644 index 00000000000..0e1f2d8a13a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheck.java @@ -0,0 +1,105 @@ +// 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.PsiElement; +import com.intellij.psi.PsiElementVisitor; +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 java.util.HashSet; +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +/** + * Abstract base class to check for the use of discouraged APIs in the code. + */ +public abstract class DetectDiscouragedAPIUsageCheck extends LocalInspectionTool { + + /** + * Provides the specific RuleConfig for the subclass context. + * + * @return RuleConfig for the subclass + */ + protected abstract RuleConfig getRuleConfig(); + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + RuleConfig ruleConfig = getRuleConfig(); + + if (ruleConfig.skipRuleCheck()) { + return PsiElementVisitor.EMPTY_VISITOR; + } + + return new DetectDiscouragedAPIUsageVisitor(holder, ruleConfig); + } + + static class DetectDiscouragedAPIUsageVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private final RuleConfig ruleConfig; + + DetectDiscouragedAPIUsageVisitor(ProblemsHolder holder, RuleConfig ruleConfig) { + this.holder = holder; + this.ruleConfig = ruleConfig; + } + + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + + // Ensure the element is a method call + if (!(element instanceof PsiMethodCallExpression methodCallExpression)) { + return; + } + + // Resolve the method being called + PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); + PsiElement resolvedMethod = methodExpression.resolve(); + if (!(resolvedMethod instanceof PsiMethod method)) { + return; + } + + // Get the containing class of the method + PsiClass containingClass = method.getContainingClass(); + if (containingClass == null) { + return; + } + + // Get qualified name of the containing class + String classQualifiedName = containingClass.getQualifiedName(); + if (classQualifiedName == null) { + return; + } + + // Check if the method is a discouraged API + String methodName = method.getName(); + if (!ruleConfig.getUsagesToCheck().contains(methodName)) { + return; + } + + // Verify package name and scope + if (!classQualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME)) { + return; + } + + Set scopesToCheck = new HashSet<>(ruleConfig.getScopeToCheck()); + if (scopesToCheck.stream().noneMatch(classQualifiedName::startsWith)) { + return; + } + + // Register a problem for the discouraged API usage + PsiElement problemElement = methodExpression.getReferenceNameElement(); + if (problemElement != null) { + holder.registerProblem(problemElement, ruleConfig.getAntiPatternMessage()); + } + } + } +} + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java new file mode 100644 index 00000000000..94f6b2911c3 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java @@ -0,0 +1,88 @@ +// 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.PsiType; +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 org.jetbrains.annotations.NotNull; + +/** + * This class extends the LocalInspectionTool to check for the use of discouraged clients in the code and suggests using + * other clients instead. The client data is loaded from the configuration file and the client name is checked against + * the discouraged client name. If the client name matches, a problem is registered with the suggestion message. + */ +public abstract class DetectDiscouragedClientCheck extends LocalInspectionTool { + /** + * Provides the specific RuleConfig for the subclass context. + * + * @return RuleConfig for the subclass + */ + protected abstract RuleConfig getRuleConfig(); + + /** + * This method builds a visitor to check for the discouraged client name in the code. If the client name matches the + * discouraged client, a problem is registered with the suggestion message. + * + * @param holder - the ProblemsHolder object to register the problem + * @param isOnTheFly - whether the inspection is on the fly - not used in this implementation but required by the + * parent class + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + RuleConfig ruleConfig = getRuleConfig(); + return new DetectDiscouragedClientVisitor(holder, ruleConfig); + } + + /** + * This class is a visitor that checks for the use of discouraged clients in the code. If the client name matches + * the discouraged client, a problem is registered with the suggestion message. + */ + static class DetectDiscouragedClientVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private final RuleConfig ruleConfig; + + public DetectDiscouragedClientVisitor(ProblemsHolder holder, RuleConfig ruleConfig) { + this.holder = holder; + this.ruleConfig = ruleConfig; + } + + /** + * This method builds a visitor to check for the discouraged client name in the code. If the client name matches + * the discouraged client, a problem is registered with the suggestion message. + */ + @Override + public void visitTypeElement(PsiTypeElement element) { + super.visitTypeElement(element); + + // Skip the rule if the configuration is empty or the rule is disabled + if (ruleConfig.skipRuleCheck()) { + return; + } + + // Ensure the element's type is not null + PsiType psiType = element.getType(); + if (psiType == null) { + return; + } + + // Get the presentable text for the type + String elementType = psiType.getPresentableText(); + + // Check if the type matches any discouraged usage + if (!ruleConfig.getUsagesToCheck().contains(elementType)) { + return; + } + + // Register a problem with the corresponding anti-pattern message + holder.registerProblem(element, ruleConfig.getAntiPatternMessage()); + } + } +} 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..2a84b2ba1d9 --- /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,160 @@ +// 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.RuleConfigLoader; +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); + } + + /** + * 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 final RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + private final ProblemsHolder holder; + + static { + final String ruleName = "DisableAutoCompleteCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + DisableAutoCompleteVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** + * 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 (!clientType.getCanonicalText().startsWith(RuleConfig.AZURE_PACKAGE_NAME)) { + return; + } + + // Check if the client type is in the list of clients to check + if (RULE_CONFIG.getScopeToCheck().contains(clientType.getPresentableText())) { + + if (!(initializer instanceof PsiMethodCallExpression)) { + return; + } + // Process the new expression initialization + if (!isAutoCompleteDisabled((PsiMethodCallExpression) initializer)) { + // Register a problem if the auto-complete feature is not disabled + 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..89c60e185ba --- /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,88 @@ +// 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.*; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +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); + } + + static class DynamicClientCreationVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG = RuleConfigLoader.getInstance() + .getRuleConfig("DynamicClientCreationCheck"); + private static final boolean SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + + public DynamicClientCreationVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + @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 qualifier.getType().getCanonicalText().startsWith(RuleConfig.AZURE_PACKAGE_NAME); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java new file mode 100644 index 00000000000..0370a511be1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java @@ -0,0 +1,169 @@ +// 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.PsiExpression; +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 org.jetbrains.annotations.NotNull; + +/** + * This class is a LocalInspectionTool that checks if the endpoint method is used with KeyCredential for non-Azure + * OpenAI clients. If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is + * registered. + */ +public class EndpointOnNonAzureOpenAIAuthCheck extends LocalInspectionTool { + + /** + * This method builds the visitor for the inspection tool. + * + * @param holder The ProblemsHolder object that holds the problems found by the inspection tool. + * @param isOnTheFly A boolean that indicates if the inspection tool is running on the fly. - this is not used in + * this implementation but is required by the method signature. + * + * @return The JavaElementVisitor object that will be used to visit the elements in the code. + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new EndpointOnNonAzureOpenAIAuthVisitor(holder); + } + + /** + * This class is a JavaElementVisitor that visits the elements in the code and checks if the endpoint method is used + * with KeyCredential for non-Azure OpenAI clients. + */ + static class EndpointOnNonAzureOpenAIAuthVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + + // The boolean that indicates if the rule should be skipped + private static final boolean SKIP_WHOLE_RULE; + + /** + * Constructor for the visitor. + * + * @param holder The ProblemsHolder object that holds the problems found by the inspection tool. + */ + EndpointOnNonAzureOpenAIAuthVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + // Static initializer block to load the configurations once + static { + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig("EndpointOnNonAzureOpenAIAuthCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + /** + * This method visits the method call expressions in the code. If the method call expression is the endpoint + * method, the method call chain is checked to see if it is a non-Azure OpenAI client. If the method call chain + * uses the credential method with KeyCredential for non-Azure OpenAI clients, a warning is registered. + * + * @param expression The method call expression to visit. + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + if (SKIP_WHOLE_RULE) { + return; + } + + PsiReferenceExpression methodExpression = expression.getMethodExpression(); + String methodName = methodExpression.getReferenceName(); + + // Skip if method is not the endpoint method or isn't in the list of check items + if (!"endpoint".equals(methodName) || !RULE_CONFIG.getUsagesToCheck().contains(methodName)) { + return; + } + + // Using KeyCredential indicates authentication of a non-Azure OpenAI client + // If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is registered + if (isUsingKeyCredential(expression)) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); + } + } + + /** + * This method checks if the method call chain uses the credential method with KeyCredential. If this is the + * case, the method call chain is checked to see if it is a non-Azure OpenAI client. + * + * @param expression The method call expression to check. + * + * @return True if the method call chain uses the credential method with KeyCredential, false otherwise. + */ + private static boolean isUsingKeyCredential(PsiMethodCallExpression expression) { + + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + + while (qualifier instanceof PsiMethodCallExpression) { + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) qualifier; + String methodName = methodCall.getMethodExpression().getReferenceName(); + + // Short-circuit if method is not relevant + if (!RULE_CONFIG.getUsagesToCheck().contains(methodName)) { + return false; + } + + if ("credential".equals(methodName)) { + PsiExpression[] args = methodCall.getArgumentList().getExpressions(); + // Return early if arguments are incorrect + if (args.length != 1 || !isKeyCredential(args[0])) { + return false; + } + + return isNonAzureOpenAIClient(methodCall); + } + qualifier = methodCall.getMethodExpression().getQualifierExpression(); + } + return false; + } + + /** + * This method checks if the expression is a KeyCredential. + * + * @param expression The expression to check. + * + * @return True if the expression is a KeyCredential, false otherwise. + */ + private static boolean isKeyCredential(PsiExpression expression) { + if (expression instanceof PsiNewExpression) { + String className = ((PsiNewExpression) expression).getClassReference().getReferenceName(); + return RULE_CONFIG.getScopeToCheck().contains(className); + } + return false; + } + + /** + * This method checks if the method call chain is on a non-Azure OpenAI client. + * + * @param expression The method call expression to check. + * + * @return True if the client is a non-Azure OpenAI client, false otherwise. + */ + private static boolean isNonAzureOpenAIClient(PsiMethodCallExpression expression) { + PsiElement parent = expression.getParent(); + while (parent instanceof PsiVariable) { + PsiType type = ((PsiVariable) parent).getType(); + if (type.getCanonicalText().startsWith("com.azure.ai.openai")) { + return true; + } + parent = parent.getParent(); + } + 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/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..576eb39d5b6 --- /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,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; + +/** + * Inspection tool to check discouraged GetCompletions API usage in openai package context. + */ +public class GetCompletionsCheck extends DetectDiscouragedAPIUsageCheck { + + @Override + protected RuleConfig getRuleConfig() { + return RuleConfigLoader.getInstance().getRuleConfig("GetCompletionsCheck"); + } +} 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..3b48922afb1 --- /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,130 @@ +// 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.PsiExpression; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiType; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.MavenUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +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); + } + + /** + * 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 final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "GetSyncPollerOnPollerFluxCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + /** + * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * + * @param holder Holder for the problems found by the inspection + */ + public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** + * 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; + } + + if (isGetSyncPollerCall(expression) && isAsyncContext(expression) && MavenUtils.isAzureClientMethodCall(expression)) { + holder.registerProblem(expression, 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; + } + + /** + * Helper method to check if the method call is on a PollerFlux type. + * + * @param methodCall Method call expression to check + * + * @return true if the method call is on a reactive type, false otherwise + */ + private boolean isAsyncContext(@NotNull PsiMethodCallExpression methodCall) { + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + if (qualifier == null) { + return false; + } + + PsiType type = qualifier.getType(); + String typeName = type.getCanonicalText(); + if (typeName != null && typeName.contains("PollerFlux")) { + 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..4804f78351a --- /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,86 @@ +// 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.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 org.jetbrains.annotations.NotNull; + +/** + * Custom inspection tool to check for hardcoded API keys and tokens in the code. + * Flags instances such as: + * 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 HardcodedAPIKeysAndTokensCheck extends LocalInspectionTool { + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new APIKeysAndTokensVisitor(holder); + } + + /** + * Visitor to inspect Java elements for hardcoded API keys and tokens. + */ + static class APIKeysAndTokensVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = ruleConfigLoader.getRuleConfig("HardcodedAPIKeysAndTokensCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + APIKeysAndTokensVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + @Override + public void visitElement(@NotNull PsiElement element) { + if (element instanceof PsiNewExpression newExpression) { + checkNewExpression(newExpression); + } + } + + private void checkNewExpression(@NotNull PsiNewExpression newExpression) { + var classReference = newExpression.getClassReference(); + if (classReference == null) { + return; + } + + String qualifiedName = classReference.getQualifiedName(); + String referenceName = classReference.getReferenceName(); + + if (qualifiedName != null && qualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME) + && RULE_CONFIG.getUsagesToCheck().contains(referenceName)) { + checkForHardcodedStrings(newExpression); + } + } + + private void checkForHardcodedStrings(@NotNull PsiNewExpression newExpression) { + for (PsiElement child : newExpression.getChildren()) { + if (child instanceof PsiLiteralExpression literal && literal.getValue() instanceof String) { + holder.registerProblem(newExpression, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } +} + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java new file mode 100644 index 00000000000..9a690f055a6 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java @@ -0,0 +1,207 @@ +// 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.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.xml.XmlFile; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.DependencyVersionFileFetcher; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.VersionUtils; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import org.jetbrains.annotations.NotNull; + +import static com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.IncompatibleDependencyCheck.IncompatibleDependencyVisitor.getFileContent; + +/** + * Inspection class to check the version of the libraries in the pom.xml file against compatible versions. The + * compatible versions are fetched from a file hosted on GitHub. The compatible versions are compared against the minor + * version of the library. Minor version is the first two parts of the version number. If the minor version is different + * from the compatible version, a warning is flagged and the compatible version is suggested. + */ +public class IncompatibleDependencyCheck extends AbstractLibraryVersionCheck { + private static final Logger LOGGER = Logger.getLogger(IncompatibleDependencyCheck.class.getName()); + + // Set to store the encountered version groups + static Set encounteredVersionGroups = new HashSet<>(); + + /** + * Abstract method to build the specific visitor for the inspection. + * + * @param holder The holder for the problems found + * @param isOnTheFly boolean to check if the inspection is on the fly - not used in this implementation but is part + * of the method signature + * + * @return The visitor for the inspection + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new IncompatibleDependencyVisitor(holder); + } + + /** + * Method to check the version of the dependency found in the project code against the compatible versions. If the + * version is not compatible, a warning is flagged and the compatible version is suggested. + * + * @param fullName The full name of the library eg "com.azure:azure-core" + * @param currentVersion The current version of the library + * @param holder The holder for the problems found + * @param versionElement The version element in the pom.xml file to check + */ + @Override + protected void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, + PsiElement versionElement) { + + // get version group of the dependency found in the project code + String versionGroup = VersionUtils.getGroupVersion(fullName, currentVersion, getFileContent()); + + if (versionGroup == null) { + return; + } + + // add an encountered version group to the encountered version groups + if (encounteredVersionGroups.isEmpty()) { + encounteredVersionGroups.add(versionGroup); + } + + // check if the versionGroup found is not already in the encounteredVersionGroups set + for (String encounteredVersionGroup : encounteredVersionGroups) { + + // check if the encountered version group is not the same as the current version group + // and the encountered version group starts with the version group's substring + // eg if versionGroup = "jackson_2.10" and encounteredVersionGroup = "jackson_2.10, no problem is flagged + // if versionGroup = "jackson_2.10" and encounteredVersionGroup = "jackson_2.11", a problem is flagged + + // The substring check is used to determine if versionGroup and encounteredVersionGroup are in the same library + if (!encounteredVersionGroup.equals(versionGroup) && + encounteredVersionGroup.startsWith(versionGroup.substring(0, versionGroup.lastIndexOf("_")))) { + String recommendedVersion = encounteredVersionGroup.substring(encounteredVersionGroup.lastIndexOf("_") + 1); + + // Check if versions are incompatible + if (VersionUtils.isIncompatibleVersion(currentVersion, recommendedVersion)) { + holder.registerProblem(versionElement, + getFormattedMessage(fullName, recommendedVersion, IncompatibleDependencyVisitor.RULE_CONFIG)); + } + return; + } + } + encounteredVersionGroups.add(versionGroup); + } + + /** + * Visitor class for the inspection. Checks the version of the libraries in the pom.xml file against compatible + * versions. The compatible versions are fetched from a file hosted on GitHub. The compatible versions are compared + * against the minor version of the library. Minor version is the first two parts of the version number. If the + * minor version is different from the compatible version, a warning is flagged and the compatible version is + * suggested. + */ + class IncompatibleDependencyVisitor extends PsiElementVisitor { + private static Map> FILE_CONTENT_REF; + + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig("IncompatibleDependencyCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + IncompatibleDependencyVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** + * Method to check the pom.xml file for the library version. + * + * @param file The pom.xml file to check + */ + @Override + public void visitFile(@NotNull PsiFile file) { + super.visitFile(file); + + if (SKIP_WHOLE_RULE) { + return; + } + if (!file.getName().equals(RuleConfig.POM_XML)) { + return; + } + if (file instanceof XmlFile && file.getName().equals("pom.xml")) { + + // Check the pom.xml file for the library version + try { + IncompatibleDependencyCheck.this.checkPomXml((XmlFile) file, holder); + } catch (IOException e) { + LOGGER.severe("Error checking pom.xml file: " + e); + } + } + } + + /** + * Method to get the version group for the library. The version group is used to get the compatible versions for + * the library. The version group is determined by the major and minor version of the library. Eg if the major + * version is 2 and the minor version is 10, the version group is "jackson_2.10". + * + * @param fullName The full name of the library eg "com.azure:azure-core" + * @param currentVersion The current version of the library + * + * @return The version group for the library + */ + private static String getGroupVersion(String fullName, String currentVersion) { + + // Split currentVersion to extract major and minor version + String[] versionParts = currentVersion.split("\\."); + String majorVersion = versionParts[0]; + String minorVersion = versionParts.length > 1 ? versionParts[1] : ""; + String versionSuffix = "_" + majorVersion + "." + minorVersion; + + // Search the file content for the version group + String versionGroup = null; + + for (Map.Entry> entry : getFileContent().entrySet()) { + + // Check if the set of artifactIds contains the fullName and the corresponding key ends with the versionSuffix + // This will be the version group of the dependency + if (entry.getValue().contains(fullName) && entry.getKey().endsWith(versionSuffix)) { + versionGroup = entry.getKey(); + break; + } + } + return versionGroup; + } + + /** + * Method to get the content of the file hosted on GitHub. The file contains the compatible versions for the + * libraries. A WeakReference is used to store the content of the file to allow for garbage collection. + * + * @return The content of the file as a map + */ + static Map> getFileContent() { + + // Load the file content from the URL if it is not already loaded + + Map> fileContent = FILE_CONTENT_REF == null ? null : FILE_CONTENT_REF; + + if (fileContent == null) { + synchronized (IncompatibleDependencyVisitor.class) { + fileContent = FILE_CONTENT_REF == null ? null : FILE_CONTENT_REF; + if (fileContent == null) { + fileContent = DependencyVersionFileFetcher.loadJsonDataFromUrl(RULE_CONFIG.SUPPORTED_VERSION_URL); + FILE_CONTENT_REF = new HashMap<>(fileContent); + } + } + } + return fileContent; + } + } +} \ 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/KustoQueriesWithTimeIntervalInQueryStringCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheck.java new file mode 100644 index 00000000000..1923f158451 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheck.java @@ -0,0 +1,201 @@ +// 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.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiPolyadicExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.impl.source.tree.java.PsiMethodCallExpressionImpl; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.MavenUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; + +/** + * This class is an inspection tool that checks for Kusto queries with time intervals in the query string. This approach + * makes queries less flexible and harder to troubleshoot. This inspection tool checks for the following anti-patterns: + *

+ *

+ * When the anti-patterns are detected as parameters of Azure client method calls, a problem is registered. + */ +public class KustoQueriesWithTimeIntervalInQueryStringCheck extends LocalInspectionTool { + + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + + // clear the timeIntervalParameters list in every visit + KustoQueriesVisitor.resetTimeIntervalParameters(); + return new KustoQueriesVisitor(holder); + } + + /** + * This class defines the visitor for the inspection tool The visitor checks for Kusto queries with time intervals + * in the query string and registers a problem if an anti-pattern is detected To check for the anti-patterns, the + * visitor uses regex patterns to match the query string Processing of polyadic expressions is also done to replace + * the variables with their values in the query string before checking for the anti-patterns. + */ + static class KustoQueriesVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + + // // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static final List REGEX_PATTERNS; + private static final boolean SKIP_WHOLE_RULE; + private static final List timeIntervalParameters = new ArrayList<>(); + + static { + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig("KustoQueriesWithTimeIntervalInQueryStringCheck"); + REGEX_PATTERNS = RULE_CONFIG.getRegexPatternsToCheck().entrySet().stream() + .filter(entry -> Objects.nonNull(entry.getValue())) + .map(entry -> Pattern.compile(entry.getValue())) // Compile each pattern into a `Pattern` object + .toList(); + + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || REGEX_PATTERNS.isEmpty(); + } + + /** + * Constructor for the KustoQueriesVisitor class The constructor initializes the ProblemsHolder and isOnTheFly + * variables + * + * @param holder - ProblemsHolder is used to register problems found in the code + */ + KustoQueriesVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** Resets the time interval parameters list. */ + static void resetTimeIntervalParameters() { + timeIntervalParameters.clear(); + } + + /** + * This method visits the element and checks for the anti-patterns The method checks if the element is a + * PsiPolyadicExpression or a PsiLocalVariable If the element is a PsiLocalVariable, the method checks the + * initializer of the variable If the element is a PsiPolyadicExpression, the method processes the expression to + * replace the variables with their values The method then checks the expression for the anti-patterns by + * matching regex patterns with the expression text and registers a problem if an anti-pattern is detected + * + * @param element - the element to visit + */ + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + + // Skip the whole rule if the rule configuration is empty + if (SKIP_WHOLE_RULE) { + return; + } + + if (element instanceof PsiMethodCallExpressionImpl) { + handleMethodCallExpression((PsiMethodCallExpressionImpl) element); + } else if (element instanceof PsiLocalVariable) { + handleLocalVariable((PsiLocalVariable) element); + } else if (element instanceof PsiPolyadicExpression) { + // if element is a polyadic expression, extract the value and replace the variable with the value + // PsiPolyadicExpressions are used to represent expressions with multiple operands + // eg ("datetime" + startDate), where startDate is a variable + handlePolyadicExpression((PsiPolyadicExpression) element); + } + } + + /** + * This method checks the expression for the anti-patterns by matching regex patterns with the expression text + * and registers a problem if an anti-pattern is detected + * + * @param expression - the expression to check + * @param element - the element to check + */ + void checkExpressionForPatterns(PsiExpression expression, PsiElement element) { + if (expression == null) { + return; + } + String text = expression.getText(); + + // Check if the expression text contains any of the regex patterns + boolean foundAntiPattern = REGEX_PATTERNS.stream().anyMatch(pattern -> pattern.matcher(text).find()); + + + // If an anti-pattern is detected, register a problem + if (foundAntiPattern) { + PsiElement parentElement = element.getParent(); + + if (parentElement instanceof PsiLocalVariable) { + PsiLocalVariable variable = (PsiLocalVariable) parentElement; + String variableName = variable.getName(); + timeIntervalParameters.add(variableName); + } + } + } + + /** + * This method handles the local variable by checking the initializer of the variable If the initializer is a + * PsiLiteralExpression, the method checks the expression for the anti-patterns by matching regex patterns with + * the expression text + * + * @param variable - the local variable to check + */ + private void handleLocalVariable(PsiLocalVariable variable) { + PsiExpression initializer = variable.getInitializer(); + if (initializer instanceof PsiLiteralExpression) { + checkExpressionForPatterns(initializer, variable); + } + } + + /** + * This method handles the polyadic expression by processing the expression to replace the variables with their + * values The method then checks the expression for the anti-patterns by matching regex patterns with the + * expression text + * + * @param polyadicExpression - the polyadic expression to check + */ + private void handlePolyadicExpression(PsiPolyadicExpression polyadicExpression) { + checkExpressionForPatterns(polyadicExpression, polyadicExpression); + } + + /** + * This method handles the method call by checking the parameters of the method call If the parameter is a + * reference to a variable, the method checks the variable name If the variable name is in the list of time + * interval parameters, the method checks if the method call is an Azure client method call If the method call + * is an Azure client method call, the method registers a problem + * + * @param methodCall - the method call to check + */ + private void handleMethodCallExpression(PsiMethodCallExpressionImpl methodCall) { + PsiExpressionList argumentList = methodCall.getArgumentList(); + for (PsiExpression argument : argumentList.getExpressions()) { + if (argument instanceof PsiReferenceExpression referenceExpression) { + PsiElement resolvedElement = referenceExpression.resolve(); + // check if the variable name is in the list of time interval parameters + // if the variable name is in the list, check if the method call is an Azure client method call + if (resolvedElement instanceof PsiVariable variable && timeIntervalParameters.contains(variable.getName())) { + if (MavenUtils.isAzureClientMethodCall(methodCall)) { + holder.registerProblem(methodCall, RULE_CONFIG.getAntiPatternMessage()); + } + } + } + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java new file mode 100644 index 00000000000..a2a21851ebd --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java @@ -0,0 +1,208 @@ +// 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.RuleConfigLoader; +import java.util.OptionalInt; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jetbrains.annotations.NotNull; + +/** + * This class extends the LocalInspectionTool and is used to inspect the usage of Azure SDK ServiceBus & + * ServiceBusProcessor clients in the code. It checks if the receive mode is set to PEEK_LOCK and the prefetch count is + * set to a value greater than 1. If the receive mode is set to PEEK_LOCK and the prefetch count is set to a value + * greater than 1, a problem is registered with the ProblemsHolder. + */ +public class ServiceBusReceiveModeCheck 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 + * @param isOnTheFly Whether the inspection is on the fly -- not in use + * + * @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 ServiceBusReceiveModeVisitor(holder, isOnTheFly); + } + + /** + * 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 receive mode is set to + * PEEK_LOCK and the prefetch count is set to a value greater than 1. If the receive mode is set to PEEK_LOCK and + * the prefetch count is set to a value greater than 1, a problem is registered with the ProblemsHolder. + */ + static class ServiceBusReceiveModeVisitor extends JavaElementVisitor { + private static final Logger LOGGER = Logger.getLogger(ServiceBusReceiveModeCheck.class.getName()); + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + static { + final String ruleName = "ServiceBusReceiveModeCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + /** + * Constructor for the visitor + * + * @param holder The holder for the problems found + * @param isOnTheFly Whether the inspection is on the fly -- not in use + */ + ServiceBusReceiveModeVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + /** + * This method is used to visit the declaration statement in the code. It checks for the declaration of the + * Azure SDK ServiceBusReceiver & ServiceBusProcessor clients and whether the receive mode is set to PEEK_LOCK + * and the prefetch count is set to a value greater than 1. If the receive mode is set to PEEK_LOCK and the + * prefetch count is set to a value greater than 1, a problem is registered with the ProblemsHolder. + * + * @param statement The declaration statement to visit + */ + @Override + public void visitDeclarationStatement(PsiDeclarationStatement statement) { + super.visitDeclarationStatement(statement); + + if (SKIP_WHOLE_RULE) { + return; + } + + // 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 if the client type is an Azure ServiceBus + * client and retrieves the client name (left side of the declaration). + * + * @param variable The variable to process + */ + private void processVariableDeclaration(PsiVariable variable) { + // Check if the client type is an Azure ServiceBus client + isRelevantClientType(variable.getType()); + + PsiExpression initializer = variable.getInitializer(); + if (initializer instanceof PsiMethodCallExpression methodCall) { + analyzeMethodCallChain(methodCall); + } + } + + private boolean isRelevantClientType(PsiType type) { + String typeName = type.getCanonicalText(); + return typeName.startsWith(RuleConfig.AZURE_PACKAGE_NAME) || RULE_CONFIG.getScopeToCheck().contains(typeName); + } + + /** + * This method is used to determine the receive mode of the Azure ServiceBus client. It checks if the receive + * mode is set to PEEK_LOCK and the prefetch count is set to a value greater than 1. If the receive mode is set + * to PEEK_LOCK and the prefetch count is set to a value greater than 1, a problem is registered with the + * ProblemsHolder. + * + * @param methodCall The method call expression to check + */ + private void analyzeMethodCallChain(PsiMethodCallExpression methodCall) { + + OptionalInt prefetchCountValue = OptionalInt.empty(); + boolean isReceiveModePeekLock = false; + PsiElement prefetchCountMethod = null; + + // Iterating up the chain of method calls + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + + // Check if the method call chain has the method to check + while (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; + } + if ("receiveMode".equals(methodName)) { + isReceiveModePeekLock = receiveModePeekLockCheck((PsiMethodCallExpression) qualifier); + } else if ("prefetchCount".equals(methodName)) { + prefetchCountValue = extractPrefetchCount((PsiMethodCallExpression) qualifier); + prefetchCountMethod = + ((PsiMethodCallExpression) qualifier).getMethodExpression().getReferenceNameElement(); + } + + // Get the qualifier of the method call expression -- the next method call in the chain + qualifier = ((PsiMethodCallExpression) qualifier).getMethodExpression().getQualifierExpression(); + + // If the receive mode is set to PEEK_LOCK and the prefetch count is set to a value greater than 1, register a problem + if (prefetchCountValue.isPresent() && prefetchCountValue.getAsInt() > 1 && isReceiveModePeekLock && + prefetchCountMethod != null) { + holder.registerProblem(prefetchCountMethod, RULE_CONFIG.getAntiPatternMessage()); + return; + } + } + } + + /** + * This method is used to check if the receive mode is set to PEEK_LOCK. + * + * @param methodCall The method call expression to check + * + * @return true if the receive mode is set to PEEK_LOCK, false otherwise + */ + private boolean receiveModePeekLockCheck(PsiMethodCallExpression methodCall) { + for (PsiExpression arg : methodCall.getArgumentList().getExpressions()) { + if ("PEEK_LOCK".equals(arg.getText())) { + return true; + } + } + return false; + } + + /** + * This method is used to get the prefetch count value. + * + * @param methodCall The method call expression to check + * + * @return The prefetch count value + */ + private OptionalInt extractPrefetchCount(PsiMethodCallExpression methodCall) { + PsiExpression[] args = methodCall.getArgumentList().getExpressions(); + if (args.length > 0) { + try { + return OptionalInt.of(Integer.parseInt(args[0].getText())); + } catch (NumberFormatException e) { + LOGGER.log(Level.SEVERE, "Invalid prefetch count: " + args[0].getText(), e); + } + } + return OptionalInt.empty(); + } + } +} 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..57a4355f1e7 --- /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,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; + +/** + * This class extends the DetectDiscouragedClientCheck to check for the use of ServiceBusReceiverAsyncClient in the code and + * suggests using ServiceBusProcessorClient instead. The client data is loaded from the configuration file and the + * client name is checked against the discouraged client name. If the client name matches, a problem is registered with + * the suggestion message. + */ +public class ServiceBusReceiverAsyncClientCheck extends DetectDiscouragedClientCheck { + + @Override + protected RuleConfig getRuleConfig() { + return RuleConfigLoader.getInstance().getRuleConfig("ConnectionStringCheck"); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java new file mode 100644 index 00000000000..ebe2f04d0cf --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java @@ -0,0 +1,295 @@ +// 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.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.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.PsiVariable; +import com.intellij.psi.PsiWhileStatement; +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.List; +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 SingleOperationInLoopCheck 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, isOnTheFly); + } + + /** + * 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 { + + // Define the holder for the problems found and whether the inspection is running on the fly + private final ProblemsHolder holder; + + // // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + private static final List SCOPE_TO_CHECK; + + static { + final String ruleName = "SingleOperationInLoopCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + SCOPE_TO_CHECK = RULE_CONFIG.getScopeToCheck(); + } + + /** + * Constructor for the visitor + * + * @param holder The holder for the problems found + * @param isOnTheFly Whether the inspection is running on the fly. If true, the inspection is running as you + * type. + */ + public SingleOperationInLoopVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + /** + * Visit the for statement and check for single Azure client operation inside the loop. + * + * @param statement The for statement to check + */ + @Override + public void visitForStatement(@NotNull PsiForStatement statement) { + if (SKIP_WHOLE_RULE) { + return; + } + checkLoopForTextAnalyticsClientOperation(statement); + } + + /** + * Visit the foreach statement and check for single Azure client operation inside the loop. + * + * @param statement The foreach statement to check + */ + @Override + public void visitForeachStatement(@NotNull PsiForeachStatement statement) { + if (SKIP_WHOLE_RULE) { + return; + } + checkLoopForTextAnalyticsClientOperation(statement); + } + + /** + * Visit the while statement and check for single Azure client operation inside the loop. + * + * @param statement The while statement to check + */ + @Override + public void visitWhileStatement(@NotNull PsiWhileStatement statement) { + if (SKIP_WHOLE_RULE) { + return; + } + checkLoopForTextAnalyticsClientOperation(statement); + } + + /** + * Visit the do-while statement and check for single Azure client operation inside the loop. + * + * @param statement The do-while statement to check + */ + @Override + public void visitDoWhileStatement(@NotNull PsiDoWhileStatement statement) { + if (SKIP_WHOLE_RULE) { + return; + } + checkLoopForTextAnalyticsClientOperation(statement); + } + + + /** + * Check the loop statement for a single Text Analytics Azure client operation inside the loop. + * + * @param loopStatement The loop statement to check + */ + private boolean checkLoopForTextAnalyticsClientOperation(PsiStatement loopStatement) { + + // extract body of the loop + PsiStatement loopBody = getLoopBody(loopStatement); + + if (loopBody == null) { + return false; + } + + if (!(loopBody instanceof PsiBlockStatement)) { + return false; + } + + // Extract the code block from the block statement + PsiBlockStatement blockStatement = (PsiBlockStatement) loopBody; + PsiCodeBlock codeBlock = blockStatement.getCodeBlock(); + + // extract statements in the loop body + for (PsiStatement statement : codeBlock.getStatements()) { + + // Check if the statement is an expression statement and is an Azure client operation + if (statement instanceof PsiExpressionStatement) { + isExpressionAzureClientOperation(statement); + } + + // Check if the statement is a declaration statement and is an Azure client operation + if (statement instanceof PsiDeclarationStatement) { + isDeclarationAzureClientOperation((PsiDeclarationStatement) statement); + } + } + return true; + } + + + /** + * Get the body of the loop statement. The body of the loop statement is the statement that is executed in the + * loop. + * + * @param loopStatement The loop statement to get the body from + * + * @return The body of the loop statement + */ + private static PsiStatement getLoopBody(PsiStatement loopStatement) { + + // Check the type of the loop statement and return the body of the loop statement + 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; + } + + /** + * If the statement is an expression statement, check if the expression is an Azure client operation. + * + * @param statement The statement to check + */ + private void isExpressionAzureClientOperation(PsiStatement statement) { + + // Get the expression from the statement + PsiExpression expression = ((PsiExpressionStatement) statement).getExpression(); + + if (expression instanceof PsiMethodCallExpression) { + // Check if the expression is an Azure client operation + if (isAzureTextAnalyticsClientOperation((PsiMethodCallExpression) expression)) { + + // get the method name + String methodName = ((PsiMethodCallExpression) expression).getMethodExpression().getReferenceName(); + holder.registerProblem(expression, + (RULE_CONFIG.getAntiPatternMessage() + methodName + "Batch")); + } + } + } + + /** + * If the statement is a declaration statement, check if the initializer is an Azure client operation. + * + * @param statement The declaration statement to check + */ + private void isDeclarationAzureClientOperation(PsiDeclarationStatement statement) { + + // getDeclaredElements() returns the variables declared in the statement + for (PsiElement element : statement.getDeclaredElements()) { + + if (!(element instanceof PsiVariable)) { + continue; + } + // Get the initializer of the variable + PsiExpression initializer = ((PsiVariable) element).getInitializer(); + + if (!(initializer instanceof PsiMethodCallExpression)) { + continue; + } + // Check if the initializer is an Azure client operation + if (isAzureTextAnalyticsClientOperation((PsiMethodCallExpression) initializer)) { + // get the method name + String methodName = + ((PsiMethodCallExpression) initializer).getMethodExpression().getReferenceName(); + holder.registerProblem(initializer, + (RULE_CONFIG.getAntiPatternMessage() + methodName + "Batch")); + } + } + } + + /** + * Check if the method call is an Azure client operation. Check the containing class of the method call and see + * if it is part of the Azure SDK. If the class is part of the Azure SDK, increment the count of Azure client + * operations. + * + * @param methodCall The method call expression to check + * + * @return True if the method call is an Azure client operation, false otherwise + */ + private static boolean isAzureTextAnalyticsClientOperation(PsiMethodCallExpression methodCall) { + + // Get the containing class of the method call + PsiClass containingClass = PsiTreeUtil.getParentOfType(methodCall, PsiClass.class); + + // Check if the method call is on a class + if (containingClass != null) { + String className = containingClass.getQualifiedName(); + + // Check if the class is part of the Azure SDK + if (className != null && SCOPE_TO_CHECK.stream().anyMatch(className::startsWith)) { + if (RULE_CONFIG.getUsagesToCheck() + .contains((methodCall.getMethodExpression().getReferenceName()) + "Batch")) { + 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/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..0d8d9f5dafd --- /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,195 @@ +// 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.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiType; +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 org.jetbrains.annotations.NotNull; + +/** + * Inspection tool to enforce usage of Azure Storage upload APIs with a 'length' parameter. + */ +public class StorageUploadWithoutLengthCheck extends LocalInspectionTool { + private static final RuleConfig RULE_CONFIG; + private static final String LENGTH_TYPE = "long"; + private static final boolean SKIP_WHOLE_RULE; + + static { + String ruleName = "StorageUploadWithoutLengthCheck"; + RuleConfigLoader configLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = configLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new StorageUploadVisitor(holder); + } + + /** + * Visitor class to check for Azure Storage upload APIs without a 'length' parameter. + */ + static class StorageUploadVisitor extends JavaRecursiveElementWalkingVisitor { + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + private static final List SCOPE_TO_CHECK; + + static { + RuleConfigLoader configLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = configLoader.getRuleConfig("StorageUploadWithoutLengthCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + SCOPE_TO_CHECK = RULE_CONFIG.getScopeToCheck(); + } + + StorageUploadVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + @Override + public void visitMethodCallExpression(PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + if (SKIP_WHOLE_RULE) { + return; + } + + String methodName = expression.getMethodExpression().getReferenceName(); + if (!RULE_CONFIG.getUsagesToCheck().contains(methodName)) { + return; + } + + if (!isInScope(expression)) { + return; + } + + if (!hasLengthArgument(expression)) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); + } + } + + /** + * Checks if the method call expression belongs to a class within the specified package scope. + * + * @param expression The method call expression to check. + * + * @return true if the method call is within the specified package scope, false otherwise. + */ + private boolean isInScope(PsiMethodCallExpression expression) { + // Get the qualifier expression and resolve its type + PsiExpression qualifierExpression = expression.getMethodExpression().getQualifierExpression(); + if (qualifierExpression == null) { + return false; + } + + PsiType qualifierType = qualifierExpression.getType(); + if (!(qualifierType instanceof PsiClassType)) { + return false; + } + + PsiClass containingClass = ((PsiClassType) qualifierType).resolve(); + if (containingClass == null) { + return false; + } + + // Check the class name against the specified scopes + String qualifiedName = containingClass.getQualifiedName(); + if (qualifiedName == null) { + return false; + } + + for (String scope : SCOPE_TO_CHECK) { + if (qualifiedName.startsWith(scope)) { + return true; + } + } + + return false; + } + + /** + * Checks if a method call expression includes a 'long' type argument directly or in its call chain. + * + * @param expression The method call expression to analyze. + * + * @return true if a 'long' type argument is found, false otherwise. + */ + private boolean hasLengthArgument(PsiMethodCallExpression expression) { + PsiExpression[] arguments = expression.getArgumentList().getExpressions(); + for (PsiExpression arg : arguments) { + if (isLongType(arg) || isLongTypeInCallChain(arg)) { + return true; + } + } + return false; + } + + /** + * Checks if an expression is of type 'long'. + * + * @param expression The expression to check. + * + * @return true if the expression is of type 'long', false otherwise. + */ + private boolean isLongType(PsiExpression expression) { + return expression.getType() != null && LENGTH_TYPE.equals(expression.getType().getCanonicalText()); + } + + /** + * Analyzes the method call chain to determine if any argument in the chain is of type 'long'. + * + * @param expression The starting expression in the call chain. + * + * @return true if a 'long' type argument is found, false otherwise. + */ + private boolean isLongTypeInCallChain(PsiExpression expression) { + while (expression instanceof PsiMethodCallExpression) { + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) expression; + expression = methodCall.getMethodExpression().getQualifierExpression(); + + if (expression instanceof PsiNewExpression && + hasLongConstructorArgument((PsiNewExpression) expression)) { + return true; + } + } + return false; + } + + /** + * Checks if a constructor has an argument of type 'long'. + * + * @param newExpression The new expression representing the constructor call. + * + * @return true if a 'long' type argument is found, false otherwise. + */ + private boolean hasLongConstructorArgument(PsiNewExpression newExpression) { + PsiExpressionList argumentList = newExpression.getArgumentList(); + if (argumentList == null) { + return false; + } + + for (PsiExpression arg : argumentList.getExpressions()) { + if (isLongType(arg)) { + 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/UpdateCheckpointAsyncSubscribeChecker.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeChecker.java new file mode 100644 index 00000000000..390abb92401 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeChecker.java @@ -0,0 +1,88 @@ +// 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.PsiMethodCallExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +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 UpdateCheckpointAsyncSubscribeChecker extends AsyncSubscribeChecker { + + /** + * 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 { + + // Define the holder to register problems + private final ProblemsHolder holder; + + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + /** + * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * + * @param holder ProblemsHolder to register problems + * @param isOnTheFly boolean to check if the inspection is on the fly. If true, the inspection is performed as + * you type. + */ + UpdateCheckpointAsyncVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + static { + final String ruleName = "UpdateCheckpointAsyncSubscribeChecker"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + /** + * 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; + } + + expression.getMethodExpression(); + if (expression.getMethodExpression().getReferenceName() == null) { + return; + } + + // Check if the method call is updateCheckpointAsync() + if (RULE_CONFIG.getUsagesToCheck().stream() + .anyMatch(usage -> usage.equals(expression.getMethodExpression().getReferenceName()))) { + + // Check if the following method is `subscribe` and + // Check if the updateCheckpointAsync() method call is called on an provided context + // (EventBatchContext) object + if (isFollowingMethodSubscribe(expression) && isCalledInProvidedContext(expression, RULE_CONFIG.getScopeToCheck())) { + holder.registerProblem(expression, 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/UpgradeLibraryVersionCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java new file mode 100644 index 00000000000..1098c5f0db5 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java @@ -0,0 +1,105 @@ +// 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.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.xml.XmlFile; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.DependencyVersionFileFetcher; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.VersionUtils; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jetbrains.annotations.NotNull; + +import static com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UpgradeLibraryVersionCheck.LibraryVersionVisitor.getRecommendedVersionMap; + +/** + * Inspection class to check the version of the libraries in the pom.xml file against the recommended version. + */ +public class UpgradeLibraryVersionCheck extends AbstractLibraryVersionCheck { + private static final Logger LOG = Logger.getLogger(UpgradeLibraryVersionCheck.class.getName()); + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new LibraryVersionVisitor(holder); + } + + @Override + protected void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, PsiElement versionTag) { + String recommendedVersion = VersionUtils.getRecommendedVersion(fullName, getRecommendedVersionMap()); + if (recommendedVersion == null) { + return; + } + + String currentMinorVersion = VersionUtils.extractMinorVersion(currentVersion); + if (currentMinorVersion == null || currentMinorVersion.equals(recommendedVersion)) { + return; + } + + holder.registerProblem(versionTag, getFormattedMessage(fullName, recommendedVersion, LibraryVersionVisitor.RULE_CONFIG)); + } + + class LibraryVersionVisitor extends PsiElementVisitor { + + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + // Cache for recommended versions + private static Map VERSION_MAP_CACHE; + + static { + RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig("UpgradeLibraryVersionCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + LibraryVersionVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + @Override + public void visitFile(@NotNull PsiFile file) { + super.visitFile(file); + + if (SKIP_WHOLE_RULE || !(file instanceof XmlFile) || !RuleConfig.POM_XML.equals(file.getName())) { + return; + } + + try { + UpgradeLibraryVersionCheck.this.checkPomXml((XmlFile) file, holder); + } catch (IOException e) { + LOG.log(Level.ALL, "Error while parsing pom.xml", e); + } + } + + static Map getRecommendedVersionMap() { + Map cachedMap = VERSION_MAP_CACHE == null ? null : VERSION_MAP_CACHE; + if (cachedMap != null) { + return cachedMap; + } + + synchronized (VersionUtils.class) { + cachedMap = VERSION_MAP_CACHE == null ? null : VERSION_MAP_CACHE; + if (cachedMap == null) { + String latestVersion = DependencyVersionFileFetcher.getLatestVersion(RULE_CONFIG.AZURE_MAVEN_METADATA_URL); + if (latestVersion != null) { + String baseUrl = RULE_CONFIG.AZURE_MAVEN_METADATA_URL.replace("/maven-metadata.xml", ""); + String pomUrl = String.format("%s/%s/azure-sdk-bom-%s.pom", baseUrl, latestVersion, latestVersion); + cachedMap = DependencyVersionFileFetcher.parsePomFile(pomUrl); + VERSION_MAP_CACHE = new HashMap<>(cachedMap); + } + } + } + return cachedMap; + } + } +} \ 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..8ce1658e5dc --- /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,157 @@ +// 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.PsiClassType; +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.RuleConfigLoader; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection tool to check for the use of blocking method calls on async clients in Azure SDK. + */ +public class UseOfBlockOnAsyncClientsCheck extends LocalInspectionTool { + + private static final String RULE_NAME = "UseOfBlockOnAsyncClientsCheck"; + private static final RuleConfig RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig(RULE_NAME); + private static final boolean SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new UseOfBlockOnAsyncClientsVisitor(holder); + } + + /** + * 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 final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig("UseOfBlockOnAsyncClientsCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + UseOfBlockOnAsyncClientsVisitor(@NotNull ProblemsHolder holder) { + this.holder = holder; + } + + /** + * 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 || !isBlockingMethodCall(expression)) { + return; + } + + if (isAsyncClientBlockingCall(expression)) { + holder.registerProblem(expression, 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) { + for (String methodToCheck : RULE_CONFIG.getUsagesToCheck()) { + if (expression.getMethodExpression().getReferenceName().equals(methodToCheck)) { + return true; + } + } + return false; + } + + /** + * 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(); + return clientClass != null && + clientClass.getQualifiedName().startsWith(RuleConfig.AZURE_PACKAGE_NAME) && + 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/DependencyVersionsDataCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java new file mode 100644 index 00000000000..e7a38bc9bae --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.models; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.io.Serializable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * A cache to store the dependency versions data for a certain period of time. + * The cache is stored in memory and also saved to a file to persist the data across IDE restarts. + * The cache is cleared at regular intervals to prevent it from growing indefinitely. + * + * @param The type of the data to be stored in the cache. + */ +public class DependencyVersionsDataCache implements Serializable { + private static final Logger LOGGER = Logger.getLogger(DependencyVersionsDataCache.class.getName()); + + // The serial version UID is used to verify that the class is compatible with the serialized object. + @Serial + private static final long serialVersionUID = 1L; + + // The cache is stored in a ConcurrentHashMap to ensure thread safety. + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + // The interval at which the cache is cleared. The cache is cleared every 5 days. + private static final long CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(5); + + // The file where the cache is saved. + private final File cacheFile; + + // The timestamp when the cache was last updated. + private long lastUpdated; + + // The timestamp when the cache will be refreshed. + private long nextRefresh; + + /** + * Creates a new instance of the DependencyVersionsDataCache class. + * + * @param cacheFileName The name of the file where the cache is saved. + */ + public DependencyVersionsDataCache(String cacheFileName) { + this.cacheFile = new File(cacheFileName); + loadCacheFromFile(); + + // Check if the cache needs to be cleared immediately + checkAndClearOnStartup(); + + // Schedule the cache cleanup task to run at regular intervals + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate(this::clear, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.MILLISECONDS); + } + + /** + * Puts a value into the cache. + * + * @param key The key to associate with the value. + * @param value The value to store in the cache. + */ + public void put(String key, T value) { + cache.put(key, value); + updateTimestamps(); + saveCacheToFile(); + } + + /** + * Gets a value from the cache. + * + * @param key The key to retrieve the value for. + * @return The value associated with the key, or null if the key is not found. + */ + public T get(String key) { + return cache.get(key); + } + + /** + * Clears the cache. + * This method is called at regular intervals to prevent the cache from growing indefinitely. + */ + void clear() { + cache.clear(); + updateTimestamps(); + saveCacheToFile(); + } + + /** + * Loads the cache from the file. + * If the file exists, the cache is loaded from the file. + * If the file does not exist, the cache is left empty. + */ + private void loadCacheFromFile() { + if (cacheFile.exists()) { + // Load the cache from the file + try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(cacheFile))) { + + // Use a temporary variable to hold the deserialized object + Object loadedObject = ois.readObject(); + + // Check if the deserialized object is an instance of ConcurrentHashMap + if (loadedObject instanceof CacheData) { + + CacheData loadedCacheData = (CacheData) loadedObject; + + // Put all the entries from the loaded cache into the current cache + // Update the lastUpdated and nextRefresh timestamps + cache.putAll(loadedCacheData.cache); + lastUpdated = loadedCacheData.lastUpdated; + nextRefresh = loadedCacheData.nextRefresh; + } else { + LOGGER.severe("Failed to load cache from file: Invalid cache format"); + } + } catch (IOException | ClassNotFoundException e) { + LOGGER.severe("Failed to load cache from file: " + e); + } + } + } + + /** + * Saves the cache to the file. + * The cache is saved to the file to persist the data across IDE restarts. + */ + private void saveCacheToFile() { + + // Save the cache to the file using the ObjectOutputStream class. + try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(cacheFile))) { + CacheData cacheData = new CacheData<>(cache, lastUpdated, nextRefresh); + oos.writeObject(cacheData); + } catch (IOException e) { + LOGGER.severe("Failed to save cache to file: " + e.getMessage()); + } + } + + /** + * Updates the timestamps for the cache. + * The lastUpdated timestamp is updated to the current time. + * The nextRefresh timestamp is updated to the current time plus the cleanup interval. + */ + private void updateTimestamps() { + lastUpdated = System.currentTimeMillis(); + nextRefresh = lastUpdated + CLEANUP_INTERVAL; + } + + /** + * Checks if the cache needs to be cleared on startup. + * The cache is cleared if the lastUpdated timestamp is older than the cleanup interval. + */ + private void checkAndClearOnStartup() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastUpdated >= CLEANUP_INTERVAL) { + clear(); + } + } + + /** + * The CacheData class is a static inner class used to store the cache data in a serializable format. + * The class contains the cache, lastUpdated, and nextRefresh timestamps. + */ + private static class CacheData implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private final ConcurrentHashMap cache; + private final long lastUpdated; + private final long nextRefresh; + + /** + * Creates a new instance of the CacheData class. + * + * @param cache The cache to store in the object. + * @param lastUpdated The timestamp when the cache was last updated. + * @param nextRefresh The timestamp when the cache will be refreshed. + */ + CacheData(ConcurrentHashMap cache, long lastUpdated, long nextRefresh) { + this.cache = cache; + this.lastUpdated = lastUpdated; + this.nextRefresh = nextRefresh; + } + } +} 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..4482bb905bc --- /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,102 @@ +// 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; + +/** + * This class contains configuration options for code style rules. It contains the methods to check, the client name, and + * the antipattern message. + */ +public class RuleConfig { + public static final String DEFAULT_SCOPE = "all"; + public static final String DEFAULT_USAGE = "all"; + public static final String AZURE_PACKAGE_NAME = "com.azure"; + public static final String AZURE_MAVEN_METADATA_URL = "https://repo1.maven.org/maven2/com/azure/azure-sdk-bom" + + "/maven-metadata.xml"; + public static final String POM_XML = "pom.xml"; + public static final String SUPPORTED_VERSION_URL = "https://raw.githubusercontent" + + ".com/Azure/azure-sdk-for-java/main/eng/versioning/supported_external_dependency_versions.json"; + public static final RuleConfig EMPTY_RULE = + new RuleConfig(Collections.singletonList(DEFAULT_USAGE), + Collections.singletonList(DEFAULT_SCOPE), + null, + Collections.emptyMap()); + + private final List usagesToCheck; + private final List scopeToCheck; + private final String antiPatternMessage; + private final Map regexPatternsToCheck; + + /** + * 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. + */ + public RuleConfig(List usagesToCheck, List scopeToCheck, String antiPatternMessage, + Map regexPatternsToCheck) { + this.usagesToCheck = usagesToCheck == null || usagesToCheck.isEmpty() + ? Collections.singletonList(DEFAULT_USAGE) + : Collections.unmodifiableList(usagesToCheck); + this.scopeToCheck = scopeToCheck == null || scopeToCheck.isEmpty() + ? Collections.singletonList(DEFAULT_SCOPE) + : Collections.unmodifiableList(scopeToCheck); + this.antiPatternMessage = antiPatternMessage; + this.regexPatternsToCheck = regexPatternsToCheck == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(regexPatternsToCheck); + } + + /** + * This method checks if the rule should be skipped. + * + * @return True if the rule should be skipped, false otherwise. + */ + public boolean skipRuleCheck() { + return this == EMPTY_RULE; + } + + // Getters + + /** + * This method returns the list of methods to check. + * + * @return List of methods to check. + */ + public List getUsagesToCheck() { + return usagesToCheck; + } + + /** + * This method returns the list of clients to check. + * + * @return List of clients to check. + */ + public List getScopeToCheck() { + return scopeToCheck; + } + + /** + * This method returns the antipattern message. + * + * @return Antipattern message. + */ + public String getAntiPatternMessage() { + return antiPatternMessage; + } + + /** + * This method returns the map of regex patterns to check. + * + * @return Map of regex patterns to check. + */ + public Map getRegexPatternsToCheck() { + return regexPatternsToCheck; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java new file mode 100644 index 00000000000..13d09d10306 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.utils; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.DependencyVersionsDataCache; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +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.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class to fetch files from their corresponding data sources + * The class fetches these sources and parses them to get the data. + * The data is used to check the version of the libraries in the pom.xml file against the recommended version. + */ +public class DependencyVersionFileFetcher { + + private static final Logger LOGGER = Logger.getLogger(DependencyVersionFileFetcher.class.getName()); + + private static final DependencyVersionsDataCache> pomCache = new DependencyVersionsDataCache<>("pomCache.ser"); + private static final DependencyVersionsDataCache versionCache = new DependencyVersionsDataCache<>("versionCache.ser"); + private static final DependencyVersionsDataCache>> incompatibleVersionsCache = new DependencyVersionsDataCache<>("incompatibleVersionsCache.ser"); + + /** + * The parsePomFile method fetches the pom.xml file from the URL and parses it to get the dependencies. + * This method is used to fetch the pom.xml file from the URL and parse it to get the dependencies. + * It is used by the UpgradeLibraryVersionCheck inspection. + * + * @param pomUrl The URL of the pom.xml file to fetch + * @return A map of the dependencies in the pom.xml file + */ + public static Map parsePomFile(String pomUrl) { + Map artifactVersionMap = pomCache.get(pomUrl); + if (artifactVersionMap != null) { + return artifactVersionMap; + } + + Document pomDoc = fetchXmlDocument(pomUrl); + NodeList dependencies = pomDoc.getElementsByTagName("dependency"); + artifactVersionMap = new HashMap<>(); + + for (int i = 0; i < dependencies.getLength(); i++) { + NodeList dependency = dependencies.item(i).getChildNodes(); + String groupId = null, artifactId = null, version = null; + + for (int j = 0; j < dependency.getLength(); j++) { + switch (dependency.item(j).getNodeName()) { + case "groupId": + groupId = dependency.item(j).getTextContent(); + break; + case "artifactId": + artifactId = dependency.item(j).getTextContent(); + break; + case "version": + version = dependency.item(j).getTextContent(); + break; + } + } + + if (groupId != null && artifactId != null && version != null) { + String minorVersion = version.substring(0, version.lastIndexOf(".")); + artifactVersionMap.put(groupId + ":" + artifactId, minorVersion); + } + } + + pomCache.put(pomUrl, artifactVersionMap); + return artifactVersionMap; + } + + /** + * The getLatestVersion method fetches the latest Azure Client release versions from Maven Central. + * This method is used to fetch the latest version of the library from the metadata file hosted on Maven Central. + * It is used by the UpgradeLibraryVersionCheck inspection. + * + * @param metadataUrl The URL of the metadata file to fetch + * @return The latest version of the library + */ + public static String getLatestVersion(String metadataUrl) { + String cachedVersion = versionCache.get(metadataUrl); + if (cachedVersion != null) { + return cachedVersion; + } + + Document metadataDoc = fetchXmlDocument(metadataUrl); + NodeList versions = metadataDoc.getElementsByTagName("version"); + String latestVersion = versions.item(versions.getLength() - 1).getTextContent(); + + versionCache.put(metadataUrl, latestVersion); + return latestVersion; + } + + /** + * The fetchXmlDocument method fetches an XML document from a URL and parses it. + * This method is used to fetch the pom.xml file from the URL and parse it to get the dependencies. + * It is used by the UpgradeLibraryVersionCheck inspection. + * + * @param urlString The URL of the XML document to fetch + * @return The parsed XML document + */ + private static Document fetchXmlDocument(String urlString) { + try (InputStream inputStream = new URL(urlString).openStream()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputStream); + } catch (IOException | ParserConfigurationException | SAXException e) { + LOGGER.log(Level.SEVERE, "Error fetching or parsing XML from URL: " + urlString, e); + throw new RuntimeException(e); + } + } + + /** + * The loadJsonDataFromUrl method fetches a .json file from GitHub and parses it to get the data. + * This method is used to fetch the data for the libraries in the pom.xml file. It is used by the IncompatibleDependencyCheck inspection. + * + * @param jsonUrl The URL of the .json file to fetch + * @return A map of the data for the libraries + */ + public static Map> loadJsonDataFromUrl(String jsonUrl) { + Map> jsonData = incompatibleVersionsCache.get(jsonUrl); + if (jsonData != null) { + return jsonData; + } + + try (InputStream inputStream = new URL(jsonUrl).openStream(); JsonReader jsonReader = JsonProviders.createReader(inputStream)) { + jsonData = parseJson(jsonReader); + incompatibleVersionsCache.put(jsonUrl, jsonData); + return jsonData; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error reading JSON from URL: " + jsonUrl, e); + return new HashMap<>(); + } + } + + /** + * Method to parse JSON data into a nested map structure + * The method reads the JSON data from the JsonReader and parses it to get the data for the libraries. + * The data is in a Map> format where the key is the group and the value is a set of artifactIds. + * For example, the data for the Jackson library is in the format + * "jackson_2.10: [com.fasterxml.jackson.core:jackson-annotations, com.fasterxml.jackson.core:jackson-core, com.fasterxml.jackson.core:jackson-databind]". + * + * @param jsonReader The JsonReader object to read the JSON data + * @return A map of the data for the libraries + * @throws IOException If an error occurs while reading the JSON data + */ + private static Map> parseJson(JsonReader jsonReader) throws IOException { + Map> versionData = new ConcurrentHashMap<>(); + if (jsonReader.nextToken() == JsonToken.START_OBJECT) { + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + String groupKey = jsonReader.getFieldName(); + Set groupSet = new HashSet<>(); + + if (jsonReader.nextToken() == JsonToken.START_ARRAY) { + while (jsonReader.nextToken() != JsonToken.END_ARRAY) { + if (jsonReader.nextToken() == JsonToken.FIELD_NAME) { + groupSet.add(jsonReader.getFieldName()); + } + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + // Skip remaining tokens in the object + } + } + } + versionData.put(groupKey, groupSet); + } + } + return versionData; + } +} 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..016998f3524 --- /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,247 @@ +// 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.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RuleConfigLoader { + private static final String CONFIG_FILE_PATH = "./META-INF/ruleConfigs.json"; + private static final Logger LOGGER = Logger.getLogger(RuleConfigLoader.class.getName()); + private static final RuleConfigLoader INSTANCE; + + private final Map ruleConfigs; + + static { + RuleConfigLoader tempInstance; + try { + tempInstance = new RuleConfigLoader(CONFIG_FILE_PATH); + } catch (IOException e) { + tempInstance = null; + LOGGER.log(Level.SEVERE, "Failed to initialize RuleConfigLoader: " + e.getMessage(), e); + } + INSTANCE = tempInstance; + } + + private RuleConfigLoader(String filePath) throws IOException { + this.ruleConfigs = loadRuleConfigurations(filePath); + } + + public static RuleConfigLoader getInstance() { + if (INSTANCE == null) { + throw new IllegalStateException("RuleConfigLoader initialization failed. Check logs for details."); + } + return INSTANCE; + } + + public RuleConfig getRuleConfig(String key) { + return ruleConfigs.getOrDefault(key, RuleConfig.EMPTY_RULE); + } + + private Map loadRuleConfigurations(String filePath) throws IOException { + InputStream configStream = getClass().getClassLoader().getResourceAsStream(filePath); + if (configStream == null) { + throw new IOException("Configuration file not found: " + filePath); + } + + 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")); + + return new RuleConfig(usages, scope, antiPatternMessage, regexPatterns); + } + + //private Map loadRuleConfigurations(String filePath) throws IOException { + // InputStream configStream = getClass().getClassLoader().getResourceAsStream(filePath); + // if (configStream == null) { + // throw new IOException("Configuration file not found: " + filePath); + // } + // + // 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(); + // 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); + // String solution = ruleNode.path("solution").asText(null); + // + // Map regexPatterns = parseRegexPatterns(ruleNode.path("regexPatterns")); + // + // return new RuleConfig(usages, scope, antiPatternMessage, regexPatterns); + //} + + 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; + } + + //private List> parseCheckItems(JsonNode checkItemsNode) { + // List> result = new ArrayList<>(); + // if (checkItemsNode.isArray()) { + // for (JsonNode itemNode : checkItemsNode) { + // if (itemNode.isObject()) { + // result.add(parseToMap(itemNode)); + // } else { + // throw new IllegalArgumentException("Each item in 'checkItems' must be a JSON object."); + // } + // } + // } else { + // throw new IllegalArgumentException("'checkItems' must be an array of JSON objects."); + // } + // return result; + //} + + //public Object parseCheckItems(JsonNode entryNode) { + // // The unified object that will contain either a List or Map depending on the input + // Map result = new HashMap<>(); + // + // if (entryNode.isArray()) { + // if (entryNode.size() > 0 && entryNode.get(0).isObject()) { + // // Case: List of Maps + // result.put("itemsAsMap", getItemsAsMap(entryNode)); + // } else { + // // Case: List of Strings + // result.put("itemsAsList", getItemsAsList(entryNode)); + // } + // } else if (entryNode.isTextual()) { + // // Case: Single String + // List singleItemList = new ArrayList<>(); + // singleItemList.add(entryNode.asText()); + // result.put("itemsAsList", singleItemList); + // } else { + // throw new IllegalArgumentException("Invalid entry format. Must be a string, list of strings, or list of objects."); + // } + // + // return result; // Return a unified object (Map) for flexible extraction + //} + + // Helper to extract items as a List of Strings + public List getItemsAsList(JsonNode entryNode) { + List result = new ArrayList<>(); + if (entryNode.isArray()) { + for (JsonNode itemNode : entryNode) { + if (itemNode.isTextual()) { + result.add(itemNode.asText()); + } else { + throw new IllegalArgumentException("Array must contain only strings."); + } + } + } + return result; + } + + // Helper to extract items as a List of Maps + public List> getItemsAsMap(JsonNode entryNode) { + List> result = new ArrayList<>(); + if (entryNode.isArray()) { + for (JsonNode itemNode : entryNode) { + if (itemNode.isObject()) { + result.add(parseToMap(itemNode)); + } else { + throw new IllegalArgumentException("List must contain only objects for 'List>' format."); + } + } + } + return result; + } + + private Object parseCheckItems(JsonNode checkItemsNode) { + if (checkItemsNode.isArray()) { + return parseToList(checkItemsNode); + } else if (checkItemsNode.isObject()) { + return parseToMap(checkItemsNode); + } + return checkItemsNode.asText(null); + } + + private Map parseToMap(JsonNode objectNode) { + Map map = new HashMap<>(); + objectNode.fields().forEachRemaining(entry -> { + JsonNode value = entry.getValue(); + if (value.isTextual()) { + map.put(entry.getKey(), value.asText()); + } else { + throw new IllegalArgumentException("All values in the 'checkItems' object must be strings."); + } + }); + return map; + } + + private List parseToList(JsonNode arrayNode) { + List list = new ArrayList<>(); + arrayNode.forEach(node -> list.add(node.asText())); + return list; + } + + private Set parseToSet(JsonNode arrayNode) { + Set set = new HashSet<>(); + arrayNode.forEach(node -> set.add(node.asText())); + return set; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java new file mode 100644 index 00000000000..07fdfdbe6f3 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.utils; + +import java.util.Map; +import java.util.Set; + +/** + * Utility class for pom version related operations. + */ +public class VersionUtils { + + /** + * Retrieves the recommended version for a given library. + * + * @param libraryName The name of the library. + * @param recommendedVersionMap The map containing the recommended versions. + * + * @return The recommended version of the library, or null if not found. + */ + public static String getRecommendedVersion(String libraryName, Map recommendedVersionMap) { + return recommendedVersionMap == null ? null : recommendedVersionMap.get(libraryName); + } + + /** + * Extracts the minor version from a given version string. + * + * @param version The version string. + * @return The minor version in the format "major.minor", or null if the input is invalid. + */ + public static String extractMinorVersion(String version) { + if (version == null || !version.matches("\\d+\\.\\d+.*")) { + return null; + } + String[] parts = version.split("\\."); + return parts.length >= 2 ? parts[0] + "." + parts[1] : null; + } + + /** + * Method to get the version group for the library. The version group is used to get the compatible versions for + * the library. The version group is determined by the major and minor version of the library. Eg if the major + * version is 2 and the minor version is 10, the version group is "jackson_2.10". + * + * @param fullName The full name of the library eg "com.azure:azure-core" + * @param currentVersion The current version of the library + * @param fileContent The content of the version file (as a map) + * + * @return The version group for the library + */ + public static String getGroupVersion(String fullName, String currentVersion, Map> fileContent) { + // Split currentVersion to extract major and minor version + String[] versionParts = currentVersion.split("\\."); + String majorVersion = versionParts[0]; + String minorVersion = versionParts.length > 1 ? versionParts[1] : ""; + String versionSuffix = "_" + majorVersion + "." + minorVersion; + + // Search the file content for the version group + String versionGroup = null; + for (Map.Entry> entry : fileContent.entrySet()) { + // Check if the set of artifactIds contains the fullName and the corresponding key ends with the versionSuffix + if (entry.getValue().contains(fullName) && entry.getKey().endsWith(versionSuffix)) { + versionGroup = entry.getKey(); + break; + } + } + return versionGroup; + } + + /** + * Method to check if two versions are incompatible based on their minor version. + * + * @param currentVersion The current version of the library + * @param recommendedVersion The recommended version of the library + * @return true if the versions are incompatible, false otherwise + */ + public static boolean isIncompatibleVersion(String currentVersion, String recommendedVersion) { + String[] currentVersionParts = currentVersion.split("\\."); + String[] recommendedVersionParts = recommendedVersion.split("\\."); + + boolean isIncompatible = false; + for (int i = 0; i < Math.min(currentVersionParts.length, recommendedVersionParts.length); i++) { + if (!currentVersionParts[i].equals(recommendedVersionParts[i])) { + isIncompatible = true; + break; + } + } + return isIncompatible; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java new file mode 100644 index 00000000000..7032e53fc85 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java @@ -0,0 +1,174 @@ +// 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.lang.StdLanguages; +import com.intellij.openapi.project.Project; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.intellij.psi.xml.XmlTagValue; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Stream; +import org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/* + * This test class is used to test the AbstractLibraryVersionCheck class. + */ +public class AbstractLibraryVersionCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private XmlFile mockFile; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockFile = mock(XmlFile.class); + + IncompatibleDependencyCheck.encounteredVersionGroups = new HashSet<>(List.of("jackson_2.10", "gson_2.10")); + } + + static Stream testCases() { + return Stream.of( + new TestCase("com.azure", "azure-messaging-servicebus", "7.0.0", 1, "7.17", UpgradeLibraryVersionCheck.LibraryVersionVisitor.class), + new TestCase("com.azure", "azure-messaging-servicebus", "7.17.1", 0, "7.17", UpgradeLibraryVersionCheck.LibraryVersionVisitor.class), + new TestCase("com.example", "example-lib", "", 0, null, UpgradeLibraryVersionCheck.LibraryVersionVisitor.class), + new TestCase("com.google.code.gson", "gson", "2.9.0", 1, "2.10", IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class), + new TestCase("com.fasterxml.jackson.core", "jackson-databind", "2.10.0", 0, "2.10", IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class), + new TestCase("com.fasterxml.jackson.core", "jackson-databind", "3.0.0", 0, null, IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) + ); + } + + @ParameterizedTest + @MethodSource("testCases") + void testLibraryVersionChecks(TestCase testCase) { + PsiElementVisitor visitor = createVisitor(testCase.visitorClass, mockHolder); + + XmlTag versionTag = setupMockXml(testCase.groupID, testCase.artifactID, testCase.version); + visitor.visitFile(mockFile); + + if (testCase.visitorClass == UpgradeLibraryVersionCheck.LibraryVersionVisitor.class) { + // Validate the full message if warnings are expected + if (testCase.expectedInvocations > 0 && testCase.expectedMessage != null) { + String expectedMessage = String.format( + "A newer stable minor version of '%s:%s' is available. We recommend you update to version %s.x.", + testCase.groupID, testCase.artifactID, testCase.expectedMessage + ); + verify(mockHolder).registerProblem(versionTag, expectedMessage); + } + } else if (testCase.visitorClass == IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) { + if (testCase.expectedInvocations > 0 && testCase.expectedMessage != null) { + String expectedMessage = String.format( + "The version of '%s:%s' is incompatible with other dependencies of the same " + + "library defined in the pom.xml. We recommend you update to version %s.x of the same library " + + "release group.", + testCase.groupID, testCase.artifactID, testCase.expectedMessage + ); + verify(mockHolder).registerProblem(versionTag, expectedMessage); + } + + } + } + + private PsiElementVisitor createVisitor(Class visitorClass, ProblemsHolder holder) { + if (visitorClass == UpgradeLibraryVersionCheck.LibraryVersionVisitor.class) { + return new UpgradeLibraryVersionCheck().new LibraryVersionVisitor(holder); + } else if (visitorClass == IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) { + return new IncompatibleDependencyCheck().new IncompatibleDependencyVisitor(holder); + } + throw new IllegalArgumentException("Unsupported visitor class: " + visitorClass); + } + + private XmlTag setupMockXml(String groupIDValue, String artifactIDValue, String versionIDValue) { + Project project = mock(Project.class); + MavenProjectsManager mavenProjectsManager = mock(MavenProjectsManager.class); + FileViewProvider viewProvider = mock(FileViewProvider.class); + XmlTag rootTag = mock(XmlTag.class); + + XmlTag dependenciesTag = mock(XmlTag.class); + XmlTag[] dependenciesTags = new XmlTag[]{dependenciesTag}; + + XmlTag dependencyTag = mock(XmlTag.class); + XmlTag[] dependencyTags = new XmlTag[]{dependencyTag}; + + XmlTag groupIdTag = mock(XmlTag.class); + XmlTagValue groupIdValue = mock(XmlTagValue.class); + XmlTag artifactIdTag = mock(XmlTag.class); + XmlTagValue artifactIdValue = mock(XmlTagValue.class); + XmlTag versionTag = mock(XmlTag.class); + XmlTagValue versionValue = mock(XmlTagValue.class); + + when(mockFile.getName()).thenReturn("pom.xml"); + when(mockFile.getProject()).thenReturn(project); + when(MavenProjectsManager.getInstance(project)).thenReturn(mavenProjectsManager); + when(mavenProjectsManager.isMavenizedProject()).thenReturn(true); + + when(mockFile.getViewProvider()).thenReturn(viewProvider); + when(viewProvider.getPsi(StdLanguages.XML)).thenReturn(mockFile); + when(mockFile.getRootTag()).thenReturn(rootTag); + when(rootTag.getName()).thenReturn("project"); + + when(rootTag.findSubTags("dependencies")).thenReturn(dependenciesTags); + when(dependenciesTag.findSubTags("dependency")).thenReturn(dependencyTags); + + when(dependencyTag.findFirstSubTag("groupId")).thenReturn(groupIdTag); + when(dependencyTag.findFirstSubTag("artifactId")).thenReturn(artifactIdTag); + when(dependencyTag.findFirstSubTag("version")).thenReturn(versionTag); + + when(groupIdTag.getValue()).thenReturn(groupIdValue); + when(artifactIdTag.getValue()).thenReturn(artifactIdValue); + when(versionTag.getValue()).thenReturn(versionValue); + + when(groupIdValue.getText()).thenReturn(groupIDValue); + when(artifactIdValue.getText()).thenReturn(artifactIDValue); + when(versionValue.getText()).thenReturn(versionIDValue); + + return versionTag; + } + + private void verifyProblemsRegistered(TestCase testCase, XmlTag versionTag) { + if (testCase.expectedInvocations > 0) { + verify(mockHolder, times(testCase.expectedInvocations)).registerProblem(eq(versionTag), contains(testCase.expectedMessage)); + } else { + verify(mockHolder, never()).registerProblem(eq(versionTag), anyString()); + } + } + + private static class TestCase { + String groupID; + String artifactID; + String version; + int expectedInvocations; + String expectedMessage; + Class visitorClass; + + TestCase(String groupID, String artifactID, String version, int expectedInvocations, String expectedMessage, Class visitorClass) { + this.groupID = groupID; + this.artifactID = artifactID; + this.version = version; + this.expectedInvocations = expectedInvocations; + this.expectedMessage = expectedMessage; + this.visitorClass = visitorClass; + } + } +} \ 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/AsyncSubscribeCheckerTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeCheckerTest.java new file mode 100644 index 00000000000..1c86618d476 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeCheckerTest.java @@ -0,0 +1,91 @@ +// 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.PsiMethodCallExpression; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiReferenceExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +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 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; + +/** + * Tests for {@link AsyncSubscribeChecker} and derived classes. + */ +public class AsyncSubscribeCheckerTest { + @Mock + private ProblemsHolder mockHolder; + + @Mock + private PsiMethodCallExpression mockMethodCallExpression; + + @BeforeEach + public void setUp() { + mockHolder = mock(ProblemsHolder.class); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testWithParameterizedCases(String packageName, String mainMethodFound, + int numOfInvocations, String followingMethod, String objectType, String expectedMessage) { + JavaElementVisitor visitor = new UpdateCheckpointAsyncSubscribeChecker.UpdateCheckpointAsyncVisitor(mockHolder, + true); + setupMockMethodCall(packageName, mainMethodFound, numOfInvocations, followingMethod, + objectType, + expectedMessage); + visitor.visitMethodCallExpression(mockMethodCallExpression); + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(mockMethodCallExpression), + contains(expectedMessage)); + } + + private static Stream provideTestCases() { + return Stream.of( + new Object[] {"com.azure", "updateCheckpointAsync", 1, + "subscribe", "EventBatchContext", + "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`"}, + new Object[] {"com.azure", "updateCheckpointAsync", 0, "notSubscribe", "EventBatchContext", + "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous " + + "version `updateCheckpoint()`"} + ); + } + + 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); + + when(mockMethodCallExpression.getMethodExpression()).thenReturn(mockReferenceExpression); + when(mockReferenceExpression.getReferenceName()).thenReturn(mainMethodFound); + when(mockMethodCallExpression.getParent()).thenReturn(mockReferenceExpression); + when(mockReferenceExpression.getParent()).thenReturn(grandParentMethodCalLExpression); + when(grandParentMethodCalLExpression.getMethodExpression()).thenReturn(parentReferenceExpression); + when(parentReferenceExpression.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(RuleConfig.AZURE_PACKAGE_NAME); + } +} \ 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/DetectDiscouragedAPIUsageCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java new file mode 100644 index 00000000000..785565a621e --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java @@ -0,0 +1,130 @@ +// 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 java.util.Collections; +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 DetectDiscouragedAPIUsageCheck} and derived classes. + */ +public class DetectDiscouragedAPIUsageCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression methodCallExpression; + + @Mock + private PsiElement problemElement; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + methodCallExpression = mock(PsiMethodCallExpression.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void detectsDiscouragedAPIUsage(TestCase testCase) { + setupMockAPI(testCase.methodToCheck, testCase.numOfInvocations, testCase.packageName, + testCase.suggestionMessage); + mockVisitor = createVisitor(testCase.ruleConfig); + mockVisitor.visitElement(methodCallExpression); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(problemElement), + eq(testCase.suggestionMessage)); + } + + private JavaElementVisitor createVisitor(RuleConfig ruleConfig) { + return new DetectDiscouragedAPIUsageCheck.DetectDiscouragedAPIUsageVisitor(mockHolder, ruleConfig); + } + + private static RuleConfig getGetCompletionsConfig() { + RuleConfig getCompletionsConfig = mock(RuleConfig.class); + when(getCompletionsConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getCompletions")); + when(getCompletionsConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure.ai.openai")); + when(getCompletionsConfig.getAntiPatternMessage()).thenReturn( + "getCompletions API detected. Use the getChatCompletions API instead."); + return getCompletionsConfig; + } + + private static RuleConfig getConnectionStringConfig() { + RuleConfig connectionStringConfig = mock(RuleConfig.class); + when(connectionStringConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("connectionString")); + when(connectionStringConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure")); + when(connectionStringConfig.getAntiPatternMessage()).thenReturn( + "Connection String detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication)."); + return connectionStringConfig; + } + + private void setupMockAPI(String methodToCheck, int numOfInvocations, String packageName, + String suggestionMessage) { + methodCallExpression = mock(PsiMethodCallExpression.class); + 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(getConnectionStringConfig(), "connectionString", "com.azure", "Connection String detected. " + + "Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication).", + 1), + new TestCase(getGetCompletionsConfig(), "getCompletions", "com.azure.ai.openai", "getCompletions API " + + "detected. Use the getChatCompletions API instead.", 1), + new TestCase(getConnectionStringConfig(), "allowedMethod", "com.azure", "", 0), + new TestCase(getConnectionStringConfig(), "connectionString", "com.microsoft.azure", "", 0), + new TestCase(getGetCompletionsConfig(), "getCompletions", "com.azure.other", "", 0) + ); + } + + private static class TestCase { + String methodToCheck; + String packageName; + String suggestionMessage; + int numOfInvocations; + RuleConfig ruleConfig; + + TestCase(RuleConfig ruleConfig, String methodToCheck, String packageName, String suggestionMessage, + int numOfInvocations) { + this.methodToCheck = methodToCheck; + this.packageName = packageName; + this.suggestionMessage = suggestionMessage; + this.numOfInvocations = numOfInvocations; + this.ruleConfig = ruleConfig; + } + } +} \ 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/DetectDiscouragedClientCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java new file mode 100644 index 00000000000..f90ea3f5ae2 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java @@ -0,0 +1,113 @@ +// 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.PsiElementVisitor; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiTypeElement; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DetectDiscouragedClientCheck.DetectDiscouragedClientVisitor; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +import java.util.Collections; +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 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()); + * + * 4. final EventHubConsumerAsyncClient consumerClient = partitionPump.getClient(); + * 5. EventHubConsumerAsyncClient eventHubConsumer = eventHubClientBuilder.buildAsyncClient() + * .createConsumer(claimedOwnership.getConsumerGroup(), prefetch, true); + */ +public class DetectDiscouragedClientCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiTypeElement mockTypeElement; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + mockTypeElement = mock(PsiTypeElement.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void detectsDiscouragedClientUsage(TestCase testCase) { + mockVisitor = createVisitor(testCase.ruleConfig); + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck, + testCase.suggestionMessage); + mockVisitor.visitTypeElement(mockTypeElement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), contains(testCase.suggestionMessage)); + } + + private JavaElementVisitor createVisitor(RuleConfig ruleConfig) { + return new DetectDiscouragedClientVisitor(mockHolder, ruleConfig); + } + + private void setupMockElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck, String suggestionMessage) { + PsiType mockType = mock(PsiType.class); + + when(mockTypeElement.getType()).thenReturn(mockType); + when(mockType.getPresentableText()).thenReturn(clientToCheck); + } + + private static Stream provideTestCases() { + RuleConfig serviceBusReceiverAsyncClientConfig = mock(RuleConfig.class); + when(serviceBusReceiverAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("ServiceBusReceiverAsyncClient")); + when(serviceBusReceiverAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead."); + + RuleConfig eventHubConsumerAsyncClientConfig = mock(RuleConfig.class); + when(eventHubConsumerAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("EventHubConsumerAsyncClient")); + when(eventHubConsumerAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers."); + + return Stream.of( + new TestCase("ServiceBusReceiverAsyncClient", "Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead.", 1, serviceBusReceiverAsyncClientConfig), + new TestCase("EventHubConsumerAsyncClient", "Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers.", 1, eventHubConsumerAsyncClientConfig), + new TestCase("ServiceBusProcessorClient", "", 0, serviceBusReceiverAsyncClientConfig), + new TestCase("", "", 0, serviceBusReceiverAsyncClientConfig) + ); + } + + private static class TestCase { + String usagesToCheck; + String suggestionMessage; + int numOfInvocations; + RuleConfig ruleConfig; + + TestCase(String usagesToCheck, String suggestionMessage, int numOfInvocations, RuleConfig ruleConfig) { + this.usagesToCheck = usagesToCheck; + this.suggestionMessage = suggestionMessage; + this.numOfInvocations = numOfInvocations; + this.ruleConfig = ruleConfig; + } + } +} 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..bf7ba90784a --- /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,123 @@ +// 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.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +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 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.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 { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiDeclarationStatement mockDeclarationStatement; + + @Mock + private PsiMethodCallExpression initializer; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new DisableAutoCompleteVisitor(mockHolder); + mockDeclarationStatement = mock(PsiDeclarationStatement.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testDisableAutoCompleteCheck(TestCase testCase) { + setupMockMethodCall(testCase.packageName, testCase.clientName, testCase.numOfInvocations, + testCase.methodFound); + mockVisitor.visitDeclarationStatement(mockDeclarationStatement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(initializer), contains( + "Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion.")); + + } + + private void setupMockMethodCall(String packageName, String clientName, int numOfInvocations, + String methodFound) { + 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(clientName); + + when(initializer.getMethodExpression()).thenReturn(expression); + when(expression.getQualifierExpression()).thenReturn(qualifier); + + when(qualifier.getMethodExpression()).thenReturn(expression); + when(expression.getQualifierExpression()).thenReturn(finalExpression); + when(expression.getReferenceName()).thenReturn(methodFound); + + when(finalExpression.getMethodExpression()).thenReturn(expression); + + if (!"disableAutoComplete".equals(methodFound)) { + 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", "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 clientName; + int numOfInvocations; + String methodFound; + + TestCase(String packageName, String clientName, int numOfInvocations, String methodFound) { + this.packageName = packageName; + this.clientName = clientName; + 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..1eb6f4dd862 --- /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,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.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.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import com.intellij.psi.PsiType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiMethodCallExpression; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DynamicClientCreationCheck.DynamicClientCreationVisitor; +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; + +/*** + * Tests for {@link DynamicClientCreationCheck} + */ +public class DynamicClientCreationCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiForStatement mockElement; + + @Mock + private PsiMethodCallExpression mockMethodCallExpression; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new DynamicClientCreationVisitor(mockHolder); + mockElement = mock(PsiForStatement.class); + 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), + contains("Dynamic client creation detected. Create a single client instance and reuse it instead.")); + } else { + setupWithDeclarationStatement(testCase.methodName, testCase.packageName, testCase.numOfInvocations); + mockVisitor.visitForStatement(mockElement); + + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockMethodCallExpression), contains("Dynamic " + + "client creation detected. Create a single client instance and reuse it instead.")); + } + } + + 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/EndpointOnNonAzureOpenAIAuthCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.java new file mode 100644 index 00000000000..ecb76671597 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.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.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiJavaCodeReferenceElement; +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 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 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 EndpointOnNonAzureOpenAIAuthCheck class. + * The EndpointOnNonAzureOpenAIAuthCheck class is a LocalInspectionTool that checks if the endpoint method is used with KeyCredential for non-Azure OpenAI clients. + * If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is registered. + * An example that should be flagged is: + * OpenAI Client client = new OpenAIClientBuilder() + * .credential(new KeyCredential("key")) + * .endpoint("endpoint") + * .buildClient(); + */ +public class EndpointOnNonAzureOpenAIAuthCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private PsiMethodCallExpression mockMethodCall; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockMethodCall = mock(PsiMethodCallExpression.class); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testEndpointOnNonAzureOpenAIClient(TestCase testCase) { + + setupMockMethodExpression(testCase.numOfInvocation, testCase.endpoint, testCase.credential, + testCase.keyCredentialPackageName, testCase.azurePackageName); + EndpointOnNonAzureOpenAIAuthCheck.EndpointOnNonAzureOpenAIAuthVisitor visitor = new EndpointOnNonAzureOpenAIAuthCheck.EndpointOnNonAzureOpenAIAuthVisitor(mockHolder); + visitor.visitMethodCallExpression(mockMethodCall); + + verify(mockHolder, times(testCase.numOfInvocation)).registerProblem(mockMethodCall, "Endpoint API should not " + + "be used with KeyCredential for non-Azure OpenAI clients."); + } + + private static Stream testCases() { + return Stream.of( + new TestCase(1, "endpoint", "credential", "KeyCredential", "com.azure.ai.openai"), + new TestCase(0, "notEndpoint", "credential", "KeyCredential", "com.azure.ai.openai"), + new TestCase(0, "endpoint", "notCredential", "KeyCredential", "com.azure.ai.openai"), + new TestCase(0, "endpoint", "com.azure.core.credential.AzureKeyCredential", "com.azure.ai.openai", "com.azure.ai.openai") + ); + } + + private void setupMockMethodExpression(int numOfInvocation, String endpoint, String credential, String keyCredentialPackageName, String azurePackageName) { + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiMethodCallExpression qualifierOne = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpressionOne = mock(PsiReferenceExpression.class); + PsiNewExpression newExpression = mock(PsiNewExpression.class); + PsiExpressionList argumentList = mock(PsiExpressionList.class); + PsiExpression[] arguments = new PsiExpression[]{newExpression}; + PsiJavaCodeReferenceElement classReference = mock(PsiJavaCodeReferenceElement.class); + PsiVariable parent = mock(PsiVariable.class); + PsiType qualifierType = mock(PsiType.class); + + when(mockMethodCall.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.getReferenceName()).thenReturn(endpoint); + when(methodExpression.getQualifierExpression()).thenReturn(qualifierOne); + when(qualifierOne.getMethodExpression()).thenReturn(methodExpressionOne); + when(methodExpressionOne.getReferenceName()).thenReturn(credential); + when(qualifierOne.getArgumentList()).thenReturn(argumentList); + when(argumentList.getExpressions()).thenReturn(arguments); + when(newExpression.getClassReference()).thenReturn(classReference); + when(classReference.getReferenceName()).thenReturn(keyCredentialPackageName); + when(qualifierOne.getParent()).thenReturn(parent); + when(parent.getType()).thenReturn(qualifierType); + when(qualifierType.getCanonicalText()).thenReturn(azurePackageName); + + } + + private static class TestCase { + int numOfInvocation; + String endpoint; + String credential; + String keyCredentialPackageName; + String azurePackageName; + + TestCase(int numOfInvocation, String endpoint, String credential, String keyCredentialPackageName, String azurePackageName) { + this.numOfInvocation = numOfInvocation; + this.endpoint = endpoint; + this.credential = credential; + this.keyCredentialPackageName = keyCredentialPackageName; + this.azurePackageName = azurePackageName; + } + } +} 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..cfd0a76f4aa --- /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,91 @@ +// 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.PsiExpression; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import com.intellij.psi.util.PsiTreeUtil; +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 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 { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression mockElement; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder); + mockElement = mock(PsiMethodCallExpression.class); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testGetSyncPollerOnPollerFluxCheck(TestCase testCase) { + mockMethodExpression(testCase.methodName, testCase.className, testCase.numberOfInvocations); + mockVisitor.visitMethodCallExpression(mockElement); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(mockElement, "Use of getSyncPoller() on a " + + "PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks."); + } + + 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.azure.core.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); + PsiTreeUtil mockTreeUtil = mock(PsiTreeUtil.class); + + when(mockElement.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getReferenceName()).thenReturn(methodName); + when(referenceExpression.getQualifierExpression()).thenReturn(expression); + when(expression.getType()).thenReturn(type); + when(type.getCanonicalText()).thenReturn(className); + when(mockTreeUtil.getParentOfType(mockElement, PsiClass.class)).thenReturn(containingClass); + when(containingClass.getQualifiedName()).thenReturn(className); + } + + 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..ce7114baaec --- /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,124 @@ +// 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.PsiElement; +import com.intellij.psi.PsiElementVisitor; +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 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.Mockito; + +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 { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private PsiElementVisitor mockVisitor; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testHardcodedAPIKeysAndTokensCheck(TestCase testCase) { + PsiNewExpression mockExpression = mockMethodExpression(testCase.apiName, testCase.numOfInvocations); + mockVisitor.visitElement(mockExpression); + + verify(mockHolder, times(testCase.numOfInvocations)) + .registerProblem(eq(mockExpression), + Mockito.contains( + "DefaultAzureCredential is recommended for authentication if the service client supports Token Credential (Entra ID Authentication). If not, then use Azure Key Credential for API key based authentication.")); + } + + @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(RuleConfig.AZURE_PACKAGE_NAME); + when(newExpression.getChildren()).thenReturn(new PsiElement[]{literalExpression}); + when(literalExpression.getValue()).thenReturn(System.getenv()); + + mockVisitor.visitElement(newExpression); + + verify(mockHolder, times(0)).registerProblem(eq(newExpression), + Mockito.contains( + "DefaultAzureCredential is recommended for authentication if the service client supports Token Credential (Entra ID Authentication). If not, then use Azure Key Credential for API key based authentication.")); + } + + private static Stream testCases() { + return Stream.of( + new TestCase("AzureKeyCredential", 1), + new TestCase("AccessToken", 1), + new TestCase("KeyCredential", 1), + new TestCase("AzureNamedKeyCredential", 1), + new TestCase("AzureSasCredential", 1), + new TestCase("AzureNamedKey", 1), + new TestCase("ClientSecretCredentialBuilder", 1), + new TestCase("UsernamePasswordCredentialBuilder", 1), + new TestCase("BasicAuthenticationCredential", 1), + new TestCase("SomeOtherClient", 0), + new TestCase("", 0) + ); + } + + private PsiNewExpression mockMethodExpression(String authServiceToCheck, int numOfInvocations) { + PsiNewExpression newExpression = mock(PsiNewExpression.class); + PsiJavaCodeReferenceElement javaCodeReferenceElement = mock(PsiJavaCodeReferenceElement.class); + PsiLiteralExpression literalExpression = mock(PsiLiteralExpression.class); + + when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); + when(javaCodeReferenceElement.getReferenceName()).thenReturn(authServiceToCheck); + when(javaCodeReferenceElement.getQualifiedName()).thenReturn(RuleConfig.AZURE_PACKAGE_NAME); + when(newExpression.getChildren()).thenReturn(new PsiElement[]{literalExpression}); + when(literalExpression.getValue()).thenReturn("hardcoded-api-token"); + + return newExpression; + } + + static class TestCase { + String apiName; + int numOfInvocations; + + TestCase(String apiName, int numOfInvocations) { + this.apiName = apiName; + 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/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java new file mode 100644 index 00000000000..6c4da435f43 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java @@ -0,0 +1,210 @@ +// 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.PsiClass; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiPolyadicExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiVariable; +import com.intellij.psi.impl.source.tree.java.PsiMethodCallExpressionImpl; +import com.intellij.psi.util.PsiTreeUtil; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.KustoQueriesWithTimeIntervalInQueryStringCheck.KustoQueriesVisitor; +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 static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * This class tests the KustoQueriesWithTimeIntervalInQueryStringCheck class. + *

+ * These are some example queries that should be flagged: + *

+ */ +public class KustoQueriesWithTimeIntervalInQueryStringCheckTest { + + @Mock + private ProblemsHolder mockHolder; + @Mock + private PsiElementVisitor mockVisitor; + @Mock + private PsiMethodCallExpressionImpl methodCall; + @Mock + private PsiLocalVariable mockVariable; + + @Mock + private PsiPolyadicExpression mockPolyadicExpression; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new KustoQueriesWithTimeIntervalInQueryStringCheck.KustoQueriesVisitor(mockHolder); + mockVariable = mock(PsiLocalVariable.class); + mockPolyadicExpression = mock(PsiPolyadicExpression.class); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testKustoQueriesWithTimeIntervalInQueryStringCheck(TestCase testCase) { + setupWithLocalVariable(testCase.queryString, testCase.packageName, testCase.numOfInvocations); + // Visit the variable to store its name if it's a query string + mockVisitor.visitElement(mockVariable); + + // Visit the method call to check if the query variable is used and the method call is to an Azure client + mockVisitor.visitElement(methodCall); + + // Verify that the problem was registered correctly for the method call + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(methodCall), contains("KQL queries " + + "with time intervals in the query string detected.")); + + } + + @ParameterizedTest + @MethodSource("testCases") + public void testKustoQueriesWithTimeIntervalInQueryWithPolyadicExpression(TestCase testCase) { + setupWithPolyadicExpression(testCase.queryString, testCase.packageName, testCase.numOfInvocations); + // Visit the variable to store its name if it's a query string + mockVisitor.visitElement(mockPolyadicExpression); + + // Visit the method call to check if the query variable is used and the method call is to an Azure client + mockVisitor.visitElement(methodCall); + + // Verify that the problem was registered correctly for the method call + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(methodCall), contains("KQL queries " + + "with " + + "time intervals in the query string detected.")); + + } + + @Test + public void testCheckExpressionWithNullExpression() { + PsiExpression nullExpression = null; + PsiLocalVariable mockElement = mock(PsiLocalVariable.class); + ((KustoQueriesVisitor) mockVisitor).checkExpressionForPatterns(nullExpression, mockElement); + } + + private static Stream testCases() { + return Stream.of( + new TestCase("datetime(startDate)", "com.azure", 1), + new TestCase("filter.datetime(2022-01-01)", "com.azure", 1), + new TestCase("time.now()", "com.azure", 1), + new TestCase("time.startofday()", "com.azure", 1), + new TestCase("time.startofmonth()", "com.azure", 1), + new TestCase("range.between(datetime(2022-01-01), datetime(2022-02-01)", "com.azure", 1), + new TestCase("datetime(startDate)", "com.microsoft.azure", 0) + ); + } + + /** + * This method tests the registerProblem method with a local variable as the query string + */ + private void setupWithLocalVariable(String queryString, String packageName, int numOfInvocations) { + + mockVariable = mock(PsiLocalVariable.class); + PsiLiteralExpression initializer = mock(PsiLiteralExpression.class); + PsiLocalVariable parentElement = mock(PsiLocalVariable.class); + PsiClass containingClass = mock(PsiClass.class); + + methodCall = mock(PsiMethodCallExpressionImpl.class); + PsiExpressionList argumentList = mock(PsiExpressionList.class); + PsiReferenceExpression argument = mock(PsiReferenceExpression.class); + PsiExpression[] arguments = new PsiExpression[]{argument}; + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiVariable resolvedElement = mock(PsiVariable.class); + PsiReferenceExpression qualifierExpression = mock(PsiReferenceExpression.class); + + // stubs for handle local variable method + when(mockVariable.getInitializer()).thenReturn(initializer); + when(mockVariable.getName()).thenReturn("stringQuery"); + + // stubs for checkExpression method + when(initializer.getText()).thenReturn(queryString); + when(mockVariable.getParent()).thenReturn(parentElement); + when(parentElement.getName()).thenReturn("stringQuery"); + + // stubs for handleMethodCall method + when(methodCall.getArgumentList()).thenReturn(argumentList); + when(argumentList.getExpressions()).thenReturn(arguments); + when(argument.resolve()).thenReturn(resolvedElement); + when(resolvedElement.getName()).thenReturn("stringQuery"); + + // stubs for isAzureClient method + when(methodCall.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(PsiTreeUtil.getParentOfType(methodCall, PsiClass.class)).thenReturn(containingClass); + when(containingClass.getQualifiedName()).thenReturn(packageName); + + } + + void setupWithPolyadicExpression(String queryString, String packageName, int numOfInvocations) { + + mockPolyadicExpression = mock(PsiPolyadicExpression.class); + PsiExpression initializer = mock(PsiExpression.class); + PsiLocalVariable parentElement = mock(PsiLocalVariable.class); + PsiClass containingClass = mock(PsiClass.class); + + // stubs for handlePolyadicExpression method + when(mockPolyadicExpression.getText()).thenReturn(queryString); + + // stubs for checkExpression method + when(initializer.getText()).thenReturn(queryString); + when(mockPolyadicExpression.getParent()).thenReturn(parentElement); + when(parentElement.getName()).thenReturn("stringQuery"); + + methodCall = mock(PsiMethodCallExpressionImpl.class); + PsiExpressionList argumentList = mock(PsiExpressionList.class); + PsiReferenceExpression argument = mock(PsiReferenceExpression.class); + PsiExpression[] arguments = new PsiExpression[]{argument}; + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiVariable resolvedElement = mock(PsiVariable.class); + PsiReferenceExpression qualifierExpression = mock(PsiReferenceExpression.class); + + // stubs for handleMethodCall method + when(methodCall.getArgumentList()).thenReturn(argumentList); + when(argumentList.getExpressions()).thenReturn(arguments); + when(argument.resolve()).thenReturn(resolvedElement); + when(resolvedElement.getName()).thenReturn("stringQuery"); + + // stubs for isAzureClient method + when(methodCall.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(PsiTreeUtil.getParentOfType(methodCall, PsiClass.class)).thenReturn(containingClass); + when(containingClass.getQualifiedName()).thenReturn(packageName); + + } + + private static class TestCase { + String queryString; + String packageName; + int numOfInvocations; + + TestCase(String queryString, String packageName, int numOfInvocations) { + this.queryString = queryString; + 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/ServiceBusReceiveModeCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheckTest.java new file mode 100644 index 00000000000..c91802f0ed7 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheckTest.java @@ -0,0 +1,153 @@ +// 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.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +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.PsiVariable; +import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +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.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; + +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.ServiceBusReceiveModeCheck.ServiceBusReceiveModeVisitor; + +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 contains the tests for the ServiceBusReceiveModeCheck class. It tests the visitDeclarationStatement method + * of the ServiceBusReceiveModeVisitor class. It tests the method with different combinations of the receiveMode and + * prefetchCount methods. + */ +public class ServiceBusReceiveModeCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiDeclarationStatement mockDeclarationStatement; + + @Mock + private PsiElement prefetchCountMethod; + + @BeforeEach + public void setUp() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new ServiceBusReceiveModeCheck.ServiceBusReceiveModeVisitor(mockHolder, true); + mockDeclarationStatement = mock(PsiDeclarationStatement.class); + prefetchCountMethod = mock(PsiElement.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testServiceBusReceiveModeCheck(TestCase testCase) { + setupPrefetchMethod(testCase.clientName, testCase.methodFoundOne, testCase.methodFoundTwo, + testCase.numOfInvocations, + testCase.prefetchCountValue); + mockVisitor.visitDeclarationStatement(mockDeclarationStatement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(prefetchCountMethod), eq( + "A high prefetch value in PEEK_LOCK detected. We recommend a prefetch value of 0 or 1 for efficient message retrieval.")); + } + + private void setupPrefetchMethod(String clientName, String methodFoundOne, String methodFoundTwo, + int numOfInvocations, String prefetchCountValue) { + PsiVariable declaredElement = mock(PsiVariable.class); + PsiElement[] declaredElements = new PsiElement[] {declaredElement}; + + PsiType clientType = mock(PsiType.class); + PsiMethodCallExpression initializer = mock(PsiMethodCallExpression.class); + + PsiReferenceExpression expressionOne = mock(PsiReferenceExpression.class); + PsiMethodCallExpression qualifierOne = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpressionOne = mock(PsiReferenceExpression.class); + + PsiExpression receiveModePeekLockParameter = mock(PsiExpression.class); + PsiReferenceExpression prefetchCountParameter = mock(PsiReferenceExpression.class); + PsiExpressionList methodArgumentList = mock(PsiExpressionList.class); + PsiExpression[] methodArguments = new PsiExpression[] {prefetchCountParameter, receiveModePeekLockParameter}; + + PsiMethodCallExpression qualifierTwo = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpressionTwo = mock(PsiReferenceExpression.class); + + prefetchCountMethod = mock(PsiElement.class); + + when(mockDeclarationStatement.getDeclaredElements()).thenReturn(declaredElements); + + when(declaredElement.getType()).thenReturn(clientType); + when(declaredElement.getInitializer()).thenReturn(initializer); + when(clientType.getCanonicalText()).thenReturn(RuleConfig.AZURE_PACKAGE_NAME); + when(clientType.getPresentableText()).thenReturn(clientName); + + when(initializer.getMethodExpression()).thenReturn(expressionOne); + when(expressionOne.getQualifierExpression()).thenReturn(qualifierOne); + when(qualifierOne.getMethodExpression()).thenReturn(methodExpressionOne); + when(methodExpressionOne.getReferenceName()).thenReturn(methodFoundOne); + + when(qualifierOne.getArgumentList()).thenReturn(methodArgumentList); + when(methodArgumentList.getExpressions()).thenReturn(methodArguments); + when(receiveModePeekLockParameter.getText()).thenReturn("PEEK_LOCK"); + + when(methodExpressionOne.getQualifierExpression()).thenReturn(qualifierTwo); + when(qualifierTwo.getMethodExpression()).thenReturn(methodExpressionTwo); + when(methodExpressionTwo.getReferenceName()).thenReturn(methodFoundTwo); + + when(qualifierTwo.getArgumentList()).thenReturn(methodArgumentList); + when(methodArgumentList.getExpressions()).thenReturn(methodArguments); + when(prefetchCountParameter.getText()).thenReturn(prefetchCountValue); + + when(methodExpressionTwo.getReferenceNameElement()).thenReturn(prefetchCountMethod); + + if (!"receiveMode".equals(methodFoundOne) || !"prefetchCount".equals(methodFoundTwo)) { + when(methodExpressionTwo.getQualifierExpression()).thenReturn(null); + } + + } + + private static Stream provideTestCases() { + return Stream.of( + new TestCase("ServiceBusReceiverClient", "receiveMode", "prefetchCount", 1, "100"), + new TestCase("ServiceBusReceiverClient", "receiveMode", "prefetchCount", 0, "1"), + new TestCase("ServiceBusReceiverClient", "notreceiveMode", "prefetchCount", 0, "100"), + new TestCase("ServiceBusReceiverClient", "receiveMode", "noprefetchCount", 0, "100"), + new TestCase("servicebus", "notreceiveMode", "noprefetchCount", 0, "100"), + new TestCase("notservicebus", "notreceiveMode", "noprefetchCount", 0, "100") + ); + } + + static class TestCase { + String clientName; + String methodFoundOne; + String methodFoundTwo; + int numOfInvocations; + String prefetchCountValue; + + TestCase(String clientName, String methodFoundOne, String methodFoundTwo, int numOfInvocations, + String prefetchCountValue) { + this.clientName = clientName; + this.methodFoundOne = methodFoundOne; + this.methodFoundTwo = methodFoundTwo; + this.numOfInvocations = numOfInvocations; + this.prefetchCountValue = prefetchCountValue; + } + } +} \ 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/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..6e8f4128f7c --- /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,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +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.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.PsiVariable; +import com.intellij.psi.PsiWhileStatement; +import com.intellij.psi.util.PsiTreeUtil; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.SingleOperationInLoopCheck.SingleOperationInLoopVisitor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElement; + +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.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.Mockito; + +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 { + + @Mock + private ProblemsHolder mockHolder; + private JavaElementVisitor mockVisitor; + private PsiMethodCallExpression initializer; + private PsiMethodCallExpression expression; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + initializer = mock(PsiMethodCallExpression.class); + expression = mock(PsiMethodCallExpression.class); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testSingleOperationInLoop(TestCase testCase) { + if (testCase.isDeclaration) { + setupWithSinglePsiDeclarationStatement(testCase.loopStatement, testCase.packageName, + testCase.numberOfInvocations, testCase.methodName); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(initializer), + Mockito.contains( + "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + + testCase.methodName + "Batch")); + } else { + setupWithSinglePsiExpressionStatement(testCase.loopStatement, testCase.packageName, + testCase.numberOfInvocations, testCase.methodName); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(expression), + Mockito.contains( + "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + + testCase.methodName + "Batch")); + } + } + + private JavaElementVisitor createVisitor() { + boolean isOnTheFly = true; + SingleOperationInLoopVisitor visitor = new SingleOperationInLoopVisitor(mockHolder, isOnTheFly); + return visitor; + } + + private void setupWithSinglePsiExpressionStatement(PsiStatement loopStatement, String packageName, + int numberOfInvocations, String methodName) { + PsiBlockStatement loopBody = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + expression = mock(PsiMethodCallExpression.class); + PsiTreeUtil treeUtil = mock(PsiTreeUtil.class); + PsiClass containingClass = mock(PsiClass.class); + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiExpressionStatement mockStatement = mock(PsiExpressionStatement.class); + PsiStatement[] statements = new PsiStatement[] {mockStatement}; + + when(mockStatement.getExpression()).thenReturn(expression); + when(loopBody.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(statements); + when(treeUtil.getParentOfType(expression, PsiClass.class)).thenReturn(containingClass); + when(containingClass.getQualifiedName()).thenReturn(packageName); + when(expression.getMethodExpression()).thenReturn(referenceExpression); + 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}; + initializer = mock(PsiMethodCallExpression.class); + PsiTreeUtil treeUtil = mock(PsiTreeUtil.class); + PsiClass containingClass = mock(PsiClass.class); + PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); + PsiDeclarationStatement mockStatement = mock(PsiDeclarationStatement.class); + PsiStatement[] statements = new PsiStatement[] {mockStatement}; + + when(mockStatement.getDeclaredElements()).thenReturn(elements); + when(loopBody.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(statements); + when(element.getInitializer()).thenReturn(initializer); + when(treeUtil.getParentOfType(initializer, PsiClass.class)).thenReturn(containingClass); + when(containingClass.getQualifiedName()).thenReturn(packageName); + when(initializer.getMethodExpression()).thenReturn(referenceExpression); + 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..ca7edb24325 --- /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,115 @@ +// 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 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 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 { + + @Mock + private ProblemsHolder mockHolder; + @Mock + private JavaRecursiveElementWalkingVisitor mockVisitor; + @Mock + private PsiMethodCallExpression mockExpression; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = new StorageUploadWithoutLengthCheck.StorageUploadVisitor(mockHolder); + mockExpression = mock(PsiMethodCallExpression.class); + } + + @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("Azure " + + "Storage upload API without length parameter detected")); + } + + 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/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..4430ef05a9b --- /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,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; + +import com.intellij.psi.PsiClassType; +import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; + +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 java.util.stream.Stream; + +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 { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression mockElement; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockElement = mock(PsiMethodCallExpression.class); + } + + @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(mockElement), + Mockito.contains( + "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead.")); + } + + private JavaElementVisitor createVisitor() { + return new UseOfBlockOnAsyncClientsVisitor(mockHolder); + } + + 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); + + } + + 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 From 18d4b1e05c539bcb4bbd6345c901e7f5b1933dad Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Sun, 5 Jan 2025 20:45:20 -0800 Subject: [PATCH 2/7] adding the resources directory --- .../azure-intellij-plugin-java-sdk.xml | 173 +++++++++++++++++- .../main/resources/META-INF/ruleConfigs.json | 143 +++++++++++++++ 2 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json 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..94fde0d3f3d 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,177 @@ 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/META-INF/ruleConfigs.json b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json new file mode 100644 index 00000000000..a93eed55597 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json @@ -0,0 +1,143 @@ +{ + "StorageUploadWithoutLengthCheck": { + "usages": [ + "upload", + "uploadWithResponse" + ], + "scope": "com.azure.storage.", + "antiPatternMessage": "Azure Storage upload API without length parameter detected. Use upload API with length parameter instead." + }, + "DisableAutoCompleteCheck": { + "usages": "disableAutoComplete", + "scope": [ + "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient" + ], + "antiPatternMessage": "Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion." + }, + "DynamicClientCreationCheck": { + "usages": [ + "buildClient", + "buildAsyncClient" + ], + "antiPatternMessage": "Dynamic client creation detected. Create a single client instance and reuse it instead." + }, + "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 Azure Key Credential for API key based authentication." + }, + "GetSyncPollerOnPollerFluxCheck": { + "usages": "getSyncPoller", + "antiPatternMessage": "Use of getSyncPoller() on a PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks." + }, + "ServiceBusReceiveModeCheck": { + "scope": [ + "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient" + ], + "usages": [ + "receiveMode", + "prefetchCount" + ], + "antiPatternMessage": "A high prefetch value in PEEK_LOCK detected. We recommend a prefetch value of 0 or 1 for efficient message retrieval." + }, + "DetectDiscouragedAPIUsageCheck": { + "hasDerivedRules": true, + "ConnectionStringCheck": { + "usages": "connectionString", + "antiPatternMessage": "Connection String detected. Use 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 Azure Key Credential / Connection Strings based authentication" + }, + "GetCompletionsCheck": { + "usages": "getCompletions", + "scope": "com.azure.ai.openai", + "antiPatternMessage": "getCompletions API detected. Use the getChatCompletions API instead." + } + }, + "DetectDiscouragedClientCheck": { + "hasDerivedRules": true, + "ServiceBusReceiverAsyncClientCheck": { + "usages": "ServiceBusReceiverAsyncClient", + "antiPatternMessage": "Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead." + }, + "EventHubConsumerAsyncClientCheck": { + "usages": "EventHubConsumerAsyncClient", + "antiPatternMessage": "Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers." + } + }, + "SingleOperationInLoopCheck": { + "usages": [ + "detectLanguageBatch", + "recognizeEntitiesBatch", + "recognizePiiEntitiesBatch", + "recognizeLinkedEntitiesBatch", + "extractKeyPhrasesBatch", + "analyzeSentimentBatch" + ], + "scope": "com.azure.ai.textanalytics", + "antiPatternMessage": "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + }, + "AsyncSubscribeChecker": { + "hasDerivedRules": true, + "UpdateCheckpointAsyncSubscribeChecker": { + "usages": "updateCheckpointAsync", + "scope": "EventBatchContext", + "antiPatternMessage": "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`" + } + }, + "KustoQueriesWithTimeIntervalInQueryStringCheck": { + "regexPatterns": { + "KQL_ANTI_PATTERN_AGO": ".*ago\\(", + "KQL_ANTI_PATTERN_DATETIME": ".*datetime\\s*\\(", + "KQL_ANTI_PATTERN_NOW": ".*now\\(", + "KQL_ANTI_PATTERN_START_OF_PERIOD": ".*startofday\\(\\)|.*startofmonth\\(\\)|.*startofyear\\(\\)", + "KQL_ANTI_PATTERN_BETWEEN": ".*between\\(datetime\\(" + }, + "antiPatternMessage": "KQL queries with time intervals in the query string detected.", + "solution": "Use the QueryTimeInterval parameter in the client method parameters to specify the time interval for the query" + }, + "EndpointOnNonAzureOpenAIAuthCheck": { + "usages": [ + "endpoint", + "credential" + ], + "scope": "KeyCredential", + "antiPatternMessage": "Endpoint API should not be used with KeyCredential for non-Azure OpenAI clients." + }, + "UseOfBlockOnAsyncClientsCheck": { + "usages": [ + "block", + "blockOptional", + "blockFirst", + "blockLast", + "toIterable", + "toStream", + "toFuture", + "blockFirstOptional", + "blockLastOptional" + ], + "scope": [ + "reactor.core.publisher.Flux", + "reactor.core.publisher.Mono" + ], + "antiPatternMessage": "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead." + }, + "UpgradeLibraryVersionCheck": { + "antiPatternMessage": "A newer stable minor version of '{{fullName}}' is available. We recommend you update to version {{recommendedVersion}}.x." + }, + "IncompatibleDependencyCheck": { + "antiPatternMessage": "The version of '{{fullName}}' is incompatible with other dependencies of the same library defined in the pom.xml. We recommend you update to version {{recommendedVersion}}.x of the same library release group." + } +} \ No newline at end of file From da3dde5fe505c269f66ddf679c2b004b46ae1389 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Sun, 5 Jan 2025 23:09:33 -0800 Subject: [PATCH 3/7] add changes for MavenUtil --- .../intellij/java/sdk/utils/MavenUtils.java | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) 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..69a4236f545 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,10 @@ package com.microsoft.azure.toolkit.intellij.java.sdk.utils; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.util.PsiTreeUtil; 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 com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -18,6 +12,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 { @@ -101,4 +104,24 @@ public static String getLatestArtifactVersion(String groupId, String artifactId) return mavenArtifactDetails.getVersion(); } } + + /** + * This method checks if the method call is an Azure client method call + * by checking the containing class of the method call + * + * @param methodCall the method call to check + * @return boolean true if the method call is an Azure client method call, false otherwise + */ + public static boolean isAzureClientMethodCall(PsiMethodCallExpression methodCall) { + + // Get the containing class of the method call + PsiClass containingClass = PsiTreeUtil.getParentOfType(methodCall, PsiClass.class); + + if (containingClass != null) { + String className = containingClass.getQualifiedName(); + // Check if the class name belongs to the com.azure namespace or any specific Azure SDK namespace + return className != null && className.startsWith(RuleConfig.AZURE_PACKAGE_NAME); + } + return false; + } } From 5bef2d9ea007c5f323e41cb0d1765eb6dba50ada Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Wed, 29 Jan 2025 22:38:43 -0800 Subject: [PATCH 4/7] apply feedback changes --- .../azure-intellij-plugin-java-sdk/readme.md | 90 +++--- .../analyzer/AbstractLibraryVersionCheck.java | 85 ----- .../sdk/analyzer/AsyncSubscribeChecker.java | 92 ------ .../sdk/analyzer/ConnectionStringCheck.java | 49 ++- .../DetectDiscouragedAPIUsageCheck.java | 105 ------- .../DetectDiscouragedClientCheck.java | 88 ------ .../analyzer/DisableAutoCompleteCheck.java | 39 ++- .../analyzer/DynamicClientCreationCheck.java | 3 +- .../EndpointOnNonAzureOpenAIAuthCheck.java | 169 ---------- .../EventHubConsumerAsyncClientCheck.java | 43 +++ .../sdk/analyzer/GetCompletionsCheck.java | 56 +++- .../GetSyncPollerOnPollerFluxCheck.java | 41 +-- .../HardcodedAPIKeysAndTokensCheck.java | 138 ++++++-- .../analyzer/IncompatibleDependencyCheck.java | 207 ------------ ...iesWithTimeIntervalInQueryStringCheck.java | 201 ------------ .../analyzer/ServiceBusReceiveModeCheck.java | 208 ------------ .../ServiceBusReceiverAsyncClientCheck.java | 44 ++- .../analyzer/SingleOperationInLoopCheck.java | 295 ------------------ ...ngleOperationInLoopTextAnalyticsCheck.java | 183 +++++++++++ .../StorageUploadWithoutLengthCheck.java | 203 ++++++------ ... UpdateCheckpointAsyncSubscribeCheck.java} | 48 +-- .../analyzer/UpgradeLibraryVersionCheck.java | 105 ------- .../UseOfBlockOnAsyncClientsCheck.java | 24 +- .../models/DependencyVersionsDataCache.java | 190 ----------- .../intellij/java/sdk/models/RuleConfig.java | 22 +- .../utils/DependencyVersionFileFetcher.java | 184 ----------- .../intellij/java/sdk/utils/HelperUtils.java | 205 ++++++++++++ .../intellij/java/sdk/utils/MavenUtils.java | 26 +- .../java/sdk/utils/RuleConfigLoader.java | 174 ++--------- .../intellij/java/sdk/utils/VersionUtils.java | 90 ------ .../azure-intellij-plugin-java-sdk.xml | 140 +++------ .../main/resources/META-INF/ruleConfigs.json | 124 +++----- .../AbstractLibraryVersionCheckTest.java | 174 ----------- .../analyzer/AsyncSubscribeCheckerTest.java | 91 ------ .../DetectDiscouragedAPIUsageCheckTest.java | 47 ++- .../DetectDiscouragedClientCheckTest.java | 53 ++-- .../DisableAutoCompleteCheckTest.java | 1 - ...EndpointOnNonAzureOpenAIAuthCheckTest.java | 112 ------- .../GetSyncPollerOnPollerFluxCheckTest.java | 27 +- .../HardcodedAPIKeysAndTokensCheckTest.java | 60 ++-- ...ithTimeIntervalInQueryStringCheckTest.java | 210 ------------- .../ServiceBusReceiveModeCheckTest.java | 153 --------- .../SingleOperationInLoopCheckTest.java | 46 ++- .../UseOfBlockOnAsyncClientsCheckTest.java | 64 ++-- 44 files changed, 1211 insertions(+), 3498 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeChecker.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopTextAnalyticsCheck.java rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/{UpdateCheckpointAsyncSubscribeChecker.java => UpdateCheckpointAsyncSubscribeCheck.java} (63%) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/HelperUtils.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeCheckerTest.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheckTest.java 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 index f815832cd8d..8befbe7390f 100644 --- 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 @@ -8,67 +8,87 @@ The **Azure Toolkit for IntelliJ** is a project designed to empower Java develop ## **Integrated Rule Sets** -### **1. 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/en-us/azure/storage/blobs/storage-blob-upload-java) for details. +### **Messaging** -### **2. Prefer ServiceBusProcessorClient over ServiceBusReceiverAsyncClient** +#### **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 here](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/servicebus/azure-messaging-servicebus/README.md#when-to-use-servicebusprocessorclient). + [Learn more](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/servicebus/azure-messaging-servicebus/README.md#when-to-use-servicebusprocessorclient). -### **3. Explicitly Disable Auto-Complete in ServiceBus Clients** +#### **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/en-us/java/api/com.azure.messaging.servicebus.servicebusclientbuilder.servicebusreceiverclientbuilder-disableautocomplete) for guidance. +- **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. -### **4. 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. +#### **3. Optimize Receive Mode and Prefetch Value** +- **Anti-pattern**: Using `PEEK_LOCK` with a high prefetch value. +- **Issue**: Can lead to performance bottlenecks and message lock expirations. - **Severity**: WARNING -- **Recommendation**: Reuse client instances throughout the application's lifecycle. - [Detailed guidance here](https://learn.microsoft.com/en-us/azure/developer/java/sdk/overview#connect-to-and-use-azure-resources-with-client-libraries). +- **Recommendation**: Balance the prefetch value for efficient and concurrent processing. + [Learn more](https://learn.microsoft.com/azure/service-bus-messaging/service-bus-prefetch?tabs=dotnet#why-is-prefetch-not-the-default-option). -### **5. Avoid Hardcoded API Keys and Tokens** +#### **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**: Use `DefaultAzureCredential` or `AzureKeyCredential` for authentication. - [Learn more](https://learn.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential?view=azure-java-stable). +- **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). + +--- -### **6. Use SyncPoller Instead of PollerFlux#getSyncPoller()** +### **Async** + +#### **6. Use SyncPoller Instead of PollerFlux#getSyncPoller()** - **Anti-pattern**: Converting asynchronous polling to synchronous with `getSyncPoller()`. - **Issue**: Adds unnecessary complexity. - **Severity**: WARNING - **Recommendation**: Use `SyncPoller` directly for synchronous operations. - [Documentation link](https://learn.microsoft.com/java/api/com.azure.core.util.polling.syncpoller?view=azure-java-stable). + [Learn more](https://learn.microsoft.com/java/api/com.azure.core.util.polling.syncpoller?view=azure-java-stable). -### **7. Optimize Receive Mode and Prefetch Value** -- **Anti-pattern**: Using `PEEK_LOCK` with a high prefetch value. -- **Issue**: Can lead to performance bottlenecks and message lock expirations. -- **Severity**: WARNING -- **Recommendation**: Balance the prefetch value for efficient and concurrent processing. - [Learn more](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-prefetch?tabs=dotnet#why-is-prefetch-not-the-default-option). +--- + +### **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. -### **8. Recommended Alternatives for Common APIs** +--- -#### a. **Authentication**: Use `DefaultAzureCredential` over connection strings. -#### b. **Azure OpenAI**: Prefer `getChatCompletions` for conversational AI instead of `getCompletions`. -[More information here](https://learn.microsoft.com/java/api/overview/azure/ai-openai-readme?view=azure-java-preview). +### **Performance Optimization** -### **9. Batch Operations Instead of Single Operations in Loops** +#### **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. 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. +--- + +#### **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/AbstractLibraryVersionCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java deleted file mode 100644 index 0f7aee68948..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheck.java +++ /dev/null @@ -1,85 +0,0 @@ -// 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.lang.StdLanguages; -import com.intellij.psi.PsiElement; -import com.intellij.psi.xml.XmlFile; -import com.intellij.psi.xml.XmlTag; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import java.io.IOException; -import org.jetbrains.idea.maven.project.MavenProjectsManager; - -/** - * Abstract class for the library version check inspection. The UpgradeLibraryVersionCheck and - * IncompatibleDependencyCheck classes extend this class. - *

- * The UpgradeLibraryVersionCheck class checks the version of the libraries in the pom.xml file against the recommended - * version. The IncompatibleDependencyCheck class checks the version of the libraries in the pom.xml file against - * compatible versions. - */ -public abstract class AbstractLibraryVersionCheck extends LocalInspectionTool { - - /** - * Method to check the pom.xml file for the libraries and their versions. - * - * @param file The pom.xml file to check for the libraries and their versions - * @param holder The holder for the problems found in the file - * - * @throws IOException If an error occurs while reading the file - */ - protected void checkPomXml(XmlFile file, ProblemsHolder holder) throws IOException { - MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstance(file.getProject()); - if (!mavenProjectsManager.isMavenizedProject()) { - return; - } - - XmlFile xmlFile = (XmlFile) file.getViewProvider().getPsi(StdLanguages.XML); - XmlTag rootTag = xmlFile.getRootTag(); - - if (rootTag != null && "project".equals(rootTag.getName())) { - for (XmlTag dependenciesTag : rootTag.findSubTags("dependencies")) { - for (XmlTag dependencyTag : dependenciesTag.findSubTags("dependency")) { - XmlTag groupIdTag = dependencyTag.findFirstSubTag("groupId"); - XmlTag artifactIdTag = dependencyTag.findFirstSubTag("artifactId"); - XmlTag versionTag = dependencyTag.findFirstSubTag("version"); - - if (groupIdTag != null && artifactIdTag != null && versionTag != null) { - String fullName = groupIdTag.getValue().getText() + ":" + artifactIdTag.getValue().getText(); - String currentVersion = versionTag.getValue().getText(); - this.checkAndFlagVersion(fullName, currentVersion, holder, versionTag); - } - } - } - } - } - - /** - * Method to get the formatted message for the anti-pattern. - * - * @param fullName The full name of the library eg "com.azure:azure-core" - * @param recommendedVersion The recommended version of the library eg "1.0" - * @param RULE_CONFIG The rule configuration object - * - * @return The formatted message for the anti-pattern with the full name and recommended version - */ - protected static String getFormattedMessage(String fullName, String recommendedVersion, RuleConfig RULE_CONFIG) { - return RULE_CONFIG.getAntiPatternMessage() - .replace("{{fullName}}", fullName) - .replace("{{recommendedVersion}}", recommendedVersion); - } - - /** - * Abstract method to check the version of the library and flag it if necessary. - * - * @param fullName The full name of the library eg "com.azure:azure-core" - * @param currentVersion The current version of the library eg "1.0" - * @param holder The holder for the problems found - * @param versionElement The element for the version of the library - */ - protected abstract void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, - PsiElement versionElement) throws IOException; -} \ 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/AsyncSubscribeChecker.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeChecker.java deleted file mode 100644 index 994ec3ede3a..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeChecker.java +++ /dev/null @@ -1,92 +0,0 @@ -// 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.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.PsiParameter; -import com.intellij.psi.PsiReferenceExpression; -import com.intellij.psi.PsiType; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import java.util.List; - -/** - * Abstract base class for checking the usage of the following method to be ''subscribe'' in provided context. - */ -public abstract class AsyncSubscribeChecker extends LocalInspectionTool { - /** - * 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. - */ - protected static boolean isFollowingMethodSubscribe(PsiMethodCallExpression expression) { - // Check if the parent element is a method call expression - if (!(expression.getParent() instanceof PsiReferenceExpression reference)) { - return false; - } - - // Check if the grandparent element is a method call expression - PsiElement grandParent = reference.getParent(); - if (!(grandParent instanceof PsiMethodCallExpression parentCall)) { - return false; - } - - // Check if the parent method call's name is "subscribe" - return "subscribe".equals(parentCall.getMethodExpression().getReferenceName()); - } - - /** - * This method checks if the method call is made on an object from the provided Azure SDK context. - * - * @param expression The method call expression to analyze. - * @param scopeToCheck The list of class contexts to check. - * - * @return True if the method call is made on an object in the provided Azure SDK context, false otherwise. - */ - protected static boolean isCalledInProvidedContext(PsiMethodCallExpression expression, List scopeToCheck) { - // Get the qualifier expression from the method call expression - PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); - if (!(qualifier instanceof PsiReferenceExpression)) { - return false; - } - - // Resolve the qualifier element - PsiElement resolvedElement = ((PsiReferenceExpression) qualifier).resolve(); - if (!(resolvedElement instanceof PsiParameter)) { - return false; - } - - // Check the parameter type - PsiParameter parameter = (PsiParameter) resolvedElement; - PsiType parameterType = parameter.getType(); - - // Verify if the parameter type is within the provided context - if (!(parameterType instanceof PsiClassType)) { - return false; - } - - PsiClassType classType = (PsiClassType) parameterType; - PsiClass psiClass = classType.resolve(); - if (psiClass == null) { - return false; - } - - String qualifiedName = psiClass.getQualifiedName(); - if (qualifiedName == null) { - return false; - } - - // Check if the qualified name matches the Azure SDK context - boolean isInProvidedScope = scopeToCheck.contains(parameterType.getCanonicalText()); - boolean isAzureSDKContext = qualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME); - - return isInProvidedScope && isAzureSDKContext; - } -} 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 index bbc840120ac..2df75aaf6b9 100644 --- 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 @@ -3,16 +3,59 @@ 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 org.jetbrains.annotations.NotNull; /** * Inspection tool to check discouraged Connection String usage. */ -public class ConnectionStringCheck extends DetectDiscouragedAPIUsageCheck { +public class ConnectionStringCheck extends LocalInspectionTool { @Override - protected RuleConfig getRuleConfig() { - return RuleConfigLoader.getInstance().getRuleConfig("ConnectionStringCheck"); + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new ConnectionStringCheckVisitor(holder); + } + + static class ConnectionStringCheckVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + ConnectionStringCheckVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + static { + RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = ruleConfigLoader.getRuleConfig("ConnectionStringCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + @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/DetectDiscouragedAPIUsageCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheck.java deleted file mode 100644 index 0e1f2d8a13a..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheck.java +++ /dev/null @@ -1,105 +0,0 @@ -// 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.PsiElement; -import com.intellij.psi.PsiElementVisitor; -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 java.util.HashSet; -import java.util.Set; -import org.jetbrains.annotations.NotNull; - -/** - * Abstract base class to check for the use of discouraged APIs in the code. - */ -public abstract class DetectDiscouragedAPIUsageCheck extends LocalInspectionTool { - - /** - * Provides the specific RuleConfig for the subclass context. - * - * @return RuleConfig for the subclass - */ - protected abstract RuleConfig getRuleConfig(); - - @NotNull - @Override - public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - RuleConfig ruleConfig = getRuleConfig(); - - if (ruleConfig.skipRuleCheck()) { - return PsiElementVisitor.EMPTY_VISITOR; - } - - return new DetectDiscouragedAPIUsageVisitor(holder, ruleConfig); - } - - static class DetectDiscouragedAPIUsageVisitor extends JavaElementVisitor { - private final ProblemsHolder holder; - private final RuleConfig ruleConfig; - - DetectDiscouragedAPIUsageVisitor(ProblemsHolder holder, RuleConfig ruleConfig) { - this.holder = holder; - this.ruleConfig = ruleConfig; - } - - @Override - public void visitElement(@NotNull PsiElement element) { - super.visitElement(element); - - // Ensure the element is a method call - if (!(element instanceof PsiMethodCallExpression methodCallExpression)) { - return; - } - - // Resolve the method being called - PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); - PsiElement resolvedMethod = methodExpression.resolve(); - if (!(resolvedMethod instanceof PsiMethod method)) { - return; - } - - // Get the containing class of the method - PsiClass containingClass = method.getContainingClass(); - if (containingClass == null) { - return; - } - - // Get qualified name of the containing class - String classQualifiedName = containingClass.getQualifiedName(); - if (classQualifiedName == null) { - return; - } - - // Check if the method is a discouraged API - String methodName = method.getName(); - if (!ruleConfig.getUsagesToCheck().contains(methodName)) { - return; - } - - // Verify package name and scope - if (!classQualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME)) { - return; - } - - Set scopesToCheck = new HashSet<>(ruleConfig.getScopeToCheck()); - if (scopesToCheck.stream().noneMatch(classQualifiedName::startsWith)) { - return; - } - - // Register a problem for the discouraged API usage - PsiElement problemElement = methodExpression.getReferenceNameElement(); - if (problemElement != null) { - holder.registerProblem(problemElement, ruleConfig.getAntiPatternMessage()); - } - } - } -} - diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java deleted file mode 100644 index 94f6b2911c3..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheck.java +++ /dev/null @@ -1,88 +0,0 @@ -// 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.PsiType; -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 org.jetbrains.annotations.NotNull; - -/** - * This class extends the LocalInspectionTool to check for the use of discouraged clients in the code and suggests using - * other clients instead. The client data is loaded from the configuration file and the client name is checked against - * the discouraged client name. If the client name matches, a problem is registered with the suggestion message. - */ -public abstract class DetectDiscouragedClientCheck extends LocalInspectionTool { - /** - * Provides the specific RuleConfig for the subclass context. - * - * @return RuleConfig for the subclass - */ - protected abstract RuleConfig getRuleConfig(); - - /** - * This method builds a visitor to check for the discouraged client name in the code. If the client name matches the - * discouraged client, a problem is registered with the suggestion message. - * - * @param holder - the ProblemsHolder object to register the problem - * @param isOnTheFly - whether the inspection is on the fly - not used in this implementation but required by the - * parent class - */ - @NotNull - @Override - public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - RuleConfig ruleConfig = getRuleConfig(); - return new DetectDiscouragedClientVisitor(holder, ruleConfig); - } - - /** - * This class is a visitor that checks for the use of discouraged clients in the code. If the client name matches - * the discouraged client, a problem is registered with the suggestion message. - */ - static class DetectDiscouragedClientVisitor extends JavaElementVisitor { - - private final ProblemsHolder holder; - private final RuleConfig ruleConfig; - - public DetectDiscouragedClientVisitor(ProblemsHolder holder, RuleConfig ruleConfig) { - this.holder = holder; - this.ruleConfig = ruleConfig; - } - - /** - * This method builds a visitor to check for the discouraged client name in the code. If the client name matches - * the discouraged client, a problem is registered with the suggestion message. - */ - @Override - public void visitTypeElement(PsiTypeElement element) { - super.visitTypeElement(element); - - // Skip the rule if the configuration is empty or the rule is disabled - if (ruleConfig.skipRuleCheck()) { - return; - } - - // Ensure the element's type is not null - PsiType psiType = element.getType(); - if (psiType == null) { - return; - } - - // Get the presentable text for the type - String elementType = psiType.getPresentableText(); - - // Check if the type matches any discouraged usage - if (!ruleConfig.getUsagesToCheck().contains(elementType)) { - return; - } - - // Register a problem with the corresponding anti-pattern message - holder.registerProblem(element, ruleConfig.getAntiPatternMessage()); - } - } -} 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 index 2a84b2ba1d9..dd2385e9694 100644 --- 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 @@ -15,6 +15,7 @@ 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 org.jetbrains.annotations.NotNull; @@ -45,21 +46,24 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is */ static class DisableAutoCompleteVisitor extends JavaElementVisitor { - private static final RuleConfig RULE_CONFIG; + private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; private final ProblemsHolder holder; - static { - final String ruleName = "DisableAutoCompleteCheck"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } DisableAutoCompleteVisitor(ProblemsHolder holder) { this.holder = holder; + initializeRuleConfig(); } + private void initializeRuleConfig() { + if (RULE_CONFIG == null) { + final String ruleName = "DisableAutoCompleteCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + } /** * 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 @@ -103,20 +107,13 @@ private void processVariableDeclaration(PsiVariable variable) { PsiExpression initializer = variable.getInitializer(); // Check if the client type is an Azure SDK client - if (!clientType.getCanonicalText().startsWith(RuleConfig.AZURE_PACKAGE_NAME)) { - return; - } - - // Check if the client type is in the list of clients to check - if (RULE_CONFIG.getScopeToCheck().contains(clientType.getPresentableText())) { - - if (!(initializer instanceof PsiMethodCallExpression)) { - return; - } - // Process the new expression initialization - if (!isAutoCompleteDisabled((PsiMethodCallExpression) initializer)) { - // Register a problem if the auto-complete feature is not disabled - holder.registerProblem(initializer, RULE_CONFIG.getAntiPatternMessage()); + 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()); + } + } } } } 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 index 89c60e185ba..a74e15ebc1b 100644 --- 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 @@ -7,6 +7,7 @@ import com.intellij.codeInspection.ProblemsHolder; import com.intellij.psi.*; 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 org.jetbrains.annotations.NotNull; @@ -82,7 +83,7 @@ private boolean isClientCreationMethod(PsiMethodCallExpression methodCallExpress return false; } - return qualifier.getType().getCanonicalText().startsWith(RuleConfig.AZURE_PACKAGE_NAME); + 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/EndpointOnNonAzureOpenAIAuthCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java deleted file mode 100644 index 0370a511be1..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheck.java +++ /dev/null @@ -1,169 +0,0 @@ -// 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.PsiExpression; -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 org.jetbrains.annotations.NotNull; - -/** - * This class is a LocalInspectionTool that checks if the endpoint method is used with KeyCredential for non-Azure - * OpenAI clients. If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is - * registered. - */ -public class EndpointOnNonAzureOpenAIAuthCheck extends LocalInspectionTool { - - /** - * This method builds the visitor for the inspection tool. - * - * @param holder The ProblemsHolder object that holds the problems found by the inspection tool. - * @param isOnTheFly A boolean that indicates if the inspection tool is running on the fly. - this is not used in - * this implementation but is required by the method signature. - * - * @return The JavaElementVisitor object that will be used to visit the elements in the code. - */ - @NotNull - @Override - public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new EndpointOnNonAzureOpenAIAuthVisitor(holder); - } - - /** - * This class is a JavaElementVisitor that visits the elements in the code and checks if the endpoint method is used - * with KeyCredential for non-Azure OpenAI clients. - */ - static class EndpointOnNonAzureOpenAIAuthVisitor extends JavaElementVisitor { - - private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - - // The boolean that indicates if the rule should be skipped - private static final boolean SKIP_WHOLE_RULE; - - /** - * Constructor for the visitor. - * - * @param holder The ProblemsHolder object that holds the problems found by the inspection tool. - */ - EndpointOnNonAzureOpenAIAuthVisitor(ProblemsHolder holder) { - this.holder = holder; - } - - // Static initializer block to load the configurations once - static { - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig("EndpointOnNonAzureOpenAIAuthCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } - - /** - * This method visits the method call expressions in the code. If the method call expression is the endpoint - * method, the method call chain is checked to see if it is a non-Azure OpenAI client. If the method call chain - * uses the credential method with KeyCredential for non-Azure OpenAI clients, a warning is registered. - * - * @param expression The method call expression to visit. - */ - @Override - public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { - super.visitMethodCallExpression(expression); - - if (SKIP_WHOLE_RULE) { - return; - } - - PsiReferenceExpression methodExpression = expression.getMethodExpression(); - String methodName = methodExpression.getReferenceName(); - - // Skip if method is not the endpoint method or isn't in the list of check items - if (!"endpoint".equals(methodName) || !RULE_CONFIG.getUsagesToCheck().contains(methodName)) { - return; - } - - // Using KeyCredential indicates authentication of a non-Azure OpenAI client - // If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is registered - if (isUsingKeyCredential(expression)) { - holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); - } - } - - /** - * This method checks if the method call chain uses the credential method with KeyCredential. If this is the - * case, the method call chain is checked to see if it is a non-Azure OpenAI client. - * - * @param expression The method call expression to check. - * - * @return True if the method call chain uses the credential method with KeyCredential, false otherwise. - */ - private static boolean isUsingKeyCredential(PsiMethodCallExpression expression) { - - PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); - - while (qualifier instanceof PsiMethodCallExpression) { - PsiMethodCallExpression methodCall = (PsiMethodCallExpression) qualifier; - String methodName = methodCall.getMethodExpression().getReferenceName(); - - // Short-circuit if method is not relevant - if (!RULE_CONFIG.getUsagesToCheck().contains(methodName)) { - return false; - } - - if ("credential".equals(methodName)) { - PsiExpression[] args = methodCall.getArgumentList().getExpressions(); - // Return early if arguments are incorrect - if (args.length != 1 || !isKeyCredential(args[0])) { - return false; - } - - return isNonAzureOpenAIClient(methodCall); - } - qualifier = methodCall.getMethodExpression().getQualifierExpression(); - } - return false; - } - - /** - * This method checks if the expression is a KeyCredential. - * - * @param expression The expression to check. - * - * @return True if the expression is a KeyCredential, false otherwise. - */ - private static boolean isKeyCredential(PsiExpression expression) { - if (expression instanceof PsiNewExpression) { - String className = ((PsiNewExpression) expression).getClassReference().getReferenceName(); - return RULE_CONFIG.getScopeToCheck().contains(className); - } - return false; - } - - /** - * This method checks if the method call chain is on a non-Azure OpenAI client. - * - * @param expression The method call expression to check. - * - * @return True if the client is a non-Azure OpenAI client, false otherwise. - */ - private static boolean isNonAzureOpenAIClient(PsiMethodCallExpression expression) { - PsiElement parent = expression.getParent(); - while (parent instanceof PsiVariable) { - PsiType type = ((PsiVariable) parent).getType(); - if (type.getCanonicalText().startsWith("com.azure.ai.openai")) { - return true; - } - parent = parent.getParent(); - } - 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/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..4d84f595037 --- /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,43 @@ +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 org.jetbrains.annotations.NotNull; + +/** + * This class is used to check if the EventHubConsumerAsyncClient is being used in the code. + */ +public class EventHubConsumerAsyncClientCheck extends LocalInspectionTool { + + private final RuleConfig ruleConfig; + private final boolean skipRuleCheck; + + public EventHubConsumerAsyncClientCheck() { + super(); + RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); + this.ruleConfig = ruleConfigLoader.getRuleConfig("EventHubConsumerAsyncClientCheck"); + this.skipRuleCheck = ruleConfig.skipRuleCheck(); + } + + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + if (skipRuleCheck) { + return new JavaElementVisitor() {}; + } + return new JavaElementVisitor() { + @Override + public void visitTypeElement(PsiTypeElement element) { + super.visitTypeElement(element); + if (HelperUtils.isItDiscouragedClient(element, ruleConfig.getUsagesToCheck())) { + holder.registerProblem(element, ruleConfig.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 index 576eb39d5b6..c5127317d55 100644 --- 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 @@ -3,16 +3,64 @@ 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.HashSet; +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +import static com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils.checkIfInUsages; /** * Inspection tool to check discouraged GetCompletions API usage in openai package context. */ -public class GetCompletionsCheck extends DetectDiscouragedAPIUsageCheck { +public class GetCompletionsCheck extends LocalInspectionTool { @Override - protected RuleConfig getRuleConfig() { - return RuleConfigLoader.getInstance().getRuleConfig("GetCompletionsCheck"); + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new GetCompletionsVisitor(holder); + } + + static class GetCompletionsVisitor extends JavaElementVisitor { + private final ProblemsHolder holder; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + GetCompletionsVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + static { + RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = ruleConfigLoader.getRuleConfig("GetCompletionsCheck"); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + + // 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 index 3b48922afb1..ec6a9fff8c1 100644 --- 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 @@ -6,11 +6,11 @@ import com.intellij.codeInspection.LocalInspectionTool; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.psi.JavaElementVisitor; -import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiMethodCallExpression; -import com.intellij.psi.PsiType; import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.MavenUtils; +import com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils; import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; import org.jetbrains.annotations.NotNull; @@ -85,8 +85,18 @@ public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expressio return; } - if (isGetSyncPollerCall(expression) && isAsyncContext(expression) && MavenUtils.isAzureClientMethodCall(expression)) { - holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); + 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()); + } + } + } } } @@ -105,26 +115,5 @@ private boolean isGetSyncPollerCall(@NotNull PsiMethodCallExpression expression) } return false; } - - /** - * Helper method to check if the method call is on a PollerFlux type. - * - * @param methodCall Method call expression to check - * - * @return true if the method call is on a reactive type, false otherwise - */ - private boolean isAsyncContext(@NotNull PsiMethodCallExpression methodCall) { - PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); - if (qualifier == null) { - return false; - } - - PsiType type = qualifier.getType(); - String typeName = type.getCanonicalText(); - if (typeName != null && typeName.contains("PollerFlux")) { - 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 index 4804f78351a..769ac554fef 100644 --- 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 @@ -6,27 +6,34 @@ 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.regex.Pattern; +import javax.annotation.Nonnull; import org.jetbrains.annotations.NotNull; /** - * Custom inspection tool to check for hardcoded API keys and tokens in the code. - * Flags instances such as: - * 1. TextAnalyticsClient client = new TextAnalyticsClientBuilder() - * .endpoint(endpoint) - * .credential(new AzureKeyCredential(apiKey)) - * .buildClient(); - * 2. TokenCredential credential = request -> { - * AccessToken token = new AccessToken("", OffsetDateTime.now().plusHours(1)); - * } + * 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) { @@ -53,14 +60,8 @@ static class APIKeysAndTokensVisitor extends JavaElementVisitor { } @Override - public void visitElement(@NotNull PsiElement element) { - if (element instanceof PsiNewExpression newExpression) { - checkNewExpression(newExpression); - } - } - - private void checkNewExpression(@NotNull PsiNewExpression newExpression) { - var classReference = newExpression.getClassReference(); + public void visitNewExpression(@NotNull PsiNewExpression newExpression) { + PsiJavaCodeReferenceElement classReference = newExpression.getClassReference(); if (classReference == null) { return; } @@ -68,19 +69,112 @@ private void checkNewExpression(@NotNull PsiNewExpression newExpression) { String qualifiedName = classReference.getQualifiedName(); String referenceName = classReference.getReferenceName(); - if (qualifiedName != null && qualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME) - && RULE_CONFIG.getUsagesToCheck().contains(referenceName)) { + 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 PsiLiteralExpression literal && literal.getValue() instanceof String) { - holder.registerProblem(newExpression, RULE_CONFIG.getAntiPatternMessage()); + 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/IncompatibleDependencyCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java deleted file mode 100644 index 9a690f055a6..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/IncompatibleDependencyCheck.java +++ /dev/null @@ -1,207 +0,0 @@ -// 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.PsiElement; -import com.intellij.psi.PsiElementVisitor; -import com.intellij.psi.PsiFile; -import com.intellij.psi.xml.XmlFile; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.DependencyVersionFileFetcher; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.VersionUtils; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; -import org.jetbrains.annotations.NotNull; - -import static com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.IncompatibleDependencyCheck.IncompatibleDependencyVisitor.getFileContent; - -/** - * Inspection class to check the version of the libraries in the pom.xml file against compatible versions. The - * compatible versions are fetched from a file hosted on GitHub. The compatible versions are compared against the minor - * version of the library. Minor version is the first two parts of the version number. If the minor version is different - * from the compatible version, a warning is flagged and the compatible version is suggested. - */ -public class IncompatibleDependencyCheck extends AbstractLibraryVersionCheck { - private static final Logger LOGGER = Logger.getLogger(IncompatibleDependencyCheck.class.getName()); - - // Set to store the encountered version groups - static Set encounteredVersionGroups = new HashSet<>(); - - /** - * Abstract method to build the specific visitor for the inspection. - * - * @param holder The holder for the problems found - * @param isOnTheFly boolean to check if the inspection is on the fly - not used in this implementation but is part - * of the method signature - * - * @return The visitor for the inspection - */ - @NotNull - @Override - public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new IncompatibleDependencyVisitor(holder); - } - - /** - * Method to check the version of the dependency found in the project code against the compatible versions. If the - * version is not compatible, a warning is flagged and the compatible version is suggested. - * - * @param fullName The full name of the library eg "com.azure:azure-core" - * @param currentVersion The current version of the library - * @param holder The holder for the problems found - * @param versionElement The version element in the pom.xml file to check - */ - @Override - protected void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, - PsiElement versionElement) { - - // get version group of the dependency found in the project code - String versionGroup = VersionUtils.getGroupVersion(fullName, currentVersion, getFileContent()); - - if (versionGroup == null) { - return; - } - - // add an encountered version group to the encountered version groups - if (encounteredVersionGroups.isEmpty()) { - encounteredVersionGroups.add(versionGroup); - } - - // check if the versionGroup found is not already in the encounteredVersionGroups set - for (String encounteredVersionGroup : encounteredVersionGroups) { - - // check if the encountered version group is not the same as the current version group - // and the encountered version group starts with the version group's substring - // eg if versionGroup = "jackson_2.10" and encounteredVersionGroup = "jackson_2.10, no problem is flagged - // if versionGroup = "jackson_2.10" and encounteredVersionGroup = "jackson_2.11", a problem is flagged - - // The substring check is used to determine if versionGroup and encounteredVersionGroup are in the same library - if (!encounteredVersionGroup.equals(versionGroup) && - encounteredVersionGroup.startsWith(versionGroup.substring(0, versionGroup.lastIndexOf("_")))) { - String recommendedVersion = encounteredVersionGroup.substring(encounteredVersionGroup.lastIndexOf("_") + 1); - - // Check if versions are incompatible - if (VersionUtils.isIncompatibleVersion(currentVersion, recommendedVersion)) { - holder.registerProblem(versionElement, - getFormattedMessage(fullName, recommendedVersion, IncompatibleDependencyVisitor.RULE_CONFIG)); - } - return; - } - } - encounteredVersionGroups.add(versionGroup); - } - - /** - * Visitor class for the inspection. Checks the version of the libraries in the pom.xml file against compatible - * versions. The compatible versions are fetched from a file hosted on GitHub. The compatible versions are compared - * against the minor version of the library. Minor version is the first two parts of the version number. If the - * minor version is different from the compatible version, a warning is flagged and the compatible version is - * suggested. - */ - class IncompatibleDependencyVisitor extends PsiElementVisitor { - private static Map> FILE_CONTENT_REF; - - private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - - static { - RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig("IncompatibleDependencyCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } - - IncompatibleDependencyVisitor(ProblemsHolder holder) { - this.holder = holder; - } - - /** - * Method to check the pom.xml file for the library version. - * - * @param file The pom.xml file to check - */ - @Override - public void visitFile(@NotNull PsiFile file) { - super.visitFile(file); - - if (SKIP_WHOLE_RULE) { - return; - } - if (!file.getName().equals(RuleConfig.POM_XML)) { - return; - } - if (file instanceof XmlFile && file.getName().equals("pom.xml")) { - - // Check the pom.xml file for the library version - try { - IncompatibleDependencyCheck.this.checkPomXml((XmlFile) file, holder); - } catch (IOException e) { - LOGGER.severe("Error checking pom.xml file: " + e); - } - } - } - - /** - * Method to get the version group for the library. The version group is used to get the compatible versions for - * the library. The version group is determined by the major and minor version of the library. Eg if the major - * version is 2 and the minor version is 10, the version group is "jackson_2.10". - * - * @param fullName The full name of the library eg "com.azure:azure-core" - * @param currentVersion The current version of the library - * - * @return The version group for the library - */ - private static String getGroupVersion(String fullName, String currentVersion) { - - // Split currentVersion to extract major and minor version - String[] versionParts = currentVersion.split("\\."); - String majorVersion = versionParts[0]; - String minorVersion = versionParts.length > 1 ? versionParts[1] : ""; - String versionSuffix = "_" + majorVersion + "." + minorVersion; - - // Search the file content for the version group - String versionGroup = null; - - for (Map.Entry> entry : getFileContent().entrySet()) { - - // Check if the set of artifactIds contains the fullName and the corresponding key ends with the versionSuffix - // This will be the version group of the dependency - if (entry.getValue().contains(fullName) && entry.getKey().endsWith(versionSuffix)) { - versionGroup = entry.getKey(); - break; - } - } - return versionGroup; - } - - /** - * Method to get the content of the file hosted on GitHub. The file contains the compatible versions for the - * libraries. A WeakReference is used to store the content of the file to allow for garbage collection. - * - * @return The content of the file as a map - */ - static Map> getFileContent() { - - // Load the file content from the URL if it is not already loaded - - Map> fileContent = FILE_CONTENT_REF == null ? null : FILE_CONTENT_REF; - - if (fileContent == null) { - synchronized (IncompatibleDependencyVisitor.class) { - fileContent = FILE_CONTENT_REF == null ? null : FILE_CONTENT_REF; - if (fileContent == null) { - fileContent = DependencyVersionFileFetcher.loadJsonDataFromUrl(RULE_CONFIG.SUPPORTED_VERSION_URL); - FILE_CONTENT_REF = new HashMap<>(fileContent); - } - } - } - return fileContent; - } - } -} \ 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/KustoQueriesWithTimeIntervalInQueryStringCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheck.java deleted file mode 100644 index 1923f158451..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheck.java +++ /dev/null @@ -1,201 +0,0 @@ -// 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.PsiExpression; -import com.intellij.psi.PsiExpressionList; -import com.intellij.psi.PsiLiteralExpression; -import com.intellij.psi.PsiLocalVariable; -import com.intellij.psi.PsiPolyadicExpression; -import com.intellij.psi.PsiReferenceExpression; -import com.intellij.psi.PsiVariable; -import com.intellij.psi.impl.source.tree.java.PsiMethodCallExpressionImpl; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.MavenUtils; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import org.jetbrains.annotations.NotNull; - -/** - * This class is an inspection tool that checks for Kusto queries with time intervals in the query string. This approach - * makes queries less flexible and harder to troubleshoot. This inspection tool checks for the following anti-patterns: - *

    - *
  • Queries that use the "ago" function with a time interval.
  • - *
  • Queries that use the "datetime" function with a specific datetime.
  • - *
  • Queries that use the "now" function to get the current timestamp.
  • - *
  • Queries that use the "startofday", "startofmonth", or "startofyear" functions.
  • - *
  • Queries that use the "between" function with datetime values.
  • - *
- *

- * When the anti-patterns are detected as parameters of Azure client method calls, a problem is registered. - */ -public class KustoQueriesWithTimeIntervalInQueryStringCheck extends LocalInspectionTool { - - @NotNull - @Override - public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - - // clear the timeIntervalParameters list in every visit - KustoQueriesVisitor.resetTimeIntervalParameters(); - return new KustoQueriesVisitor(holder); - } - - /** - * This class defines the visitor for the inspection tool The visitor checks for Kusto queries with time intervals - * in the query string and registers a problem if an anti-pattern is detected To check for the anti-patterns, the - * visitor uses regex patterns to match the query string Processing of polyadic expressions is also done to replace - * the variables with their values in the query string before checking for the anti-patterns. - */ - static class KustoQueriesVisitor extends JavaElementVisitor { - - private final ProblemsHolder holder; - - // // Define constants for string literals - private static final RuleConfig RULE_CONFIG; - private static final List REGEX_PATTERNS; - private static final boolean SKIP_WHOLE_RULE; - private static final List timeIntervalParameters = new ArrayList<>(); - - static { - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig("KustoQueriesWithTimeIntervalInQueryStringCheck"); - REGEX_PATTERNS = RULE_CONFIG.getRegexPatternsToCheck().entrySet().stream() - .filter(entry -> Objects.nonNull(entry.getValue())) - .map(entry -> Pattern.compile(entry.getValue())) // Compile each pattern into a `Pattern` object - .toList(); - - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || REGEX_PATTERNS.isEmpty(); - } - - /** - * Constructor for the KustoQueriesVisitor class The constructor initializes the ProblemsHolder and isOnTheFly - * variables - * - * @param holder - ProblemsHolder is used to register problems found in the code - */ - KustoQueriesVisitor(ProblemsHolder holder) { - this.holder = holder; - } - - /** Resets the time interval parameters list. */ - static void resetTimeIntervalParameters() { - timeIntervalParameters.clear(); - } - - /** - * This method visits the element and checks for the anti-patterns The method checks if the element is a - * PsiPolyadicExpression or a PsiLocalVariable If the element is a PsiLocalVariable, the method checks the - * initializer of the variable If the element is a PsiPolyadicExpression, the method processes the expression to - * replace the variables with their values The method then checks the expression for the anti-patterns by - * matching regex patterns with the expression text and registers a problem if an anti-pattern is detected - * - * @param element - the element to visit - */ - @Override - public void visitElement(@NotNull PsiElement element) { - super.visitElement(element); - - // Skip the whole rule if the rule configuration is empty - if (SKIP_WHOLE_RULE) { - return; - } - - if (element instanceof PsiMethodCallExpressionImpl) { - handleMethodCallExpression((PsiMethodCallExpressionImpl) element); - } else if (element instanceof PsiLocalVariable) { - handleLocalVariable((PsiLocalVariable) element); - } else if (element instanceof PsiPolyadicExpression) { - // if element is a polyadic expression, extract the value and replace the variable with the value - // PsiPolyadicExpressions are used to represent expressions with multiple operands - // eg ("datetime" + startDate), where startDate is a variable - handlePolyadicExpression((PsiPolyadicExpression) element); - } - } - - /** - * This method checks the expression for the anti-patterns by matching regex patterns with the expression text - * and registers a problem if an anti-pattern is detected - * - * @param expression - the expression to check - * @param element - the element to check - */ - void checkExpressionForPatterns(PsiExpression expression, PsiElement element) { - if (expression == null) { - return; - } - String text = expression.getText(); - - // Check if the expression text contains any of the regex patterns - boolean foundAntiPattern = REGEX_PATTERNS.stream().anyMatch(pattern -> pattern.matcher(text).find()); - - - // If an anti-pattern is detected, register a problem - if (foundAntiPattern) { - PsiElement parentElement = element.getParent(); - - if (parentElement instanceof PsiLocalVariable) { - PsiLocalVariable variable = (PsiLocalVariable) parentElement; - String variableName = variable.getName(); - timeIntervalParameters.add(variableName); - } - } - } - - /** - * This method handles the local variable by checking the initializer of the variable If the initializer is a - * PsiLiteralExpression, the method checks the expression for the anti-patterns by matching regex patterns with - * the expression text - * - * @param variable - the local variable to check - */ - private void handleLocalVariable(PsiLocalVariable variable) { - PsiExpression initializer = variable.getInitializer(); - if (initializer instanceof PsiLiteralExpression) { - checkExpressionForPatterns(initializer, variable); - } - } - - /** - * This method handles the polyadic expression by processing the expression to replace the variables with their - * values The method then checks the expression for the anti-patterns by matching regex patterns with the - * expression text - * - * @param polyadicExpression - the polyadic expression to check - */ - private void handlePolyadicExpression(PsiPolyadicExpression polyadicExpression) { - checkExpressionForPatterns(polyadicExpression, polyadicExpression); - } - - /** - * This method handles the method call by checking the parameters of the method call If the parameter is a - * reference to a variable, the method checks the variable name If the variable name is in the list of time - * interval parameters, the method checks if the method call is an Azure client method call If the method call - * is an Azure client method call, the method registers a problem - * - * @param methodCall - the method call to check - */ - private void handleMethodCallExpression(PsiMethodCallExpressionImpl methodCall) { - PsiExpressionList argumentList = methodCall.getArgumentList(); - for (PsiExpression argument : argumentList.getExpressions()) { - if (argument instanceof PsiReferenceExpression referenceExpression) { - PsiElement resolvedElement = referenceExpression.resolve(); - // check if the variable name is in the list of time interval parameters - // if the variable name is in the list, check if the method call is an Azure client method call - if (resolvedElement instanceof PsiVariable variable && timeIntervalParameters.contains(variable.getName())) { - if (MavenUtils.isAzureClientMethodCall(methodCall)) { - holder.registerProblem(methodCall, RULE_CONFIG.getAntiPatternMessage()); - } - } - } - } - } - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java deleted file mode 100644 index a2a21851ebd..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheck.java +++ /dev/null @@ -1,208 +0,0 @@ -// 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.RuleConfigLoader; -import java.util.OptionalInt; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.jetbrains.annotations.NotNull; - -/** - * This class extends the LocalInspectionTool and is used to inspect the usage of Azure SDK ServiceBus & - * ServiceBusProcessor clients in the code. It checks if the receive mode is set to PEEK_LOCK and the prefetch count is - * set to a value greater than 1. If the receive mode is set to PEEK_LOCK and the prefetch count is set to a value - * greater than 1, a problem is registered with the ProblemsHolder. - */ -public class ServiceBusReceiveModeCheck 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 - * @param isOnTheFly Whether the inspection is on the fly -- not in use - * - * @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 ServiceBusReceiveModeVisitor(holder, isOnTheFly); - } - - /** - * 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 receive mode is set to - * PEEK_LOCK and the prefetch count is set to a value greater than 1. If the receive mode is set to PEEK_LOCK and - * the prefetch count is set to a value greater than 1, a problem is registered with the ProblemsHolder. - */ - static class ServiceBusReceiveModeVisitor extends JavaElementVisitor { - private static final Logger LOGGER = Logger.getLogger(ServiceBusReceiveModeCheck.class.getName()); - private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - static { - final String ruleName = "ServiceBusReceiveModeCheck"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } - - /** - * Constructor for the visitor - * - * @param holder The holder for the problems found - * @param isOnTheFly Whether the inspection is on the fly -- not in use - */ - ServiceBusReceiveModeVisitor(ProblemsHolder holder, boolean isOnTheFly) { - this.holder = holder; - } - - /** - * This method is used to visit the declaration statement in the code. It checks for the declaration of the - * Azure SDK ServiceBusReceiver & ServiceBusProcessor clients and whether the receive mode is set to PEEK_LOCK - * and the prefetch count is set to a value greater than 1. If the receive mode is set to PEEK_LOCK and the - * prefetch count is set to a value greater than 1, a problem is registered with the ProblemsHolder. - * - * @param statement The declaration statement to visit - */ - @Override - public void visitDeclarationStatement(PsiDeclarationStatement statement) { - super.visitDeclarationStatement(statement); - - if (SKIP_WHOLE_RULE) { - return; - } - - // 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 if the client type is an Azure ServiceBus - * client and retrieves the client name (left side of the declaration). - * - * @param variable The variable to process - */ - private void processVariableDeclaration(PsiVariable variable) { - // Check if the client type is an Azure ServiceBus client - isRelevantClientType(variable.getType()); - - PsiExpression initializer = variable.getInitializer(); - if (initializer instanceof PsiMethodCallExpression methodCall) { - analyzeMethodCallChain(methodCall); - } - } - - private boolean isRelevantClientType(PsiType type) { - String typeName = type.getCanonicalText(); - return typeName.startsWith(RuleConfig.AZURE_PACKAGE_NAME) || RULE_CONFIG.getScopeToCheck().contains(typeName); - } - - /** - * This method is used to determine the receive mode of the Azure ServiceBus client. It checks if the receive - * mode is set to PEEK_LOCK and the prefetch count is set to a value greater than 1. If the receive mode is set - * to PEEK_LOCK and the prefetch count is set to a value greater than 1, a problem is registered with the - * ProblemsHolder. - * - * @param methodCall The method call expression to check - */ - private void analyzeMethodCallChain(PsiMethodCallExpression methodCall) { - - OptionalInt prefetchCountValue = OptionalInt.empty(); - boolean isReceiveModePeekLock = false; - PsiElement prefetchCountMethod = null; - - // Iterating up the chain of method calls - PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); - - // Check if the method call chain has the method to check - while (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; - } - if ("receiveMode".equals(methodName)) { - isReceiveModePeekLock = receiveModePeekLockCheck((PsiMethodCallExpression) qualifier); - } else if ("prefetchCount".equals(methodName)) { - prefetchCountValue = extractPrefetchCount((PsiMethodCallExpression) qualifier); - prefetchCountMethod = - ((PsiMethodCallExpression) qualifier).getMethodExpression().getReferenceNameElement(); - } - - // Get the qualifier of the method call expression -- the next method call in the chain - qualifier = ((PsiMethodCallExpression) qualifier).getMethodExpression().getQualifierExpression(); - - // If the receive mode is set to PEEK_LOCK and the prefetch count is set to a value greater than 1, register a problem - if (prefetchCountValue.isPresent() && prefetchCountValue.getAsInt() > 1 && isReceiveModePeekLock && - prefetchCountMethod != null) { - holder.registerProblem(prefetchCountMethod, RULE_CONFIG.getAntiPatternMessage()); - return; - } - } - } - - /** - * This method is used to check if the receive mode is set to PEEK_LOCK. - * - * @param methodCall The method call expression to check - * - * @return true if the receive mode is set to PEEK_LOCK, false otherwise - */ - private boolean receiveModePeekLockCheck(PsiMethodCallExpression methodCall) { - for (PsiExpression arg : methodCall.getArgumentList().getExpressions()) { - if ("PEEK_LOCK".equals(arg.getText())) { - return true; - } - } - return false; - } - - /** - * This method is used to get the prefetch count value. - * - * @param methodCall The method call expression to check - * - * @return The prefetch count value - */ - private OptionalInt extractPrefetchCount(PsiMethodCallExpression methodCall) { - PsiExpression[] args = methodCall.getArgumentList().getExpressions(); - if (args.length > 0) { - try { - return OptionalInt.of(Integer.parseInt(args[0].getText())); - } catch (NumberFormatException e) { - LOGGER.log(Level.SEVERE, "Invalid prefetch count: " + args[0].getText(), e); - } - } - return OptionalInt.empty(); - } - } -} 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 index 57a4355f1e7..de46d9e3eaa 100644 --- 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 @@ -1,21 +1,43 @@ -// 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.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 org.jetbrains.annotations.NotNull; /** - * This class extends the DetectDiscouragedClientCheck to check for the use of ServiceBusReceiverAsyncClient in the code and - * suggests using ServiceBusProcessorClient instead. The client data is loaded from the configuration file and the - * client name is checked against the discouraged client name. If the client name matches, a problem is registered with - * the suggestion message. + * This class is used to check if the ServiceBusReceiverAsyncClient is being used in the code. */ -public class ServiceBusReceiverAsyncClientCheck extends DetectDiscouragedClientCheck { +public class ServiceBusReceiverAsyncClientCheck extends LocalInspectionTool { + + private final RuleConfig ruleConfig; + private final boolean skipRuleCheck; + + public ServiceBusReceiverAsyncClientCheck() { + super(); + RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); + this.ruleConfig = ruleConfigLoader.getRuleConfig("ServiceBusReceiverAsyncClientCheck"); + this.skipRuleCheck = ruleConfig.skipRuleCheck(); + } + @NotNull @Override - protected RuleConfig getRuleConfig() { - return RuleConfigLoader.getInstance().getRuleConfig("ConnectionStringCheck"); + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + if (skipRuleCheck) { + return new JavaElementVisitor() {}; + } + return new JavaElementVisitor() { + @Override + public void visitTypeElement(PsiTypeElement element) { + super.visitTypeElement(element); + if (HelperUtils.isItDiscouragedClient(element, ruleConfig.getUsagesToCheck())) { + holder.registerProblem(element, ruleConfig.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/SingleOperationInLoopCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java deleted file mode 100644 index ebe2f04d0cf..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/SingleOperationInLoopCheck.java +++ /dev/null @@ -1,295 +0,0 @@ -// 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.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.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.PsiVariable; -import com.intellij.psi.PsiWhileStatement; -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.List; -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 SingleOperationInLoopCheck 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, isOnTheFly); - } - - /** - * 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 { - - // Define the holder for the problems found and whether the inspection is running on the fly - private final ProblemsHolder holder; - - // // Define constants for string literals - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - private static final List SCOPE_TO_CHECK; - - static { - final String ruleName = "SingleOperationInLoopCheck"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - - // Get the RuleConfig object for the rule - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - SCOPE_TO_CHECK = RULE_CONFIG.getScopeToCheck(); - } - - /** - * Constructor for the visitor - * - * @param holder The holder for the problems found - * @param isOnTheFly Whether the inspection is running on the fly. If true, the inspection is running as you - * type. - */ - public SingleOperationInLoopVisitor(ProblemsHolder holder, boolean isOnTheFly) { - this.holder = holder; - } - - /** - * Visit the for statement and check for single Azure client operation inside the loop. - * - * @param statement The for statement to check - */ - @Override - public void visitForStatement(@NotNull PsiForStatement statement) { - if (SKIP_WHOLE_RULE) { - return; - } - checkLoopForTextAnalyticsClientOperation(statement); - } - - /** - * Visit the foreach statement and check for single Azure client operation inside the loop. - * - * @param statement The foreach statement to check - */ - @Override - public void visitForeachStatement(@NotNull PsiForeachStatement statement) { - if (SKIP_WHOLE_RULE) { - return; - } - checkLoopForTextAnalyticsClientOperation(statement); - } - - /** - * Visit the while statement and check for single Azure client operation inside the loop. - * - * @param statement The while statement to check - */ - @Override - public void visitWhileStatement(@NotNull PsiWhileStatement statement) { - if (SKIP_WHOLE_RULE) { - return; - } - checkLoopForTextAnalyticsClientOperation(statement); - } - - /** - * Visit the do-while statement and check for single Azure client operation inside the loop. - * - * @param statement The do-while statement to check - */ - @Override - public void visitDoWhileStatement(@NotNull PsiDoWhileStatement statement) { - if (SKIP_WHOLE_RULE) { - return; - } - checkLoopForTextAnalyticsClientOperation(statement); - } - - - /** - * Check the loop statement for a single Text Analytics Azure client operation inside the loop. - * - * @param loopStatement The loop statement to check - */ - private boolean checkLoopForTextAnalyticsClientOperation(PsiStatement loopStatement) { - - // extract body of the loop - PsiStatement loopBody = getLoopBody(loopStatement); - - if (loopBody == null) { - return false; - } - - if (!(loopBody instanceof PsiBlockStatement)) { - return false; - } - - // Extract the code block from the block statement - PsiBlockStatement blockStatement = (PsiBlockStatement) loopBody; - PsiCodeBlock codeBlock = blockStatement.getCodeBlock(); - - // extract statements in the loop body - for (PsiStatement statement : codeBlock.getStatements()) { - - // Check if the statement is an expression statement and is an Azure client operation - if (statement instanceof PsiExpressionStatement) { - isExpressionAzureClientOperation(statement); - } - - // Check if the statement is a declaration statement and is an Azure client operation - if (statement instanceof PsiDeclarationStatement) { - isDeclarationAzureClientOperation((PsiDeclarationStatement) statement); - } - } - return true; - } - - - /** - * Get the body of the loop statement. The body of the loop statement is the statement that is executed in the - * loop. - * - * @param loopStatement The loop statement to get the body from - * - * @return The body of the loop statement - */ - private static PsiStatement getLoopBody(PsiStatement loopStatement) { - - // Check the type of the loop statement and return the body of the loop statement - 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; - } - - /** - * If the statement is an expression statement, check if the expression is an Azure client operation. - * - * @param statement The statement to check - */ - private void isExpressionAzureClientOperation(PsiStatement statement) { - - // Get the expression from the statement - PsiExpression expression = ((PsiExpressionStatement) statement).getExpression(); - - if (expression instanceof PsiMethodCallExpression) { - // Check if the expression is an Azure client operation - if (isAzureTextAnalyticsClientOperation((PsiMethodCallExpression) expression)) { - - // get the method name - String methodName = ((PsiMethodCallExpression) expression).getMethodExpression().getReferenceName(); - holder.registerProblem(expression, - (RULE_CONFIG.getAntiPatternMessage() + methodName + "Batch")); - } - } - } - - /** - * If the statement is a declaration statement, check if the initializer is an Azure client operation. - * - * @param statement The declaration statement to check - */ - private void isDeclarationAzureClientOperation(PsiDeclarationStatement statement) { - - // getDeclaredElements() returns the variables declared in the statement - for (PsiElement element : statement.getDeclaredElements()) { - - if (!(element instanceof PsiVariable)) { - continue; - } - // Get the initializer of the variable - PsiExpression initializer = ((PsiVariable) element).getInitializer(); - - if (!(initializer instanceof PsiMethodCallExpression)) { - continue; - } - // Check if the initializer is an Azure client operation - if (isAzureTextAnalyticsClientOperation((PsiMethodCallExpression) initializer)) { - // get the method name - String methodName = - ((PsiMethodCallExpression) initializer).getMethodExpression().getReferenceName(); - holder.registerProblem(initializer, - (RULE_CONFIG.getAntiPatternMessage() + methodName + "Batch")); - } - } - } - - /** - * Check if the method call is an Azure client operation. Check the containing class of the method call and see - * if it is part of the Azure SDK. If the class is part of the Azure SDK, increment the count of Azure client - * operations. - * - * @param methodCall The method call expression to check - * - * @return True if the method call is an Azure client operation, false otherwise - */ - private static boolean isAzureTextAnalyticsClientOperation(PsiMethodCallExpression methodCall) { - - // Get the containing class of the method call - PsiClass containingClass = PsiTreeUtil.getParentOfType(methodCall, PsiClass.class); - - // Check if the method call is on a class - if (containingClass != null) { - String className = containingClass.getQualifiedName(); - - // Check if the class is part of the Azure SDK - if (className != null && SCOPE_TO_CHECK.stream().anyMatch(className::startsWith)) { - if (RULE_CONFIG.getUsagesToCheck() - .contains((methodCall.getMethodExpression().getReferenceName()) + "Batch")) { - 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/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..91a5c62718a --- /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,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.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 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); + } + + /** + * 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 final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + private static final List SCOPE_TO_CHECK; + + static { + final String ruleName = "SingleOperationInLoopTextAnalyticsCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + SCOPE_TO_CHECK = RULE_CONFIG.getScopeToCheck(); + } + + public SingleOperationInLoopVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + @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 && SCOPE_TO_CHECK.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 index 0d8d9f5dafd..c3e526e2e2f 100644 --- 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 @@ -8,14 +8,18 @@ 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.PsiExpressionList; +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 org.jetbrains.annotations.NotNull; @@ -23,16 +27,6 @@ * Inspection tool to enforce usage of Azure Storage upload APIs with a 'length' parameter. */ public class StorageUploadWithoutLengthCheck extends LocalInspectionTool { - private static final RuleConfig RULE_CONFIG; - private static final String LENGTH_TYPE = "long"; - private static final boolean SKIP_WHOLE_RULE; - - static { - String ruleName = "StorageUploadWithoutLengthCheck"; - RuleConfigLoader configLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = configLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } @NotNull @Override @@ -44,6 +38,7 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is * 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 final RuleConfig RULE_CONFIG; private static final boolean SKIP_WHOLE_RULE; @@ -64,16 +59,7 @@ static class StorageUploadVisitor extends JavaRecursiveElementWalkingVisitor { public void visitMethodCallExpression(PsiMethodCallExpression expression) { super.visitMethodCallExpression(expression); - if (SKIP_WHOLE_RULE) { - return; - } - - String methodName = expression.getMethodExpression().getReferenceName(); - if (!RULE_CONFIG.getUsagesToCheck().contains(methodName)) { - return; - } - - if (!isInScope(expression)) { + if (SKIP_WHOLE_RULE || !shouldAnalyze(expression)) { return; } @@ -82,112 +68,151 @@ public void visitMethodCallExpression(PsiMethodCallExpression expression) { } } - /** - * Checks if the method call expression belongs to a class within the specified package scope. - * - * @param expression The method call expression to check. - * - * @return true if the method call is within the specified package scope, false otherwise. - */ - private boolean isInScope(PsiMethodCallExpression expression) { - // Get the qualifier expression and resolve its type - PsiExpression qualifierExpression = expression.getMethodExpression().getQualifierExpression(); - if (qualifierExpression == null) { - return false; - } - - PsiType qualifierType = qualifierExpression.getType(); - if (!(qualifierType instanceof PsiClassType)) { - return false; - } + private boolean shouldAnalyze(PsiMethodCallExpression expression) { + String methodName = expression.getMethodExpression().getReferenceName(); + return RULE_CONFIG.getUsagesToCheck().contains(methodName) && isInScope(expression); + } - PsiClass containingClass = ((PsiClassType) qualifierType).resolve(); - if (containingClass == null) { + private boolean isInScope(PsiMethodCallExpression expression) { + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + if (!(qualifier != null && qualifier.getType() instanceof PsiClassType)) { return false; } - // Check the class name against the specified scopes - String qualifiedName = containingClass.getQualifiedName(); - if (qualifiedName == null) { + PsiClass containingClass = ((PsiClassType) qualifier.getType()).resolve(); + if (containingClass == null || containingClass.getQualifiedName() == null) { return false; } - for (String scope : SCOPE_TO_CHECK) { - if (qualifiedName.startsWith(scope)) { - return true; - } - } - - return false; + return SCOPE_TO_CHECK.stream() + .anyMatch(scope -> containingClass.getQualifiedName().startsWith(scope)); } /** - * Checks if a method call expression includes a 'long' type argument directly or in its call chain. + * Checks if the given method call expression has a 'length' argument. * - * @param expression The method call expression to analyze. + * @param expression the method call expression to check * - * @return true if a 'long' type argument is found, false otherwise. + * @return true if the method call expression has a 'length' argument, false otherwise */ private boolean hasLengthArgument(PsiMethodCallExpression expression) { - PsiExpression[] arguments = expression.getArgumentList().getExpressions(); - for (PsiExpression arg : arguments) { - if (isLongType(arg) || isLongTypeInCallChain(arg)) { - return true; - } - } - return false; + return Arrays.stream(expression.getArgumentList().getExpressions()) + .anyMatch(arg -> isLongType(arg) || isLengthMethodCall(arg) || isLongTypeInCallChain(arg) + || (arg instanceof PsiNewExpression && hasLongConstructorArgument((PsiNewExpression) arg))); } /** - * Checks if an expression is of type 'long'. + * Checks if the given expression is a method call that returns a long value. * - * @param expression The expression to check. + * @param expression the expression to check * - * @return true if the expression is of type 'long', false otherwise. - */ - private boolean isLongType(PsiExpression expression) { - return expression.getType() != null && LENGTH_TYPE.equals(expression.getType().getCanonicalText()); - } - - /** - * Analyzes the method call chain to determine if any argument in the chain is of type 'long'. - * - * @param expression The starting expression in the call chain. - * - * @return true if a 'long' type argument is found, false otherwise. + * @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; - expression = methodCall.getMethodExpression().getQualifierExpression(); + PsiExpression qualifier = methodCall.getMethodExpression().getQualifierExpression(); + + if (qualifier instanceof PsiReferenceExpression && + resolvesToVariableWithLength((PsiReferenceExpression) qualifier)) { + return false; + } - if (expression instanceof PsiNewExpression && - hasLongConstructorArgument((PsiNewExpression) expression)) { - return true; + if (qualifier instanceof PsiNewExpression && + hasLengthArgumentInConstructor(((PsiNewExpression) qualifier).resolveConstructor())) { + return false; } + + expression = qualifier; } return false; } /** - * Checks if a constructor has an argument of type 'long'. + * Checks if the given qualifier resolves to a variable with a 'length' field or method. * - * @param newExpression The new expression representing the constructor call. + * @param qualifier the qualifier to check * - * @return true if a 'long' type argument is found, false otherwise. + * @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) { - PsiExpressionList argumentList = newExpression.getArgumentList(); - if (argumentList == null) { + 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())); + } - for (PsiExpression arg : argumentList.getExpressions()) { - if (isLongType(arg)) { - return true; - } + private boolean hasLengthFieldOrMethod(PsiType type) { + if (!(type instanceof PsiClassType)) { + return false; } - 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/UpdateCheckpointAsyncSubscribeChecker.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 similarity index 63% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeChecker.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheck.java index 390abb92401..48550d78970 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeChecker.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 @@ -3,10 +3,15 @@ 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 org.jetbrains.annotations.NotNull; @@ -16,7 +21,20 @@ * updateCheckpointAsync(). If the method call is updateCheckpointAsync() and the following method is subscribe, a * problem is registered. */ -public class UpdateCheckpointAsyncSubscribeChecker extends AsyncSubscribeChecker { +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); + } /** * This class extends the JavaElementVisitor to visit the elements in the code. It checks if the method call is @@ -32,22 +50,17 @@ static class UpdateCheckpointAsyncVisitor extends JavaElementVisitor { private static final boolean SKIP_WHOLE_RULE; /** - * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * Constructor to initialize the visitor with the holder. * * @param holder ProblemsHolder to register problems - * @param isOnTheFly boolean to check if the inspection is on the fly. If true, the inspection is performed as - * you type. */ - UpdateCheckpointAsyncVisitor(ProblemsHolder holder, boolean isOnTheFly) { + UpdateCheckpointAsyncVisitor(ProblemsHolder holder) { this.holder = holder; } static { - final String ruleName = "UpdateCheckpointAsyncSubscribeChecker"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - - // Get the RuleConfig object for the rule - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); + RULE_CONFIG = ruleConfigLoader.getRuleConfig("UpdateCheckpointAsyncSubscribeCheck"); SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); } @@ -67,20 +80,19 @@ public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expressio return; } - expression.getMethodExpression(); if (expression.getMethodExpression().getReferenceName() == null) { return; } - // Check if the method call is updateCheckpointAsync() - if (RULE_CONFIG.getUsagesToCheck().stream() - .anyMatch(usage -> usage.equals(expression.getMethodExpression().getReferenceName()))) { + // 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 - // Check if the updateCheckpointAsync() method call is called on an provided context - // (EventBatchContext) object - if (isFollowingMethodSubscribe(expression) && isCalledInProvidedContext(expression, RULE_CONFIG.getScopeToCheck())) { - holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); + if (HelperUtils.isFollowedBySubscribe(expression)) { + PsiElement problemElement = expression.getMethodExpression().getReferenceNameElement(); + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessage()); } } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java deleted file mode 100644 index 1098c5f0db5..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpgradeLibraryVersionCheck.java +++ /dev/null @@ -1,105 +0,0 @@ -// 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.PsiElement; -import com.intellij.psi.PsiElementVisitor; -import com.intellij.psi.PsiFile; -import com.intellij.psi.xml.XmlFile; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.DependencyVersionFileFetcher; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.RuleConfigLoader; -import com.microsoft.azure.toolkit.intellij.java.sdk.utils.VersionUtils; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.jetbrains.annotations.NotNull; - -import static com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UpgradeLibraryVersionCheck.LibraryVersionVisitor.getRecommendedVersionMap; - -/** - * Inspection class to check the version of the libraries in the pom.xml file against the recommended version. - */ -public class UpgradeLibraryVersionCheck extends AbstractLibraryVersionCheck { - private static final Logger LOG = Logger.getLogger(UpgradeLibraryVersionCheck.class.getName()); - - @NotNull - @Override - public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new LibraryVersionVisitor(holder); - } - - @Override - protected void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, PsiElement versionTag) { - String recommendedVersion = VersionUtils.getRecommendedVersion(fullName, getRecommendedVersionMap()); - if (recommendedVersion == null) { - return; - } - - String currentMinorVersion = VersionUtils.extractMinorVersion(currentVersion); - if (currentMinorVersion == null || currentMinorVersion.equals(recommendedVersion)) { - return; - } - - holder.registerProblem(versionTag, getFormattedMessage(fullName, recommendedVersion, LibraryVersionVisitor.RULE_CONFIG)); - } - - class LibraryVersionVisitor extends PsiElementVisitor { - - private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - - // Cache for recommended versions - private static Map VERSION_MAP_CACHE; - - static { - RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig("UpgradeLibraryVersionCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } - - LibraryVersionVisitor(ProblemsHolder holder) { - this.holder = holder; - } - - @Override - public void visitFile(@NotNull PsiFile file) { - super.visitFile(file); - - if (SKIP_WHOLE_RULE || !(file instanceof XmlFile) || !RuleConfig.POM_XML.equals(file.getName())) { - return; - } - - try { - UpgradeLibraryVersionCheck.this.checkPomXml((XmlFile) file, holder); - } catch (IOException e) { - LOG.log(Level.ALL, "Error while parsing pom.xml", e); - } - } - - static Map getRecommendedVersionMap() { - Map cachedMap = VERSION_MAP_CACHE == null ? null : VERSION_MAP_CACHE; - if (cachedMap != null) { - return cachedMap; - } - - synchronized (VersionUtils.class) { - cachedMap = VERSION_MAP_CACHE == null ? null : VERSION_MAP_CACHE; - if (cachedMap == null) { - String latestVersion = DependencyVersionFileFetcher.getLatestVersion(RULE_CONFIG.AZURE_MAVEN_METADATA_URL); - if (latestVersion != null) { - String baseUrl = RULE_CONFIG.AZURE_MAVEN_METADATA_URL.replace("/maven-metadata.xml", ""); - String pomUrl = String.format("%s/%s/azure-sdk-bom-%s.pom", baseUrl, latestVersion, latestVersion); - cachedMap = DependencyVersionFileFetcher.parsePomFile(pomUrl); - VERSION_MAP_CACHE = new HashMap<>(cachedMap); - } - } - } - return cachedMap; - } - } -} \ 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 index 8ce1658e5dc..72d6df209d7 100644 --- 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 @@ -5,16 +5,20 @@ 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 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. @@ -58,12 +62,17 @@ static class UseOfBlockOnAsyncClientsVisitor extends JavaElementVisitor { public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { super.visitMethodCallExpression(expression); - if (SKIP_WHOLE_RULE || !isBlockingMethodCall(expression)) { + if (SKIP_WHOLE_RULE) { + return; + } + + if (!isBlockingMethodCall(expression)) { return; } if (isAsyncClientBlockingCall(expression)) { - holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessage()); + PsiElement problemElement = expression.getMethodExpression().getReferenceNameElement(); + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessage()); } } @@ -98,12 +107,7 @@ private boolean isAsyncClientBlockingCall(@NotNull PsiMethodCallExpression expre * @return true if it's a blocking method call on a reactive type, false otherwise */ private boolean isBlockingMethodCall(@NotNull PsiMethodCallExpression expression) { - for (String methodToCheck : RULE_CONFIG.getUsagesToCheck()) { - if (expression.getMethodExpression().getReferenceName().equals(methodToCheck)) { - return true; - } - } - return false; + return RULE_CONFIG.getUsagesToCheck().stream().anyMatch(expression.getMethodExpression().getReferenceName()::equals); } /** @@ -119,8 +123,8 @@ private boolean isAzureAsyncClient(PsiMethodCallExpression qualifierMethodCall) PsiType clientType = clientExpression.getType(); if (clientType instanceof PsiClassType) { PsiClass clientClass = ((PsiClassType) clientType).resolve(); - return clientClass != null && - clientClass.getQualifiedName().startsWith(RuleConfig.AZURE_PACKAGE_NAME) && + String qualifiedName = clientClass.getQualifiedName(); + return qualifiedName != null && HelperUtils.isAzurePackage(clientClass.getQualifiedName()) && clientClass.getQualifiedName().endsWith("AsyncClient"); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java deleted file mode 100644 index e7a38bc9bae..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/models/DependencyVersionsDataCache.java +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.azure.toolkit.intellij.java.sdk.models; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serial; -import java.io.Serializable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -/** - * A cache to store the dependency versions data for a certain period of time. - * The cache is stored in memory and also saved to a file to persist the data across IDE restarts. - * The cache is cleared at regular intervals to prevent it from growing indefinitely. - * - * @param The type of the data to be stored in the cache. - */ -public class DependencyVersionsDataCache implements Serializable { - private static final Logger LOGGER = Logger.getLogger(DependencyVersionsDataCache.class.getName()); - - // The serial version UID is used to verify that the class is compatible with the serialized object. - @Serial - private static final long serialVersionUID = 1L; - - // The cache is stored in a ConcurrentHashMap to ensure thread safety. - private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - - // The interval at which the cache is cleared. The cache is cleared every 5 days. - private static final long CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(5); - - // The file where the cache is saved. - private final File cacheFile; - - // The timestamp when the cache was last updated. - private long lastUpdated; - - // The timestamp when the cache will be refreshed. - private long nextRefresh; - - /** - * Creates a new instance of the DependencyVersionsDataCache class. - * - * @param cacheFileName The name of the file where the cache is saved. - */ - public DependencyVersionsDataCache(String cacheFileName) { - this.cacheFile = new File(cacheFileName); - loadCacheFromFile(); - - // Check if the cache needs to be cleared immediately - checkAndClearOnStartup(); - - // Schedule the cache cleanup task to run at regular intervals - ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - scheduler.scheduleAtFixedRate(this::clear, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.MILLISECONDS); - } - - /** - * Puts a value into the cache. - * - * @param key The key to associate with the value. - * @param value The value to store in the cache. - */ - public void put(String key, T value) { - cache.put(key, value); - updateTimestamps(); - saveCacheToFile(); - } - - /** - * Gets a value from the cache. - * - * @param key The key to retrieve the value for. - * @return The value associated with the key, or null if the key is not found. - */ - public T get(String key) { - return cache.get(key); - } - - /** - * Clears the cache. - * This method is called at regular intervals to prevent the cache from growing indefinitely. - */ - void clear() { - cache.clear(); - updateTimestamps(); - saveCacheToFile(); - } - - /** - * Loads the cache from the file. - * If the file exists, the cache is loaded from the file. - * If the file does not exist, the cache is left empty. - */ - private void loadCacheFromFile() { - if (cacheFile.exists()) { - // Load the cache from the file - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(cacheFile))) { - - // Use a temporary variable to hold the deserialized object - Object loadedObject = ois.readObject(); - - // Check if the deserialized object is an instance of ConcurrentHashMap - if (loadedObject instanceof CacheData) { - - CacheData loadedCacheData = (CacheData) loadedObject; - - // Put all the entries from the loaded cache into the current cache - // Update the lastUpdated and nextRefresh timestamps - cache.putAll(loadedCacheData.cache); - lastUpdated = loadedCacheData.lastUpdated; - nextRefresh = loadedCacheData.nextRefresh; - } else { - LOGGER.severe("Failed to load cache from file: Invalid cache format"); - } - } catch (IOException | ClassNotFoundException e) { - LOGGER.severe("Failed to load cache from file: " + e); - } - } - } - - /** - * Saves the cache to the file. - * The cache is saved to the file to persist the data across IDE restarts. - */ - private void saveCacheToFile() { - - // Save the cache to the file using the ObjectOutputStream class. - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(cacheFile))) { - CacheData cacheData = new CacheData<>(cache, lastUpdated, nextRefresh); - oos.writeObject(cacheData); - } catch (IOException e) { - LOGGER.severe("Failed to save cache to file: " + e.getMessage()); - } - } - - /** - * Updates the timestamps for the cache. - * The lastUpdated timestamp is updated to the current time. - * The nextRefresh timestamp is updated to the current time plus the cleanup interval. - */ - private void updateTimestamps() { - lastUpdated = System.currentTimeMillis(); - nextRefresh = lastUpdated + CLEANUP_INTERVAL; - } - - /** - * Checks if the cache needs to be cleared on startup. - * The cache is cleared if the lastUpdated timestamp is older than the cleanup interval. - */ - private void checkAndClearOnStartup() { - long currentTime = System.currentTimeMillis(); - if (currentTime - lastUpdated >= CLEANUP_INTERVAL) { - clear(); - } - } - - /** - * The CacheData class is a static inner class used to store the cache data in a serializable format. - * The class contains the cache, lastUpdated, and nextRefresh timestamps. - */ - private static class CacheData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - private final ConcurrentHashMap cache; - private final long lastUpdated; - private final long nextRefresh; - - /** - * Creates a new instance of the CacheData class. - * - * @param cache The cache to store in the object. - * @param lastUpdated The timestamp when the cache was last updated. - * @param nextRefresh The timestamp when the cache will be refreshed. - */ - CacheData(ConcurrentHashMap cache, long lastUpdated, long nextRefresh) { - this.cache = cache; - this.lastUpdated = lastUpdated; - this.nextRefresh = nextRefresh; - } - } -} 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 index 4482bb905bc..ce37f157507 100644 --- 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 @@ -14,22 +14,16 @@ public class RuleConfig { public static final String DEFAULT_SCOPE = "all"; public static final String DEFAULT_USAGE = "all"; - public static final String AZURE_PACKAGE_NAME = "com.azure"; - public static final String AZURE_MAVEN_METADATA_URL = "https://repo1.maven.org/maven2/com/azure/azure-sdk-bom" + - "/maven-metadata.xml"; - public static final String POM_XML = "pom.xml"; - public static final String SUPPORTED_VERSION_URL = "https://raw.githubusercontent" + - ".com/Azure/azure-sdk-for-java/main/eng/versioning/supported_external_dependency_versions.json"; public static final RuleConfig EMPTY_RULE = new RuleConfig(Collections.singletonList(DEFAULT_USAGE), Collections.singletonList(DEFAULT_SCOPE), null, - Collections.emptyMap()); - + Collections.emptyMap(), true); private final List usagesToCheck; private final List scopeToCheck; private final String antiPatternMessage; private final Map regexPatternsToCheck; + private final boolean skipRuleCheck; /** * Constructor for RuleConfig. @@ -38,19 +32,21 @@ public class RuleConfig { * @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) { + Map regexPatternsToCheck, boolean skipRuleCheck) { this.usagesToCheck = usagesToCheck == null || usagesToCheck.isEmpty() - ? Collections.singletonList(DEFAULT_USAGE) + ? Collections.emptyList() : Collections.unmodifiableList(usagesToCheck); this.scopeToCheck = scopeToCheck == null || scopeToCheck.isEmpty() - ? Collections.singletonList(DEFAULT_SCOPE) + ? Collections.emptyList() : Collections.unmodifiableList(scopeToCheck); this.antiPatternMessage = antiPatternMessage; this.regexPatternsToCheck = regexPatternsToCheck == null ? Collections.emptyMap() : Collections.unmodifiableMap(regexPatternsToCheck); + this.skipRuleCheck = skipRuleCheck; } /** @@ -59,11 +55,9 @@ public RuleConfig(List usagesToCheck, List scopeToCheck, String * @return True if the rule should be skipped, false otherwise. */ public boolean skipRuleCheck() { - return this == EMPTY_RULE; + return skipRuleCheck; } - // Getters - /** * This method returns the list of methods to check. * diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java deleted file mode 100644 index 13d09d10306..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/DependencyVersionFileFetcher.java +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.azure.toolkit.intellij.java.sdk.utils; - -import com.azure.json.JsonProviders; -import com.azure.json.JsonReader; -import com.azure.json.JsonToken; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.DependencyVersionsDataCache; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -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.URL; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Class to fetch files from their corresponding data sources - * The class fetches these sources and parses them to get the data. - * The data is used to check the version of the libraries in the pom.xml file against the recommended version. - */ -public class DependencyVersionFileFetcher { - - private static final Logger LOGGER = Logger.getLogger(DependencyVersionFileFetcher.class.getName()); - - private static final DependencyVersionsDataCache> pomCache = new DependencyVersionsDataCache<>("pomCache.ser"); - private static final DependencyVersionsDataCache versionCache = new DependencyVersionsDataCache<>("versionCache.ser"); - private static final DependencyVersionsDataCache>> incompatibleVersionsCache = new DependencyVersionsDataCache<>("incompatibleVersionsCache.ser"); - - /** - * The parsePomFile method fetches the pom.xml file from the URL and parses it to get the dependencies. - * This method is used to fetch the pom.xml file from the URL and parse it to get the dependencies. - * It is used by the UpgradeLibraryVersionCheck inspection. - * - * @param pomUrl The URL of the pom.xml file to fetch - * @return A map of the dependencies in the pom.xml file - */ - public static Map parsePomFile(String pomUrl) { - Map artifactVersionMap = pomCache.get(pomUrl); - if (artifactVersionMap != null) { - return artifactVersionMap; - } - - Document pomDoc = fetchXmlDocument(pomUrl); - NodeList dependencies = pomDoc.getElementsByTagName("dependency"); - artifactVersionMap = new HashMap<>(); - - for (int i = 0; i < dependencies.getLength(); i++) { - NodeList dependency = dependencies.item(i).getChildNodes(); - String groupId = null, artifactId = null, version = null; - - for (int j = 0; j < dependency.getLength(); j++) { - switch (dependency.item(j).getNodeName()) { - case "groupId": - groupId = dependency.item(j).getTextContent(); - break; - case "artifactId": - artifactId = dependency.item(j).getTextContent(); - break; - case "version": - version = dependency.item(j).getTextContent(); - break; - } - } - - if (groupId != null && artifactId != null && version != null) { - String minorVersion = version.substring(0, version.lastIndexOf(".")); - artifactVersionMap.put(groupId + ":" + artifactId, minorVersion); - } - } - - pomCache.put(pomUrl, artifactVersionMap); - return artifactVersionMap; - } - - /** - * The getLatestVersion method fetches the latest Azure Client release versions from Maven Central. - * This method is used to fetch the latest version of the library from the metadata file hosted on Maven Central. - * It is used by the UpgradeLibraryVersionCheck inspection. - * - * @param metadataUrl The URL of the metadata file to fetch - * @return The latest version of the library - */ - public static String getLatestVersion(String metadataUrl) { - String cachedVersion = versionCache.get(metadataUrl); - if (cachedVersion != null) { - return cachedVersion; - } - - Document metadataDoc = fetchXmlDocument(metadataUrl); - NodeList versions = metadataDoc.getElementsByTagName("version"); - String latestVersion = versions.item(versions.getLength() - 1).getTextContent(); - - versionCache.put(metadataUrl, latestVersion); - return latestVersion; - } - - /** - * The fetchXmlDocument method fetches an XML document from a URL and parses it. - * This method is used to fetch the pom.xml file from the URL and parse it to get the dependencies. - * It is used by the UpgradeLibraryVersionCheck inspection. - * - * @param urlString The URL of the XML document to fetch - * @return The parsed XML document - */ - private static Document fetchXmlDocument(String urlString) { - try (InputStream inputStream = new URL(urlString).openStream()) { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - return builder.parse(inputStream); - } catch (IOException | ParserConfigurationException | SAXException e) { - LOGGER.log(Level.SEVERE, "Error fetching or parsing XML from URL: " + urlString, e); - throw new RuntimeException(e); - } - } - - /** - * The loadJsonDataFromUrl method fetches a .json file from GitHub and parses it to get the data. - * This method is used to fetch the data for the libraries in the pom.xml file. It is used by the IncompatibleDependencyCheck inspection. - * - * @param jsonUrl The URL of the .json file to fetch - * @return A map of the data for the libraries - */ - public static Map> loadJsonDataFromUrl(String jsonUrl) { - Map> jsonData = incompatibleVersionsCache.get(jsonUrl); - if (jsonData != null) { - return jsonData; - } - - try (InputStream inputStream = new URL(jsonUrl).openStream(); JsonReader jsonReader = JsonProviders.createReader(inputStream)) { - jsonData = parseJson(jsonReader); - incompatibleVersionsCache.put(jsonUrl, jsonData); - return jsonData; - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error reading JSON from URL: " + jsonUrl, e); - return new HashMap<>(); - } - } - - /** - * Method to parse JSON data into a nested map structure - * The method reads the JSON data from the JsonReader and parses it to get the data for the libraries. - * The data is in a Map> format where the key is the group and the value is a set of artifactIds. - * For example, the data for the Jackson library is in the format - * "jackson_2.10: [com.fasterxml.jackson.core:jackson-annotations, com.fasterxml.jackson.core:jackson-core, com.fasterxml.jackson.core:jackson-databind]". - * - * @param jsonReader The JsonReader object to read the JSON data - * @return A map of the data for the libraries - * @throws IOException If an error occurs while reading the JSON data - */ - private static Map> parseJson(JsonReader jsonReader) throws IOException { - Map> versionData = new ConcurrentHashMap<>(); - if (jsonReader.nextToken() == JsonToken.START_OBJECT) { - while (jsonReader.nextToken() != JsonToken.END_OBJECT) { - String groupKey = jsonReader.getFieldName(); - Set groupSet = new HashSet<>(); - - if (jsonReader.nextToken() == JsonToken.START_ARRAY) { - while (jsonReader.nextToken() != JsonToken.END_ARRAY) { - if (jsonReader.nextToken() == JsonToken.FIELD_NAME) { - groupSet.add(jsonReader.getFieldName()); - } - while (jsonReader.nextToken() != JsonToken.END_OBJECT) { - // Skip remaining tokens in the object - } - } - } - versionData.put(groupKey, groupSet); - } - } - return versionData; - } -} 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..577d62cdd1d --- /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,205 @@ +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 com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; +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 || !expression.isValid()) { + 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; + } + + public static boolean isAzurePackage(String classQualifiedName) { + return classQualifiedName.startsWith(AZURE_PACKAGE_NAME); + } + + public 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; + + } + + 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; + } + + public static boolean checkIfInUsages(List usages, String methodName) { + if (usages.isEmpty()) { + return true; + } + return usages.stream().anyMatch(usage -> usage.equals(methodName)); + } + + public static boolean checkIfInScope(List scope, String classQualifiedName) { + if (scope.isEmpty()) { + return true; + } + return scope.stream().anyMatch(classQualifiedName::contains); + } + + public static boolean isItDiscouragedAPI(PsiMethod method, List usages, List scopes) { + + // 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); + } + + 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; + } +} 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 69a4236f545..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,10 +1,6 @@ package com.microsoft.azure.toolkit.intellij.java.sdk.utils; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiMethodCallExpression; -import com.intellij.psi.util.PsiTreeUtil; import com.microsoft.azure.toolkit.intellij.java.sdk.models.MavenArtifactDetails; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -32,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. @@ -104,24 +100,4 @@ public static String getLatestArtifactVersion(String groupId, String artifactId) return mavenArtifactDetails.getVersion(); } } - - /** - * This method checks if the method call is an Azure client method call - * by checking the containing class of the method call - * - * @param methodCall the method call to check - * @return boolean true if the method call is an Azure client method call, false otherwise - */ - public static boolean isAzureClientMethodCall(PsiMethodCallExpression methodCall) { - - // Get the containing class of the method call - PsiClass containingClass = PsiTreeUtil.getParentOfType(methodCall, PsiClass.class); - - if (containingClass != null) { - String className = containingClass.getQualifiedName(); - // Check if the class name belongs to the com.azure namespace or any specific Azure SDK namespace - return className != null && className.startsWith(RuleConfig.AZURE_PACKAGE_NAME); - } - 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/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 index 016998f3524..78eac24e47b 100644 --- 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 @@ -10,18 +10,18 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import static com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig.EMPTY_RULE; + public class RuleConfigLoader { private static final String CONFIG_FILE_PATH = "./META-INF/ruleConfigs.json"; private static final Logger LOGGER = Logger.getLogger(RuleConfigLoader.class.getName()); private static final RuleConfigLoader INSTANCE; - + private static boolean initializationFailed = false; private final Map ruleConfigs; static { @@ -30,30 +30,52 @@ public class RuleConfigLoader { tempInstance = new RuleConfigLoader(CONFIG_FILE_PATH); } catch (IOException e) { tempInstance = null; + initializationFailed = true; LOGGER.log(Level.SEVERE, "Failed to initialize RuleConfigLoader: " + e.getMessage(), e); } INSTANCE = tempInstance; } + private RuleConfigLoader() { + this.ruleConfigs = new HashMap<>(); + } + private RuleConfigLoader(String filePath) throws IOException { this.ruleConfigs = loadRuleConfigurations(filePath); } + /** + * Gets the singleton instance of RuleConfigLoader. + * + * @return The singleton instance of RuleConfigLoader. + */ public static RuleConfigLoader getInstance() { - if (INSTANCE == null) { - throw new IllegalStateException("RuleConfigLoader initialization failed. Check logs for details."); + if (initializationFailed) { + LOGGER.log(Level.WARNING, "RuleConfigLoader initialization failed. Returning default instance."); + return new RuleConfigLoader(); } return INSTANCE; } + /** + * Gets the rule configuration for the specified key. + * + * @param key The key of the rule configuration. + * @return The rule configuration for the specified key. + */ public RuleConfig getRuleConfig(String key) { - return ruleConfigs.getOrDefault(key, RuleConfig.EMPTY_RULE); + if (initializationFailed) { + return EMPTY_RULE; + } + return ruleConfigs.getOrDefault(key, EMPTY_RULE); } private Map loadRuleConfigurations(String filePath) throws IOException { InputStream configStream = getClass().getClassLoader().getResourceAsStream(filePath); if (configStream == null) { - throw new IOException("Configuration file not found: " + filePath); + initializationFailed = true; + LOGGER.log(Level.WARNING, "Rule configuration file not found: " + filePath); + return new HashMap<>(); } ObjectMapper objectMapper = new ObjectMapper(); @@ -88,41 +110,11 @@ private RuleConfig parseRuleConfig(JsonNode ruleNode) { 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); + return new RuleConfig(usages, scope, antiPatternMessage, regexPatterns, skipRuleCheck); } - //private Map loadRuleConfigurations(String filePath) throws IOException { - // InputStream configStream = getClass().getClassLoader().getResourceAsStream(filePath); - // if (configStream == null) { - // throw new IOException("Configuration file not found: " + filePath); - // } - // - // 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(); - // 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); - // String solution = ruleNode.path("solution").asText(null); - // - // Map regexPatterns = parseRegexPatterns(ruleNode.path("regexPatterns")); - // - // return new RuleConfig(usages, scope, antiPatternMessage, regexPatterns); - //} - private List parseStringOrArray(JsonNode node) { List values = new ArrayList<>(); if (node.isTextual()) { @@ -140,108 +132,4 @@ private Map parseRegexPatterns(JsonNode regexPatternsNode) { } return regexPatterns; } - - //private List> parseCheckItems(JsonNode checkItemsNode) { - // List> result = new ArrayList<>(); - // if (checkItemsNode.isArray()) { - // for (JsonNode itemNode : checkItemsNode) { - // if (itemNode.isObject()) { - // result.add(parseToMap(itemNode)); - // } else { - // throw new IllegalArgumentException("Each item in 'checkItems' must be a JSON object."); - // } - // } - // } else { - // throw new IllegalArgumentException("'checkItems' must be an array of JSON objects."); - // } - // return result; - //} - - //public Object parseCheckItems(JsonNode entryNode) { - // // The unified object that will contain either a List or Map depending on the input - // Map result = new HashMap<>(); - // - // if (entryNode.isArray()) { - // if (entryNode.size() > 0 && entryNode.get(0).isObject()) { - // // Case: List of Maps - // result.put("itemsAsMap", getItemsAsMap(entryNode)); - // } else { - // // Case: List of Strings - // result.put("itemsAsList", getItemsAsList(entryNode)); - // } - // } else if (entryNode.isTextual()) { - // // Case: Single String - // List singleItemList = new ArrayList<>(); - // singleItemList.add(entryNode.asText()); - // result.put("itemsAsList", singleItemList); - // } else { - // throw new IllegalArgumentException("Invalid entry format. Must be a string, list of strings, or list of objects."); - // } - // - // return result; // Return a unified object (Map) for flexible extraction - //} - - // Helper to extract items as a List of Strings - public List getItemsAsList(JsonNode entryNode) { - List result = new ArrayList<>(); - if (entryNode.isArray()) { - for (JsonNode itemNode : entryNode) { - if (itemNode.isTextual()) { - result.add(itemNode.asText()); - } else { - throw new IllegalArgumentException("Array must contain only strings."); - } - } - } - return result; - } - - // Helper to extract items as a List of Maps - public List> getItemsAsMap(JsonNode entryNode) { - List> result = new ArrayList<>(); - if (entryNode.isArray()) { - for (JsonNode itemNode : entryNode) { - if (itemNode.isObject()) { - result.add(parseToMap(itemNode)); - } else { - throw new IllegalArgumentException("List must contain only objects for 'List>' format."); - } - } - } - return result; - } - - private Object parseCheckItems(JsonNode checkItemsNode) { - if (checkItemsNode.isArray()) { - return parseToList(checkItemsNode); - } else if (checkItemsNode.isObject()) { - return parseToMap(checkItemsNode); - } - return checkItemsNode.asText(null); - } - - private Map parseToMap(JsonNode objectNode) { - Map map = new HashMap<>(); - objectNode.fields().forEachRemaining(entry -> { - JsonNode value = entry.getValue(); - if (value.isTextual()) { - map.put(entry.getKey(), value.asText()); - } else { - throw new IllegalArgumentException("All values in the 'checkItems' object must be strings."); - } - }); - return map; - } - - private List parseToList(JsonNode arrayNode) { - List list = new ArrayList<>(); - arrayNode.forEach(node -> list.add(node.asText())); - return list; - } - - private Set parseToSet(JsonNode arrayNode) { - Set set = new HashSet<>(); - arrayNode.forEach(node -> set.add(node.asText())); - return set; - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java deleted file mode 100644 index 07fdfdbe6f3..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/VersionUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.azure.toolkit.intellij.java.sdk.utils; - -import java.util.Map; -import java.util.Set; - -/** - * Utility class for pom version related operations. - */ -public class VersionUtils { - - /** - * Retrieves the recommended version for a given library. - * - * @param libraryName The name of the library. - * @param recommendedVersionMap The map containing the recommended versions. - * - * @return The recommended version of the library, or null if not found. - */ - public static String getRecommendedVersion(String libraryName, Map recommendedVersionMap) { - return recommendedVersionMap == null ? null : recommendedVersionMap.get(libraryName); - } - - /** - * Extracts the minor version from a given version string. - * - * @param version The version string. - * @return The minor version in the format "major.minor", or null if the input is invalid. - */ - public static String extractMinorVersion(String version) { - if (version == null || !version.matches("\\d+\\.\\d+.*")) { - return null; - } - String[] parts = version.split("\\."); - return parts.length >= 2 ? parts[0] + "." + parts[1] : null; - } - - /** - * Method to get the version group for the library. The version group is used to get the compatible versions for - * the library. The version group is determined by the major and minor version of the library. Eg if the major - * version is 2 and the minor version is 10, the version group is "jackson_2.10". - * - * @param fullName The full name of the library eg "com.azure:azure-core" - * @param currentVersion The current version of the library - * @param fileContent The content of the version file (as a map) - * - * @return The version group for the library - */ - public static String getGroupVersion(String fullName, String currentVersion, Map> fileContent) { - // Split currentVersion to extract major and minor version - String[] versionParts = currentVersion.split("\\."); - String majorVersion = versionParts[0]; - String minorVersion = versionParts.length > 1 ? versionParts[1] : ""; - String versionSuffix = "_" + majorVersion + "." + minorVersion; - - // Search the file content for the version group - String versionGroup = null; - for (Map.Entry> entry : fileContent.entrySet()) { - // Check if the set of artifactIds contains the fullName and the corresponding key ends with the versionSuffix - if (entry.getValue().contains(fullName) && entry.getKey().endsWith(versionSuffix)) { - versionGroup = entry.getKey(); - break; - } - } - return versionGroup; - } - - /** - * Method to check if two versions are incompatible based on their minor version. - * - * @param currentVersion The current version of the library - * @param recommendedVersion The recommended version of the library - * @return true if the versions are incompatible, false otherwise - */ - public static boolean isIncompatibleVersion(String currentVersion, String recommendedVersion) { - String[] currentVersionParts = currentVersion.split("\\."); - String[] recommendedVersionParts = recommendedVersion.split("\\."); - - boolean isIncompatible = false; - for (int i = 0; i < Math.min(currentVersionParts.length, recommendedVersionParts.length); i++) { - if (!currentVersionParts[i].equals(recommendedVersionParts[i])) { - isIncompatible = true; - break; - } - } - return isIncompatible; - } -} 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 94fde0d3f3d..2f60ea01dad 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 @@ -8,173 +8,111 @@ implementation="com.microsoft.azure.toolkit.intellij.java.sdk.MavenProjectReportGenerator"/> + level="WARNING" + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.ConnectionStringCheck"> - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.ServiceBusReceiverAsyncClientCheck"> - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.EventHubConsumerAsyncClientCheck"> - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.GetCompletionsCheck"> - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DisableAutoCompleteCheck"> - - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DynamicClientCreationCheck"> - - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.GetSyncPollerOnPollerFluxCheck"> - - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.HardcodedAPIKeysAndTokensCheck"> - - - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.SingleOperationInLoopTextAnalyticsCheck"> - - - + level="INFO" + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.StorageUploadWithoutLengthCheck"> - + implementationClass="com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UpdateCheckpointAsyncSubscribeCheck"> - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json index a93eed55597..56333211da1 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/META-INF/ruleConfigs.json @@ -1,20 +1,8 @@ { - "StorageUploadWithoutLengthCheck": { - "usages": [ - "upload", - "uploadWithResponse" - ], - "scope": "com.azure.storage.", - "antiPatternMessage": "Azure Storage upload API without length parameter detected. Use upload API with length parameter instead." - }, - "DisableAutoCompleteCheck": { - "usages": "disableAutoComplete", - "scope": [ - "ServiceBusReceiverClient", - "ServiceBusReceiverAsyncClient", - "ServiceBusProcessorClient" - ], - "antiPatternMessage": "Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion." + "UpdateCheckpointAsyncSubscribeCheck": { + "usages": "updateCheckpointAsync", + "scope": "EventBatchContext", + "antiPatternMessage": "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`" }, "DynamicClientCreationCheck": { "usages": [ @@ -23,47 +11,17 @@ ], "antiPatternMessage": "Dynamic client creation detected. Create a single client instance and reuse it instead." }, - "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 Azure Key Credential for API key based authentication." - }, - "GetSyncPollerOnPollerFluxCheck": { - "usages": "getSyncPoller", - "antiPatternMessage": "Use of getSyncPoller() on a PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks." - }, - "ServiceBusReceiveModeCheck": { - "scope": [ - "ServiceBusReceiverClient", - "ServiceBusReceiverAsyncClient", - "ServiceBusProcessorClient" - ], - "usages": [ - "receiveMode", - "prefetchCount" - ], - "antiPatternMessage": "A high prefetch value in PEEK_LOCK detected. We recommend a prefetch value of 0 or 1 for efficient message retrieval." - }, "DetectDiscouragedAPIUsageCheck": { "hasDerivedRules": true, "ConnectionStringCheck": { "usages": "connectionString", - "antiPatternMessage": "Connection String detected. Use 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 Azure Key Credential / Connection Strings based authentication" + "antiPatternMessage": "Connection String API usage detected. Use 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 detected. Use the getChatCompletions API instead." + "antiPatternMessage": "getCompletions API usage detected. Use the getChatCompletions API instead." } }, "DetectDiscouragedClientCheck": { @@ -77,7 +35,34 @@ "antiPatternMessage": "Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers." } }, - "SingleOperationInLoopCheck": { + "DisableAutoCompleteCheck": { + "usages": "disableAutoComplete", + "scope": [ + "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient" + ], + "antiPatternMessage": "Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion." + }, + "GetSyncPollerOnPollerFluxCheck": { + "usages": "getSyncPoller", + "antiPatternMessage": "Use of getSyncPoller() on a PollerFlux detected. Directly use SyncPoller 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", @@ -87,34 +72,15 @@ "analyzeSentimentBatch" ], "scope": "com.azure.ai.textanalytics", - "antiPatternMessage": "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + "antiPatternMessage": "This SDK provides a batch operation API, use it to perform multiple actions in a single request." }, - "AsyncSubscribeChecker": { - "hasDerivedRules": true, - "UpdateCheckpointAsyncSubscribeChecker": { - "usages": "updateCheckpointAsync", - "scope": "EventBatchContext", - "antiPatternMessage": "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`" - } - }, - "KustoQueriesWithTimeIntervalInQueryStringCheck": { - "regexPatterns": { - "KQL_ANTI_PATTERN_AGO": ".*ago\\(", - "KQL_ANTI_PATTERN_DATETIME": ".*datetime\\s*\\(", - "KQL_ANTI_PATTERN_NOW": ".*now\\(", - "KQL_ANTI_PATTERN_START_OF_PERIOD": ".*startofday\\(\\)|.*startofmonth\\(\\)|.*startofyear\\(\\)", - "KQL_ANTI_PATTERN_BETWEEN": ".*between\\(datetime\\(" - }, - "antiPatternMessage": "KQL queries with time intervals in the query string detected.", - "solution": "Use the QueryTimeInterval parameter in the client method parameters to specify the time interval for the query" - }, - "EndpointOnNonAzureOpenAIAuthCheck": { + "StorageUploadWithoutLengthCheck": { "usages": [ - "endpoint", - "credential" + "upload", + "uploadWithResponse" ], - "scope": "KeyCredential", - "antiPatternMessage": "Endpoint API should not be used with KeyCredential for non-Azure OpenAI clients." + "scope": "com.azure.storage.", + "antiPatternMessage": "Azure Storage upload API without length parameter detected. Consider using upload API with length parameter instead." }, "UseOfBlockOnAsyncClientsCheck": { "usages": [ @@ -133,11 +99,5 @@ "reactor.core.publisher.Mono" ], "antiPatternMessage": "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead." - }, - "UpgradeLibraryVersionCheck": { - "antiPatternMessage": "A newer stable minor version of '{{fullName}}' is available. We recommend you update to version {{recommendedVersion}}.x." - }, - "IncompatibleDependencyCheck": { - "antiPatternMessage": "The version of '{{fullName}}' is incompatible with other dependencies of the same library defined in the pom.xml. We recommend you update to version {{recommendedVersion}}.x of the same library release group." } -} \ 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/AbstractLibraryVersionCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java deleted file mode 100644 index 7032e53fc85..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AbstractLibraryVersionCheckTest.java +++ /dev/null @@ -1,174 +0,0 @@ -// 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.lang.StdLanguages; -import com.intellij.openapi.project.Project; -import com.intellij.psi.FileViewProvider; -import com.intellij.psi.PsiElementVisitor; -import com.intellij.psi.xml.XmlFile; -import com.intellij.psi.xml.XmlTag; -import com.intellij.psi.xml.XmlTagValue; -import java.util.HashSet; -import java.util.List; -import java.util.stream.Stream; -import org.jetbrains.idea.maven.project.MavenProjectsManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; - -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.contains; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/* - * This test class is used to test the AbstractLibraryVersionCheck class. - */ -public class AbstractLibraryVersionCheckTest { - - @Mock - private ProblemsHolder mockHolder; - - @Mock - private XmlFile mockFile; - - @BeforeEach - public void setup() { - mockHolder = mock(ProblemsHolder.class); - mockFile = mock(XmlFile.class); - - IncompatibleDependencyCheck.encounteredVersionGroups = new HashSet<>(List.of("jackson_2.10", "gson_2.10")); - } - - static Stream testCases() { - return Stream.of( - new TestCase("com.azure", "azure-messaging-servicebus", "7.0.0", 1, "7.17", UpgradeLibraryVersionCheck.LibraryVersionVisitor.class), - new TestCase("com.azure", "azure-messaging-servicebus", "7.17.1", 0, "7.17", UpgradeLibraryVersionCheck.LibraryVersionVisitor.class), - new TestCase("com.example", "example-lib", "", 0, null, UpgradeLibraryVersionCheck.LibraryVersionVisitor.class), - new TestCase("com.google.code.gson", "gson", "2.9.0", 1, "2.10", IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class), - new TestCase("com.fasterxml.jackson.core", "jackson-databind", "2.10.0", 0, "2.10", IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class), - new TestCase("com.fasterxml.jackson.core", "jackson-databind", "3.0.0", 0, null, IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) - ); - } - - @ParameterizedTest - @MethodSource("testCases") - void testLibraryVersionChecks(TestCase testCase) { - PsiElementVisitor visitor = createVisitor(testCase.visitorClass, mockHolder); - - XmlTag versionTag = setupMockXml(testCase.groupID, testCase.artifactID, testCase.version); - visitor.visitFile(mockFile); - - if (testCase.visitorClass == UpgradeLibraryVersionCheck.LibraryVersionVisitor.class) { - // Validate the full message if warnings are expected - if (testCase.expectedInvocations > 0 && testCase.expectedMessage != null) { - String expectedMessage = String.format( - "A newer stable minor version of '%s:%s' is available. We recommend you update to version %s.x.", - testCase.groupID, testCase.artifactID, testCase.expectedMessage - ); - verify(mockHolder).registerProblem(versionTag, expectedMessage); - } - } else if (testCase.visitorClass == IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) { - if (testCase.expectedInvocations > 0 && testCase.expectedMessage != null) { - String expectedMessage = String.format( - "The version of '%s:%s' is incompatible with other dependencies of the same " + - "library defined in the pom.xml. We recommend you update to version %s.x of the same library " + - "release group.", - testCase.groupID, testCase.artifactID, testCase.expectedMessage - ); - verify(mockHolder).registerProblem(versionTag, expectedMessage); - } - - } - } - - private PsiElementVisitor createVisitor(Class visitorClass, ProblemsHolder holder) { - if (visitorClass == UpgradeLibraryVersionCheck.LibraryVersionVisitor.class) { - return new UpgradeLibraryVersionCheck().new LibraryVersionVisitor(holder); - } else if (visitorClass == IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) { - return new IncompatibleDependencyCheck().new IncompatibleDependencyVisitor(holder); - } - throw new IllegalArgumentException("Unsupported visitor class: " + visitorClass); - } - - private XmlTag setupMockXml(String groupIDValue, String artifactIDValue, String versionIDValue) { - Project project = mock(Project.class); - MavenProjectsManager mavenProjectsManager = mock(MavenProjectsManager.class); - FileViewProvider viewProvider = mock(FileViewProvider.class); - XmlTag rootTag = mock(XmlTag.class); - - XmlTag dependenciesTag = mock(XmlTag.class); - XmlTag[] dependenciesTags = new XmlTag[]{dependenciesTag}; - - XmlTag dependencyTag = mock(XmlTag.class); - XmlTag[] dependencyTags = new XmlTag[]{dependencyTag}; - - XmlTag groupIdTag = mock(XmlTag.class); - XmlTagValue groupIdValue = mock(XmlTagValue.class); - XmlTag artifactIdTag = mock(XmlTag.class); - XmlTagValue artifactIdValue = mock(XmlTagValue.class); - XmlTag versionTag = mock(XmlTag.class); - XmlTagValue versionValue = mock(XmlTagValue.class); - - when(mockFile.getName()).thenReturn("pom.xml"); - when(mockFile.getProject()).thenReturn(project); - when(MavenProjectsManager.getInstance(project)).thenReturn(mavenProjectsManager); - when(mavenProjectsManager.isMavenizedProject()).thenReturn(true); - - when(mockFile.getViewProvider()).thenReturn(viewProvider); - when(viewProvider.getPsi(StdLanguages.XML)).thenReturn(mockFile); - when(mockFile.getRootTag()).thenReturn(rootTag); - when(rootTag.getName()).thenReturn("project"); - - when(rootTag.findSubTags("dependencies")).thenReturn(dependenciesTags); - when(dependenciesTag.findSubTags("dependency")).thenReturn(dependencyTags); - - when(dependencyTag.findFirstSubTag("groupId")).thenReturn(groupIdTag); - when(dependencyTag.findFirstSubTag("artifactId")).thenReturn(artifactIdTag); - when(dependencyTag.findFirstSubTag("version")).thenReturn(versionTag); - - when(groupIdTag.getValue()).thenReturn(groupIdValue); - when(artifactIdTag.getValue()).thenReturn(artifactIdValue); - when(versionTag.getValue()).thenReturn(versionValue); - - when(groupIdValue.getText()).thenReturn(groupIDValue); - when(artifactIdValue.getText()).thenReturn(artifactIDValue); - when(versionValue.getText()).thenReturn(versionIDValue); - - return versionTag; - } - - private void verifyProblemsRegistered(TestCase testCase, XmlTag versionTag) { - if (testCase.expectedInvocations > 0) { - verify(mockHolder, times(testCase.expectedInvocations)).registerProblem(eq(versionTag), contains(testCase.expectedMessage)); - } else { - verify(mockHolder, never()).registerProblem(eq(versionTag), anyString()); - } - } - - private static class TestCase { - String groupID; - String artifactID; - String version; - int expectedInvocations; - String expectedMessage; - Class visitorClass; - - TestCase(String groupID, String artifactID, String version, int expectedInvocations, String expectedMessage, Class visitorClass) { - this.groupID = groupID; - this.artifactID = artifactID; - this.version = version; - this.expectedInvocations = expectedInvocations; - this.expectedMessage = expectedMessage; - this.visitorClass = visitorClass; - } - } -} \ 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/AsyncSubscribeCheckerTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeCheckerTest.java deleted file mode 100644 index 1c86618d476..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/AsyncSubscribeCheckerTest.java +++ /dev/null @@ -1,91 +0,0 @@ -// 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.PsiMethodCallExpression; -import com.intellij.psi.PsiParameter; -import com.intellij.psi.PsiReferenceExpression; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -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 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; - -/** - * Tests for {@link AsyncSubscribeChecker} and derived classes. - */ -public class AsyncSubscribeCheckerTest { - @Mock - private ProblemsHolder mockHolder; - - @Mock - private PsiMethodCallExpression mockMethodCallExpression; - - @BeforeEach - public void setUp() { - mockHolder = mock(ProblemsHolder.class); - mockMethodCallExpression = mock(PsiMethodCallExpression.class); - } - - @ParameterizedTest - @MethodSource("provideTestCases") - public void testWithParameterizedCases(String packageName, String mainMethodFound, - int numOfInvocations, String followingMethod, String objectType, String expectedMessage) { - JavaElementVisitor visitor = new UpdateCheckpointAsyncSubscribeChecker.UpdateCheckpointAsyncVisitor(mockHolder, - true); - setupMockMethodCall(packageName, mainMethodFound, numOfInvocations, followingMethod, - objectType, - expectedMessage); - visitor.visitMethodCallExpression(mockMethodCallExpression); - verify(mockHolder, times(numOfInvocations)).registerProblem(eq(mockMethodCallExpression), - contains(expectedMessage)); - } - - private static Stream provideTestCases() { - return Stream.of( - new Object[] {"com.azure", "updateCheckpointAsync", 1, - "subscribe", "EventBatchContext", - "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`"}, - new Object[] {"com.azure", "updateCheckpointAsync", 0, "notSubscribe", "EventBatchContext", - "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous " + - "version `updateCheckpoint()`"} - ); - } - - 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); - - when(mockMethodCallExpression.getMethodExpression()).thenReturn(mockReferenceExpression); - when(mockReferenceExpression.getReferenceName()).thenReturn(mainMethodFound); - when(mockMethodCallExpression.getParent()).thenReturn(mockReferenceExpression); - when(mockReferenceExpression.getParent()).thenReturn(grandParentMethodCalLExpression); - when(grandParentMethodCalLExpression.getMethodExpression()).thenReturn(parentReferenceExpression); - when(parentReferenceExpression.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(RuleConfig.AZURE_PACKAGE_NAME); - } -} \ 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/DetectDiscouragedAPIUsageCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java index 785565a621e..6778d5e9577 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java @@ -7,6 +7,7 @@ import com.intellij.psi.JavaElementVisitor; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiMethodCallExpression; import com.intellij.psi.PsiReferenceExpression; @@ -26,7 +27,7 @@ import static org.mockito.Mockito.when; /** - * Tests for {@link DetectDiscouragedAPIUsageCheck} and derived classes. + * Tests for {@link GetCompletionsCheck} and {@link ConnectionStringCheck} . */ public class DetectDiscouragedAPIUsageCheckTest { @@ -52,24 +53,20 @@ public void setup() { @ParameterizedTest @MethodSource("provideTestCases") public void detectsDiscouragedAPIUsage(TestCase testCase) { + JavaElementVisitor visitor = createVisitor(testCase.visitorClass); setupMockAPI(testCase.methodToCheck, testCase.numOfInvocations, testCase.packageName, testCase.suggestionMessage); - mockVisitor = createVisitor(testCase.ruleConfig); - mockVisitor.visitElement(methodCallExpression); + visitor.visitMethodCallExpression(methodCallExpression); verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(problemElement), eq(testCase.suggestionMessage)); } - private JavaElementVisitor createVisitor(RuleConfig ruleConfig) { - return new DetectDiscouragedAPIUsageCheck.DetectDiscouragedAPIUsageVisitor(mockHolder, ruleConfig); - } - private static RuleConfig getGetCompletionsConfig() { RuleConfig getCompletionsConfig = mock(RuleConfig.class); when(getCompletionsConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getCompletions")); when(getCompletionsConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure.ai.openai")); when(getCompletionsConfig.getAntiPatternMessage()).thenReturn( - "getCompletions API detected. Use the getChatCompletions API instead."); + "getCompletions API usage detected. Use the getChatCompletions API instead."); return getCompletionsConfig; } @@ -78,7 +75,7 @@ private static RuleConfig getConnectionStringConfig() { when(connectionStringConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("connectionString")); when(connectionStringConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure")); when(connectionStringConfig.getAntiPatternMessage()).thenReturn( - "Connection String detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication)."); + "Connection String API usage detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication)."); return connectionStringConfig; } @@ -98,28 +95,46 @@ private void setupMockAPI(String methodToCheck, int numOfInvocations, String pac when(methodExpression.getReferenceNameElement()).thenReturn(problemElement); } + private JavaElementVisitor createVisitor(Class visitorClass) { + if (visitorClass == GetCompletionsCheck.GetCompletionsVisitor.class) { + return new GetCompletionsCheck.GetCompletionsVisitor(mockHolder); + } else if (visitorClass == ConnectionStringCheck.ConnectionStringCheckVisitor.class) { + return new ConnectionStringCheck.ConnectionStringCheckVisitor(mockHolder); + } + throw new IllegalArgumentException("Unsupported visitor class: " + visitorClass); + } + private static Stream provideTestCases() { return Stream.of( - new TestCase(getConnectionStringConfig(), "connectionString", "com.azure", "Connection String detected. " + - "Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication).", + new TestCase(ConnectionStringCheck.ConnectionStringCheckVisitor.class, + getConnectionStringConfig(), + "connectionString", "com.azure", + "Connection String API usage detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication).", 1), - new TestCase(getGetCompletionsConfig(), "getCompletions", "com.azure.ai.openai", "getCompletions API " + + new TestCase(GetCompletionsCheck.GetCompletionsVisitor.class, + getGetCompletionsConfig(), "getCompletions", "com.azure.ai.openai", "getCompletions API " + "detected. Use the getChatCompletions API instead.", 1), - new TestCase(getConnectionStringConfig(), "allowedMethod", "com.azure", "", 0), - new TestCase(getConnectionStringConfig(), "connectionString", "com.microsoft.azure", "", 0), - new TestCase(getGetCompletionsConfig(), "getCompletions", "com.azure.other", "", 0) + new TestCase(ConnectionStringCheck.ConnectionStringCheckVisitor.class, + getConnectionStringConfig(), "allowedMethod", "com.azure", "", 0), + new TestCase(ConnectionStringCheck.ConnectionStringCheckVisitor.class, + getConnectionStringConfig(), "connectionString", "com.microsoft.azure", "", 0), + new TestCase(GetCompletionsCheck.GetCompletionsVisitor.class, + getGetCompletionsConfig(), "getCompletions", "com.azure.other", "", 0) ); } private static class TestCase { + Class visitorClass; String methodToCheck; String packageName; String suggestionMessage; int numOfInvocations; RuleConfig ruleConfig; - TestCase(RuleConfig ruleConfig, String methodToCheck, String packageName, String suggestionMessage, + TestCase(Class visitorClass, RuleConfig ruleConfig, String methodToCheck, String packageName, + String suggestionMessage, int numOfInvocations) { + this.visitorClass = visitorClass; this.methodToCheck = methodToCheck; this.packageName = packageName; this.suggestionMessage = suggestionMessage; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java index f90ea3f5ae2..4735554201d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java @@ -5,10 +5,10 @@ import com.intellij.codeInspection.ProblemsHolder; import com.intellij.psi.JavaElementVisitor; -import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; import com.intellij.psi.PsiType; import com.intellij.psi.PsiTypeElement; -import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.DetectDiscouragedClientCheck.DetectDiscouragedClientVisitor; import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; import java.util.Collections; import java.util.stream.Stream; @@ -60,43 +60,56 @@ public void setUp() { } @ParameterizedTest - @MethodSource("provideTestCases") - public void detectsDiscouragedClientUsage(TestCase testCase) { - mockVisitor = createVisitor(testCase.ruleConfig); - setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck, - testCase.suggestionMessage); - mockVisitor.visitTypeElement(mockTypeElement); + @MethodSource("provideServiceBusReceiverAsyncClientTestCases") + void detectsServiceBusReceiverAsyncClientUsage(TestCase testCase) { + JavaElementVisitor visitor = new ServiceBusReceiverAsyncClientCheck().buildVisitor(mockHolder, false); + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck, testCase.suggestionMessage); + visitor.visitTypeElement(mockTypeElement); verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), contains(testCase.suggestionMessage)); } - private JavaElementVisitor createVisitor(RuleConfig ruleConfig) { - return new DetectDiscouragedClientVisitor(mockHolder, ruleConfig); + @ParameterizedTest + @MethodSource("provideEventHubConsumerAsyncClientTestCases") + void detectsEventHubConsumerAsyncClientUsage(TestCase testCase) { + JavaElementVisitor visitor = new EventHubConsumerAsyncClientCheck().buildVisitor(mockHolder, false); + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck, testCase.suggestionMessage); + visitor.visitTypeElement(mockTypeElement); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), contains(testCase.suggestionMessage)); } - private void setupMockElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck, String suggestionMessage) { - PsiType mockType = mock(PsiType.class); + private static Stream provideEventHubConsumerAsyncClientTestCases() { + RuleConfig eventHubConsumerAsyncClientConfig = mock(RuleConfig.class); + when(eventHubConsumerAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("EventHubConsumerAsyncClient")); + when(eventHubConsumerAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead."); - when(mockTypeElement.getType()).thenReturn(mockType); - when(mockType.getPresentableText()).thenReturn(clientToCheck); + return Stream.of( + new TestCase("EventHubConsumerAsyncClient", "Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers.", 1, eventHubConsumerAsyncClientConfig), + new TestCase("EventProcessorClient", "", 0, eventHubConsumerAsyncClientConfig), + new TestCase("", "", 0, eventHubConsumerAsyncClientConfig) + ); } - private static Stream provideTestCases() { + private static Stream provideServiceBusReceiverAsyncClientTestCases() { RuleConfig serviceBusReceiverAsyncClientConfig = mock(RuleConfig.class); when(serviceBusReceiverAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("ServiceBusReceiverAsyncClient")); when(serviceBusReceiverAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead."); - RuleConfig eventHubConsumerAsyncClientConfig = mock(RuleConfig.class); - when(eventHubConsumerAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("EventHubConsumerAsyncClient")); - when(eventHubConsumerAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers."); - return Stream.of( new TestCase("ServiceBusReceiverAsyncClient", "Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead.", 1, serviceBusReceiverAsyncClientConfig), - new TestCase("EventHubConsumerAsyncClient", "Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers.", 1, eventHubConsumerAsyncClientConfig), new TestCase("ServiceBusProcessorClient", "", 0, serviceBusReceiverAsyncClientConfig), new TestCase("", "", 0, serviceBusReceiverAsyncClientConfig) ); } + private void setupMockElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck, String suggestionMessage) { + 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; String suggestionMessage; 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 index bf7ba90784a..24fca61740b 100644 --- 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 @@ -31,7 +31,6 @@ * 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 { @Mock diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.java deleted file mode 100644 index ecb76671597..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EndpointOnNonAzureOpenAIAuthCheckTest.java +++ /dev/null @@ -1,112 +0,0 @@ -// 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.PsiExpression; -import com.intellij.psi.PsiExpressionList; -import com.intellij.psi.PsiJavaCodeReferenceElement; -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 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 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 EndpointOnNonAzureOpenAIAuthCheck class. - * The EndpointOnNonAzureOpenAIAuthCheck class is a LocalInspectionTool that checks if the endpoint method is used with KeyCredential for non-Azure OpenAI clients. - * If the endpoint method is used with KeyCredential for non-Azure OpenAI clients, a warning is registered. - * An example that should be flagged is: - * OpenAI Client client = new OpenAIClientBuilder() - * .credential(new KeyCredential("key")) - * .endpoint("endpoint") - * .buildClient(); - */ -public class EndpointOnNonAzureOpenAIAuthCheckTest { - - @Mock - private ProblemsHolder mockHolder; - - @Mock - private PsiMethodCallExpression mockMethodCall; - - @BeforeEach - public void setup() { - mockHolder = mock(ProblemsHolder.class); - mockMethodCall = mock(PsiMethodCallExpression.class); - } - - @ParameterizedTest - @MethodSource("testCases") - public void testEndpointOnNonAzureOpenAIClient(TestCase testCase) { - - setupMockMethodExpression(testCase.numOfInvocation, testCase.endpoint, testCase.credential, - testCase.keyCredentialPackageName, testCase.azurePackageName); - EndpointOnNonAzureOpenAIAuthCheck.EndpointOnNonAzureOpenAIAuthVisitor visitor = new EndpointOnNonAzureOpenAIAuthCheck.EndpointOnNonAzureOpenAIAuthVisitor(mockHolder); - visitor.visitMethodCallExpression(mockMethodCall); - - verify(mockHolder, times(testCase.numOfInvocation)).registerProblem(mockMethodCall, "Endpoint API should not " + - "be used with KeyCredential for non-Azure OpenAI clients."); - } - - private static Stream testCases() { - return Stream.of( - new TestCase(1, "endpoint", "credential", "KeyCredential", "com.azure.ai.openai"), - new TestCase(0, "notEndpoint", "credential", "KeyCredential", "com.azure.ai.openai"), - new TestCase(0, "endpoint", "notCredential", "KeyCredential", "com.azure.ai.openai"), - new TestCase(0, "endpoint", "com.azure.core.credential.AzureKeyCredential", "com.azure.ai.openai", "com.azure.ai.openai") - ); - } - - private void setupMockMethodExpression(int numOfInvocation, String endpoint, String credential, String keyCredentialPackageName, String azurePackageName) { - PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); - PsiMethodCallExpression qualifierOne = mock(PsiMethodCallExpression.class); - PsiReferenceExpression methodExpressionOne = mock(PsiReferenceExpression.class); - PsiNewExpression newExpression = mock(PsiNewExpression.class); - PsiExpressionList argumentList = mock(PsiExpressionList.class); - PsiExpression[] arguments = new PsiExpression[]{newExpression}; - PsiJavaCodeReferenceElement classReference = mock(PsiJavaCodeReferenceElement.class); - PsiVariable parent = mock(PsiVariable.class); - PsiType qualifierType = mock(PsiType.class); - - when(mockMethodCall.getMethodExpression()).thenReturn(methodExpression); - when(methodExpression.getReferenceName()).thenReturn(endpoint); - when(methodExpression.getQualifierExpression()).thenReturn(qualifierOne); - when(qualifierOne.getMethodExpression()).thenReturn(methodExpressionOne); - when(methodExpressionOne.getReferenceName()).thenReturn(credential); - when(qualifierOne.getArgumentList()).thenReturn(argumentList); - when(argumentList.getExpressions()).thenReturn(arguments); - when(newExpression.getClassReference()).thenReturn(classReference); - when(classReference.getReferenceName()).thenReturn(keyCredentialPackageName); - when(qualifierOne.getParent()).thenReturn(parent); - when(parent.getType()).thenReturn(qualifierType); - when(qualifierType.getCanonicalText()).thenReturn(azurePackageName); - - } - - private static class TestCase { - int numOfInvocation; - String endpoint; - String credential; - String keyCredentialPackageName; - String azurePackageName; - - TestCase(int numOfInvocation, String endpoint, String credential, String keyCredentialPackageName, String azurePackageName) { - this.numOfInvocation = numOfInvocation; - this.endpoint = endpoint; - this.credential = credential; - this.keyCredentialPackageName = keyCredentialPackageName; - this.azurePackageName = azurePackageName; - } - } -} 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 index cfd0a76f4aa..78fdd51b45e 100644 --- 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 @@ -6,7 +6,9 @@ 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; @@ -34,20 +36,24 @@ public class GetSyncPollerOnPollerFluxCheckTest { private JavaElementVisitor mockVisitor; @Mock - private PsiMethodCallExpression mockElement; + private PsiMethodCallExpression mockMethodCallExpression; + + @Mock + private PsiElement mockElement; @BeforeEach public void setup() { mockHolder = mock(ProblemsHolder.class); mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder); - mockElement = mock(PsiMethodCallExpression.class); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + mockElement = mock(PsiElement.class); } @ParameterizedTest @MethodSource("testCases") public void testGetSyncPollerOnPollerFluxCheck(TestCase testCase) { mockMethodExpression(testCase.methodName, testCase.className, testCase.numberOfInvocations); - mockVisitor.visitMethodCallExpression(mockElement); + mockVisitor.visitMethodCallExpression(mockMethodCallExpression); verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(mockElement, "Use of getSyncPoller() on a " + "PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks."); } @@ -56,7 +62,7 @@ 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.azure.core.util.polling.DifferentClassName", 0), + new TestCase("getSyncPoller", "com.not.azure..util.polling.DifferentClassName", 0), new TestCase("getSyncPoller", null, 0) ); } @@ -66,15 +72,14 @@ private void mockMethodExpression(String methodName, String className, int numbe PsiExpression expression = mock(PsiExpression.class); PsiType type = mock(PsiType.class); PsiClass containingClass = mock(PsiClass.class); - PsiTreeUtil mockTreeUtil = mock(PsiTreeUtil.class); + PsiMethod method = mock(PsiMethod.class); - when(mockElement.getMethodExpression()).thenReturn(referenceExpression); - when(referenceExpression.getReferenceName()).thenReturn(methodName); - when(referenceExpression.getQualifierExpression()).thenReturn(expression); - when(expression.getType()).thenReturn(type); - when(type.getCanonicalText()).thenReturn(className); - when(mockTreeUtil.getParentOfType(mockElement, PsiClass.class)).thenReturn(containingClass); + 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 { 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 index ce7114baaec..dd260725355 100644 --- 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 @@ -4,19 +4,19 @@ 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.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.PsiNewExpression; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; 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.Mockito; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -39,11 +39,13 @@ */ 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 PsiElementVisitor mockVisitor; + private JavaElementVisitor mockVisitor; @BeforeEach public void setup() { @@ -54,13 +56,12 @@ public void setup() { @ParameterizedTest @MethodSource("testCases") public void testHardcodedAPIKeysAndTokensCheck(TestCase testCase) { - PsiNewExpression mockExpression = mockMethodExpression(testCase.apiName, testCase.numOfInvocations); - mockVisitor.visitElement(mockExpression); + PsiNewExpression mockExpression = mockMethodExpression(testCase.apiName, testCase.credentialString, + testCase.numOfInvocations); + mockVisitor.visitNewExpression(mockExpression); verify(mockHolder, times(testCase.numOfInvocations)) - .registerProblem(eq(mockExpression), - Mockito.contains( - "DefaultAzureCredential is recommended for authentication if the service client supports Token Credential (Entra ID Authentication). If not, then use Azure Key Credential for API key based authentication.")); + .registerProblem(eq(mockExpression),eq(SUGGESTION_MESSAGE)); } @Test @@ -71,53 +72,52 @@ public void testNoFlagIfNoHardcodedAPIKeysAndTokens() { when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); when(javaCodeReferenceElement.getReferenceName()).thenReturn("AzureKeyCredential"); - when(javaCodeReferenceElement.getQualifiedName()).thenReturn(RuleConfig.AZURE_PACKAGE_NAME); + when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure"); when(newExpression.getChildren()).thenReturn(new PsiElement[]{literalExpression}); when(literalExpression.getValue()).thenReturn(System.getenv()); - mockVisitor.visitElement(newExpression); + mockVisitor.visitNewExpression(newExpression); - verify(mockHolder, times(0)).registerProblem(eq(newExpression), - Mockito.contains( - "DefaultAzureCredential is recommended for authentication if the service client supports Token Credential (Entra ID Authentication). If not, then use Azure Key Credential for API key based authentication.")); + verify(mockHolder, times(0)).registerProblem(eq(newExpression), eq(SUGGESTION_MESSAGE)); } private static Stream testCases() { return Stream.of( - new TestCase("AzureKeyCredential", 1), - new TestCase("AccessToken", 1), - new TestCase("KeyCredential", 1), - new TestCase("AzureNamedKeyCredential", 1), - new TestCase("AzureSasCredential", 1), - new TestCase("AzureNamedKey", 1), - new TestCase("ClientSecretCredentialBuilder", 1), - new TestCase("UsernamePasswordCredentialBuilder", 1), - new TestCase("BasicAuthenticationCredential", 1), - new TestCase("SomeOtherClient", 0), - new TestCase("", 0) + 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, int numOfInvocations) { + 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(RuleConfig.AZURE_PACKAGE_NAME); - when(newExpression.getChildren()).thenReturn(new PsiElement[]{literalExpression}); - when(literalExpression.getValue()).thenReturn("hardcoded-api-token"); + 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, 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/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java deleted file mode 100644 index 6c4da435f43..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java +++ /dev/null @@ -1,210 +0,0 @@ -// 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.PsiClass; -import com.intellij.psi.PsiElementVisitor; -import com.intellij.psi.PsiExpression; -import com.intellij.psi.PsiExpressionList; -import com.intellij.psi.PsiLiteralExpression; -import com.intellij.psi.PsiLocalVariable; -import com.intellij.psi.PsiPolyadicExpression; -import com.intellij.psi.PsiReferenceExpression; -import com.intellij.psi.PsiVariable; -import com.intellij.psi.impl.source.tree.java.PsiMethodCallExpressionImpl; -import com.intellij.psi.util.PsiTreeUtil; -import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.KustoQueriesWithTimeIntervalInQueryStringCheck.KustoQueriesVisitor; -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 static org.mockito.ArgumentMatchers.contains; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * This class tests the KustoQueriesWithTimeIntervalInQueryStringCheck class. - *

- * These are some example queries that should be flagged: - *

    - *
  • "String query1 = \"| where timestamp > ago(1d)\";": This query uses the ago function to create a time interval of 1 day. This should be FLAGGED
  • - *
  • "String query2 = \"| where timestamp > datetime(2021-01-01)\";": This query compares the timestamp to a specific datetime. This should be FLAGGED
  • - *
  • "String query3 = \"| where timestamp > now()\";": This query uses the now function to get the current timestamp. This should be FLAGGED
  • - *
  • "String query4 = \"| where timestamp > startofday()\";": This query uses the startofday function to get the start of the current day. This should be FLAGGED
  • - *
  • "String query5 = \"| where timestamp > startofmonth()\";": This query uses the startofmonth function to get the start of the current month. This should be FLAGGED
  • - *
  • "String query11 = \"| where timestamp > ago(" + days + ")\";": This query uses a variable to define the time interval. This should be FLAGGED
  • - *
  • "String query12 = \"| where timestamp > datetime(" + date + ")\";": This query uses a variable to define the datetime. This should not be FLAGGED
  • - *
- */ -public class KustoQueriesWithTimeIntervalInQueryStringCheckTest { - - @Mock - private ProblemsHolder mockHolder; - @Mock - private PsiElementVisitor mockVisitor; - @Mock - private PsiMethodCallExpressionImpl methodCall; - @Mock - private PsiLocalVariable mockVariable; - - @Mock - private PsiPolyadicExpression mockPolyadicExpression; - - @BeforeEach - public void setup() { - mockHolder = mock(ProblemsHolder.class); - mockVisitor = new KustoQueriesWithTimeIntervalInQueryStringCheck.KustoQueriesVisitor(mockHolder); - mockVariable = mock(PsiLocalVariable.class); - mockPolyadicExpression = mock(PsiPolyadicExpression.class); - } - - @ParameterizedTest - @MethodSource("testCases") - public void testKustoQueriesWithTimeIntervalInQueryStringCheck(TestCase testCase) { - setupWithLocalVariable(testCase.queryString, testCase.packageName, testCase.numOfInvocations); - // Visit the variable to store its name if it's a query string - mockVisitor.visitElement(mockVariable); - - // Visit the method call to check if the query variable is used and the method call is to an Azure client - mockVisitor.visitElement(methodCall); - - // Verify that the problem was registered correctly for the method call - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(methodCall), contains("KQL queries " + - "with time intervals in the query string detected.")); - - } - - @ParameterizedTest - @MethodSource("testCases") - public void testKustoQueriesWithTimeIntervalInQueryWithPolyadicExpression(TestCase testCase) { - setupWithPolyadicExpression(testCase.queryString, testCase.packageName, testCase.numOfInvocations); - // Visit the variable to store its name if it's a query string - mockVisitor.visitElement(mockPolyadicExpression); - - // Visit the method call to check if the query variable is used and the method call is to an Azure client - mockVisitor.visitElement(methodCall); - - // Verify that the problem was registered correctly for the method call - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(methodCall), contains("KQL queries " + - "with " + - "time intervals in the query string detected.")); - - } - - @Test - public void testCheckExpressionWithNullExpression() { - PsiExpression nullExpression = null; - PsiLocalVariable mockElement = mock(PsiLocalVariable.class); - ((KustoQueriesVisitor) mockVisitor).checkExpressionForPatterns(nullExpression, mockElement); - } - - private static Stream testCases() { - return Stream.of( - new TestCase("datetime(startDate)", "com.azure", 1), - new TestCase("filter.datetime(2022-01-01)", "com.azure", 1), - new TestCase("time.now()", "com.azure", 1), - new TestCase("time.startofday()", "com.azure", 1), - new TestCase("time.startofmonth()", "com.azure", 1), - new TestCase("range.between(datetime(2022-01-01), datetime(2022-02-01)", "com.azure", 1), - new TestCase("datetime(startDate)", "com.microsoft.azure", 0) - ); - } - - /** - * This method tests the registerProblem method with a local variable as the query string - */ - private void setupWithLocalVariable(String queryString, String packageName, int numOfInvocations) { - - mockVariable = mock(PsiLocalVariable.class); - PsiLiteralExpression initializer = mock(PsiLiteralExpression.class); - PsiLocalVariable parentElement = mock(PsiLocalVariable.class); - PsiClass containingClass = mock(PsiClass.class); - - methodCall = mock(PsiMethodCallExpressionImpl.class); - PsiExpressionList argumentList = mock(PsiExpressionList.class); - PsiReferenceExpression argument = mock(PsiReferenceExpression.class); - PsiExpression[] arguments = new PsiExpression[]{argument}; - PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); - PsiVariable resolvedElement = mock(PsiVariable.class); - PsiReferenceExpression qualifierExpression = mock(PsiReferenceExpression.class); - - // stubs for handle local variable method - when(mockVariable.getInitializer()).thenReturn(initializer); - when(mockVariable.getName()).thenReturn("stringQuery"); - - // stubs for checkExpression method - when(initializer.getText()).thenReturn(queryString); - when(mockVariable.getParent()).thenReturn(parentElement); - when(parentElement.getName()).thenReturn("stringQuery"); - - // stubs for handleMethodCall method - when(methodCall.getArgumentList()).thenReturn(argumentList); - when(argumentList.getExpressions()).thenReturn(arguments); - when(argument.resolve()).thenReturn(resolvedElement); - when(resolvedElement.getName()).thenReturn("stringQuery"); - - // stubs for isAzureClient method - when(methodCall.getMethodExpression()).thenReturn(referenceExpression); - when(referenceExpression.getQualifierExpression()).thenReturn(qualifierExpression); - when(PsiTreeUtil.getParentOfType(methodCall, PsiClass.class)).thenReturn(containingClass); - when(containingClass.getQualifiedName()).thenReturn(packageName); - - } - - void setupWithPolyadicExpression(String queryString, String packageName, int numOfInvocations) { - - mockPolyadicExpression = mock(PsiPolyadicExpression.class); - PsiExpression initializer = mock(PsiExpression.class); - PsiLocalVariable parentElement = mock(PsiLocalVariable.class); - PsiClass containingClass = mock(PsiClass.class); - - // stubs for handlePolyadicExpression method - when(mockPolyadicExpression.getText()).thenReturn(queryString); - - // stubs for checkExpression method - when(initializer.getText()).thenReturn(queryString); - when(mockPolyadicExpression.getParent()).thenReturn(parentElement); - when(parentElement.getName()).thenReturn("stringQuery"); - - methodCall = mock(PsiMethodCallExpressionImpl.class); - PsiExpressionList argumentList = mock(PsiExpressionList.class); - PsiReferenceExpression argument = mock(PsiReferenceExpression.class); - PsiExpression[] arguments = new PsiExpression[]{argument}; - PsiReferenceExpression referenceExpression = mock(PsiReferenceExpression.class); - PsiVariable resolvedElement = mock(PsiVariable.class); - PsiReferenceExpression qualifierExpression = mock(PsiReferenceExpression.class); - - // stubs for handleMethodCall method - when(methodCall.getArgumentList()).thenReturn(argumentList); - when(argumentList.getExpressions()).thenReturn(arguments); - when(argument.resolve()).thenReturn(resolvedElement); - when(resolvedElement.getName()).thenReturn("stringQuery"); - - // stubs for isAzureClient method - when(methodCall.getMethodExpression()).thenReturn(referenceExpression); - when(referenceExpression.getQualifierExpression()).thenReturn(qualifierExpression); - when(PsiTreeUtil.getParentOfType(methodCall, PsiClass.class)).thenReturn(containingClass); - when(containingClass.getQualifiedName()).thenReturn(packageName); - - } - - private static class TestCase { - String queryString; - String packageName; - int numOfInvocations; - - TestCase(String queryString, String packageName, int numOfInvocations) { - this.queryString = queryString; - 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/ServiceBusReceiveModeCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheckTest.java deleted file mode 100644 index c91802f0ed7..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiveModeCheckTest.java +++ /dev/null @@ -1,153 +0,0 @@ -// 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.PsiDeclarationStatement; -import com.intellij.psi.PsiElement; -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.PsiVariable; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -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.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mock; - -import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.ServiceBusReceiveModeCheck.ServiceBusReceiveModeVisitor; - -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 contains the tests for the ServiceBusReceiveModeCheck class. It tests the visitDeclarationStatement method - * of the ServiceBusReceiveModeVisitor class. It tests the method with different combinations of the receiveMode and - * prefetchCount methods. - */ -public class ServiceBusReceiveModeCheckTest { - - @Mock - private ProblemsHolder mockHolder; - - @Mock - private JavaElementVisitor mockVisitor; - - @Mock - private PsiDeclarationStatement mockDeclarationStatement; - - @Mock - private PsiElement prefetchCountMethod; - - @BeforeEach - public void setUp() { - mockHolder = mock(ProblemsHolder.class); - mockVisitor = new ServiceBusReceiveModeCheck.ServiceBusReceiveModeVisitor(mockHolder, true); - mockDeclarationStatement = mock(PsiDeclarationStatement.class); - prefetchCountMethod = mock(PsiElement.class); - } - - @ParameterizedTest - @MethodSource("provideTestCases") - public void testServiceBusReceiveModeCheck(TestCase testCase) { - setupPrefetchMethod(testCase.clientName, testCase.methodFoundOne, testCase.methodFoundTwo, - testCase.numOfInvocations, - testCase.prefetchCountValue); - mockVisitor.visitDeclarationStatement(mockDeclarationStatement); - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(prefetchCountMethod), eq( - "A high prefetch value in PEEK_LOCK detected. We recommend a prefetch value of 0 or 1 for efficient message retrieval.")); - } - - private void setupPrefetchMethod(String clientName, String methodFoundOne, String methodFoundTwo, - int numOfInvocations, String prefetchCountValue) { - PsiVariable declaredElement = mock(PsiVariable.class); - PsiElement[] declaredElements = new PsiElement[] {declaredElement}; - - PsiType clientType = mock(PsiType.class); - PsiMethodCallExpression initializer = mock(PsiMethodCallExpression.class); - - PsiReferenceExpression expressionOne = mock(PsiReferenceExpression.class); - PsiMethodCallExpression qualifierOne = mock(PsiMethodCallExpression.class); - PsiReferenceExpression methodExpressionOne = mock(PsiReferenceExpression.class); - - PsiExpression receiveModePeekLockParameter = mock(PsiExpression.class); - PsiReferenceExpression prefetchCountParameter = mock(PsiReferenceExpression.class); - PsiExpressionList methodArgumentList = mock(PsiExpressionList.class); - PsiExpression[] methodArguments = new PsiExpression[] {prefetchCountParameter, receiveModePeekLockParameter}; - - PsiMethodCallExpression qualifierTwo = mock(PsiMethodCallExpression.class); - PsiReferenceExpression methodExpressionTwo = mock(PsiReferenceExpression.class); - - prefetchCountMethod = mock(PsiElement.class); - - when(mockDeclarationStatement.getDeclaredElements()).thenReturn(declaredElements); - - when(declaredElement.getType()).thenReturn(clientType); - when(declaredElement.getInitializer()).thenReturn(initializer); - when(clientType.getCanonicalText()).thenReturn(RuleConfig.AZURE_PACKAGE_NAME); - when(clientType.getPresentableText()).thenReturn(clientName); - - when(initializer.getMethodExpression()).thenReturn(expressionOne); - when(expressionOne.getQualifierExpression()).thenReturn(qualifierOne); - when(qualifierOne.getMethodExpression()).thenReturn(methodExpressionOne); - when(methodExpressionOne.getReferenceName()).thenReturn(methodFoundOne); - - when(qualifierOne.getArgumentList()).thenReturn(methodArgumentList); - when(methodArgumentList.getExpressions()).thenReturn(methodArguments); - when(receiveModePeekLockParameter.getText()).thenReturn("PEEK_LOCK"); - - when(methodExpressionOne.getQualifierExpression()).thenReturn(qualifierTwo); - when(qualifierTwo.getMethodExpression()).thenReturn(methodExpressionTwo); - when(methodExpressionTwo.getReferenceName()).thenReturn(methodFoundTwo); - - when(qualifierTwo.getArgumentList()).thenReturn(methodArgumentList); - when(methodArgumentList.getExpressions()).thenReturn(methodArguments); - when(prefetchCountParameter.getText()).thenReturn(prefetchCountValue); - - when(methodExpressionTwo.getReferenceNameElement()).thenReturn(prefetchCountMethod); - - if (!"receiveMode".equals(methodFoundOne) || !"prefetchCount".equals(methodFoundTwo)) { - when(methodExpressionTwo.getQualifierExpression()).thenReturn(null); - } - - } - - private static Stream provideTestCases() { - return Stream.of( - new TestCase("ServiceBusReceiverClient", "receiveMode", "prefetchCount", 1, "100"), - new TestCase("ServiceBusReceiverClient", "receiveMode", "prefetchCount", 0, "1"), - new TestCase("ServiceBusReceiverClient", "notreceiveMode", "prefetchCount", 0, "100"), - new TestCase("ServiceBusReceiverClient", "receiveMode", "noprefetchCount", 0, "100"), - new TestCase("servicebus", "notreceiveMode", "noprefetchCount", 0, "100"), - new TestCase("notservicebus", "notreceiveMode", "noprefetchCount", 0, "100") - ); - } - - static class TestCase { - String clientName; - String methodFoundOne; - String methodFoundTwo; - int numOfInvocations; - String prefetchCountValue; - - TestCase(String clientName, String methodFoundOne, String methodFoundTwo, int numOfInvocations, - String prefetchCountValue) { - this.clientName = clientName; - this.methodFoundOne = methodFoundOne; - this.methodFoundTwo = methodFoundTwo; - this.numOfInvocations = numOfInvocations; - this.prefetchCountValue = prefetchCountValue; - } - } -} \ 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/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 index 6e8f4128f7c..4c8d3908cde 100644 --- 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 @@ -3,31 +3,29 @@ 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.intellij.psi.util.PsiTreeUtil; -import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.SingleOperationInLoopCheck.SingleOperationInLoopVisitor; -import com.intellij.codeInspection.ProblemsHolder; -import com.intellij.psi.PsiElement; - 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.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.Mockito; @@ -58,6 +56,7 @@ * in enhanced for loop.")) .subscribe(); } */ public class SingleOperationInLoopCheckTest { + private static final String PROBLEM_DESCRIPTION = "This SDK provides a batch operation API, use it to perform multiple actions in a single request. Consider using %sBatch instead."; @Mock private ProblemsHolder mockHolder; @@ -80,44 +79,40 @@ public void testSingleOperationInLoop(TestCase testCase) { setupWithSinglePsiDeclarationStatement(testCase.loopStatement, testCase.packageName, testCase.numberOfInvocations, testCase.methodName); verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(initializer), - Mockito.contains( - "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + - testCase.methodName + "Batch")); + Mockito.eq(String.format(PROBLEM_DESCRIPTION, testCase.methodName))); } else { setupWithSinglePsiExpressionStatement(testCase.loopStatement, testCase.packageName, testCase.numberOfInvocations, testCase.methodName); verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(expression), - Mockito.contains( - "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + - testCase.methodName + "Batch")); + Mockito.eq(String.format(PROBLEM_DESCRIPTION, testCase.methodName))); } } private JavaElementVisitor createVisitor() { - boolean isOnTheFly = true; - SingleOperationInLoopVisitor visitor = new SingleOperationInLoopVisitor(mockHolder, isOnTheFly); - return visitor; + return new SingleOperationInLoopTextAnalyticsCheck.SingleOperationInLoopVisitor(mockHolder); } private void setupWithSinglePsiExpressionStatement(PsiStatement loopStatement, String packageName, int numberOfInvocations, String methodName) { PsiBlockStatement loopBody = mock(PsiBlockStatement.class); PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); - expression = mock(PsiMethodCallExpression.class); - PsiTreeUtil treeUtil = mock(PsiTreeUtil.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(treeUtil.getParentOfType(expression, PsiClass.class)).thenReturn(containingClass); - when(containingClass.getQualifiedName()).thenReturn(packageName); 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); @@ -139,20 +134,23 @@ private void setupWithSinglePsiDeclarationStatement(PsiStatement loopStatement, PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); PsiVariable element = mock(PsiVariable.class); PsiElement[] elements = new PsiElement[] {element}; - initializer = mock(PsiMethodCallExpression.class); - PsiTreeUtil treeUtil = mock(PsiTreeUtil.class); 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(treeUtil.getParentOfType(initializer, PsiClass.class)).thenReturn(containingClass); - when(containingClass.getQualifiedName()).thenReturn(packageName); 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) { 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 index 4430ef05a9b..38264b50f64 100644 --- 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 @@ -4,6 +4,7 @@ package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiElement; import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor; import com.intellij.codeInspection.ProblemsHolder; @@ -26,28 +27,18 @@ 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(); - * } + * 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 { @@ -60,11 +51,15 @@ public class UseOfBlockOnAsyncClientsCheckTest { @Mock private PsiMethodCallExpression mockElement; + @Mock + private PsiElement problemElement; + @BeforeEach public void setup() { mockHolder = mock(ProblemsHolder.class); mockVisitor = createVisitor(); mockElement = mock(PsiMethodCallExpression.class); + problemElement = mock(PsiElement.class); } @ParameterizedTest @@ -74,16 +69,17 @@ public void testUseOfBlockOnAsyncClient(TestCase testCase) { testCase.reactivePackageName); mockVisitor.visitMethodCallExpression(mockElement); - verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(mockElement), - Mockito.contains( - "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead.")); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(problemElement), + Mockito.eq( + "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead.")); } private JavaElementVisitor createVisitor() { return new UseOfBlockOnAsyncClientsVisitor(mockHolder); } - private void setupMockMethodCallExpression(String methodName, String clientPackageName, int numberOfInvocations, String reactivePackageName) { + 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); @@ -108,17 +104,21 @@ private void setupMockMethodCallExpression(String methodName, String clientPacka 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("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("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") + new TestCase("block", "com.azure.messaging.servicebus.ServiceBusReceiverClient", 0, + "reactor.core.publisher.Mono") ); } From c5e1757da64722054a5841f9bb400c4df183deed Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Wed, 29 Jan 2025 23:25:09 -0800 Subject: [PATCH 5/7] readme updates --- .../azure-intellij-plugin-java-sdk/readme.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 index 8befbe7390f..a8753d0a67b 100644 --- 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 @@ -23,11 +23,12 @@ The **Azure Toolkit for IntelliJ** is a project designed to empower Java develop - **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. Optimize Receive Mode and Prefetch Value** -- **Anti-pattern**: Using `PEEK_LOCK` with a high prefetch value. -- **Issue**: Can lead to performance bottlenecks and message lock expirations. +#### **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**: Balance the prefetch value for efficient and concurrent processing. +- **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** @@ -53,7 +54,7 @@ The **Azure Toolkit for IntelliJ** is a project designed to empower Java develop #### **6. Use SyncPoller Instead of PollerFlux#getSyncPoller()** - **Anti-pattern**: Converting asynchronous polling to synchronous with `getSyncPoller()`. -- **Issue**: Adds unnecessary complexity. +- **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). From e275a81714af1ec65f2425dab76c027ee16150b0 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Mon, 3 Feb 2025 15:27:24 -0800 Subject: [PATCH 6/7] move ruleconfig loading to startup activity + testing changes --- .../sdk/analyzer/ConnectionStringCheck.java | 19 ++- .../analyzer/DisableAutoCompleteCheck.java | 23 ++- .../analyzer/DynamicClientCreationCheck.java | 32 +++- .../EventHubConsumerAsyncClientCheck.java | 70 ++++++--- .../sdk/analyzer/GetCompletionsCheck.java | 23 ++- .../GetSyncPollerOnPollerFluxCheck.java | 28 ++-- .../HardcodedAPIKeysAndTokensCheck.java | 22 +-- .../ServiceBusReceiverAsyncClientCheck.java | 58 ++++--- ...ngleOperationInLoopTextAnalyticsCheck.java | 28 ++-- .../StorageUploadWithoutLengthCheck.java | 27 ++-- .../UpdateCheckpointAsyncSubscribeCheck.java | 21 +-- .../UseOfBlockOnAsyncClientsCheck.java | 25 ++- .../intellij/java/sdk/utils/HelperUtils.java | 81 +++++++--- .../java/sdk/utils/RuleConfigLoader.java | 27 ++-- .../utils/RuleConfigLoaderInitializer.java | 20 +++ .../azure-intellij-plugin-java-sdk.xml | 3 +- .../main/resources/META-INF/ruleConfigs.json | 24 +-- .../analyzer/ConnectionStringCheckTest.java | 110 +++++++++++++ .../DetectDiscouragedAPIUsageCheckTest.java | 145 ------------------ .../DetectDiscouragedClientCheckTest.java | 126 --------------- .../DisableAutoCompleteCheckTest.java | 68 +++++--- .../DynamicClientCreationCheckTest.java | 29 +++- .../EventHubConsumerAsyncClientCheckTest.java | 93 +++++++++++ .../sdk/analyzer/GetCompletionsCheckTest.java | 108 +++++++++++++ .../GetSyncPollerOnPollerFluxCheckTest.java | 23 ++- .../HardcodedAPIKeysAndTokensCheckTest.java | 36 ++++- ...erviceBusReceiverAsyncClientCheckTest.java | 103 +++++++++++++ .../SingleOperationInLoopCheckTest.java | 28 +++- .../StorageUploadWithoutLengthCheckTest.java | 21 ++- ...dateCheckpointAsyncSubscribeCheckTest.java | 116 ++++++++++++++ .../UseOfBlockOnAsyncClientsCheckTest.java | 45 ++++-- 31 files changed, 1046 insertions(+), 536 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ConnectionStringCheckTest.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/EventHubConsumerAsyncClientCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/GetCompletionsCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/ServiceBusReceiverAsyncClientCheckTest.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/UpdateCheckpointAsyncSubscribeCheckTest.java 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 index 2df75aaf6b9..ca448ec32c2 100644 --- 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 @@ -22,22 +22,25 @@ public class ConnectionStringCheck extends LocalInspectionTool { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new ConnectionStringCheckVisitor(holder); + return new ConnectionStringCheckVisitor(holder, RuleConfigLoader.getInstance()); } static class ConnectionStringCheckVisitor extends JavaElementVisitor { private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; - ConnectionStringCheckVisitor(ProblemsHolder holder) { + ConnectionStringCheckVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - static { - RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = ruleConfigLoader.getRuleConfig("ConnectionStringCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "ConnectionStringCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } } @Override 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 index dd2385e9694..0db1ad635ed 100644 --- 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 @@ -36,7 +36,7 @@ public class DisableAutoCompleteCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new DisableAutoCompleteVisitor(holder); + return new DisableAutoCompleteVisitor(holder, RuleConfigLoader.getInstance()); } /** @@ -51,19 +51,18 @@ static class DisableAutoCompleteVisitor extends JavaElementVisitor { private final ProblemsHolder holder; - DisableAutoCompleteVisitor(ProblemsHolder holder) { - this.holder = holder; - initializeRuleConfig(); - } + DisableAutoCompleteVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + this.holder = holder; + initializeRuleConfig(ruleConfigLoader); + } - private void initializeRuleConfig() { - if (RULE_CONFIG == null) { - final String ruleName = "DisableAutoCompleteCheck"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "DisableAutoCompleteCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); } + } /** * 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 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 index a74e15ebc1b..02929fc9d06 100644 --- 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 @@ -5,7 +5,19 @@ import com.intellij.codeInspection.LocalInspectionTool; import com.intellij.codeInspection.ProblemsHolder; -import com.intellij.psi.*; +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; @@ -19,18 +31,26 @@ public class DynamicClientCreationCheck extends LocalInspectionTool { @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new DynamicClientCreationVisitor(holder); + return new DynamicClientCreationVisitor(holder, RuleConfigLoader.getInstance()); } static class DynamicClientCreationVisitor extends JavaElementVisitor { private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG = RuleConfigLoader.getInstance() - .getRuleConfig("DynamicClientCreationCheck"); - private static final boolean SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; - public DynamicClientCreationVisitor(ProblemsHolder holder) { + public DynamicClientCreationVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); + } + + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "DynamicClientCreationCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } } @Override 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 index 4d84f595037..fdecc9148fe 100644 --- 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 @@ -13,31 +13,59 @@ * This class is used to check if the EventHubConsumerAsyncClient is being used in the code. */ public class EventHubConsumerAsyncClientCheck extends LocalInspectionTool { - - private final RuleConfig ruleConfig; - private final boolean skipRuleCheck; - - public EventHubConsumerAsyncClientCheck() { - super(); - RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); - this.ruleConfig = ruleConfigLoader.getRuleConfig("EventHubConsumerAsyncClientCheck"); - this.skipRuleCheck = ruleConfig.skipRuleCheck(); - } - + /** + * 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) { - if (skipRuleCheck) { - return new JavaElementVisitor() {}; + return new EventHubConsumerAsyncClientCheck.EventHubConsumerAsyncClientVisitor(holder, + RuleConfigLoader.getInstance()); + } + + /** + * 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 ruleConfigLoader RuleConfigLoader object to load the rule configuration + */ + public EventHubConsumerAsyncClientVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - return new JavaElementVisitor() { - @Override - public void visitTypeElement(PsiTypeElement element) { - super.visitTypeElement(element); - if (HelperUtils.isItDiscouragedClient(element, ruleConfig.getUsagesToCheck())) { - holder.registerProblem(element, ruleConfig.getAntiPatternMessage()); - } + + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "EventHubConsumerAsyncClientCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + } + + @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 index c5127317d55..e707d9c22d7 100644 --- 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 @@ -13,12 +13,8 @@ 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.HashSet; -import java.util.Set; import org.jetbrains.annotations.NotNull; -import static com.microsoft.azure.toolkit.intellij.java.sdk.utils.HelperUtils.checkIfInUsages; - /** * Inspection tool to check discouraged GetCompletions API usage in openai package context. */ @@ -26,22 +22,25 @@ public class GetCompletionsCheck extends LocalInspectionTool { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new GetCompletionsVisitor(holder); + return new GetCompletionsCheck.GetCompletionsVisitor(holder, RuleConfigLoader.getInstance()); } static class GetCompletionsVisitor extends JavaElementVisitor { private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; - GetCompletionsVisitor(ProblemsHolder holder) { + GetCompletionsVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - static { - RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = ruleConfigLoader.getRuleConfig("GetCompletionsCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "GetCompletionsCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } } // visit all methodcalls 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 index ec6a9fff8c1..010a830061a 100644 --- 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 @@ -39,7 +39,7 @@ public class GetSyncPollerOnPollerFluxCheck extends LocalInspectionTool { @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new GetSyncPollerOnPollerFluxVisitor(holder); + return new GetSyncPollerOnPollerFluxVisitor(holder, RuleConfigLoader.getInstance()); } /** @@ -49,26 +49,26 @@ public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean i class GetSyncPollerOnPollerFluxVisitor extends JavaElementVisitor { private final ProblemsHolder holder; - - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - - static { - final String ruleName = "GetSyncPollerOnPollerFluxCheck"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); - - // Get the RuleConfig object for the rule - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } + 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 ruleConfigLoader RuleConfigLoader object to load the rule configuration */ - public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder) { + public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); + } + + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "GetSyncPollerOnPollerFluxCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } } /** 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 index 769ac554fef..28821636988 100644 --- 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 @@ -37,7 +37,7 @@ public class HardcodedAPIKeysAndTokensCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new APIKeysAndTokensVisitor(holder); + return new APIKeysAndTokensVisitor(holder, RuleConfigLoader.getInstance()); } /** @@ -46,19 +46,21 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is static class APIKeysAndTokensVisitor extends JavaElementVisitor { private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; - static { - RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = ruleConfigLoader.getRuleConfig("HardcodedAPIKeysAndTokensCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - } - - APIKeysAndTokensVisitor(ProblemsHolder holder) { + APIKeysAndTokensVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "HardcodedAPIKeysAndTokensCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + } @Override public void visitNewExpression(@NotNull PsiNewExpression newExpression) { PsiJavaCodeReferenceElement classReference = newExpression.getClassReference(); 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 index de46d9e3eaa..6f2739733e3 100644 --- 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 @@ -14,30 +14,48 @@ */ public class ServiceBusReceiverAsyncClientCheck extends LocalInspectionTool { - private final RuleConfig ruleConfig; - private final boolean skipRuleCheck; - - public ServiceBusReceiverAsyncClientCheck() { - super(); - RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); - this.ruleConfig = ruleConfigLoader.getRuleConfig("ServiceBusReceiverAsyncClientCheck"); - this.skipRuleCheck = ruleConfig.skipRuleCheck(); - } - @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - if (skipRuleCheck) { - return new JavaElementVisitor() {}; + return new ServiceBusReceiverAsyncClientCheck.ServiceBusReceiverAsyncClientCheckVisitor(holder, + RuleConfigLoader.getInstance()); + } + + + 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 ruleConfigLoader RuleConfigLoader object to load the rule configuration + */ + public ServiceBusReceiverAsyncClientCheckVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - return new JavaElementVisitor() { - @Override - public void visitTypeElement(PsiTypeElement element) { - super.visitTypeElement(element); - if (HelperUtils.isItDiscouragedClient(element, ruleConfig.getUsagesToCheck())) { - holder.registerProblem(element, ruleConfig.getAntiPatternMessage()); - } + + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "ServiceBusReceiverAsyncClientCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + } + + @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 index 91a5c62718a..cdd83cda717 100644 --- 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 @@ -52,7 +52,7 @@ public class SingleOperationInLoopTextAnalyticsCheck extends LocalInspectionTool @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new SingleOperationInLoopVisitor(holder); + return new SingleOperationInLoopVisitor(holder, RuleConfigLoader.getInstance()); } /** @@ -62,23 +62,21 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is * a batch alternative, a problem will be registered. */ static class SingleOperationInLoopVisitor extends JavaElementVisitor { - private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - private static final List SCOPE_TO_CHECK; - - static { - final String ruleName = "SingleOperationInLoopTextAnalyticsCheck"; - RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; - RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - SCOPE_TO_CHECK = RULE_CONFIG.getScopeToCheck(); + public SingleOperationInLoopVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - public SingleOperationInLoopVisitor(ProblemsHolder holder) { - this.holder = holder; + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "SingleOperationInLoopTextAnalyticsCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } } @Override @@ -171,7 +169,7 @@ private static boolean isAzureTextAnalyticsClientOperation(PsiMethodCallExpressi PsiType qualifierType = qualifierExpression.getType(); if (qualifierType != null) { String qualifiedTypeName = qualifierType.getCanonicalText(); - if (qualifiedTypeName != null && SCOPE_TO_CHECK.stream().anyMatch(qualifiedTypeName::startsWith)) { + if (qualifiedTypeName != null && RULE_CONFIG.getScopeToCheck().stream().anyMatch(qualifiedTypeName::startsWith)) { String methodName = methodCall.getMethodExpression().getReferenceName(); return RULE_CONFIG.getUsagesToCheck().contains(methodName + "Batch"); } 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 index c3e526e2e2f..90c4e6ed3eb 100644 --- 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 @@ -31,7 +31,7 @@ public class StorageUploadWithoutLengthCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new StorageUploadVisitor(holder); + return new StorageUploadVisitor(holder, RuleConfigLoader.getInstance()); } /** @@ -40,21 +40,22 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is static class StorageUploadVisitor extends JavaRecursiveElementWalkingVisitor { private static final String LENGTH_TYPE = "long"; private final ProblemsHolder holder; - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; - private static final List SCOPE_TO_CHECK; - - static { - RuleConfigLoader configLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = configLoader.getRuleConfig("StorageUploadWithoutLengthCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - SCOPE_TO_CHECK = RULE_CONFIG.getScopeToCheck(); - } + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + - StorageUploadVisitor(ProblemsHolder holder) { + StorageUploadVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "StorageUploadWithoutLengthCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } + } @Override public void visitMethodCallExpression(PsiMethodCallExpression expression) { super.visitMethodCallExpression(expression); @@ -84,7 +85,7 @@ private boolean isInScope(PsiMethodCallExpression expression) { return false; } - return SCOPE_TO_CHECK.stream() + return RULE_CONFIG.getScopeToCheck().stream() .anyMatch(scope -> containingClass.getQualifiedName().startsWith(scope)); } 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 index 48550d78970..2a4156aaefc 100644 --- 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 @@ -33,7 +33,7 @@ public class UpdateCheckpointAsyncSubscribeCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new UpdateCheckpointAsyncVisitor(holder); + return new UpdateCheckpointAsyncVisitor(holder, RuleConfigLoader.getInstance()); } /** @@ -43,25 +43,26 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is */ static class UpdateCheckpointAsyncVisitor extends JavaElementVisitor { - // Define the holder to register problems private final ProblemsHolder holder; - - private static final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; + 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) { + UpdateCheckpointAsyncVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - static { - RuleConfigLoader ruleConfigLoader = RuleConfigLoader.getInstance(); - RULE_CONFIG = ruleConfigLoader.getRuleConfig("UpdateCheckpointAsyncSubscribeCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "UpdateCheckpointAsyncSubscribeCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + } } /** 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 index 72d6df209d7..0440d41fb2e 100644 --- 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 @@ -25,32 +25,31 @@ */ public class UseOfBlockOnAsyncClientsCheck extends LocalInspectionTool { - private static final String RULE_NAME = "UseOfBlockOnAsyncClientsCheck"; - private static final RuleConfig RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig(RULE_NAME); - private static final boolean SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); - @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new UseOfBlockOnAsyncClientsVisitor(holder); + return new UseOfBlockOnAsyncClientsVisitor(holder, RuleConfigLoader.getInstance()); } /** * 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 final RuleConfig RULE_CONFIG; - private static final boolean SKIP_WHOLE_RULE; + private static RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; - static { - RULE_CONFIG = RuleConfigLoader.getInstance().getRuleConfig("UseOfBlockOnAsyncClientsCheck"); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + UseOfBlockOnAsyncClientsVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + this.holder = holder; + initializeRuleConfig(ruleConfigLoader); } - UseOfBlockOnAsyncClientsVisitor(@NotNull ProblemsHolder holder) { - this.holder = holder; + private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + if (RULE_CONFIG == null) { + final String ruleName = "UseOfBlockOnAsyncClientsCheck"; + RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.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 index 577d62cdd1d..6f99f1f9293 100644 --- 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 @@ -13,7 +13,6 @@ import com.intellij.psi.PsiType; import com.intellij.psi.PsiTypeElement; import com.intellij.psi.PsiWhiteSpace; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; import java.util.List; /* @@ -30,7 +29,7 @@ public class HelperUtils { * @return True if the method call is following a `subscribe` method call, false otherwise. */ public static boolean isFollowedBySubscribe(PsiMethodCallExpression expression) { - if (expression == null || !expression.isValid()) { + if (expression == null) { return false; } @@ -113,30 +112,20 @@ private static boolean isVariableStoredAndSubscribed(PsiMethodCallExpression exp 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); } - public 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; - - } - + /** + * 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)) { @@ -152,6 +141,12 @@ public static PsiMethod getResolvedMethod(PsiElement element) { 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; @@ -159,6 +154,12 @@ public static boolean checkIfInUsages(List usages, String methodName) { 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; @@ -166,7 +167,17 @@ public static boolean checkIfInScope(List scope, String classQualifiedNa 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(); @@ -189,6 +200,12 @@ public static boolean isItDiscouragedAPI(PsiMethod method, List usages, 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) { @@ -202,4 +219,24 @@ public static boolean isItDiscouragedClient(PsiTypeElement element, List } 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/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 index 78eac24e47b..e15dccfd518 100644 --- 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 @@ -20,22 +20,10 @@ public class RuleConfigLoader { private static final String CONFIG_FILE_PATH = "./META-INF/ruleConfigs.json"; private static final Logger LOGGER = Logger.getLogger(RuleConfigLoader.class.getName()); - private static final RuleConfigLoader INSTANCE; + private static RuleConfigLoader INSTANCE; private static boolean initializationFailed = false; private final Map ruleConfigs; - static { - RuleConfigLoader tempInstance; - try { - tempInstance = new RuleConfigLoader(CONFIG_FILE_PATH); - } catch (IOException e) { - tempInstance = null; - initializationFailed = true; - LOGGER.log(Level.SEVERE, "Failed to initialize RuleConfigLoader: " + e.getMessage(), e); - } - INSTANCE = tempInstance; - } - private RuleConfigLoader() { this.ruleConfigs = new HashMap<>(); } @@ -57,6 +45,19 @@ public static RuleConfigLoader getInstance() { return INSTANCE; } + /** + * Initializes the RuleConfigLoader instance asynchronously. + */ + public static void initialize() { + try { + INSTANCE = new RuleConfigLoader(CONFIG_FILE_PATH); + } catch (IOException e) { + INSTANCE = new RuleConfigLoader(); + initializationFailed = true; + LOGGER.log(Level.SEVERE, "Failed to initialize RuleConfigLoader: " + e.getMessage(), e); + } + } + /** * Gets the rule configuration for the specified key. * diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java new file mode 100644 index 00000000000..820b6df4615 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java @@ -0,0 +1,20 @@ +package com.microsoft.azure.toolkit.intellij.java.sdk.utils; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import javax.annotation.Nonnull; +import kotlin.Unit; +import kotlin.coroutines.Continuation; + +/** + * The RuleConfigLoaderInitializer class is responsible for initializing the RuleConfigLoader class. + */ +public class RuleConfigLoaderInitializer implements ProjectActivity { + + @javax.annotation.Nullable + @Override + public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { + RuleConfigLoader.initialize(); + return null; + } +} \ No newline at end of file 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 2f60ea01dad..16d91101407 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 @@ -6,6 +6,7 @@ + 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/DetectDiscouragedAPIUsageCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java deleted file mode 100644 index 6778d5e9577..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedAPIUsageCheckTest.java +++ /dev/null @@ -1,145 +0,0 @@ -// 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.PsiElementVisitor; -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 java.util.Collections; -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} and {@link ConnectionStringCheck} . - */ -public class DetectDiscouragedAPIUsageCheckTest { - - @Mock - private ProblemsHolder mockHolder; - - @Mock - private JavaElementVisitor mockVisitor; - - @Mock - private PsiMethodCallExpression methodCallExpression; - - @Mock - private PsiElement problemElement; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - mockHolder = mock(ProblemsHolder.class); - methodCallExpression = mock(PsiMethodCallExpression.class); - } - - @ParameterizedTest - @MethodSource("provideTestCases") - public void detectsDiscouragedAPIUsage(TestCase testCase) { - JavaElementVisitor visitor = createVisitor(testCase.visitorClass); - setupMockAPI(testCase.methodToCheck, testCase.numOfInvocations, testCase.packageName, - testCase.suggestionMessage); - visitor.visitMethodCallExpression(methodCallExpression); - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(problemElement), - eq(testCase.suggestionMessage)); - } - - private static RuleConfig getGetCompletionsConfig() { - RuleConfig getCompletionsConfig = mock(RuleConfig.class); - when(getCompletionsConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getCompletions")); - when(getCompletionsConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure.ai.openai")); - when(getCompletionsConfig.getAntiPatternMessage()).thenReturn( - "getCompletions API usage detected. Use the getChatCompletions API instead."); - return getCompletionsConfig; - } - - private static RuleConfig getConnectionStringConfig() { - RuleConfig connectionStringConfig = mock(RuleConfig.class); - when(connectionStringConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("connectionString")); - when(connectionStringConfig.getScopeToCheck()).thenReturn(Collections.singletonList("com.azure")); - when(connectionStringConfig.getAntiPatternMessage()).thenReturn( - "Connection String API usage detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication)."); - return connectionStringConfig; - } - - private void setupMockAPI(String methodToCheck, int numOfInvocations, String packageName, - String suggestionMessage) { - methodCallExpression = mock(PsiMethodCallExpression.class); - 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 JavaElementVisitor createVisitor(Class visitorClass) { - if (visitorClass == GetCompletionsCheck.GetCompletionsVisitor.class) { - return new GetCompletionsCheck.GetCompletionsVisitor(mockHolder); - } else if (visitorClass == ConnectionStringCheck.ConnectionStringCheckVisitor.class) { - return new ConnectionStringCheck.ConnectionStringCheckVisitor(mockHolder); - } - throw new IllegalArgumentException("Unsupported visitor class: " + visitorClass); - } - - private static Stream provideTestCases() { - return Stream.of( - new TestCase(ConnectionStringCheck.ConnectionStringCheckVisitor.class, - getConnectionStringConfig(), - "connectionString", "com.azure", - "Connection String API usage detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication).", - 1), - new TestCase(GetCompletionsCheck.GetCompletionsVisitor.class, - getGetCompletionsConfig(), "getCompletions", "com.azure.ai.openai", "getCompletions API " + - "detected. Use the getChatCompletions API instead.", 1), - new TestCase(ConnectionStringCheck.ConnectionStringCheckVisitor.class, - getConnectionStringConfig(), "allowedMethod", "com.azure", "", 0), - new TestCase(ConnectionStringCheck.ConnectionStringCheckVisitor.class, - getConnectionStringConfig(), "connectionString", "com.microsoft.azure", "", 0), - new TestCase(GetCompletionsCheck.GetCompletionsVisitor.class, - getGetCompletionsConfig(), "getCompletions", "com.azure.other", "", 0) - ); - } - - private static class TestCase { - Class visitorClass; - String methodToCheck; - String packageName; - String suggestionMessage; - int numOfInvocations; - RuleConfig ruleConfig; - - TestCase(Class visitorClass, RuleConfig ruleConfig, String methodToCheck, String packageName, - String suggestionMessage, - int numOfInvocations) { - this.visitorClass = visitorClass; - this.methodToCheck = methodToCheck; - this.packageName = packageName; - this.suggestionMessage = suggestionMessage; - this.numOfInvocations = numOfInvocations; - this.ruleConfig = ruleConfig; - } - } -} \ 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/DetectDiscouragedClientCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java deleted file mode 100644 index 4735554201d..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/java/sdk/analyzer/DetectDiscouragedClientCheckTest.java +++ /dev/null @@ -1,126 +0,0 @@ -// 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.PsiType; -import com.intellij.psi.PsiTypeElement; -import com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig; -import java.util.Collections; -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 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()); - * - * 4. final EventHubConsumerAsyncClient consumerClient = partitionPump.getClient(); - * 5. EventHubConsumerAsyncClient eventHubConsumer = eventHubClientBuilder.buildAsyncClient() - * .createConsumer(claimedOwnership.getConsumerGroup(), prefetch, true); - */ -public class DetectDiscouragedClientCheckTest { - - @Mock - private ProblemsHolder mockHolder; - - @Mock - private JavaElementVisitor mockVisitor; - - @Mock - private PsiTypeElement mockTypeElement; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - mockHolder = mock(ProblemsHolder.class); - mockTypeElement = mock(PsiTypeElement.class); - } - - @ParameterizedTest - @MethodSource("provideServiceBusReceiverAsyncClientTestCases") - void detectsServiceBusReceiverAsyncClientUsage(TestCase testCase) { - JavaElementVisitor visitor = new ServiceBusReceiverAsyncClientCheck().buildVisitor(mockHolder, false); - setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck, testCase.suggestionMessage); - visitor.visitTypeElement(mockTypeElement); - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), contains(testCase.suggestionMessage)); - } - - @ParameterizedTest - @MethodSource("provideEventHubConsumerAsyncClientTestCases") - void detectsEventHubConsumerAsyncClientUsage(TestCase testCase) { - JavaElementVisitor visitor = new EventHubConsumerAsyncClientCheck().buildVisitor(mockHolder, false); - setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck, testCase.suggestionMessage); - visitor.visitTypeElement(mockTypeElement); - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), contains(testCase.suggestionMessage)); - } - - private static Stream provideEventHubConsumerAsyncClientTestCases() { - RuleConfig eventHubConsumerAsyncClientConfig = mock(RuleConfig.class); - when(eventHubConsumerAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("EventHubConsumerAsyncClient")); - when(eventHubConsumerAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead."); - - return Stream.of( - new TestCase("EventHubConsumerAsyncClient", "Use of EventHubConsumerAsyncClient detected. Use EventProcessorClient instead which provides a higher-level abstraction that simplifies event processing, making it the preferred choice for most developers.", 1, eventHubConsumerAsyncClientConfig), - new TestCase("EventProcessorClient", "", 0, eventHubConsumerAsyncClientConfig), - new TestCase("", "", 0, eventHubConsumerAsyncClientConfig) - ); - } - - private static Stream provideServiceBusReceiverAsyncClientTestCases() { - RuleConfig serviceBusReceiverAsyncClientConfig = mock(RuleConfig.class); - when(serviceBusReceiverAsyncClientConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("ServiceBusReceiverAsyncClient")); - when(serviceBusReceiverAsyncClientConfig.getAntiPatternMessage()).thenReturn("Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead."); - - return Stream.of( - new TestCase("ServiceBusReceiverAsyncClient", "Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead.", 1, serviceBusReceiverAsyncClientConfig), - new TestCase("ServiceBusProcessorClient", "", 0, serviceBusReceiverAsyncClientConfig), - new TestCase("", "", 0, serviceBusReceiverAsyncClientConfig) - ); - } - - private void setupMockElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck, String suggestionMessage) { - 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; - String suggestionMessage; - int numOfInvocations; - RuleConfig ruleConfig; - - TestCase(String usagesToCheck, String suggestionMessage, int numOfInvocations, RuleConfig ruleConfig) { - this.usagesToCheck = usagesToCheck; - this.suggestionMessage = suggestionMessage; - this.numOfInvocations = numOfInvocations; - this.ruleConfig = ruleConfig; - } - } -} 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 index 24fca61740b..2dc718c922f 100644 --- 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 @@ -5,16 +5,21 @@ 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.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; @@ -33,6 +38,8 @@ */ 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; @@ -44,28 +51,34 @@ public class DisableAutoCompleteCheckTest { @Mock private PsiMethodCallExpression initializer; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); - mockVisitor = new DisableAutoCompleteVisitor(mockHolder); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("DisableAutoCompleteCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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); + mockVisitor = new DisableAutoCompleteVisitor(mockHolder, mockRuleConfigLoader); } @ParameterizedTest @MethodSource("provideTestCases") public void testDisableAutoCompleteCheck(TestCase testCase) { - setupMockMethodCall(testCase.packageName, testCase.clientName, testCase.numOfInvocations, - testCase.methodFound); + setupMockMethodCall(testCase.methodFound, testCase.numOfInvocations, testCase.packageName, testCase.className); mockVisitor.visitDeclarationStatement(mockDeclarationStatement); - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(initializer), contains( - "Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion.")); - + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(initializer), eq(SUGGESTION_MESSAGE)); } - private void setupMockMethodCall(String packageName, String clientName, int numOfInvocations, - String methodFound) { + private void setupMockMethodCall(String methodToCheck, int numOfInvocations, String packageName, String className) { PsiVariable declaredElement = mock(PsiVariable.class); PsiElement[] declaredElements = new PsiElement[] {declaredElement}; @@ -80,41 +93,50 @@ private void setupMockMethodCall(String packageName, String clientName, int numO when(declaredElement.getType()).thenReturn(clientType); when(declaredElement.getInitializer()).thenReturn(initializer); when(clientType.getCanonicalText()).thenReturn(packageName); - when(clientType.getPresentableText()).thenReturn(clientName); + 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(expression); - when(expression.getQualifierExpression()).thenReturn(qualifier); + 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(expression.getReferenceName()).thenReturn(methodFound); - when(finalExpression.getMethodExpression()).thenReturn(expression); + when(expression.getReferenceName()).thenReturn(methodToCheck); - if (!"disableAutoComplete".equals(methodFound)) { + 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", "ServiceBusRuleManagerClient", 0, "notDisableAutoComplete"), - new TestCase("com.azure", "ServiceBusReceiverClient", 0, "disableAutoComplete"), - new TestCase("com.microsoft.azure", "ServiceBusReceiverClient", 0, "disableAutoComplete") + 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 clientName; + String className; int numOfInvocations; String methodFound; - TestCase(String packageName, String clientName, int numOfInvocations, String methodFound) { + TestCase(String packageName, String className, int numOfInvocations, String methodFound) { this.packageName = packageName; - this.clientName = clientName; + this.className = className; this.numOfInvocations = numOfInvocations; this.methodFound = methodFound; } 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 index 1eb6f4dd862..59bdf2d7913 100644 --- 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 @@ -3,6 +3,8 @@ 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; @@ -12,13 +14,15 @@ 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.intellij.codeInspection.ProblemsHolder; -import com.intellij.psi.JavaElementVisitor; -import com.intellij.psi.PsiMethodCallExpression; 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.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -26,7 +30,6 @@ 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; @@ -37,6 +40,9 @@ * 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; @@ -49,12 +55,20 @@ public class DynamicClientCreationCheckTest { @Mock private PsiMethodCallExpression mockMethodCallExpression; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); - mockVisitor = new DynamicClientCreationVisitor(mockHolder); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("DynamicClientCreationCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("buildClient", "buildAsyncClient")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + mockVisitor = new DynamicClientCreationVisitor(mockHolder, mockRuleConfigLoader); mockElement = mock(PsiForStatement.class); mockMethodCallExpression = mock(PsiMethodCallExpression.class); } @@ -67,13 +81,12 @@ public void testDynamicClientCreation(TestCase testCase) { mockVisitor.visitForStatement(mockElement); verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockMethodCallExpression), - contains("Dynamic client creation detected. Create a single client instance and reuse it instead.")); + eq(SUGGESTION_MESSAGE)); } else { setupWithDeclarationStatement(testCase.methodName, testCase.packageName, testCase.numOfInvocations); mockVisitor.visitForStatement(mockElement); - verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockMethodCallExpression), contains("Dynamic " + - "client creation detected. Create a single client instance and reuse it instead.")); + verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockMethodCallExpression), eq(SUGGESTION_MESSAGE)); } } 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..d5ed59fe319 --- /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,93 @@ +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.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(mockRuleConfigLoader.getRuleConfig("EventHubConsumerAsyncClientCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("EventHubConsumerAsyncClient")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + + mockTypeElement = mock(PsiTypeElement.class); + } + + @ParameterizedTest + @MethodSource("provideEventHubConsumerAsyncClientTestCases") + void detectsEventHubConsumerAsyncClientUsage(TestCase testCase) { + JavaElementVisitor visitor = + new EventHubConsumerAsyncClientCheck.EventHubConsumerAsyncClientVisitor(mockHolder, mockRuleConfigLoader); setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); + visitor.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..38b8097838f --- /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.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(mockRuleConfigLoader.getRuleConfig("GetCompletionsCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getCompletions")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + mockVisitor = new GetCompletionsCheck.GetCompletionsVisitor(mockHolder, mockRuleConfigLoader); + + } + + @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 index 78fdd51b45e..34fe6d3a3dd 100644 --- 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 @@ -13,11 +13,16 @@ 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.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; @@ -28,6 +33,8 @@ * 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; @@ -40,11 +47,20 @@ public class GetSyncPollerOnPollerFluxCheckTest { @Mock private PsiElement mockElement; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; @BeforeEach - public void setup() { + public void setUp() { + MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); - mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("GetSyncPollerOnPollerFluxCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getSyncPoller")); + when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); + when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); + mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder, mockRuleConfigLoader); mockMethodCallExpression = mock(PsiMethodCallExpression.class); mockElement = mock(PsiElement.class); } @@ -54,8 +70,7 @@ public void setup() { public void testGetSyncPollerOnPollerFluxCheck(TestCase testCase) { mockMethodExpression(testCase.methodName, testCase.className, testCase.numberOfInvocations); mockVisitor.visitMethodCallExpression(mockMethodCallExpression); - verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(mockElement, "Use of getSyncPoller() on a " + - "PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks."); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(mockElement, SUGGESTION_MESSAGE); } private static Stream testCases() { 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 index dd260725355..1eb5b2a6fdd 100644 --- 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 @@ -11,12 +11,17 @@ 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.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; @@ -40,17 +45,38 @@ 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."; + "`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() { + public void setUp() { + MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); - mockVisitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("HardcodedAPIKeysAndTokensCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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()); + mockVisitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder, mockRuleConfigLoader); } @ParameterizedTest @@ -72,7 +98,7 @@ public void testNoFlagIfNoHardcodedAPIKeysAndTokens() { when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); when(javaCodeReferenceElement.getReferenceName()).thenReturn("AzureKeyCredential"); - when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure"); + when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure."); when(newExpression.getChildren()).thenReturn(new PsiElement[]{literalExpression}); when(literalExpression.getValue()).thenReturn(System.getenv()); @@ -102,7 +128,7 @@ private PsiNewExpression mockMethodExpression(String authServiceToCheck, String when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); when(javaCodeReferenceElement.getReferenceName()).thenReturn(authServiceToCheck); - when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure"); + 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); 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..5b929849d41 --- /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.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(mockRuleConfigLoader.getRuleConfig("ServiceBusReceiverAsyncClientCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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); + } + + @ParameterizedTest + @MethodSource("provideServiceBusReceiverAsyncClientTestCases") + void detectsServiceBusReceiverAsyncClientUsage(TestCase testCase) { + JavaElementVisitor visitor = new ServiceBusReceiverAsyncClientCheck.ServiceBusReceiverAsyncClientCheckVisitor(mockHolder, mockRuleConfigLoader); + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); + visitor.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 index 4c8d3908cde..c3dcc48fca9 100644 --- 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 @@ -21,13 +21,17 @@ import com.intellij.psi.PsiType; import com.intellij.psi.PsiVariable; import com.intellij.psi.PsiWhileStatement; -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.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; @@ -56,17 +60,28 @@ * in enhanced for loop.")) .subscribe(); } */ public class SingleOperationInLoopCheckTest { - private static final String PROBLEM_DESCRIPTION = "This SDK provides a batch operation API, use it to perform multiple actions in a single request. Consider using %sBatch instead."; - + 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(mockRuleConfigLoader.getRuleConfig("SingleOperationInLoopTextAnalyticsCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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")); mockVisitor = createVisitor(); initializer = mock(PsiMethodCallExpression.class); expression = mock(PsiMethodCallExpression.class); @@ -75,21 +90,22 @@ public void setup() { @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(String.format(PROBLEM_DESCRIPTION, testCase.methodName))); + Mockito.eq(expectedMessage)); } else { setupWithSinglePsiExpressionStatement(testCase.loopStatement, testCase.packageName, testCase.numberOfInvocations, testCase.methodName); verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(expression), - Mockito.eq(String.format(PROBLEM_DESCRIPTION, testCase.methodName))); + Mockito.eq(expectedMessage)); } } private JavaElementVisitor createVisitor() { - return new SingleOperationInLoopTextAnalyticsCheck.SingleOperationInLoopVisitor(mockHolder); + return new SingleOperationInLoopTextAnalyticsCheck.SingleOperationInLoopVisitor(mockHolder, mockRuleConfigLoader); } private void setupWithSinglePsiExpressionStatement(PsiStatement loopStatement, String packageName, 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 index ca7edb24325..b67aa18d894 100644 --- 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 @@ -13,11 +13,16 @@ 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.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; @@ -43,6 +48,7 @@ * 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; @@ -50,11 +56,21 @@ public class StorageUploadWithoutLengthCheckTest { 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); - mockVisitor = new StorageUploadWithoutLengthCheck.StorageUploadVisitor(mockHolder); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("StorageUploadWithoutLengthCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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.")); + mockVisitor = new StorageUploadWithoutLengthCheck.StorageUploadVisitor(mockHolder, mockRuleConfigLoader); mockExpression = mock(PsiMethodCallExpression.class); } @@ -63,8 +79,7 @@ public void setup() { public void testStorageUploadWithoutLengthCheck(TestCase testCase) { setupStorageCall(testCase.methodName, testCase.numberOfInvocations); mockVisitor.visitMethodCallExpression(mockExpression); - verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(eq(mockExpression), contains("Azure " + - "Storage upload API without length parameter detected")); + verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(eq(mockExpression), contains(SUGGESTION_MESSAGE)); } private void setupStorageCall(String methodName, int 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..26360dbfd6b --- /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,116 @@ +// 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.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; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockHolder = mock(ProblemsHolder.class); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("UpdateCheckpointAsyncSubscribeCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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); + } + + @ParameterizedTest + @MethodSource("provideTestCases") + public void testWithParameterizedCases(String packageName, String mainMethodFound, + int numOfInvocations, String followingMethod, String objectType) { + JavaElementVisitor visitor = new UpdateCheckpointAsyncSubscribeCheck.UpdateCheckpointAsyncVisitor(mockHolder, + mockRuleConfigLoader); + setupMockMethodCall(packageName, mainMethodFound, numOfInvocations, followingMethod, + objectType, + SUGGESTION_MESSAGE); + visitor.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 index 38264b50f64..c7c2708750e 100644 --- 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 @@ -3,23 +3,24 @@ package com.microsoft.azure.toolkit.intellij.java.sdk.analyzer; -import com.intellij.psi.PsiClassType; -import com.intellij.psi.PsiElement; -import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor; - 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.analyzer.UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor; +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.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 java.util.stream.Stream; +import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -41,6 +42,8 @@ * } 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; @@ -54,10 +57,29 @@ public class UseOfBlockOnAsyncClientsCheckTest { @Mock private PsiElement problemElement; + @Mock private RuleConfigLoader mockRuleConfigLoader; + @Mock private RuleConfig mockRuleConfig; + @BeforeEach public void setup() { + MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); - mockVisitor = createVisitor(); + // Set up mock rule config + when(mockRuleConfigLoader.getRuleConfig("UseOfBlockOnAsyncClientsCheck")).thenReturn(mockRuleConfig); + when(mockRuleConfig.skipRuleCheck()).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")); + mockVisitor = new UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor(mockHolder, mockRuleConfigLoader); mockElement = mock(PsiMethodCallExpression.class); problemElement = mock(PsiElement.class); } @@ -70,12 +92,7 @@ public void testUseOfBlockOnAsyncClient(TestCase testCase) { mockVisitor.visitMethodCallExpression(mockElement); verify(mockHolder, times(testCase.numberOfInvocations)).registerProblem(Mockito.eq(problemElement), - Mockito.eq( - "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead.")); - } - - private JavaElementVisitor createVisitor() { - return new UseOfBlockOnAsyncClientsVisitor(mockHolder); + Mockito.eq(SUGGESTION_MESSAGE)); } private void setupMockMethodCallExpression(String methodName, String clientPackageName, int numberOfInvocations, From 595e1617aeae7597ccf287d7721b3ffe9817fc00 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Wed, 5 Feb 2025 22:55:06 -0800 Subject: [PATCH 7/7] remove RuleConfigLoaderInitializer + comments --- .../sdk/analyzer/ConnectionStringCheck.java | 13 ++-- .../analyzer/DisableAutoCompleteCheck.java | 14 ++-- .../analyzer/DynamicClientCreationCheck.java | 13 ++-- .../EventHubConsumerAsyncClientCheck.java | 15 ++-- .../sdk/analyzer/GetCompletionsCheck.java | 13 ++-- .../GetSyncPollerOnPollerFluxCheck.java | 15 ++-- .../HardcodedAPIKeysAndTokensCheck.java | 13 ++-- .../ServiceBusReceiverAsyncClientCheck.java | 15 ++-- ...ngleOperationInLoopTextAnalyticsCheck.java | 13 ++-- .../StorageUploadWithoutLengthCheck.java | 13 ++-- .../UpdateCheckpointAsyncSubscribeCheck.java | 13 ++-- .../UseOfBlockOnAsyncClientsCheck.java | 13 ++-- .../intellij/java/sdk/models/RuleConfig.java | 54 +------------- .../java/sdk/utils/RuleConfigLoader.java | 73 +++++++++---------- .../utils/RuleConfigLoaderInitializer.java | 20 ----- .../azure-intellij-plugin-java-sdk.xml | 2 +- .../resources/{META-INF => }/ruleConfigs.json | 0 .../analyzer/ConnectionStringCheckTest.java | 13 ++-- .../DisableAutoCompleteCheckTest.java | 7 +- .../DynamicClientCreationCheckTest.java | 7 +- .../EventHubConsumerAsyncClientCheckTest.java | 12 +-- .../sdk/analyzer/GetCompletionsCheckTest.java | 8 +- .../GetSyncPollerOnPollerFluxCheckTest.java | 7 +- .../HardcodedAPIKeysAndTokensCheckTest.java | 8 +- ...erviceBusReceiverAsyncClientCheckTest.java | 10 +-- .../SingleOperationInLoopCheckTest.java | 11 +-- .../StorageUploadWithoutLengthCheckTest.java | 7 +- ...dateCheckpointAsyncSubscribeCheckTest.java | 14 ++-- .../UseOfBlockOnAsyncClientsCheckTest.java | 8 +- 29 files changed, 184 insertions(+), 240 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/resources/{META-INF => }/ruleConfigs.json (100%) 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 index ca448ec32c2..caa0ce5d4e8 100644 --- 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 @@ -13,6 +13,7 @@ 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; /** @@ -22,7 +23,7 @@ public class ConnectionStringCheck extends LocalInspectionTool { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new ConnectionStringCheckVisitor(holder, RuleConfigLoader.getInstance()); + return new ConnectionStringCheckVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } static class ConnectionStringCheckVisitor extends JavaElementVisitor { @@ -30,16 +31,16 @@ static class ConnectionStringCheckVisitor extends JavaElementVisitor { private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; - ConnectionStringCheckVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + ConnectionStringCheckVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "ConnectionStringCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index 0db1ad635ed..60ff5635ba4 100644 --- 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 @@ -17,6 +17,7 @@ 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; /** @@ -36,7 +37,7 @@ public class DisableAutoCompleteCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new DisableAutoCompleteVisitor(holder, RuleConfigLoader.getInstance()); + return new DisableAutoCompleteVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -50,17 +51,16 @@ static class DisableAutoCompleteVisitor extends JavaElementVisitor { private static boolean SKIP_WHOLE_RULE; private final ProblemsHolder holder; - - DisableAutoCompleteVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + DisableAutoCompleteVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "DisableAutoCompleteCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } /** 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 index 02929fc9d06..71b21373ef3 100644 --- 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 @@ -21,6 +21,7 @@ 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; /** @@ -31,7 +32,7 @@ public class DynamicClientCreationCheck extends LocalInspectionTool { @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new DynamicClientCreationVisitor(holder, RuleConfigLoader.getInstance()); + return new DynamicClientCreationVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } static class DynamicClientCreationVisitor extends JavaElementVisitor { @@ -40,16 +41,16 @@ static class DynamicClientCreationVisitor extends JavaElementVisitor { private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; - public DynamicClientCreationVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + public DynamicClientCreationVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "DynamicClientCreationCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index fdecc9148fe..9051eac3daf 100644 --- 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 @@ -7,6 +7,7 @@ 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; /** @@ -24,7 +25,7 @@ public class EventHubConsumerAsyncClientCheck extends LocalInspectionTool { @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new EventHubConsumerAsyncClientCheck.EventHubConsumerAsyncClientVisitor(holder, - RuleConfigLoader.getInstance()); + RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -41,18 +42,18 @@ static class EventHubConsumerAsyncClientVisitor extends JavaElementVisitor { * Constructor to initialize the visitor with the holder and isOnTheFly flag. * * @param holder Holder for the problems found by the inspection - * @param ruleConfigLoader RuleConfigLoader object to load the rule configuration + * @param ruleConfigs Rule configurations for the inspection */ - public EventHubConsumerAsyncClientVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + public EventHubConsumerAsyncClientVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "EventHubConsumerAsyncClientCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index e707d9c22d7..8ee07be789f 100644 --- 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 @@ -13,6 +13,7 @@ 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; /** @@ -22,7 +23,7 @@ public class GetCompletionsCheck extends LocalInspectionTool { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new GetCompletionsCheck.GetCompletionsVisitor(holder, RuleConfigLoader.getInstance()); + return new GetCompletionsCheck.GetCompletionsVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } static class GetCompletionsVisitor extends JavaElementVisitor { @@ -30,16 +31,16 @@ static class GetCompletionsVisitor extends JavaElementVisitor { private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; - GetCompletionsVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + GetCompletionsVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "GetCompletionsCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index 010a830061a..d8d04abf5f8 100644 --- 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 @@ -12,6 +12,7 @@ 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; /** @@ -39,7 +40,7 @@ public class GetSyncPollerOnPollerFluxCheck extends LocalInspectionTool { @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new GetSyncPollerOnPollerFluxVisitor(holder, RuleConfigLoader.getInstance()); + return new GetSyncPollerOnPollerFluxVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -56,18 +57,18 @@ class GetSyncPollerOnPollerFluxVisitor extends JavaElementVisitor { * Constructor to initialize the visitor with the holder and isOnTheFly flag. * * @param holder Holder for the problems found by the inspection - * @param ruleConfigLoader RuleConfigLoader object to load the rule configuration + * @param ruleConfigs Rule configurations for the inspection */ - public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "GetSyncPollerOnPollerFluxCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index 28821636988..a631468fbbb 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -37,7 +38,7 @@ public class HardcodedAPIKeysAndTokensCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new APIKeysAndTokensVisitor(holder, RuleConfigLoader.getInstance()); + return new APIKeysAndTokensVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -49,16 +50,16 @@ static class APIKeysAndTokensVisitor extends JavaElementVisitor { private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; - APIKeysAndTokensVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + APIKeysAndTokensVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "HardcodedAPIKeysAndTokensCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } @Override 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 index 6f2739733e3..6e9bfbaa98a 100644 --- 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 @@ -7,6 +7,7 @@ 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; /** @@ -18,7 +19,7 @@ public class ServiceBusReceiverAsyncClientCheck extends LocalInspectionTool { @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { return new ServiceBusReceiverAsyncClientCheck.ServiceBusReceiverAsyncClientCheckVisitor(holder, - RuleConfigLoader.getInstance()); + RuleConfigLoader.getInstance().getRuleConfigs()); } @@ -32,18 +33,18 @@ static class ServiceBusReceiverAsyncClientCheckVisitor extends JavaElementVisito * Constructor to initialize the visitor with the holder and isOnTheFly flag. * * @param holder Holder for the problems found by the inspection - * @param ruleConfigLoader RuleConfigLoader object to load the rule configuration + * @param ruleConfigs Rule configurations for the inspection */ - public ServiceBusReceiverAsyncClientCheckVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + public ServiceBusReceiverAsyncClientCheckVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "ServiceBusReceiverAsyncClientCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index cdd83cda717..f679667c3c6 100644 --- 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 @@ -24,6 +24,7 @@ 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; /** @@ -52,7 +53,7 @@ public class SingleOperationInLoopTextAnalyticsCheck extends LocalInspectionTool @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new SingleOperationInLoopVisitor(holder, RuleConfigLoader.getInstance()); + return new SingleOperationInLoopVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -66,16 +67,16 @@ static class SingleOperationInLoopVisitor extends JavaElementVisitor { private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; - public SingleOperationInLoopVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + public SingleOperationInLoopVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "SingleOperationInLoopTextAnalyticsCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index 90c4e6ed3eb..ab6d07bd099 100644 --- 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 @@ -21,6 +21,7 @@ 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; /** @@ -31,7 +32,7 @@ public class StorageUploadWithoutLengthCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new StorageUploadVisitor(holder, RuleConfigLoader.getInstance()); + return new StorageUploadVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -44,16 +45,16 @@ static class StorageUploadVisitor extends JavaRecursiveElementWalkingVisitor { private static boolean SKIP_WHOLE_RULE; - StorageUploadVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + StorageUploadVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "StorageUploadWithoutLengthCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } @Override 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 index 2a4156aaefc..6d6a04b8e8f 100644 --- 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 @@ -13,6 +13,7 @@ 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; /** @@ -33,7 +34,7 @@ public class UpdateCheckpointAsyncSubscribeCheck extends LocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new UpdateCheckpointAsyncVisitor(holder, RuleConfigLoader.getInstance()); + return new UpdateCheckpointAsyncVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -52,16 +53,16 @@ static class UpdateCheckpointAsyncVisitor extends JavaElementVisitor { * * @param holder ProblemsHolder to register problems */ - UpdateCheckpointAsyncVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + UpdateCheckpointAsyncVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "UpdateCheckpointAsyncSubscribeCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index 0440d41fb2e..9922feb975f 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -28,7 +29,7 @@ public class UseOfBlockOnAsyncClientsCheck extends LocalInspectionTool { @NotNull @Override public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new UseOfBlockOnAsyncClientsVisitor(holder, RuleConfigLoader.getInstance()); + return new UseOfBlockOnAsyncClientsVisitor(holder, RuleConfigLoader.getInstance().getRuleConfigs()); } /** @@ -39,16 +40,16 @@ static class UseOfBlockOnAsyncClientsVisitor extends JavaElementVisitor { private static RuleConfig RULE_CONFIG; private static boolean SKIP_WHOLE_RULE; - UseOfBlockOnAsyncClientsVisitor(ProblemsHolder holder, RuleConfigLoader ruleConfigLoader) { + UseOfBlockOnAsyncClientsVisitor(ProblemsHolder holder, Map ruleConfigs) { this.holder = holder; - initializeRuleConfig(ruleConfigLoader); + initializeRuleConfig(ruleConfigs); } - private void initializeRuleConfig(RuleConfigLoader ruleConfigLoader) { + private void initializeRuleConfig(Map ruleConfigs) { if (RULE_CONFIG == null) { final String ruleName = "UseOfBlockOnAsyncClientsCheck"; - RULE_CONFIG = ruleConfigLoader.getRuleConfig(ruleName); - SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck(); + RULE_CONFIG = ruleConfigs.get(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.isSkipRuleCheck(); } } 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 index ce37f157507..2f1ded692fa 100644 --- 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 @@ -6,19 +6,14 @@ 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 { - public static final String DEFAULT_SCOPE = "all"; - public static final String DEFAULT_USAGE = "all"; - public static final RuleConfig EMPTY_RULE = - new RuleConfig(Collections.singletonList(DEFAULT_USAGE), - Collections.singletonList(DEFAULT_SCOPE), - null, - Collections.emptyMap(), true); private final List usagesToCheck; private final List scopeToCheck; private final String antiPatternMessage; @@ -48,49 +43,4 @@ public RuleConfig(List usagesToCheck, List scopeToCheck, String : Collections.unmodifiableMap(regexPatternsToCheck); this.skipRuleCheck = skipRuleCheck; } - - /** - * This method checks if the rule should be skipped. - * - * @return True if the rule should be skipped, false otherwise. - */ - public boolean skipRuleCheck() { - return skipRuleCheck; - } - - /** - * This method returns the list of methods to check. - * - * @return List of methods to check. - */ - public List getUsagesToCheck() { - return usagesToCheck; - } - - /** - * This method returns the list of clients to check. - * - * @return List of clients to check. - */ - public List getScopeToCheck() { - return scopeToCheck; - } - - /** - * This method returns the antipattern message. - * - * @return Antipattern message. - */ - public String getAntiPatternMessage() { - return antiPatternMessage; - } - - /** - * This method returns the map of regex patterns to check. - * - * @return Map of regex patterns to check. - */ - public Map getRegexPatternsToCheck() { - return regexPatternsToCheck; - } } 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 index e15dccfd518..d89134a28b0 100644 --- 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 @@ -5,77 +5,70 @@ 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 java.util.logging.Level; -import java.util.logging.Logger; - -import static com.microsoft.azure.toolkit.intellij.java.sdk.models.RuleConfig.EMPTY_RULE; - -public class RuleConfigLoader { - private static final String CONFIG_FILE_PATH = "./META-INF/ruleConfigs.json"; - private static final Logger LOGGER = Logger.getLogger(RuleConfigLoader.class.getName()); +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 static boolean initializationFailed = false; - private final Map ruleConfigs; + private Map ruleConfigs; private RuleConfigLoader() { this.ruleConfigs = new HashMap<>(); } - private RuleConfigLoader(String filePath) throws IOException { - this.ruleConfigs = loadRuleConfigurations(filePath); - } - /** * Gets the singleton instance of RuleConfigLoader. * * @return The singleton instance of RuleConfigLoader. */ public static RuleConfigLoader getInstance() { - if (initializationFailed) { - LOGGER.log(Level.WARNING, "RuleConfigLoader initialization failed. Returning default instance."); - return new RuleConfigLoader(); - } return INSTANCE; } - /** - * Initializes the RuleConfigLoader instance asynchronously. - */ - public static void initialize() { - try { - INSTANCE = new RuleConfigLoader(CONFIG_FILE_PATH); - } catch (IOException e) { - INSTANCE = new RuleConfigLoader(); - initializationFailed = true; - LOGGER.log(Level.SEVERE, "Failed to initialize RuleConfigLoader: " + e.getMessage(), e); - } + @Nullable + @Override + public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { + initialize(); + return null; } /** - * Gets the rule configuration for the specified key. + * Get the rule configurations. * - * @param key The key of the rule configuration. - * @return The rule configuration for the specified key. + * @return the rule configurations */ - public RuleConfig getRuleConfig(String key) { - if (initializationFailed) { - return EMPTY_RULE; + 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); } - return ruleConfigs.getOrDefault(key, EMPTY_RULE); } - private Map loadRuleConfigurations(String filePath) throws IOException { - InputStream configStream = getClass().getClassLoader().getResourceAsStream(filePath); + private Map loadRuleConfigurations() throws IOException { + InputStream configStream = getClass().getClassLoader().getResourceAsStream(RuleConfigLoader.CONFIG_FILE_PATH); if (configStream == null) { - initializationFailed = true; - LOGGER.log(Level.WARNING, "Rule configuration file not found: " + filePath); + log.info("Rule configuration file not found: " + RuleConfigLoader.CONFIG_FILE_PATH); return new HashMap<>(); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java deleted file mode 100644 index 820b6df4615..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-java-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/java/sdk/utils/RuleConfigLoaderInitializer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.microsoft.azure.toolkit.intellij.java.sdk.utils; - -import com.intellij.openapi.project.Project; -import com.intellij.openapi.startup.ProjectActivity; -import javax.annotation.Nonnull; -import kotlin.Unit; -import kotlin.coroutines.Continuation; - -/** - * The RuleConfigLoaderInitializer class is responsible for initializing the RuleConfigLoader class. - */ -public class RuleConfigLoaderInitializer implements ProjectActivity { - - @javax.annotation.Nullable - @Override - public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { - RuleConfigLoader.initialize(); - return null; - } -} \ No newline at end of file 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 16d91101407..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 @@ -6,7 +6,7 @@ - + mockRules = Map.of("ConnectionStringCheck", mockRuleConfig); + mockVisitor = new ConnectionStringCheck.ConnectionStringCheckVisitor(mockHolder, mockRules); } @ParameterizedTest 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 index 2dc718c922f..ab0cc991d9a 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -59,15 +60,15 @@ public void setUp() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("DisableAutoCompleteCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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); - mockVisitor = new DisableAutoCompleteVisitor(mockHolder, mockRuleConfigLoader); + Map mockRules = Map.of("DisableAutoCompleteCheck", mockRuleConfig); + mockVisitor = new DisableAutoCompleteVisitor(mockHolder, mockRules); } @ParameterizedTest 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 index 59bdf2d7913..1f98c31da25 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -63,13 +64,13 @@ public void setup() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("DynamicClientCreationCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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()); - mockVisitor = new DynamicClientCreationVisitor(mockHolder, mockRuleConfigLoader); mockElement = mock(PsiForStatement.class); + Map mockRules = Map.of("DynamicClientCreationCheck", mockRuleConfig); + mockVisitor = new DynamicClientCreationVisitor(mockHolder, mockRules); mockMethodCallExpression = mock(PsiMethodCallExpression.class); } 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 index d5ed59fe319..8b8529d0c4f 100644 --- 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 @@ -8,6 +8,7 @@ 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; @@ -45,11 +46,13 @@ public void setUp() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("EventHubConsumerAsyncClientCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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); } @@ -57,9 +60,8 @@ public void setUp() { @ParameterizedTest @MethodSource("provideEventHubConsumerAsyncClientTestCases") void detectsEventHubConsumerAsyncClientUsage(TestCase testCase) { - JavaElementVisitor visitor = - new EventHubConsumerAsyncClientCheck.EventHubConsumerAsyncClientVisitor(mockHolder, mockRuleConfigLoader); setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); - visitor.visitTypeElement(mockTypeElement); + setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); + mockVisitor.visitTypeElement(mockTypeElement); verify(mockHolder, times(testCase.numOfInvocations)).registerProblem(eq(mockTypeElement), eq(SUGGESTION_MESSAGE)); } 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 index 38b8097838f..c83494c8e1a 100644 --- 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 @@ -14,6 +14,7 @@ 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; @@ -53,12 +54,11 @@ public void setup() { mockHolder = mock(ProblemsHolder.class); methodCallExpression = mock(PsiMethodCallExpression.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("GetCompletionsCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getCompletions")); when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); - mockVisitor = new GetCompletionsCheck.GetCompletionsVisitor(mockHolder, mockRuleConfigLoader); - + Map mockRules = Map.of("GetCompletionsCheck", mockRuleConfig); + mockVisitor = new GetCompletionsCheck.GetCompletionsVisitor(mockHolder, mockRules); } @ParameterizedTest 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 index 34fe6d3a3dd..855b2e84078 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -55,14 +56,14 @@ public void setUp() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("GetSyncPollerOnPollerFluxCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); when(mockRuleConfig.getUsagesToCheck()).thenReturn(Collections.singletonList("getSyncPoller")); when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); - mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder, mockRuleConfigLoader); mockMethodCallExpression = mock(PsiMethodCallExpression.class); mockElement = mock(PsiElement.class); + Map mockRules = Map.of("GetSyncPollerOnPollerFluxCheck", mockRuleConfig); + mockVisitor = new GetSyncPollerOnPollerFluxCheck().new GetSyncPollerOnPollerFluxVisitor(mockHolder, mockRules); } @ParameterizedTest 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 index 1eb5b2a6fdd..2e7d54b4633 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -63,8 +64,7 @@ public void setUp() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("HardcodedAPIKeysAndTokensCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("AzureKeyCredential", "AccessToken", "KeyCredential", @@ -76,7 +76,9 @@ public void setUp() { "BasicAuthenticationCredential")); when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); when(mockRuleConfig.getScopeToCheck()).thenReturn(Collections.emptyList()); - mockVisitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder, mockRuleConfigLoader); + Map mockRules = Map.of("HardcodedAPIKeysAndTokensCheck", mockRuleConfig); + + mockVisitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder, mockRules); } @ParameterizedTest 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 index 5b929849d41..30b50b1f883 100644 --- 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 @@ -11,6 +11,7 @@ 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; @@ -56,25 +57,24 @@ public void setUp() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("ServiceBusReceiverAsyncClientCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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) { - JavaElementVisitor visitor = new ServiceBusReceiverAsyncClientCheck.ServiceBusReceiverAsyncClientCheckVisitor(mockHolder, mockRuleConfigLoader); setupMockElement(mockTypeElement, testCase.numOfInvocations, testCase.usagesToCheck); - visitor.visitTypeElement(mockTypeElement); + 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), 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 index c3dcc48fca9..6ee29ab051a 100644 --- 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 @@ -25,6 +25,7 @@ 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; @@ -75,16 +76,16 @@ public void setup() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("SingleOperationInLoopTextAnalyticsCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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")); - mockVisitor = createVisitor(); initializer = mock(PsiMethodCallExpression.class); expression = mock(PsiMethodCallExpression.class); + Map mockRules = Map.of("SingleOperationInLoopTextAnalyticsCheck", mockRuleConfig); + mockVisitor = new SingleOperationInLoopTextAnalyticsCheck.SingleOperationInLoopVisitor(mockHolder, mockRules); } @ParameterizedTest @@ -104,10 +105,6 @@ public void testSingleOperationInLoop(TestCase testCase) { } } - private JavaElementVisitor createVisitor() { - return new SingleOperationInLoopTextAnalyticsCheck.SingleOperationInLoopVisitor(mockHolder, mockRuleConfigLoader); - } - private void setupWithSinglePsiExpressionStatement(PsiStatement loopStatement, String packageName, int numberOfInvocations, String methodName) { PsiBlockStatement loopBody = mock(PsiBlockStatement.class); 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 index b67aa18d894..ba45c40615a 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -64,14 +65,14 @@ public void setup() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("StorageUploadWithoutLengthCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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.")); - mockVisitor = new StorageUploadWithoutLengthCheck.StorageUploadVisitor(mockHolder, mockRuleConfigLoader); mockExpression = mock(PsiMethodCallExpression.class); + Map mockRules = Map.of("StorageUploadWithoutLengthCheck", mockRuleConfig); + mockVisitor = new StorageUploadWithoutLengthCheck.StorageUploadVisitor(mockHolder, mockRules); } @ParameterizedTest 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 index 26360dbfd6b..8d993d64c43 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -44,31 +45,34 @@ public class UpdateCheckpointAsyncSubscribeCheckTest { @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(mockRuleConfigLoader.getRuleConfig("UpdateCheckpointAsyncSubscribeCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + 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) { - JavaElementVisitor visitor = new UpdateCheckpointAsyncSubscribeCheck.UpdateCheckpointAsyncVisitor(mockHolder, - mockRuleConfigLoader); setupMockMethodCall(packageName, mainMethodFound, numOfInvocations, followingMethod, objectType, SUGGESTION_MESSAGE); - visitor.visitMethodCallExpression(mockMethodCallExpression); + mockVisitor.visitMethodCallExpression(mockMethodCallExpression); verify(mockHolder, times(numOfInvocations)).registerProblem(eq(mockPsiElement), eq(SUGGESTION_MESSAGE)); } 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 index c7c2708750e..f5d1bcff423 100644 --- 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 @@ -10,10 +10,10 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiMethodCallExpression; import com.intellij.psi.PsiReferenceExpression; -import com.microsoft.azure.toolkit.intellij.java.sdk.analyzer.UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor; 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; @@ -65,8 +65,7 @@ public void setup() { MockitoAnnotations.openMocks(this); mockHolder = mock(ProblemsHolder.class); // Set up mock rule config - when(mockRuleConfigLoader.getRuleConfig("UseOfBlockOnAsyncClientsCheck")).thenReturn(mockRuleConfig); - when(mockRuleConfig.skipRuleCheck()).thenReturn(false); + when(mockRuleConfig.isSkipRuleCheck()).thenReturn(false); when(mockRuleConfig.getUsagesToCheck()).thenReturn(Arrays.asList("block", "blockOptional", "blockFirst", @@ -79,9 +78,10 @@ public void setup() { when(mockRuleConfig.getAntiPatternMessage()).thenReturn(SUGGESTION_MESSAGE); when(mockRuleConfig.getScopeToCheck()).thenReturn(Arrays.asList("reactor.core.publisher.Flux", "reactor.core.publisher.Mono")); - mockVisitor = new UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor(mockHolder, mockRuleConfigLoader); mockElement = mock(PsiMethodCallExpression.class); problemElement = mock(PsiElement.class); + Map mockRules = Map.of("UseOfBlockOnAsyncClientsCheck", mockRuleConfig); + mockVisitor = new UseOfBlockOnAsyncClientsCheck.UseOfBlockOnAsyncClientsVisitor(mockHolder, mockRules); } @ParameterizedTest