diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.gitignore b/PluginsAndFeatures/azure-toolkit-for-intellij/.gitignore index 6691dbbf974..8631128eac3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.gitignore +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/.gitignore @@ -44,3 +44,9 @@ gen-external-apklibs/ # Jar dependencies generated in build **/resources/spark/spark-tools-*.jar + +#.idea +/azure-intellij-plugin-azure-sdk/.idea/ + +# IntelliJ +.idea/ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/codeStyles/Project.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/codeStyles/Project.xml deleted file mode 100644 index 6735a734ceb..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/codeStyles/codeStyleConfig.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 39dce05fd51..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/copyright/Microsoft.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/copyright/Microsoft.xml deleted file mode 100644 index 09b5df2c7a7..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/copyright/Microsoft.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/findbugs-idea.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/findbugs-idea.xml deleted file mode 100644 index 68840ef99a7..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/findbugs-idea.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - max - High - - - - - - - - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml deleted file mode 100644 index 0e2731864ba..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/inspectionProfiles/Project_Default.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 422c782e967..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,2835 +0,0 @@ - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/misc.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/misc.xml deleted file mode 100644 index 57337953a0f..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/misc.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/vcs.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/vcs.xml deleted file mode 100644 index b2bdec2d71b..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md new file mode 100644 index 00000000000..78f2c3415fe --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/README.md @@ -0,0 +1,303 @@ +# Project: Java Code Quality Analyzer + +## Project Description + +The Java Code Quality Analyzer is a plugin designed to improve the quality of Java code. It provides an interactive tool +window that offers real-time code suggestions. The following are proposed essential features, including rule set +integration, telemetry connectivity, and Azure Toolkit integration. + +## Key Features + +- **Rule Set Integration**: This feature enables users to import and customize rule sets, tailoring the plugin to their + specific needs. +- **Telemetry Integration**: This feature connects the plugin to the backend with Application Insights, allowing for + efficient data transmission. Refer to [configure-telmetry.md](configure-telemetry.md) on details to setup this feature +- **Azure Toolkit for IntelliJ Integration**: This feature allows the plugin to integrate with the Azure Toolkit for + IntelliJ, providing extended functionality +- **Editor Integration**: This feature offers continuous analysis and real-time code suggestions, enhancing the coding + experience. +- **Quick-Fix Actions**: This feature identifies issues and suggests quick actions within the tool window, facilitating + immediate problem resolution. + +## User Interface + +- **Telemetry Configuration Panel** : This space allows users to enable or disable telemetry if desired. + +## Rules + +1. #### Storage Upload without Length Check + +- **Anti-pattern**: Using Azure Storage upload APIs that don’t take a length parameter, causing the entire data payload + to be buffered into memory before uploading. +- **Issue**: This can lead to OutOfMemoryErrors, especially with large files or high-volume uploads. +- **Severity: INFO** +- **Recommendation**: Use APIs that take a length parameter. Please refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-upload-java) + for additional information. + +2. #### Use ServiceBusProcessorClient instead of ServiceBusReceiverAsyncClient. + +- **Anti-pattern**: The use of the Reactor receiver, specifically the `ServiceBusReceiverAsyncClient`, is an + anti-pattern. This is because it's a low-level API that provides fine-grained control over message handling. While + this might seem beneficial, it requires a high level of proficiency in Reactive programming and is mainly useful when + building a Reactive library or an end-to-end Reactive application. + +- **Issue**: The main issue with using `ServiceBusReceiverAsyncClient` is its complexity and the requirement for a deep + understanding of Reactive programming. This can make it difficult to use correctly and efficiently, especially for + developers who are not familiar with Reactive programming paradigms. +- **Severity: WARNING** +- **Recommendation**: Instead of using the low-level `ServiceBusReceiverAsyncClient`, it's recommended to use + the `ServiceBusProcessorClient`. The `ServiceBusProcessorClient` is a higher-level abstraction that simplifies message + consumption. It's designed for most common use cases and should be the primary choice for consuming messages. This + makes it a more suitable option for most developers and scenarios. + Please refer to + the [Azure SDK for Java documentation](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/servicebus/azure-messaging-servicebus/README.md#when-to-use-servicebusprocessorclient) + for additional information. + +3. #### Disable Auto-complete when using ServiceBusReceiver or Processor clients + +- **Anti-pattern**: When using ServiceBusReceiver or Processor clients, auto-complete is enabled by default, but this + behavior is not explicitly verified or disabled when necessary. +- **Issue**: Auto-complete being enabled by default might lead to messages being marked as completed even if the message + processing fails or encounters an error. + Errors in message processing might not be noticed since the message is automatically completed regardless of success + or failure, making it harder to identify and handle issues. +- **Severity: WARNING** +- **Recommendation**: Explicitly Disable Auto-Complete: When creating ServiceBusReceiver or Processor clients, + explicitly use the + disableAutoComplete() method call to prevent automatic message completion. + Please refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/en-us/java/api/com.azure.messaging.servicebus.servicebusclientbuilder.servicebusreceiverclientbuilder?view=azure-java-stable#com-azure-messaging-servicebus-servicebusclientbuilder-servicebusreceiverclientbuilder-disableautocomplete()) + for additional information. + +4. #### Dynamic Client Creation is wasteful + +- **Anti-pattern**: Dynamic client creation refers to creating a new client instance for each operation, without reusing + existing instances. +- **Issue**: This process can be resource-intensive and slow, especially if repeated frequently. It can lead to + performance issues, increased memory usage, and unnecessary overhead. +- **Severity: WARNING** +- **Recommendation**: Instead of creating a new client instance for each operation, consider reusing existing client + instances. + It's recommended to create client instances once and reuse them throughout the application's lifecycle. + This approach can lead to better performance and efficiency. + Please refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/en-us/azure/developer/java/sdk/overview#connect-to-and-use-azure-resources-with-client-libraries) + for additional information. + +5. #### Hardcoded APIs & Access Tokens Check + +- **Anti-pattern**: Hardcoding API keys and tokens in the source code. +- **Issue**: The source code contains hardcoded API keys and tokens, which is a security risk. It exposes sensitive + information that could be exploited if the code is publicly accessible or falls into the wrong hands. +- **Severity: WARNING** +- **Recommendation**: 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. Please + refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential?view=azure-java-stable) + for additional information. + +6. #### Use SyncPoller instead of PollerFlux#getSyncPoller() + +- **Anti-pattern**: Using `getSyncPoller()` on a `PollerFlux` instance is an anti-pattern. +- **Issue**: The main issue with using `getSyncPoller()` is that it introduces additional complexity by converting an + asynchronous polling mechanism to a synchronous one, which should be avoided. +- **Severity: WARNING** +- **Recommendation**: Instead of using `getSyncPoller()`, it's recommended to use the `SyncPoller` directly to handle + synchronous polling tasks. `SyncPoller` provides a synchronous way to interact with the poller and is the preferred + method for synchronous operations. + Please refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/java/api/com.azure.core.util.polling.syncpoller?view=azure-java-stable) + for additional information. + +7. #### Managing Receive Mode and Prefetch Value in Azure Service Bus + +- **Anti-pattern**: Setting the receive mode as PEEK_LOCK with a high prefetch value (e.g., 50 or 100) in Azure Service + Bus. +- **Severity: WARNING** +- **Issue**: + 1. **Suboptimal Performance:** A high prefetch value in PEEK_LOCK mode can result in suboptimal performance, as one + client + locks all prefetched messages, potentially leading to processing bottlenecks. + 2. **Message Lock Expiry:** Messages in the prefetch queue do not have their locks renewed automatically. + Consequently, + the + message lock may expire by the time they are processed. + 3. **Dead-Letter Queue:** Expired message locks can result in messages being inadvertently sent to the dead-letter + queue, + causing potential data loss or requiring additional handling to recover these messages. +- **Recommendation**: Optimize Prefetch Value - Set a prefetch value that balances between efficient message + retrieval and the ability for multiple clients to process messages concurrently. Please refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-prefetch?tabs=dotnet#why-is-prefetch-not-the-default-option) + for additional information. + +8. #### Use These Encouraged Alternatives Instead of Their Corresponding Discouraged APIs + + #### a. Azure service client authentication instead of Connection Strings to build Azure Service Clients + + - **Anti-pattern**: Using Connection Strings for Authenticating Azure SDK Clients. + - **Issue**: Connection strings authentication is not recommended in Azure SDKs for Java due to potential + security + vulnerabilities. + - **Severity: WARNING** + - **Recommendation**: Azure service client authentication is recommended if the service client supports Token + Credential (Entra ID Authentication). If not, then use Azure Key Credential or Connection Strings based + authentication. Please refer to + the [Azure SDK for Java documentation](https://learn.microsoft.com/java/api/com.azure.identity.defaultazurecredential?view=azure-java-stable) + for additional information. + + #### b. Use Azure OpenAI's `getChatCompletions` for Chat Applications instead of `getCompletions` API + + - **Anti-pattern**: Using the getCompletions API + - **Issue**: Issue: Functionality Mismatch - The `getCompletions` API is designed for general-purpose completion + tasks. + whereas `getChatCompletions` is specifically optimized for conversational contexts. + - **Severity: WARNING** + - **Recommendation**: Use `getChatCompletions` for Chat Applications: Specifically use `getChatCompletions` API + when + generating responses for chatbot or conversational AI applications. + - Please refer to + the [Azure OpenAI client library for Java](https://learn.microsoft.com/java/api/overview/azure/ai-openai-readme?view=azure-java-preview) + for additional information. + +9. #### Use these encouraged clients instead of their corresponding discouraged clients + + ##### a. Use **`ServiceBusProcessorClient`** instead of **`ServiceBusReceiverAsyncClient`** + + ##### b. Use **`EventProcessorClient`** instead of **`EventHubConsumerAsyncClient`** + + ##### Anti-pattern: + + - Both `ServiceBusReceiverAsyncClient` and `EventHubConsumerAsyncClient` are low-level APIs. They provide fine-grained + control over message/event handling but require a high level of proficiency in Reactive programming. + - Due to their complexity and the need for a deep understanding of Reactive programming, there is a higher risk of these + clients being used incorrectly or inefficiently, especially by developers who are not familiar with Reactive + paradigms. + + ##### Issue: + + ##### a. **ServiceBusReceiverAsyncClient** + + - **Anti-pattern**: The `ServiceBusReceiverAsyncClient` is considered an anti-pattern because it demands detailed + handling of messages, which can be overly complex and unnecessary for most common use cases. + - **Severity: WARNING** + - **Recommendation**: Instead of using `ServiceBusReceiverAsyncClient`, it is recommended to + use `ServiceBusProcessorClient`. The `ServiceBusProcessorClient` is a higher-level abstraction that simplifies + message consumption, making it a more suitable option for most developers and scenarios. + - Please refer to + the [Azure Service Bus client for Java](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/servicebus/azure-messaging-servicebus/README.md#when-to-use-servicebusprocessorclient) + for more information. + + ##### b. **EventHubConsumerAsyncClient** + + - **Anti-pattern**: The `EventHubConsumerAsyncClient` is considered an anti-pattern due to its low-level nature and the + complexity involved in event handling. + - **Severity: WARNING** + - **Recommendation**: Instead of using `EventHubConsumerAsyncClient`, it is advised to use `EventProcessorClient`. + The `EventProcessorClient` provides a higher-level abstraction that simplifies event processing, making it the + preferred choice for most developers. + - Please refer to + the [EventProcessorClient Class](https://learn.microsoft.com/en-us/java/api/com.azure.messaging.eventhubs.eventprocessorclient?view=azure-java-stable) + for more information. + +10. #### Using Batch Operations Instead of Single Operations in a Loop + +- **Anti-pattern**: Calling a single operation in a loop when a batch operation API exists in the SDK that can handle + multiple actions in one request. +- **Issue**: + - Repeatedly calling a single operation in a loop leads to multiple network requests, which can be inefficient and + slow. + - Multiple requests also consume more resources (e.g., network bandwidth, server processing) compared to a single + batch request. +- **Severity: WARNING** +- **Recommendation**: Use Batch Operations: If the SDK provides a batch operation API, use it to perform multiple + actions in a single request. + +11. #### EventProcessorClient: Using `updateCheckpointAsync()` API of EventBatchContext object: + #### Anti-pattern a. Without `block()` or `block(timeout)` + + - **Issue**: Calling `updateCheckpointAsync()` without `block()` will not do anything. + - **Severity: WARNING** + - **Recommendation**: Use `block()` operator with a timeout or consider using the synchronous + version `updateCheckpoint()`. + + #### Anti-pattern b. With `subscribe()` + + - **Issue**: If you call `subscribe()` with `updateCheckpointAsync()`, you might get the next batch of events before + you + finish checkpointing the previous batch, or you might have checkpointing of several batches happening out of order + - **Severity: WARNING** + - **Recommendation**: Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous + version + `updateCheckpoint()`. + + Please refer to + the [updateCheckpointAsync documentation](https://learn.microsoft.com/en-us/java/api/com.azure.messaging.eventhubs.models.eventbatchcontext?view=azure-java-stable#com-azure-messaging-eventhubs-models-eventbatchcontext-updatecheckpointasync()) + for additional information. + + +12. #### Kusto Queries Having a Time Interval in the Query String + +- **Anti-pattern**: Writing KQL queries with hard-coded time intervals directly in the query string. +- **Issue**: This approach makes queries less flexible and harder to troubleshoot. +- **Recommendation**: Consider using the `QueryTimeInterval` parameter in the client method parameters to specify the + time interval for the query. + By passing the time range as an argument in the method call, you make it easier to troubleshoot and understand the + context of an API call. + Please refer to + the [QueryTimeInterval Class documentation](https://learn.microsoft.com/java/api/com.azure.monitor.query.models.querytimeinterval?view=azure-java-stable) + for additional information. + +13. #### Authenticating a Non-Azure OpenAI Client with KeyCredential + +- **Anti-pattern**: Assigning the endpoint value when creating a Non-Azure OpenAI client using the KeyCredential in + .credential(KeyCredential). +- **Issue**: KeyCredential is the only required parameter in `.credential(KeyCredential)` for authenticating requests to + non-Azure OpenAI APIs. +- **Severity: WARNING** +- **Recommendation**: Omit Endpoint: Only specify the endpoint parameter if you are working with Azure OpenAI services + that require it. Otherwise, it is not necessary to authenticate non-Azure Open-AI clients. + Please refer to + the [KeyCredential Class documentation](https://learn.microsoft.com/java/api/com.azure.core.credential.keycredential?view=azure-java-stable) + for more information. + +14. #### Use sync client operation if calling blocking calls on asynchronous operations of an Azure asynchronous client. + +- **Anti-Pattern**: Calling blocking calls on asynchronous operations of an Azure asynchronous client. This practice + turns an asynchronous operation into a synchronous + one. +- **Issue**: Blocking calls go against the non-blocking nature of reactive streams. + It can lead to performance issues because it blocks one of the few available threads. + In reactive applications, avoiding blocking operations is crucial for scalability and responsiveness. +- **Severity Level: WARNING** +- **Recommendation**: If you find yourself frequently using blocking calls in your code, consider switching to the sync + client. + The sync client performs operations synchronously without requiring locking calls. + Using the sync client can make your code more straightforward and easier to understand. + +15. #### Upgrading library versions if versions in use known to have performance or reliability issues + +- **Anti-pattern**: Using library versions known to have performance or reliability issues. +- **Issue**: Using outdated library versions can lead to performance bottlenecks, security vulnerabilities, and + compatibility + issues. +- **Severity: WARNING** +- **Recommendation**: Upgrade to the Latest Minor Version. It's recommended to upgrade to the latest minor version of + the + library to benefit from performance improvements, bug fixes, and security patches. Importantly, if you encounter any + issues while using Service Bus Clients, you should first attempt to solve them by upgrading to the latest version of + the Service Bus SDK. Please refer to + the [ServiceBus Azure SDK Java documentation](https://learn.microsoft.com/azure/developer/java/sdk/troubleshooting-messaging-service-bus-overview#upgrade-to-715x-or-latest) + for more information on the latest version of the Service Bus SDK. + +16. #### Using Incompatible Versions of Dependencies + +- **Anti-pattern**: Using incompatible versions of dependencies in the project. +- **Issue**: Incompatible versions of dependencies can lead to runtime errors, classpath conflicts, and unexpected + behavior. +- **Severity: WARNING** +- **Recommendation**: Use a consistent version of dependencies across the project. It's recommended to use a consistent + version of dependencies across the project to avoid compatibility issues and ensure smooth integration. Please refer + to + the [Troubleshoot dependency version conflicts documentation](https://learn.microsoft.com/en-us/azure/developer/java/sdk/troubleshooting-dependency-version-conflict) + for additional information on resolving dependency version conflicts. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/build.gradle b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/build.gradle index 66d77731651..efc841775f4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/build.gradle +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/build.gradle @@ -10,5 +10,15 @@ dependencies { implementation 'com.microsoft.azure:azure-toolkit-common-lib' implementation 'com.microsoft.azure:azure-toolkit-ide-common-lib' aspect 'com.microsoft.azure:azure-toolkit-common-lib' + implementation group: 'org.json', name: 'json', version: '20240303' + testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation group: 'org.mockito', name: 'mockito-core' + implementation group: 'com.azure', name: 'azure-json', version: '1.1.0' + implementation 'com.microsoft.azure:applicationinsights-core:2.6.2' + implementation 'com.microsoft.azure:applicationinsights-web:2.6.2' +} +tasks.named('test', Test) { + useJUnitPlatform() } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/configure-telemetry.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/configure-telemetry.md new file mode 100644 index 00000000000..0e4e3c25a6b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/configure-telemetry.md @@ -0,0 +1,82 @@ +### Configuring MS Application Insights + +#### Prerequisites +- An Azure account with an active subscription. + +#### 1. Generate the Instrumentation Key + +1. **Log in to the Azure Portal**: Go to the [Azure Portal](https://portal.azure.com/). + +2. **Create a new Application Insights resource**: + - In the Azure Portal, click on `Create a resource`. + - Search for `Application Insights` and select it. + - Click `Create`. + - Fill in the required fields such as `Name`, `Subscription`, `Resource Group`, and `Region`. + - Click `Review + create`, then click `Create`. + +3. **Get the Instrumentation Key**: + - After the resource is created, navigate to the Application Insights resource. + - In the left-hand menu, under `Configure`, click on `Properties`. + - Copy the `Instrumentation Key`. + +#### 2. Add the Instrumentation Key to the Application + +1. **Create `applicationinsights.json`**: + - Navigate to the `resources/META-INF` directory of your project. If the directory doesn't exist, create it. + - Create a file named `applicationinsights.json` in the `resources/META-INF` directory. + +2. **Add the Instrumentation Key to `applicationinsights.json`**: + - Open the `applicationinsights.json` file and add the following JSON content: + ```json + { + "instrumentationKey": "your-instrumentation-key-goes-here" + } + ``` + - Replace `"your-instrumentation-key-goes-here"` with the actual instrumentation key you copied from the Azure Portal. + +#### 3. Add `applicationinsights.json` to `.gitignore` + +To prevent exposing your instrumentation key in version control, add the `applicationinsights.json` file to your `.gitignore` file. + +1. **Open `.gitignore`**: + - If you don't have a `.gitignore` file in your project root, create one. + +2. **Add `applicationinsights.json` to `.gitignore`**: + - Add the following line to the `.gitignore` file: + ``` + resources/META-INF/applicationinsights.json + ``` + +#### 4. App Insights Configuration in Code +1. **Accurate Tracking of Method Calls**: + - The `visitMethodCallExpression` method tracks the number of times each method of each client is called. + - The `methodCounts` map stores counts in a nested map structure, where the outer map's key is the client name, and the value is another map. This inner map's key is the method name, and the value is the count of calls to that method. + +2. **Periodic Reporting of Data**: + - The `startTelemetryService` method schedules the `sendTelemetryData` method to run at fixed intervals (every 3 minutes, starting 2 minutes after the service starts). + - Each execution of `sendTelemetryData` will report the accumulated counts since the last report. + +3. **Structured Telemetry Events**: + - Each method call count is reported as an event with the name `azure_sdk_usage_frequency`. + - Custom dimensions are added to these events, including: + - `clientName`: The name of the client (e.g., `BlobClient`). + - `methodName`: The name of the method (e.g., `download`). + - `count`: The count of calls to the method (e.g., `5`), stored as a string. + +### Sample Output + +The telemetry system will generate events that can be queried and analyzed. Here is an example of what the telemetry data might look like: + +#### Event 1 +- **Name**: `azure_sdk_usage_frequency` +- **Custom Dimensions**: + - `clientName`: `BlobClient` + - `methodName`: `download` + - `count`: `5` + +#### Event 2 +- **Name**: `azure_sdk_usage_frequency` +- **Custom Dimensions**: + - `clientName`: `BlobClient` + - `methodName`: `upload` + - `count`: `3` \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractLibraryVersionCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractLibraryVersionCheck.java new file mode 100644 index 00000000000..122a985a49f --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractLibraryVersionCheck.java @@ -0,0 +1,90 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.lang.StdLanguages; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import org.jetbrains.idea.maven.project.MavenProjectsManager; + +import java.io.IOException; + +/** + * 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 { + + // Get the MavenProjectsManager for the file + MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstance(file.getProject()); + if (!mavenProjectsManager.isMavenizedProject()) { + return; + } + + // Get the root tag of the file + FileViewProvider viewProvider = file.getViewProvider(); + XmlFile xmlFile = (XmlFile) viewProvider.getPsi(StdLanguages.XML); + XmlTag rootTag = xmlFile.getRootTag(); + + // Check the dependencies in the file and get the groupId, artifactId, and version + if (rootTag != null && "project".equals(rootTag.getName())) { + XmlTag[] dependenciesTags = rootTag.findSubTags("dependencies"); + for (XmlTag dependenciesTag : dependenciesTags) { + XmlTag[] dependencyTags = dependenciesTag.findSubTags("dependency"); + for (XmlTag dependencyTag : dependencyTags) { + XmlTag groupIdTag = dependencyTag.findFirstSubTag("groupId"); + XmlTag artifactIdTag = dependencyTag.findFirstSubTag("artifactId"); + XmlTag versionTag = dependencyTag.findFirstSubTag("version"); + + if (groupIdTag != null && artifactIdTag != null && versionTag != null) { + String groupId = groupIdTag.getValue().getText(); + String artifactId = artifactIdTag.getValue().getText(); + String currentVersion = versionTag.getValue().getText(); + + // Get the full name of the library + String fullName = groupId + ":" + artifactId; + + // Determine if the version should be flagged + 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.getAntiPatternMessageMap().get("antiPatternMessage").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; +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractUpdateCheckpointAsyncChecker.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractUpdateCheckpointAsyncChecker.java new file mode 100644 index 00000000000..27573835566 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractUpdateCheckpointAsyncChecker.java @@ -0,0 +1,124 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; +import org.jetbrains.annotations.NotNull; + +/** + * Abstract base class for checking the usage of the updateCheckpointAsync() method call in the code. + */ +public abstract class AbstractUpdateCheckpointAsyncChecker extends LocalInspectionTool { + + /** + * An abstract method to create the visitor for the inspection tool. + * This method is implemented by the subclasses to create the visitor for the specific inspection tool. + */ + protected abstract JavaElementVisitor createVisitor(ProblemsHolder holder, boolean isOnTheFly); + + // Define constants for string literals + protected static final RuleConfig RULE_CONFIG; + protected static final String BLOCK_METHOD = "block"; + protected static final String BLOCK_WITH_TIMEOUT_METHOD = "block_with_timeout"; + + static { + final String ruleName = "AbstractUpdateCheckpointAsyncChecker"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + } + + /** + * This method checks if the method call is following an updateCheckpointAsync() method call. + * If the method call is following an updateCheckpointAsync(), the method name is returned. + * + * @param expression The method call expression + * @return The method name if the method call is following a specific method call, null otherwise + */ + protected static String getFollowingMethodName(PsiMethodCallExpression expression) { + // Get the parent of the method call expression + PsiElement parent = expression.getParent(); + + if (!(parent instanceof PsiReferenceExpression)) { + return null; + } + PsiElement grandParent = parent.getParent(); + + if (grandParent instanceof PsiMethodCallExpression) { + PsiMethodCallExpression parentCall = (PsiMethodCallExpression) grandParent; + + // Get the method name from the parent call + String methodName = parentCall.getMethodExpression().getReferenceName(); + + // Check if the method name is in the list of methods to check + if (RULE_CONFIG.getMethodsToCheck().contains(methodName)) { + if (BLOCK_METHOD.equals(methodName)) { + PsiExpressionList arguments = parentCall.getArgumentList(); + if (arguments.getExpressions().length == 1 && arguments.getExpressions()[0].getType() != null && arguments.getExpressions()[0].getType().equalsToText("java.time.Duration")) { + return BLOCK_WITH_TIMEOUT_METHOD; + } + } + } + return methodName; + } + return null; + } + + /** + * This method checks if the method call is called on an EventBatchContext object from the Azure SDK. + * + * @param expression The method call expression + * @return True if the method call is called on an EventBatchContext object from the Azure SDK, false otherwise + */ + protected static boolean isCalledOnEventBatchContext(PsiMethodCallExpression expression) { + // 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(); + + // Check if the resolved element is a parameter + if (!(resolvedElement instanceof PsiParameter)) { + return false; + } + + // Check if the resolved element is a class type + PsiParameter parameter = (PsiParameter) resolvedElement; + PsiType parameterType = parameter.getType(); + + // Check if the parameter type is an EventBatchContext object from the Azure SDK + if (!"EventBatchContext".equals(parameterType.getPresentableText())) { + return false; + } + + if (!(parameterType instanceof PsiClassType)) { + return false; + } + + PsiClassType classType = (PsiClassType) parameterType; + PsiClass psiClass = classType.resolve(); + + if (psiClass != null) { + String qualifiedName = psiClass.getQualifiedName(); + + // Check if the qualified name starts with the Azure package name + return qualifiedName != null && qualifiedName.startsWith(RuleConfig.AZURE_PACKAGE_NAME); + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DependencyVersionFileFetcher.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DependencyVersionFileFetcher.java new file mode 100644 index 00000000000..2edf90eb641 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DependencyVersionFileFetcher.java @@ -0,0 +1,254 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +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.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +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. + */ +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 + */ + static Map parsePomFile(String pomUrl) { + + // Check the cache first + Map artifactVersionMap = pomCache.get(pomUrl); + if (artifactVersionMap != null) { + return artifactVersionMap; + } + // Fetch the pom.xml file from the URL and parse it to get the dependencies + Document pomDoc = fetchXmlDocument(pomUrl); + + // Get the list of dependencies from the pom.xml file + NodeList dependencies = pomDoc.getElementsByTagName("dependency"); + + // Parse the dependencies and get the groupId, artifactId, and version + artifactVersionMap = new HashMap<>(); + + for (int i = 0; i < dependencies.getLength(); i++) { + NodeList dependency = dependencies.item(i).getChildNodes(); + String groupId = null; + String artifactId = null; + String version = null; + + for (int j = 0; j < dependency.getLength(); j++) { + if (dependency.item(j).getNodeName().equals("groupId")) { + groupId = dependency.item(j).getTextContent(); + } else if (dependency.item(j).getNodeName().equals("artifactId")) { + artifactId = dependency.item(j).getTextContent(); + } else if (dependency.item(j).getNodeName().equals("version")) { + version = dependency.item(j).getTextContent(); + } + } + + if (groupId != null && artifactId != null && version != null) { + + // if we have version: 4.1.9 get 4.1 + String minorVersion = version.substring(0, version.lastIndexOf(".")); + + // Add the groupId and artifactId to the map with the minor version as the value + artifactVersionMap.put((groupId + ":" + artifactId), minorVersion); + } + } + + // Update the cache + 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 + */ + static String getLatestVersion(String metadataUrl) { + + // Check the cache first + String cachedVersion = versionCache.get(metadataUrl); + if (cachedVersion != null) { + return cachedVersion; + } + // Fetch the metadata file from the URL and parse it + Document metadataDoc = fetchXmlDocument(metadataUrl); + + // Get the list of versions from the metadata file + NodeList versions = metadataDoc.getElementsByTagName("version"); + + String latestVersion = versions.item(versions.getLength() - 1).getTextContent(); + + // Update the cache + 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) { + + // Open connection to the URL and get the input stream + HttpURLConnection conn = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + inputStream = conn.getInputStream(); + + // Create a document builder to parse the XML document + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // Parse the document from the connection input stream + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputStream); + + } catch (MalformedURLException e) { + LOGGER.log(Level.SEVERE, "Invalid URL: " + urlString, e); + throw new RuntimeException(e); + } catch (ProtocolException e) { + LOGGER.log(Level.SEVERE, "Protocol error while fetching URL: " + urlString, e); + throw new RuntimeException(e); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "I/O error while fetching URL: " + urlString, e); + throw new RuntimeException(e); + } catch (ParserConfigurationException | SAXException e) { + LOGGER.log(Level.SEVERE, "Error parsing XML from URL: " + urlString, e); + throw new RuntimeException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Failed to close input stream", e); + } + } + if (conn != null) { + conn.disconnect(); + } + } + } + + /** + * 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 + */ + static Map> loadJsonDataFromUrl(String jsonUrl) { + + Map> jsonData = incompatibleVersionsCache.get(jsonUrl); + + if (jsonData != null) { + return jsonData; + } + HttpURLConnection connection; + try { + URL url = new URL(jsonUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (InputStream inputStream = connection.getInputStream(); JsonReader jsonReader = JsonProviders.createReader(inputStream)) { + jsonData = parseJson(jsonReader); + incompatibleVersionsCache.put(jsonUrl, jsonData); + return jsonData; + } + } catch (IOException e) { + LOGGER.severe("Error reading file from GitHub: " + e); + } catch (SecurityException e) { + LOGGER.severe("Security exception accessing URL: " + e); + } catch (IllegalArgumentException e) { + LOGGER.severe("Illegal argument: " + e); + } catch (IllegalStateException e) { + LOGGER.severe("Illegal state: " + e); + } catch (Exception e) { + LOGGER.severe("Unexpected error: " + 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<>(); + // Read the start of the JSON object + if (jsonReader.nextToken() == JsonToken.START_OBJECT) { + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + // Read the key for the current group (e.g., "jackson_2.10") + String groupKey = jsonReader.getFieldName(); + Set groupSet = new HashSet<>(); + + // Ensure we're at the start of the array for the current group + if (jsonReader.nextToken() == JsonToken.START_ARRAY) { + while (jsonReader.nextToken() != JsonToken.END_ARRAY) { + // Ensure we're at the start of an object within the array + if (jsonReader.nextToken() == JsonToken.FIELD_NAME) { + // Read the artifactId and version within the object + String groupAndArtifactId = jsonReader.getFieldName(); + groupSet.add(groupAndArtifactId); + } + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + // Do nothing just skip + } + } + } + versionData.put(groupKey, groupSet); + } + } + return versionData; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DependencyVersionsDataCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DependencyVersionsDataCache.java new file mode 100644 index 00000000000..a49be01cb75 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DependencyVersionsDataCache.java @@ -0,0 +1,187 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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. + */ +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. + */ + 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. + */ + 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. + */ + 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-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedAPIUsageCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedAPIUsageCheck.java new file mode 100644 index 00000000000..828c59f7044 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedAPIUsageCheck.java @@ -0,0 +1,121 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 org.jetbrains.annotations.NotNull; + +/** + * This class extends the LocalInspectionTool to check for the use of discouraged APIs in the code. + * If the method is called from an Azure client class, a problem is registered with the suggestion message. + */ +public class DetectDiscouragedAPIUsageCheck extends LocalInspectionTool { + + + /** + * This method builds the visitor for the inspection tool. + * + * @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. + * @return PsiElementVisitor visitor to inspect elements in the code + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new DetectDiscouragedAPIUsageVisitor(holder, isOnTheFly); + } + + /** + * This class extends the JavaElementVisitor to visit the elements in the code. + * It checks if the method call is a discouraged API call and if the class is an Azure client. + * If both conditions are met, a problem is registered with the suggestion message. + */ + static class DetectDiscouragedAPIUsageVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + + // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "DetectDiscouragedAPIUsageCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE || RULE_CONFIG.getAntiPatternMessageMap().isEmpty(); + } + + /** + * 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 - This is not in use + */ + public DetectDiscouragedAPIUsageVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + /** + * This method visits the element in the code. + * It checks if the method call is a discouraged API call and if the class is an Azure client. + * If both conditions are met, a problem is registered with the suggestion message. + * + * @param element PsiElement to visit + */ + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + + // skip the whole rule if the rule is empty + if (SKIP_WHOLE_RULE) { + return; + } + + // check if an element is an azure client + if (element instanceof PsiMethodCallExpression) { + + PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression) element; + PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); + + // resolvedMethod is the method that is being called + PsiElement resolvedMethod = methodExpression.resolve(); + + // check if the method is a discouraged API call by accessing the keys of the map stored in the configuration file + if (resolvedMethod != null && resolvedMethod instanceof PsiMethod && RULE_CONFIG.getAntiPatternMessageMap().containsKey(((PsiMethod) resolvedMethod).getName())) { + + PsiMethod method = (PsiMethod) resolvedMethod; + + // containingClass is the client class that is being called. check if the class is an azure client + PsiClass containingClass = method.getContainingClass(); + + // compare the package name of the containing class to the azure package name from the configuration file + if (containingClass != null && containingClass.getQualifiedName() != null && containingClass.getQualifiedName().startsWith("com.azure")) { + + if (method.getName().equals("getCompletions")) { + if (containingClass != null && containingClass.getQualifiedName() != null && !containingClass.getQualifiedName().startsWith("com.azure.ai.openai")) { + return; // Exit if the method is getCompletions but the class's qualified name does not start with com.azure.ai.openai + } + } + + PsiElement problemElement = methodExpression.getReferenceNameElement(); + + if (problemElement == null) { + return; + } + // give the suggestion of the discouraged method + holder.registerProblem(problemElement, RULE_CONFIG.getAntiPatternMessageMap().get(method.getName())); + } + } + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedClientCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedClientCheck.java new file mode 100644 index 00000000000..4b8b100eff4 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedClientCheck.java @@ -0,0 +1,88 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiTypeElement; +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 class DetectDiscouragedClientCheck extends LocalInspectionTool { + + /** + * 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) { + return new DetectDiscouragedClientVisitor(holder); + } + + /** + * 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 { + + // Define the fields for the visitor + private final ProblemsHolder holder; + + /** + * Constructor for the visitor + * + * @param holder - the ProblemsHolder object to register the problem + */ + DetectDiscouragedClientVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "DetectDiscouragedClientCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || RULE_CONFIG.getAntiPatternMessageMap().isEmpty(); + } + + /** + * 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 whole rule if the configuration is empty + if (SKIP_WHOLE_RULE) { + return; + } + // Check if the element is an instance of PsiTypeElement + if (element instanceof PsiTypeElement && element.getType() != null) { + + String elementType = element.getType().getPresentableText(); + + if (!RULE_CONFIG.getAntiPatternMessageMap().containsKey(elementType)) { + return; + } + + // Register a problem if the client used matches a discouraged client + holder.registerProblem(element, RULE_CONFIG.getAntiPatternMessageMap().get(elementType)); + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DisableAutoCompleteCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DisableAutoCompleteCheck.java new file mode 100644 index 00000000000..d769c3bb692 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DisableAutoCompleteCheck.java @@ -0,0 +1,166 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 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, 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 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 { + + // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "DisableAutoCompleteCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE || RULE_CONFIG.getClientsToCheck().isEmpty(); + } + + // Define the holder for the problems found + private final ProblemsHolder holder; + + /** + * Constructor for the visitor + * + * @param holder The holder for the problems found + */ + public DisableAutoCompleteVisitor(ProblemsHolder holder, boolean isOnTheFly) { + 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.getClientsToCheck().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.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + + /** + * 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.getMethodsToCheck().get(0).equals(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-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DynamicClientCreationCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DynamicClientCreationCheck.java new file mode 100644 index 00000000000..95f534f3599 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DynamicClientCreationCheck.java @@ -0,0 +1,193 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiBlockStatement; +import com.intellij.psi.PsiCodeBlock; +import com.intellij.psi.PsiDeclarationStatement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiExpressionStatement; +import com.intellij.psi.PsiForStatement; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiStatement; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * This class is used to check for the dynamic creation of clients in the code. + * It extends the LocalInspectionTool class, which is used to create custom code inspections. + * The visitor inspects a for loop and checks for methodcall expressions that create clients using the buildClient or buildAsyncClient methods. + * If a client creation method is found building a client from the com.azure package, a problem is registered. + */ +public class DynamicClientCreationCheck extends LocalInspectionTool { + + + /** + * This method builds the visitor that checks for the dynamic creation of clients in the code. + * + * @param holder The holder for the problems found + * @return PsiElementVisitor + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new DynamicClientCreationVisitor(holder); + } + + + /** + * This class extends the JavaElementVisitor to check for the dynamic creation of clients in the code. + */ + static class DynamicClientCreationVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + + // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "DynamicClientCreationCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE || RULE_CONFIG.getMethodsToCheck().isEmpty(); + } + + /** + * This constructor initializes the ProblemsHolder object. + * It is used to register problems found in the code. + * + * @param holder The holder for the problems found + */ + public DynamicClientCreationVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** + * This method checks for the dynamic creation of clients in the code. + * + * @param statement The for loop statement to visit + */ + @Override + public void visitForStatement(@NotNull PsiForStatement statement) { + super.visitForStatement(statement); + + if (SKIP_WHOLE_RULE) { + return; + } + + // Extract the body of the for loop + PsiStatement body = statement.getBody(); + + if (!(body instanceof PsiBlockStatement)) { + return; + } + + // Extract the code block from the block statement + PsiBlockStatement blockStatement = (PsiBlockStatement) body; + PsiCodeBlock codeBlock = blockStatement.getCodeBlock(); + + // Traverse the statements within the code block + for (PsiStatement blockChild : codeBlock.getStatements()) { + checkClientCreation(blockChild); + } + } + + /** + * This method checks for the dynamic creation of clients in the code block of a for loop. + * Each assignment statement and declaration statement is checked for the creation of clients. + * + * @param blockChild The statement to check for client creation + */ + + private void checkClientCreation(PsiStatement blockChild) { + + // This is a check for the expression statement + if (blockChild instanceof PsiExpressionStatement) { + PsiExpression expression = ((PsiExpressionStatement) blockChild).getExpression(); + + if (!(expression instanceof PsiAssignmentExpression)) { + return; + } + + // Extract the assignment expression + PsiAssignmentExpression assignment = (PsiAssignmentExpression) expression; + + // Extract the right-hand side of the assignment + PsiExpression rhs = assignment.getRExpression(); + + // Check if the right-hand side is a method call expression + if (rhs instanceof PsiMethodCallExpression && isClientCreationMethod((PsiMethodCallExpression) rhs)) { + holder.registerProblem(rhs, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } else if (blockChild instanceof PsiDeclarationStatement) { // This is a check for the declaration statement + + PsiDeclarationStatement declarationStatement = (PsiDeclarationStatement) blockChild; + + // Traverse the declared elements within the declaration statement + for (PsiElement declaredElement : declarationStatement.getDeclaredElements()) { + + // Check if the declared element is a local variable + if (!(declaredElement instanceof PsiLocalVariable)) { + continue; + } + + // Extract the local variable and its initializer + PsiLocalVariable localVariable = (PsiLocalVariable) declaredElement; + PsiExpression initializer = localVariable.getInitializer(); + + if (!(initializer instanceof PsiMethodCallExpression)) { + continue; + } + + // Check if the initializer is a method call expression + PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression) initializer; + if (isClientCreationMethod(methodCallExpression)) { + holder.registerProblem(methodCallExpression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + } + + /** + * This method checks if the method call expression is a client creation method. + * It checks the method name and the type of the qualifier expression. + * If the method name is buildClient or AsyncBuildClient and the qualifier expression is of type com.azure, + * then it is considered a client creation method. + * + * @param methodCallExpression The method call expression to check + * @return boolean - true if the method call expression is a client creation method + */ + public boolean isClientCreationMethod(PsiMethodCallExpression methodCallExpression) { + + // Extract the method expression from the method call expression + PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression(); + + // Extract the method name + String methodName = methodExpression.getReferenceName(); + + // Check if the method name is buildClient or AsyncBuildClient + if (methodName != null && RULE_CONFIG.getMethodsToCheck().contains(methodName)) { + + // Extract the qualifier expression + PsiExpression qualifierExpression = methodExpression.getQualifierExpression(); + + if (qualifierExpression == null || qualifierExpression.getType() == null) { + return false; + } + + // Check if the qualifier expression is of type com.azure + return qualifierExpression.getType().getCanonicalText().startsWith(RuleConfig.AZURE_PACKAGE_NAME); + } + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/EndpointOnNonAzureOpenAIAuthCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/EndpointOnNonAzureOpenAIAuthCheck.java new file mode 100644 index 00000000000..b59f3b8108c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/EndpointOnNonAzureOpenAIAuthCheck.java @@ -0,0 +1,180 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 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 { + String ruleName = "EndpointOnNonAzureOpenAIAuthCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || RULE_CONFIG.getMethodsToCheck().isEmpty(); + } + + /** + * 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(); + + if (!(RULE_CONFIG.getMethodsToCheck().contains(methodName))) { + return; + } + + // Check if the method is the endpoint method + if ("endpoint".equals(methodName)) { + + // 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.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + + /** + * 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) { + + // Iterating up the chain of method calls + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + + // Check if the method call chain has 'credential' method + while (qualifier instanceof PsiMethodCallExpression) { + + PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) qualifier).getMethodExpression(); + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) qualifier; + String methodName = methodExpression.getReferenceName(); + + if (!(RULE_CONFIG.getMethodsToCheck().contains(methodName))) { + return false; + } + + if ("credential".equals(methodName)) { + + // Check if the credential method is used with KeyCredential + PsiExpression[] arguments = methodCall.getArgumentList().getExpressions(); + if (arguments.length == 1 && isKeyCredential(arguments[0])) { + return isNonAzureOpenAIClient(methodCall); + } + return false; + } + + // Iterating up the chain of method calls + 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) { + + // Cast the element to a new expression + PsiNewExpression newExpression = (PsiNewExpression) expression; + + // Get the class reference name from the new expression + String classReference = newExpression.getClassReference().getReferenceName(); + if (classReference != null) { + return RULE_CONFIG.getServicesToCheck().contains(classReference); + } + } + 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 != null) { + if (parent instanceof PsiVariable) { + + PsiType type = ((PsiVariable) parent).getType(); + if (type != null) { + return type.getCanonicalText().startsWith("com.azure.ai.openai"); + } + } + parent = parent.getParent(); + } + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/GetSyncPollerOnPollerFluxCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/GetSyncPollerOnPollerFluxCheck.java new file mode 100644 index 00000000000..78c25779ba1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/GetSyncPollerOnPollerFluxCheck.java @@ -0,0 +1,158 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +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.PsiType; +import com.intellij.psi.util.PsiTreeUtil; +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, isOnTheFly); + } + + /** + * 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 GetSyncPollerOnPollerFluxVisitor extends JavaElementVisitor { + + // Instance variables + private final ProblemsHolder holder; + + // Define constants for string literals + 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 == RuleConfig.EMPTY_RULE; + } + + /** + * Constructor to initialize the visitor with the holder and isOnTheFly flag. + * + * @param holder Holder for the problems found by the inspection + * @param isOnTheFly Flag to indicate if the inspection is running on the fly -- not used in this inspection + */ + public GetSyncPollerOnPollerFluxVisitor(ProblemsHolder holder, boolean isOnTheFly) { + 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; + } + + // Check if the element is a method call expression + if (!(expression instanceof PsiMethodCallExpression)) { + return; + } + + PsiMethodCallExpression methodCall = expression; + + // Check if the method call is getSyncPoller + if (methodCall.getMethodExpression().getReferenceName().startsWith(RULE_CONFIG.getMethodsToCheck().get(0))) { + boolean isAsyncContext = checkIfAsyncContext(methodCall); + + if (isAsyncContext && isAzureClient(methodCall)) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + + /** + * Helper method to check if the method call is within an async context. + * This method will 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 checkIfAsyncContext(@NotNull PsiMethodCallExpression methodCall) { + + String pollerFluxType = "PollerFlux"; + + PsiExpression expression = methodCall.getMethodExpression().getQualifierExpression(); + + if (expression == null) { + return false; + } + PsiType type = expression.getType(); + + // Check if the type is a reactive type + if (type == null) { + return false; + } + String typeName = type.getCanonicalText(); + + // Check for PollerFlux type + if (typeName != null && typeName.contains(pollerFluxType)) { + return true; + } + return false; + } + + /** + * Helper method to check if the method call is on an Azure SDK client. + * This method will check if the method call is on a class that is part of the Azure SDK. + * + * @param methodCall Method call expression to check + * @return true if the method call is on an Azure SDK client, false otherwise + */ + private boolean isAzureClient(@NotNull PsiMethodCallExpression methodCall) { + + PsiClass containingClass = PsiTreeUtil.getParentOfType(methodCall, PsiClass.class); + + // Check if the method call is on a class + if (containingClass == null) { + return false; + } + String className = containingClass.getQualifiedName(); + + // Check if the class is part of the Azure SDK + if (className != null && className.startsWith(RuleConfig.AZURE_PACKAGE_NAME)) { + return true; + } + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/HardcodedAPIKeysAndTokensCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/HardcodedAPIKeysAndTokensCheck.java new file mode 100644 index 00000000000..0f89529a7fc --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/HardcodedAPIKeysAndTokensCheck.java @@ -0,0 +1,87 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.PsiNewExpression; +import org.jetbrains.annotations.NotNull; + +/** + * This class is a custom inspection tool that checks for hardcoded API keys and tokens in the code. + * It extends the LocalInspectionTool class and overrides the buildVisitor method to create a visitor for the inspection. + * The visitor checks for the use of specific methods that are known to be used for API keys and tokens. + * These are AzureKeyCredential and AccessToken. + *

+ * 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 HardcodedAPIKeysAndTokensCheck extends LocalInspectionTool { + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new APIKeysAndTokensVisitor(holder, isOnTheFly); + } + + // This class is a visitor that checks for the use of specific methods that authenticate API keys and tokens. + static class APIKeysAndTokensVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + + // // Define constants for string literals + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "HardcodedAPIKeysAndTokensCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE || RULE_CONFIG.getServicesToCheck().isEmpty(); + } + + + // This constructor is used to create a visitor for the inspection + public APIKeysAndTokensVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + @Override + public void visitElement(@NotNull PsiElement element) { + super.visitElement(element); + + if (SKIP_WHOLE_RULE) { + return; + } + + // Check if the element is a new expression -- i.e., a constructor call + if (element instanceof PsiNewExpression) { + + // Cast the element to a new expression + PsiNewExpression newExpression = (PsiNewExpression) element; + if (newExpression.getClassReference() == null) { + return; + } + + // Get the class reference name from the new expression + String classReference = newExpression.getClassReference().getReferenceName(); + + // Check if the class reference is not null, the qualifier name starts with "com.azure" and + // the class reference is in the list of clients to check + if (newExpression.getClassReference() != null && newExpression.getClassReference().getQualifiedName().startsWith(RuleConfig.AZURE_PACKAGE_NAME) && RULE_CONFIG.getServicesToCheck().contains(classReference)) { + this.holder.registerProblem(newExpression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/IncompatibleDependencyCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/IncompatibleDependencyCheck.java new file mode 100644 index 00000000000..8f593480c68 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/IncompatibleDependencyCheck.java @@ -0,0 +1,209 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +/** + * 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 { + + // 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 = IncompatibleDependencyVisitor.getGroupVersion(fullName, currentVersion); + + 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); + + // Flag the version if the minor version is different from the recommended version + String message = getFormattedMessage(fullName, recommendedVersion, IncompatibleDependencyVisitor.RULE_CONFIG); + holder.registerProblem(versionElement, message); + 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 { + + // Holder for the problems found + private final ProblemsHolder holder; + + // Map to store the compatible versions for each library + private static WeakReference>> FILE_CONTENT_REF; + private static final Logger LOGGER = Logger.getLogger(IncompatibleDependencyCheck.class.getName()); + + /** + * Constructs a new instance of the IncompatibleDependencyVisitor. + * + * @param holder The holder for the problems found + */ + IncompatibleDependencyVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + + static { + final String ruleName = "IncompatibleDependencyCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE; + } + + /** + * 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("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 + */ + private 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.get(); + + if (fileContent == null) { + synchronized (IncompatibleDependencyVisitor.class) { + fileContent = FILE_CONTENT_REF == null ? null : FILE_CONTENT_REF.get(); + if (fileContent == null) { + String fileUrl = RULE_CONFIG.getListedItemsToCheck().get(0); + fileContent = DependencyVersionFileFetcher.loadJsonDataFromUrl(fileUrl); + FILE_CONTENT_REF = new WeakReference<>(fileContent); + } + } + } + return fileContent; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/KustoQueriesWithTimeIntervalInQueryStringCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/KustoQueriesWithTimeIntervalInQueryStringCheck.java new file mode 100644 index 00000000000..0da18bc491d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/KustoQueriesWithTimeIntervalInQueryStringCheck.java @@ -0,0 +1,260 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.PsiExpression; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethodCallExpression; +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 org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 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 { + + /** + * This method builds the visitor for the inspection tool + * + * @param holder ProblemsHolder is used to register problems found in the code + * @param isOnTheFly boolean to indicate if the inspection is done on the fly - THis is not used in this implementation but is required by the method signature + * @return PsiElementVisitor + */ + @NotNull + @Override + public JavaElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + + // clear the timeIntervalParameters list in every visit + KustoQueriesVisitor.timeIntervalParameters.clear(); + + 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; + + static { + final String ruleName = "KustoQueriesWithTimeIntervalInQueryStringCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + + List regexPatterns = RULE_CONFIG.getListedItemsToCheck(); + List tempPatterns = new ArrayList<>(); + // Compile the regex patterns + for (String pattern : regexPatterns) { + tempPatterns.add(Pattern.compile(pattern)); + } + REGEX_PATTERNS = Collections.unmodifiableList(tempPatterns); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || REGEX_PATTERNS.isEmpty(); + } + + // empty list to store time interval parameter names + private static final List timeIntervalParameters = new ArrayList<>(); + + /** + * 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; + } + + /** + * 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 PsiPolyadicExpression || element instanceof PsiLocalVariable || element instanceof PsiMethodCallExpressionImpl)) { + return; + } + + if (element instanceof PsiMethodCallExpressionImpl) { + checkParameterNames((PsiMethodCallExpressionImpl) element); + } + + if (element instanceof PsiLocalVariable) { + PsiLocalVariable variable = (PsiLocalVariable) element; + handleLocalVariable(variable); + } + + // 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 + if (element instanceof PsiPolyadicExpression) { + 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 checkExpression(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 != null && initializer instanceof PsiLiteralExpression) { + checkExpression(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) { + // Process the polyadic expression to replace the variables with their values + PsiExpression initializer = polyadicExpression instanceof PsiPolyadicExpression ? polyadicExpression : null; + checkExpression(initializer, 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 checkParameterNames(PsiMethodCallExpressionImpl methodCall) { + // check the parameters of the method call + PsiExpressionList argumentList = methodCall.getArgumentList(); + PsiExpression[] arguments = argumentList.getExpressions(); + + // for each argument in the method call, check if the argument is a reference to a variable + for (PsiExpression argument : arguments) { + if (!(argument instanceof PsiReferenceExpression)) { + continue; + } + PsiReferenceExpression referenceExpression = (PsiReferenceExpression) argument; + PsiElement resolvedElement = referenceExpression.resolve(); + + if (!(resolvedElement instanceof PsiVariable)) { + continue; + } + PsiVariable variable = (PsiVariable) resolvedElement; + String variableName = variable.getName(); + // 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 (!(timeIntervalParameters.contains(variableName))) { + continue; + } + + if (isAzureClient(methodCall)) { + holder.registerProblem(methodCall, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + + + /** + * 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 + */ + private boolean isAzureClient(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-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/RuleConfig.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/RuleConfig.java new file mode 100644 index 00000000000..a1a1f9997de --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/RuleConfig.java @@ -0,0 +1,98 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 + */ +class RuleConfig { + private final List methodsToCheck; + private final List clientsToCheck; + private final List servicesToCheck; + private final Map antiPatternMessageMap; + private final List listedItemsToCheck; + + static final String AZURE_PACKAGE_NAME = "com.azure"; + + static final RuleConfig EMPTY_RULE = new RuleConfig(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptyList()); + + /** + * Constructor for RuleConfig. + * + * @param methodsToCheck List of methods to check. + * @param clientsToCheck List of clients to check. + * @param servicesToCheck List of services to check. + * @param antiPatternMessageMap Map of antipattern messages to display. + * @param listedItemsToCheck List of items to check for. + */ + RuleConfig(List methodsToCheck, List clientsToCheck, List servicesToCheck, Map antiPatternMessageMap, List listedItemsToCheck) { + this.methodsToCheck = methodsToCheck; + this.clientsToCheck = clientsToCheck; + this.servicesToCheck = servicesToCheck; + this.antiPatternMessageMap = antiPatternMessageMap; + this.listedItemsToCheck = listedItemsToCheck; + } + + /** + * This method checks if the rule should be skipped. + * + * @return True if the rule should be skipped, false otherwise. + */ + boolean skipRuleCheck() { + return this == RuleConfig.EMPTY_RULE; + } + + // Getters + /** + * This method returns the list of methods to check + * + * @return List of methods to check + */ + List getMethodsToCheck() { + return methodsToCheck; + } + + /** + * This method returns the list of clients to check + * + * @return List of clients to check + */ + List getClientsToCheck() { + return clientsToCheck; + } + + /** + * This method returns the list of services to check + * + * @return List of services to check + */ + List getServicesToCheck() { + return servicesToCheck; + } + + /** + * This method returns a map of antipattern messages. + * The key is the antipattern message key and the value is the antipattern message. + * Generally, most rules have an antipattern message key of "anti_pattern_message". + * Discouraged identifiers have an antipattern message key of the discouraged client or API being used. + * "UpdateCheckpointAsync" rule has antipattern message keys of "with_subscribe" and "no_block". + * @return Map of antipattern messages + */ + Map getAntiPatternMessageMap() { + return antiPatternMessageMap; + } + + /** + * This method returns the list of items to check + * The items to check are any types that are not an Azure service, client or method + * This includes regex patterns, object types etc + * + * @return List of items to check + */ + List getListedItemsToCheck() { + return listedItemsToCheck; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/RuleConfigLoader.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/RuleConfigLoader.java new file mode 100644 index 00000000000..09c60719aef --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/RuleConfigLoader.java @@ -0,0 +1,288 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.azure.json.JsonProviders; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is used to load the rule configurations from a JSON file. + * The rule configurations are loaded once and stored in a map. + * The map contains the key-value pairs where the key is the rule name and the value is the RuleConfig object. + * The RuleConfig object for a given key can be retrieved using the getRuleConfig method. + */ +class RuleConfigLoader { + + private static final RuleConfigLoader instance; + private final Map ruleConfigs; + + private static final Logger LOGGER = Logger.getLogger(RuleConfigLoader.class.getName()); + + private static final String CONFIG_FILE_PATH = "/META-INF/ruleConfigs.json"; + + // Static initializer block to load the configurations once + static { + RuleConfigLoader instanceTemp; + try { + instanceTemp = new RuleConfigLoader(CONFIG_FILE_PATH); + } catch (FileNotFoundException e) { + instanceTemp = null; + LOGGER.log(Level.SEVERE, "Configuration file not found at path: " + CONFIG_FILE_PATH + ". Please ensure the file exists and is accessible. Error: " + e); + } catch (IOException e) { + instanceTemp = null; + LOGGER.log(Level.SEVERE, "IO error while reading configuration file from path: " + CONFIG_FILE_PATH + ". Please check file permissions and retry. Error: " + e); + } + instance = instanceTemp; + } + + /** + * Constructor to load the rule configurations from the JSON file + * + * @param filePath - the path to the JSON file + * @throws IOException - if there is an error reading the file + */ + private RuleConfigLoader(String filePath) throws IOException { + this.ruleConfigs = loadRuleConfigurations(filePath); + } + + /** + * This method returns the instance of the CentralRuleConfigLoader + * + * @return CentralRuleConfigLoader instance + */ + static RuleConfigLoader getInstance() { + return instance; + } + + /** + * This method returns the RuleConfig object for the given key + * + * @param key - the key to get the RuleConfig object + * @return RuleConfig object or empty rule if one does not exist, {@link RuleConfig#EMPTY_RULE} + */ + RuleConfig getRuleConfig(String key) { + + RuleConfig ruleConfig = ruleConfigs.get(key); + if (ruleConfig == null) { + return RuleConfig.EMPTY_RULE; + } else { + return ruleConfig; + } + } + + /** + * This method loads the rule configurations from the JSON file + * + * @param filePath - the path to the JSON file + * @return Map of RuleConfig objects + */ + private Map loadRuleConfigurations(String filePath) { + + // temporary map to store the RuleConfig objects that will then be returned to the final map + Map ruleConfigs = new HashMap<>(); + + // Open the input stream to the JSON file + try (InputStream is = RuleConfigLoader.class.getResourceAsStream(filePath); + + // Create a JsonReader to read the JSON file -- need another try to close the json reader + JsonReader reader = JsonProviders.createReader(is)) { + + // Check if the JSON file starts with an object + // If not, throw an exception + // This is to ensure that the JSON file is in the correct format + if (reader.nextToken() != JsonToken.START_OBJECT) { + throw new IllegalArgumentException("Expected start of object"); + } + + // Read the JSON file and parse the RuleConfig objects + while (reader.nextToken() != JsonToken.END_OBJECT) { + String key = reader.getFieldName(); + RuleConfig ruleConfig = getRuleConfig(reader); + + // Add the RuleConfig object to the map + ruleConfigs.put(key, ruleConfig); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "IO error while parsing Json from path: " + CONFIG_FILE_PATH + ". Please check file permissions and retry. Error: " + e); + } + return ruleConfigs; + } + + /** + * This method parses the RuleConfig object from the JSON file + * The RuleConfig object either contains the methods to check, clients to check or services to check, + * and the anti-pattern messages + * + * @param reader - the JsonReader object to read the JSON file + * @return RuleConfig object parsed from the JSON file + * @throws IOException - if there is an error reading the file + */ + private RuleConfig getRuleConfig(JsonReader reader) throws IOException { + List methodsToCheck = new ArrayList<>(); + List clientsToCheck = new ArrayList<>(); + List servicesToCheck = new ArrayList<>(); + Map antiPatternMessageMap = new HashMap<>(); + List listedItemsToCheck = new ArrayList<>(); + + // Check if the JSON file starts with an object + if (reader.nextToken() != JsonToken.START_OBJECT) { + throw new IllegalArgumentException("Expected start of object"); + } + + // Read the JSON file and parse the RuleConfig object + while (reader.nextToken() != JsonToken.END_OBJECT) { + + // Get the field name + String fieldName = reader.getFieldName(); + // Check the field name and set the corresponding field in the RuleConfig object + switch (fieldName) { + case "methodsToCheck": + methodsToCheck = getListFromJsonArray(reader); + break; + case "antiPatternMessage": + antiPatternMessageMap = getMapFromJsonObject(reader, antiPatternMessageMap); + break; + case "clientsToCheck": + clientsToCheck = getListFromJsonArray(reader); + break; + case "servicesToCheck": + servicesToCheck = getListFromJsonArray(reader); + break; + case "typesToCheck": + listedItemsToCheck = getListFromJsonArray(reader); + break; + case "regexPatterns": + listedItemsToCheck = getValuesFromJsonReader(reader); + break; + case "url": + listedItemsToCheck = getListFromJsonArray(reader); + break; + default: + if (fieldName.endsWith("Check")) { + // Move to the next token to process the nested object + reader.nextToken(); + antiPatternMessageMap = getMapFromJsonObject(reader, antiPatternMessageMap); + } else { + reader.skipChildren(); + } + break; + } + } + return new RuleConfig(methodsToCheck, clientsToCheck, servicesToCheck, antiPatternMessageMap, listedItemsToCheck); + } + + /** + * This method parses the list of strings from the JSON array + * + * @param reader - the JsonReader object to read the JSON file + * @return List of strings parsed from the JSON array + * @throws IOException - if there is an error reading the file + */ + private List getListFromJsonArray(JsonReader reader) throws IOException { + List list = new ArrayList<>(); + + // Check if the JSON file starts with an array + if (reader.nextToken() != JsonToken.START_ARRAY) { + + // check if a string has been passed + if (reader.currentToken() == JsonToken.STRING) { + list.add(reader.getString()); + return list; + } else { + throw new IllegalArgumentException("Expected start of array"); + } + } + + // Read the JSON file and parse the list of strings + while (reader.nextToken() != JsonToken.END_ARRAY) { + list.add(reader.getString()); + } + return list; + } + + /** + * This method is used to parse the anti-pattern messages from a JSON file + * The anti-pattern messages can be for a set of methods, clients, or services + * The anti-pattern messages can also be for specific rules like "no_block" and "with_subscribe" + * + * @param reader - the JsonReader object to read the JSON file + * @param antiPatternMessageMap - the map to store the anti-pattern messages + * @return Map of strings parsed from the JSON object + * @throws IOException - if there is an error reading the file + */ + private Map getMapFromJsonObject(JsonReader reader, Map antiPatternMessageMap) throws IOException { + + String identifiersToCheck = null; + String antiPatternMessage = null; + + // Read the JSON file and parse the RuleConfig object + while (reader.nextToken() != JsonToken.END_OBJECT) { + + // Get the field name + String fieldName = reader.getFieldName(); + switch (fieldName) { + case "methodsToCheck": + case "clientsToCheck": + identifiersToCheck = getListFromJsonArray(reader).get(0); + break; + case "antiPatternMessage": + antiPatternMessage = reader.getString(); + break; + default: + reader.skipChildren(); + break; + } + // Add to map based on conditions + if (antiPatternMessage != null) { + + // This map is for base classes that have a set of discouraged identifiers and a corresponding set of antipattern messages + if (identifiersToCheck != null) { + antiPatternMessageMap.put(identifiersToCheck, antiPatternMessage); + } else { + // This map is for single anti-pattern messages of a particular rule + antiPatternMessageMap.put(fieldName, antiPatternMessage); + return antiPatternMessageMap; + } + } + } + // This map is to return mapped anti-pattern messages that have a set of discouraged identifiers and a corresponding set of antipattern messages + return antiPatternMessageMap; + } + + /** + * This method parses the values from the JSON object + * + * @param reader - the JsonReader object to read the JSON file + * @return List of strings parsed from the JSON object + * @throws IOException - if there is an error reading the file + */ + private List getValuesFromJsonReader(JsonReader reader) throws IOException { + + List values = new ArrayList<>(); + + // Check if the JSON file starts with an object + if (reader.nextToken() != JsonToken.START_OBJECT) { + throw new IOException("Expected start of object"); + } + + // Read the JSON file and parse the values + while (reader.nextToken() != JsonToken.END_OBJECT) { + + // Skip the field name and read only the value + reader.getFieldName(); // Move to the field name + reader.nextToken(); // Move to the value + String value = reader.getString(); // Read the value + values.add(value); + } + return values; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiveModeCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiveModeCheck.java new file mode 100644 index 00000000000..08886e9f260 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiveModeCheck.java @@ -0,0 +1,234 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 org.jetbrains.annotations.NotNull; + +import java.util.OptionalInt; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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 { + + // Define the holder for the problems found + 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 Logger LOGGER = Logger.getLogger(ServiceBusReceiveModeCheck.class.getName()); + + static { + final String ruleName = "ServiceBusReceiveModeCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || RULE_CONFIG.getMethodsToCheck().isEmpty(); + } + + /** + * 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) { + + // Retrieve the client name (left side of the declaration) + PsiType clientType = variable.getType(); + + // Check if the client type is an Azure ServiceBus client + if (!clientType.getCanonicalText().startsWith(RuleConfig.AZURE_PACKAGE_NAME) && !RULE_CONFIG.getClientsToCheck().contains(clientType.getCanonicalText())) { + return; + } + + // Check the assignment part (right side) + PsiExpression initializer = variable.getInitializer(); + + if (!(initializer instanceof PsiMethodCallExpression)) { + return; + } + + // Process the new expression initialization + determineReceiveMode((PsiMethodCallExpression) initializer); + + } + + /** + * 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 determineReceiveMode(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.getMethodsToCheck().contains(methodName))) { + return; + } + if ("receiveMode".equals(methodName)) { + isReceiveModePeekLock = receiveModePeekLockCheck((PsiMethodCallExpression) qualifier); + } else if ("prefetchCount".equals(methodName)) { + prefetchCountValue = getPrefetchCount((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.getAntiPatternMessageMap().get("antiPatternMessage")); + return; + } + } + } + + /** + * This method is used to check if the receive mode is set to PEEK_LOCK. + * + * @param qualifier The method call expression to check + * @return true if the receive mode is set to PEEK_LOCK, false otherwise + */ + private boolean receiveModePeekLockCheck(PsiMethodCallExpression qualifier) { + + String peekLockArgument = "PEEK_LOCK"; + + // Get the arguments of the method call expression + PsiExpression[] arguments = qualifier.getArgumentList().getExpressions(); + + for (PsiExpression argument : arguments) { + String argumentText = argument.getText(); + + // Check if the argument is set to PEEK_LOCK + if (argumentText.contains(peekLockArgument)) { + return true; + } + } + return false; + } + + /** + * This method is used to get the prefetch count value. + * + * @param qualifier The method call expression to check + * @return The prefetch count value + */ + private OptionalInt getPrefetchCount(PsiMethodCallExpression qualifier) { + + // Get the arguments of the method call expression + PsiExpression[] arguments = qualifier.getArgumentList().getExpressions(); + if (arguments.length > 0) { + + // Get the argument text + String argumentText = arguments[0].getText(); + try { + // Parse the argument text to an integer to get the prefetch count value + return OptionalInt.of(Integer.parseInt(argumentText)); + } catch (NumberFormatException e) { + LOGGER.log(Level.SEVERE, "Failed to parse prefetch count: " + argumentText, e); + } + } + // OptionalInt is used here as a container that can either hold an integer value or indicate that no value is present. + // This approach is necessary because the method signature specifies an int return type, which cannot be null + // Using OptionalInt avoids the ambiguity of returning a special value (like 0) to indicate absence, especially since 0 is a legitimate value for prefetchCount. + return OptionalInt.empty(); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiverAsyncClientCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiverAsyncClientCheck.java new file mode 100644 index 00000000000..250f59ddacc --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiverAsyncClientCheck.java @@ -0,0 +1,68 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiTypeElement; +import org.jetbrains.annotations.NotNull; + + +/** + * This class extends the LocalInspectionTool 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 LocalInspectionTool { + + // Define constants for string literals + private static final String CLIENT_NAME; + private static final String SUGGESTION; + private static final boolean SKIP_WHOLE_RULE; + + // Static initializer block to load the client data once + static { + final String ruleName = "ServiceBusReceiverAsyncClientCheck"; + + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + final RuleConfig ruleConfig = centralRuleConfigLoader.getRuleConfig(ruleName); + + SKIP_WHOLE_RULE = ruleConfig == RuleConfig.EMPTY_RULE; + CLIENT_NAME = ruleConfig.getClientsToCheck().get(0); + SUGGESTION = ruleConfig.getAntiPatternMessageMap().get("antiPatternMessage"); + } + + /** + * 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 ProblemsHolder object to register the problem + * @param isOnTheFly boolean to check if the inspection is on the fly -- This is not in use + * @return PsiElementVisitor object to visit the elements in the code + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new JavaElementVisitor() { + + @Override + public void visitTypeElement(PsiTypeElement element) { + super.visitTypeElement(element); + + if (SKIP_WHOLE_RULE) { + return; + } + + // Check if the element is an instance of PsiTypeElement + if (element instanceof PsiTypeElement && element.getType() != null) { + + // Register a problem if the client used matches the discouraged client + if (element.getType().getPresentableText().equals(CLIENT_NAME)) { + holder.registerProblem(element, SUGGESTION); + } + } + } + }; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/SingleOperationInLoopCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/SingleOperationInLoopCheck.java new file mode 100644 index 00000000000..1ec96f5263b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/SingleOperationInLoopCheck.java @@ -0,0 +1,281 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 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; + + 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() || RULE_CONFIG.getAntiPatternMessageMap().isEmpty(); + } + + /** + * 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.getAntiPatternMessageMap().get("antiPatternMessage") + 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.getAntiPatternMessageMap().get("antiPatternMessage") + 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) { + + String packageName = "com.azure.ai.textanalytics"; + + // 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 && className.startsWith(packageName)) { + + if (RULE_CONFIG.getMethodsToCheck().contains((methodCall.getMethodExpression().getReferenceName()) + "Batch")) { + return true; + } + } + } + return false; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StorageUploadWithoutLengthCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StorageUploadWithoutLengthCheck.java new file mode 100644 index 00000000000..ff77e0ca84a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StorageUploadWithoutLengthCheck.java @@ -0,0 +1,142 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaRecursiveElementWalkingVisitor; +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 org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * This class extends the LocalInspectionTool and is used to inspect the usage of Azure Storage upload APIs in the code. + * It checks if the upload methods are being called without a 'length' parameter of type 'long'. + * The methods to check are defined in a JSON configuration file. + * If such a method call is detected, a problem is registered with the ProblemsHolder. + */ + +public class StorageUploadWithoutLengthCheck extends LocalInspectionTool { + @Nls + @NotNull + @Override + public String getDisplayName() { + return "Ensure Storage APIs use Length Parameter"; + } + + private static final RuleConfig RULE_CONFIG; + private static final String LENGTH_TYPE = "long"; + private static final boolean SKIP_WHOLE_RULE; + + + static { + final String ruleName = "StorageUploadWithoutLengthCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE || RULE_CONFIG.getMethodsToCheck().isEmpty(); + } + + /** + * This method is used to build a PsiElementVisitor that will be used to visit the method calls in the code. + * + * @param holder ProblemsHolder object to register the problem + * @param isOnTheFly boolean to check if the inspection is on the fly -- Not in use + * @return PsiElementVisitor object to visit the elements in the code + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new JavaRecursiveElementWalkingVisitor() { + + @Override + public void visitMethodCallExpression(PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + String methodName = expression.getMethodExpression().getReferenceName(); + + if (!RULE_CONFIG.getMethodsToCheck().contains(methodName)) { + return; + } + + if (SKIP_WHOLE_RULE) { + return; + } + + boolean hasLengthArg = false; + + PsiExpressionList argumentList = expression.getArgumentList(); + PsiExpression[] arguments = argumentList.getExpressions(); + + for (PsiExpression arg : arguments) { + // Check if the argument is of type 'long' + if (arg.getType() != null && arg.getType().toString().equals(LENGTH_TYPE)) { + hasLengthArg = true; + break; + } + // Check if the argument is a method call + if (arg instanceof PsiMethodCallExpression) { + PsiMethodCallExpression argMethodCall = (PsiMethodCallExpression) arg; + + // Analysing arguments that are method calls to check for 'long' type arguments + hasLengthArg = checkMethodCallChain(argMethodCall); + } + } + if (!hasLengthArg) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + }; + } + + /** + * Analysing arguments that are chained method calls + * This method is used to check if the method call chain has a constructor with 'long' type arguments. + * The iteration starts from the end of the chain and goes up the chain. + * The qualifier of the method call is checked for a constructor with 'long' type arguments. + * + * @param expression - The method call expression + * @return boolean - true if the constructor has 'long' type arguments + */ + public boolean checkMethodCallChain(PsiMethodCallExpression expression) { + + // Iterating up the chain of method calls + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + + while (qualifier instanceof PsiMethodCallExpression) { + PsiMethodCallExpression qualifierMethodCall = (PsiMethodCallExpression) qualifier; + + qualifier = qualifierMethodCall.getMethodExpression().getQualifierExpression(); + + // Checking for constructor with 'long' type arguments + if (qualifier instanceof PsiNewExpression) { + return isLengthArgumentInCall(qualifier); + } + } + return false; + } + + /** + * This method is used to check if the constructor of the new expression has a 'long' type argument. + * + * @param qualifier - The qualifier of the method call + * @return boolean + */ + private boolean isLengthArgumentInCall(PsiExpression qualifier) { + PsiNewExpression newExpression = (PsiNewExpression) qualifier; + + // Getting the arguments of the constructor + PsiExpressionList newExpressionArgumentList = newExpression.getArgumentList(); + PsiExpression[] newExpressionArguments = newExpressionArgumentList.getExpressions(); + + // Checking if the arguments are of type 'long' + for (PsiExpression newExpressionArgument : newExpressionArguments) { + if (newExpressionArgument.getType().toString().equals(LENGTH_TYPE)) { + return true; + } + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryClientProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryClientProvider.java new file mode 100644 index 00000000000..90a0223af94 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryClientProvider.java @@ -0,0 +1,326 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.project.Project; +import com.intellij.psi.JavaElementVisitor; +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.PsiMethodCallExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiType; + +import java.io.FileNotFoundException; + +import com.intellij.psi.PsiVariable; +import com.microsoft.applicationinsights.TelemetryClient; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; + +import java.io.InputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * This class reads the instrumentation key from the applicationInsights.json file + * and returns a TelemetryClient object with the instrumentation key set. + * This object is used to send telemetry data to Application Insights. + */ +public class TelemetryClientProvider extends LocalInspectionTool { + + /** + * This method is called by the IntelliJ platform to build a visitor for the inspection. + * + * @param holder The ProblemsHolder object that holds the problems found in the code. + * @param isOnTheFly A boolean that indicates if the inspection is running on the fly. + * @return A PsiElementVisitor object that visits the method calls in the code. + */ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + + // Reset the methodCounts map for each new visitor + TelemetryClientProviderVisitor.methodCounts.clear(); + TelemetryClientProviderVisitor visitor = new TelemetryClientProviderVisitor(holder, isOnTheFly); + + // Set the visitor instance in TelemetryToggleAction + TelemetryToggleAction.setTelemetryService(visitor); + + return visitor; + } + + /** + * This class is a visitor that visits the method calls in the code and tracks the method calls. + */ + static class TelemetryClientProviderVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + private static boolean running = false; // Flag to indicate if the telemetry service is running + private static ScheduledExecutorService executorService; // Executor service to schedule telemetry data sending + + // Create a TelemetryClient object + // not final because the test involves Injecting the mock telemetry client to telemetryClient + // Package-private to allow access from tests in the same package + static TelemetryClient telemetryClient = getTelemetryClient(); + // Create a map to store the method counts + static Map> methodCounts = new HashMap<>(); + + // Create a Project object + private static Project project; + + // Create a list of prefixes for Azure service method calls -- Source in the link below + /** + * Azure SDK Java Service Methods + */ + static final List AZURE_METHOD_PREFIXES = Arrays.asList("upsert", "set", "create", "update", "replace", "delete", "add", "get", "list", "upload"); + + // Create a logger object + private static final Logger LOGGER = Logger.getLogger(TelemetryClientProvider.class.getName()); + + /** + * This constructor is used to create a visitor for the inspection + * It initializes the holder and isOnTheFly fields. + * + * @param holder The ProblemsHolder object that holds the problems found in the code. + * @param isOnTheFly A boolean that indicates if the inspection is running on the fly. - This is not used in this implementation. + */ + TelemetryClientProviderVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + + // Initialize start telemetry service when project is null + // This is to ensure that the telemetry service is started only once + if (project == null) { + startTelemetryService(); + } + project = holder.getProject(); + } + + /** + * This method is called by the IntelliJ platform to visit the elements in the code. + * It visits the method calls in the code and tracks the method calls. + * + * @param element The element to be visited. + */ + @Override + public void visitElement(PsiElement element) { + super.visitElement(element); + + // Handle method call expressions + if (element instanceof PsiMethodCallExpression) { + + PsiMethodCallExpression methodCall = (PsiMethodCallExpression) element; + + PsiReferenceExpression methodExpression = methodCall.getMethodExpression(); + String methodName = methodExpression.getReferenceName(); + String clientName = getClientName(methodCall); // Method to get client name + + if (clientName == null) { + return; + } + + if (methodName == null || !isAzureServiceMethodCall(methodName)) { + return; + } + + synchronized (methodCounts) { + + // Increment the count of the method call for the client + methodCounts.computeIfAbsent(clientName, k -> new HashMap<>()).put(methodName, methodCounts.get(clientName).getOrDefault(methodName, 0) + 1); + } + } + } + + + /** + * This method is used to get the client name from the method call expression. + * It extracts the client name from the method call expression. + * + * @param expression The method call expression from which the client name is extracted. + * @return The client name extracted from the method call expression. + */ + private String getClientName(PsiMethodCallExpression expression) { + + PsiExpression qualifier = expression.getMethodExpression().getQualifierExpression(); + + if (!(qualifier instanceof PsiReferenceExpression)) { + return null; + } + + PsiElement resolvedElement = ((PsiReferenceExpression) qualifier).resolve(); + + if (!(resolvedElement instanceof PsiVariable)) { + return null; + } + + PsiVariable variable = (PsiVariable) resolvedElement; + PsiType type = variable.getType(); + + if (!(type instanceof PsiClassType)) { + return null; + } + PsiClassType classType = (PsiClassType) type; + + PsiClass psiClass = classType.resolve(); + + if (psiClass == null) { + return null; + } + String className = psiClass.getQualifiedName(); + + if (isAzureSdkClient(className)) { + return classType.getPresentableText(); + } + return null; + } + + + /** + * This method checks if the class name is an Azure SDK client. + * It checks if the class name starts with "com.azure." and ends with "Client". + * + * @param className The class name to be checked. + * @return A boolean indicating if the class name is an Azure SDK client. + */ + private boolean isAzureSdkClient(String className) { + return className != null && className.startsWith(RuleConfig.AZURE_PACKAGE_NAME) && className.endsWith("Client"); + } + + /** + * This method checks if the method call is an Azure service method call. + * It checks if the method name starts with any of the prefixes in the AZURE_METHOD_PREFIXES list. + * + * @param methodName The method name to be checked. + * @return A boolean indicating if the method call is an Azure service method call. + */ + private boolean isAzureServiceMethodCall(String methodName) { + for (String prefix : AZURE_METHOD_PREFIXES) { + if (methodName.startsWith(prefix)) { + return true; + } + } + return false; + } + + /** + * This method starts the telemetry service. + * It creates a single-threaded ScheduledExecutorService and + * schedules the telemetry data to be sent every 2 minutes. + */ + static void startTelemetryService() { + + if (!running) { + running = true; + executorService = Executors.newSingleThreadScheduledExecutor(); + + executorService.scheduleAtFixedRate(() -> TelemetryClientProviderVisitor.sendTelemetryData(), 2, 3, TimeUnit.MINUTES); + } + } + + /** + * This method stops the telemetry service. + * It shuts down the executor service. + */ + static void stopTelemetryService() { + if (running) { + running = false; + if (executorService != null) { + executorService.shutdown(); + } + } + } + + /** + * This method checks if the telemetry service is running. + * + * @return A boolean indicating if the telemetry service is running. + */ + public static boolean isRunning() { + return running; + } + + /** + * This method sends the telemetry data to Application Insights. + * It sends the method counts as events. + */ + static void sendTelemetryData() { + + // Outer loop: Iterate over each client entry in the methodCounts map + for (Map.Entry> clientEntry : methodCounts.entrySet()) { + String clientName = clientEntry.getKey(); // Extract the client name + Map methods = clientEntry.getValue(); // Extract the methods map for this client + + // Inner loop: Iterate over each method entry in the methods map + for (Map.Entry methodEntry : methods.entrySet()) { + String methodName = methodEntry.getKey(); // Extract the method name + int count = methodEntry.getValue(); // Extract the call count for this method + + // Create custom dimensions map + Map customDimensions = new HashMap<>(); + customDimensions.put("clientName", clientName); + customDimensions.put("methodName", methodName); + + /// Convert count to a double and create a properties map + Map properties = new HashMap<>(); + properties.put("count", (double) count); + + // Report as event + telemetryClient.trackEvent("azure_sdk_usage_frequency", customDimensions, properties); + } + } + telemetryClient.flush(); + } + + /** + * This method reads the instrumentation key from the applicationInsights.json file + * and returns a TelemetryClient object with the instrumentation key set. + * This object is used to send telemetry data to Application Insights. + * + * @return A TelemetryClient object with the instrumentation key set. + */ + static TelemetryClient getTelemetryClient() { + + String configFilePath = "META-INF/applicationInsights.json"; + String instrumentationKeyJsonKey = "instrumentationKey"; + + // Create a new TelemetryClient object + TelemetryClient telemetry = new TelemetryClient(); + StringBuilder jsonBuilder = new StringBuilder(); + + // Read the instrumentation key from the applicationInsights.json file + try (InputStream inputStream = TelemetryClient.class.getClassLoader().getResourceAsStream(configFilePath); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + if (inputStream == null) { + LOGGER.log(Level.SEVERE, "Configuration file not found at path: " + configFilePath + ". Please ensure the file exists and is accessible.", new FileNotFoundException()); + return telemetry; // Return the telemetry client even if the config file is not found + } + + // while loop to read the json file + // this is more memory efficient than reading the entire file at once and holding it in memory + String line; + while ((line = reader.readLine()) != null) { + jsonBuilder.append(line).append("\n"); + } + + JSONObject jsonObject = new JSONObject(jsonBuilder.toString()); + String instrumentationKey = jsonObject.getString(instrumentationKeyJsonKey); + telemetry.getContext().setInstrumentationKey(instrumentationKey); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unexpected error while loading instrumentation key" + ". Please investigate further.", e); + } + return telemetry; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryToggleAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryToggleAction.java new file mode 100644 index 00000000000..efe98efcbae --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryToggleAction.java @@ -0,0 +1,67 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.project.DumbAware; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.TelemetryClientProvider.TelemetryClientProviderVisitor; + +/** + * This class is responsible for toggling the telemetry service on and off. + */ +public class TelemetryToggleAction extends AnAction implements DumbAware { + + // This is the telemetry service that will be toggled on and off. + private static TelemetryClientProviderVisitor telemetryService; + + // This method sets the telemetry service + public static void setTelemetryService(TelemetryClientProviderVisitor service) { + telemetryService = service; + } + + /** + * This method is called when the action is performed. + * It toggles the telemetry service on and off. + */ + @Override + public void actionPerformed(AnActionEvent e) { + Project project = e.getProject(); + if (project == null || telemetryService == null) { + return; + } + + // If the telemetry service is running, stop it. Otherwise, start it. + if (telemetryService.isRunning()) { + telemetryService.stopTelemetryService(); + } else { + telemetryService.startTelemetryService(); + } + // Update the presentation of the action. + updatePresentation(e.getPresentation()); + } + + /** + * This method updates the presentation of the action. + * "Presentation of the action" means the UI representation of the action + * It ensures the UI is updated to reflect the current state of the telemetry service. + * It sets the text of the action to "Toggle Telemetry ON" if the telemetry service is not running. + * Otherwise, it sets the text of the action to "Toggle Telemetry OFF". + */ + private void updatePresentation(Presentation presentation) { + if (telemetryService != null && telemetryService.isRunning()) { + presentation.setText("Toggle Telemetry OFF"); + } else { + presentation.setText("Toggle Telemetry ON"); + } + } + + /** + * This method updates the action event. + */ + @Override + public void update(AnActionEvent e) { + updatePresentation(e.getPresentation()); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpdateCheckpointAsyncBlockChecker.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpdateCheckpointAsyncBlockChecker.java new file mode 100644 index 00000000000..be716ddcc98 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpdateCheckpointAsyncBlockChecker.java @@ -0,0 +1,94 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiMethodCallExpression; +import org.jetbrains.annotations.NotNull; + +/** + * This class extends the AbstractUpdateCheckpointChecker to check for the use of the updateCheckpointAsync() method call in the code. + * If the method is called from an EventBatchContext object and the following method is not `block` or `block_with_timeout`, + * a problem is registered with the suggestion message. + */ +public class UpdateCheckpointAsyncBlockChecker extends AbstractUpdateCheckpointAsyncChecker { + + /** + * This method creates the visitor for the inspection tool. + * + * @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. - This parameter is not used in the method but is required by the method signature. + * @return PsiElementVisitor visitor to inspect elements in the code + */ + @Override + protected JavaElementVisitor createVisitor(ProblemsHolder holder, boolean isOnTheFly) { + return new BlockVisitor(holder, isOnTheFly); + } + + /** + * 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 not `block` or `block_with_timeout`. + * If both conditions are met, a problem is registered with the suggestion message. + */ + static class BlockVisitor extends JavaElementVisitor { + + // Define the holder to register problems + private final ProblemsHolder holder; + + /** + * 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. + */ + BlockVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + // Define constants for string literals + protected static final RuleConfig RULE_CONFIG; + protected static final boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "UpdateCheckpointAsyncBlockChecker"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || RULE_CONFIG.getAntiPatternMessageMap().isEmpty(); + } + + /** + * This method visits the method call expressions in the code. + * It checks if the method call is updateCheckpointAsync() and if the following method is not `block` or `block_with_timeout`. + * If both conditions are met, a problem is registered with the suggestion message. + * + * @param expression The method call expression to visit + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + // Check if the rule should be skipped + if (SKIP_WHOLE_RULE) { + return; + } + + if (expression.getMethodExpression() == null || expression.getMethodExpression().getReferenceName() == null) { + return; + } + + // Check if the method call is updateCheckpointAsync() + if ("updateCheckpointAsync".equals(expression.getMethodExpression().getReferenceName())) { + + // Get the method name following the updateCheckpointAsync() method call + String followingMethod = getFollowingMethodName(expression); + + // Check if the following method is not `block` or `block_with_timeout` and + // Check if the updateCheckpointAsync() method call is called on an EventBatchContext object + if ((followingMethod == null || !RULE_CONFIG.getMethodsToCheck().contains(followingMethod)) && isCalledOnEventBatchContext(expression)) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpdateCheckpointAsyncSubscribeChecker.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpdateCheckpointAsyncSubscribeChecker.java new file mode 100644 index 00000000000..b4a4419cb9d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpdateCheckpointAsyncSubscribeChecker.java @@ -0,0 +1,94 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiMethodCallExpression; +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 AbstractUpdateCheckpointAsyncChecker { + + /** + * This method creates the visitor for the inspection tool. + * + * @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. - This parameter is not used in the method but is required by the method signature. + * @return PsiElementVisitor visitor to inspect elements in the code + */ + @Override + protected JavaElementVisitor createVisitor(ProblemsHolder holder, boolean isOnTheFly) { + return new SubscribeVisitor(holder, isOnTheFly); + } + + /** + * 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 SubscribeVisitor extends JavaElementVisitor { + + // Define the holder to register problems + private final ProblemsHolder holder; + + /** + * 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. + */ + SubscribeVisitor(ProblemsHolder holder, boolean isOnTheFly) { + this.holder = holder; + } + + // Define constants for string literals + protected static final RuleConfig RULE_CONFIG; + protected static final boolean SKIP_WHOLE_RULE; + + 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() || RULE_CONFIG.getAntiPatternMessageMap().isEmpty(); + } + + /** + * This method visits the method call expressions in the code. + * It checks if the method call is updateCheckpointAsync() and if the following method is `subscribe`. + * If both conditions are met, a problem is registered with the suggestion message. + * + * @param expression The method call expression to inspect + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + // Check if the rule should be skipped + if (SKIP_WHOLE_RULE) { + return; + } + + if (expression.getMethodExpression() == null || expression.getMethodExpression().getReferenceName() == null) { + return; + } + + // Check if the method call is updateCheckpointAsync() + if ("updateCheckpointAsync".equals(expression.getMethodExpression().getReferenceName())) { + + // Get the method name following the updateCheckpointAsync() method call + String followingMethod = getFollowingMethodName(expression); + + // Check if the following method is `subscribe` and + // Check if the updateCheckpointAsync() method call is called on an EventBatchContext object + if (RULE_CONFIG.getMethodsToCheck().get(0).equals(followingMethod) && isCalledOnEventBatchContext(expression)) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + } + } + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpgradeLibraryVersionCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpgradeLibraryVersionCheck.java new file mode 100644 index 00000000000..025e374b8b0 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UpgradeLibraryVersionCheck.java @@ -0,0 +1,163 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Map; + +/** + * Inspection class to check the version of the libraries in the pom.xml file against the recommended version. + * The recommended version is fetched from a file hosted on GitHub. + * The recommended version is 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 recommended version, a warning is flagged and the recommended version is suggested. + */ +public class UpgradeLibraryVersionCheck extends AbstractLibraryVersionCheck { + + /** + * 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 UpgradeLibraryVersionVisitor(holder); + } + + /** + * Method to check the version of the library against the recommended version. + * + * @param fullName The full name of the library + * @param currentVersion The current version of the library + * @param holder The holder for the problems found + * @param versionTag The version tag in the pom.xml file for the library version + */ + @Override + protected void checkAndFlagVersion(String fullName, String currentVersion, ProblemsHolder holder, PsiElement versionTag) { + + // Check if the recommended version is available for the library + if (!(UpgradeLibraryVersionVisitor.getLibraryRecommendedVersionMap().containsKey(fullName))) { + return; + } + String recommendedVersion = UpgradeLibraryVersionVisitor.getLibraryRecommendedVersionMap().get(fullName); + + // Compare minor versions only + String[] currentVersionParts = currentVersion.split("\\."); + + // Check if the version is in the correct format + if (!(currentVersionParts.length > 1)) { + return; + } + + // Parse to get the minor version + String currentMinor = currentVersionParts[0] + "." + currentVersionParts[1]; + + // Flag the version if the minor version is different from the recommended version + if (!currentMinor.equals(recommendedVersion)) { + holder.registerProblem(versionTag, getFormattedMessage(fullName, recommendedVersion, UpgradeLibraryVersionVisitor.RULE_CONFIG)); + } + } + + /** + * Visitor class for the inspection. + * Checks the version of the libraries in the pom.xml file against the recommended version. + * The recommended version is fetched from a file hosted on GitHub. + * The recommended version is 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 recommended version, a warning is flagged and the recommended version is suggested. + */ + class UpgradeLibraryVersionVisitor extends PsiElementVisitor { + + private final ProblemsHolder holder; + + /** + * Constructs a new instance of the visitor. + * + * @param holder The holder for the problems found + */ + UpgradeLibraryVersionVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + // Map to store the recommended version for each library + private static WeakReference> LIBRARY_RECOMMENDED_VERSION_MAP_REF; + + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + final String ruleName = "UpgradeLibraryVersionCheck"; + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(ruleName); + SKIP_WHOLE_RULE = RULE_CONFIG == RuleConfig.EMPTY_RULE; + } + + /** + * Visitor to check the pom.xml file for the library version. + * + * @param file The pom.xml file + */ + @Override + public void visitFile(@NotNull PsiFile file) { + super.visitFile(file); + + if (SKIP_WHOLE_RULE) { + return; + } + + if (!file.getName().equals("pom.xml")) { + return; + } + + if (file instanceof XmlFile && file.getName().equals("pom.xml")) { + try { + UpgradeLibraryVersionCheck.this.checkPomXml((XmlFile) file, holder); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Method to get the recommended version for each library from the file hosted on GitHub. + * The file is fetched only once and the content is stored in a WeakReference. + * A WeakReference is used to allow the garbage collector to collect the content if necessary. + * If the content is not available in the WeakReference, the file is fetched again. + * + * @return The map of the recommended version for each library + */ + private static Map getLibraryRecommendedVersionMap() { + + // Load the file content from the URL if it is not already loaded + Map fileContent = LIBRARY_RECOMMENDED_VERSION_MAP_REF == null ? null : LIBRARY_RECOMMENDED_VERSION_MAP_REF.get(); + + if (fileContent == null) { + synchronized (IncompatibleDependencyCheck.IncompatibleDependencyVisitor.class) { + fileContent = LIBRARY_RECOMMENDED_VERSION_MAP_REF == null ? null : LIBRARY_RECOMMENDED_VERSION_MAP_REF.get(); + if (fileContent == null) { + + String metadataUrl = RULE_CONFIG.getListedItemsToCheck().get(0); + String latestVersion = DependencyVersionFileFetcher.getLatestVersion(metadataUrl); + + if (latestVersion != null) { + String pomUrl = String.format("https://repo1.maven.org/maven2/com/azure/azure-sdk-bom/%s/azure-sdk-bom-%s.pom", latestVersion, latestVersion); + fileContent = DependencyVersionFileFetcher.parsePomFile(pomUrl); + LIBRARY_RECOMMENDED_VERSION_MAP_REF = new WeakReference<>(fileContent); + } + } + } + } + return fileContent; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UseOfBlockOnAsyncClientsCheck.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UseOfBlockOnAsyncClientsCheck.java new file mode 100644 index 00000000000..2e98efafe53 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UseOfBlockOnAsyncClientsCheck.java @@ -0,0 +1,171 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 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 { + + /** + * This method is used to get the visitor for the inspection tool. + * The visitor is used to check for the use of blocking calls on async clients in Azure SDK. + * The visitor checks if the method call is a blocking method call on a reactive type + * and if the reactive type is an async client in Azure SDK. + * If the method call is a blocking call on an async Azure client, it reports a problem. + * + * @param holder ProblemsHolder - the holder to report the problems + * isOnTheFly boolean - whether the inspection is on the fly - not used in this implementation but required by the interface + */ + @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. + * The visitor checks if the method call is a blocking method call on a reactive type + * and if the reactive type is an async client in Azure SDK. + * If the method call is a blocking method call on an async client, it reports a problem. + */ + static class UseOfBlockOnAsyncClientsVisitor extends JavaElementVisitor { + + private final ProblemsHolder holder; + + // Define constants for string literals + private static final String RULE_NAME = "UseOfBlockOnAsyncClientsCheck"; + private static final RuleConfig RULE_CONFIG; + private static final boolean SKIP_WHOLE_RULE; + + static { + RuleConfigLoader centralRuleConfigLoader = RuleConfigLoader.getInstance(); + + // Get the RuleConfig object for the rule + RULE_CONFIG = centralRuleConfigLoader.getRuleConfig(RULE_NAME); + SKIP_WHOLE_RULE = RULE_CONFIG.skipRuleCheck() || RULE_CONFIG.getListedItemsToCheck().isEmpty(); + } + + /** + * Constructor to initialize the visitor with the holder + * + * @param holder ProblemsHolder - the holder to report the problems + */ + UseOfBlockOnAsyncClientsVisitor(ProblemsHolder holder) { + this.holder = holder; + } + + /** + * This method is used to visit the method call expression. + * The method call expression is checked to see if it is a blocking method call on an async client. + * If the method call is a blocking method call on an async client, it reports a problem. + * + * @param expression PsiMethodCallExpression - the method call expression to visit + */ + @Override + public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { + super.visitMethodCallExpression(expression); + + if (SKIP_WHOLE_RULE) { + return; + } + + // Check if the method call is a blocking method call on a reactive type + for (String methodToCheck : RULE_CONFIG.getMethodsToCheck()) { + if (expression.getMethodExpression().getReferenceName().equals(methodToCheck)) { + + // Check if the method call is on a reactive type + boolean isAsyncContext = checkIfAsyncContext(expression); + + if (isAsyncContext) { + holder.registerProblem(expression, RULE_CONFIG.getAntiPatternMessageMap().get("antiPatternMessage")); + return; + } + } + } + } + + /** + * This method is used to check if the method call is a blocking method calls on a reactive type. + * The method call is checked to see if it is a blocking method call on a reactive type + * and if the reactive type is an async client in Azure SDK. + * + * @param methodCall PsiMethodCallExpression - the method call expression to check + * @return true if the method call is a blocking method calls on an async client, false otherwise + */ + private boolean checkIfAsyncContext(@NotNull PsiMethodCallExpression methodCall) { + + // Get the qualifier expression of the method call -- the expression before the 'block' method call + PsiExpression qualifierExpression = methodCall.getMethodExpression().getQualifierExpression(); + + if (qualifierExpression instanceof PsiMethodCallExpression) { + PsiMethodCallExpression qualifierMethodCall = (PsiMethodCallExpression) qualifierExpression; + + // Get the return type of the qualifier method call + PsiType qualifierReturnType = qualifierMethodCall.getType(); + + if (qualifierReturnType instanceof PsiClassType) { + PsiClass qualifierReturnTypeClass = ((PsiClassType) qualifierReturnType).resolve(); + + // Check if the return type is a subclass of Mono or Flux + if (qualifierReturnTypeClass != null && isReactiveType(qualifierReturnTypeClass)) { + + return isAzureAsyncClient(qualifierMethodCall); + } + } + } + return false; + } + + /** + * This method is used to check if the method call is on an async client in Azure SDK. + * The method call is checked to see if it is on an async client in Azure SDK. + * + * @param qualifierMethodCall PsiMethodCallExpression - the method call expression to check + * @return true if the method call is on an async client in Azure SDK, false otherwise + */ + private boolean isAzureAsyncClient(PsiMethodCallExpression qualifierMethodCall) { + + // Get the expression that calls the method returning a reactive type + PsiExpression clientExpression = qualifierMethodCall.getMethodExpression().getQualifierExpression(); + + // Travel up the method call chain to get the client expression + while (clientExpression instanceof PsiMethodCallExpression) { + + clientExpression = ((PsiMethodCallExpression) clientExpression).getMethodExpression().getQualifierExpression(); + } + + // a ReferenceExpression is the last expression in the chain - the client object + 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; + } + + /** + * Helper method to check if the class / return type is a subclass of Mono or Flux. + * + * @param psiClass PsiClass - the class to check + * @return true if the class is a subclass of Mono or Flux, false otherwise + */ + private boolean isReactiveType(PsiClass psiClass) { + return RULE_CONFIG.getListedItemsToCheck().contains(psiClass.getQualifiedName()); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/applicationInsights.json b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/applicationInsights.json new file mode 100644 index 00000000000..c496cbb3125 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/applicationInsights.json @@ -0,0 +1,3 @@ +{ + "instrumentationKey":"" +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml index fd1f041cf1b..14668726b33 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/azure-intellij-plugin-azure-sdk.xml @@ -4,12 +4,21 @@ com.intellij.modules.java - - - + + + + + + + + + @@ -23,5 +32,234 @@ implementationClass="com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.MavenProjectInspection"> com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.MavenProjectInspection + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.StorageUploadWithoutLengthCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.DisableAutoCompleteCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.DynamicClientCreationCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.HardcodedAPIKeysAndTokensCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.DetectDiscouragedAPIUsageCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.GetSyncPollerOnPollerFluxCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.ServiceBusReceiveModeCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.SingleOperationInLoopCheck + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.TelemetryClientProvider + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.DetectDiscouragedClientCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.UpdateCheckpointAsyncSubscribeChecker + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.UpdateCheckpointAsyncBlockChecker + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.KustoQueriesWithTimeIntervalInQueryStringCheck + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.UseOfBlockOnAsyncClientsCheck + + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.EndpointOnNonAzureOpenAIAuthCheck + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.UpgradeLibraryVersionCheck + + + + + + com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.IncompatibleDependencyCheck + + - \ No newline at end of file + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json new file mode 100644 index 00000000000..ed53fc1cb91 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/main/resources/META-INF/ruleConfigs.json @@ -0,0 +1,147 @@ +{ + "StorageUploadWithoutLengthCheck": { + "methodsToCheck": [ + "upload", + "uploadWithResponse" + ], + "antiPatternMessage": "Azure Storage upload API without length parameter detected. Use upload API with length parameter instead." + }, + "DisableAutoCompleteCheck": { + "methodsToCheck": "disableAutoComplete", + "clientsToCheck": [ + "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient" + ], + "antiPatternMessage": "Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion." + }, + "DynamicClientCreationCheck": { + "methodsToCheck": [ + "buildClient", + "buildAsyncClient" + ], + "antiPatternMessage": "Dynamic client creation detected. Create a single client instance and reuse it instead." + }, + "HardcodedAPIKeysAndTokensCheck": { + "servicesToCheck": [ + "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.", + "solution": "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": { + "methodsToCheck": "getSyncPoller", + "antiPatternMessage": "Use of getSyncPoller() on a PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks" + }, + "ServiceBusReceiveModeCheck": { + "clientsToCheck": [ + "ServiceBusReceiverClient", + "ServiceBusReceiverAsyncClient", + "ServiceBusProcessorClient" + ], + "methodsToCheck": [ + "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": { + "ConnectionStringCheck": { + "methodsToCheck": "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" + }, + "GetCompletionsInChatApplicationsCheck": { + "methodsToCheck": "getCompletions", + "antiPatternMessage": "getCompletions API detected. Use the getChatCompletions API instead." + } + }, + "DetectDiscouragedClientCheck": { + "ServiceBusReceiverAsyncClientCheck": { + "clientsToCheck": "ServiceBusReceiverAsyncClient", + "antiPatternMessage": "Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead." + }, + "EventHubConsumerAsyncClientCheck": { + "clientsToCheck": "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": { + "methodsToCheck": [ + "detectLanguageBatch", + "recognizeEntitiesBatch", + "recognizePiiEntitiesBatch", + "recognizeLinkedEntitiesBatch", + "extractKeyPhrasesBatch", + "analyzeSentimentBatch" + ], + "antiPatternMessage": "Single operation found in loop. This SDK provides a batch operation API, use it to perform multiple actions in a single request: " + }, + "AbstractUpdateCheckpointAsyncChecker": { + "methodsToCheck": "updateCheckpointAsync" + }, + "UpdateCheckpointAsyncSubscribeChecker": { + "methodsToCheck": "subscribe", + "antiPatternMessage": "Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`" + }, + "UpdateCheckpointAsyncBlockChecker": { + "methodsToCheck": [ + "block", + "block_with_timeout" + ], + "antiPatternMessage": "Calling updateCheckpointAsync() without block() will not do anything, use `block()` or `block` operator with a timeout, or consider using 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": { + "methodsToCheck": [ + "endpoint", + "credential" + ], + "servicesToCheck": "KeyCredential", + "antiPatternMessage": "Endpoint should not be used with KeyCredential for non-Azure OpenAI clients" + }, + "UseOfBlockOnAsyncClientsCheck": { + "methodsToCheck": [ + "block", + "blockOptional", + "blockFirst", + "blockLast", + "toIterable", + "toStream", + "toFuture", + "blockFirstOptional", + "blockLastOptional" + ], + "typesToCheck": [ + "reactor.core.publisher.Flux", + "reactor.core.publisher.Mono" + ], + "antiPatternMessage": "Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead." + }, + "UpgradeLibraryVersionCheck": { + "url": "https://repo1.maven.org/maven2/com/azure/azure-sdk-bom/maven-metadata.xml", + "antiPatternMessage": "A newer stable minor version of {{fullName}} is available. We recommend you update to version {{recommendedVersion}}.x" + }, + "IncompatibleDependencyCheck": { + "url": "https://raw.githubusercontent.com/Azure/azure-sdk-for-java/main/eng/versioning/supported_external_dependency_versions.json", + "antiPatternMessage": "The version of {{fullName}} is not compatible with other dependencies of the same library defined in the pom.xml. Please use versions of the same library release group {{recommendedVersion}}.x to ensure proper functionality." + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractLibraryVersionCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractLibraryVersionCheckTest.java new file mode 100644 index 00000000000..d9110fa1842 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractLibraryVersionCheckTest.java @@ -0,0 +1,218 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.UpgradeLibraryVersionCheck.UpgradeLibraryVersionVisitor; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.IncompatibleDependencyCheck.IncompatibleDependencyVisitor; + +import java.util.Arrays; +import java.util.HashSet; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 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. + */ +class AbstractLibraryVersionCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private UpgradeLibraryVersionVisitor mockUpgradeLibraryVersionVisitor; + + @Mock + private IncompatibleDependencyVisitor mockIncompatibleDependencyVisitor; + + @Mock + private XmlFile mockFile; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockFile = mock(XmlFile.class); + + // Set the version groups that are "discovered" by the visitor to test the IncompatibleDependencyVisitor + IncompatibleDependencyCheck.encounteredVersionGroups = new HashSet<>(Arrays.asList("jackson_2.10", "gson_2.10")); + + UpgradeLibraryVersionCheck mockCheck = new UpgradeLibraryVersionCheck(); + mockUpgradeLibraryVersionVisitor = mockCheck.new UpgradeLibraryVersionVisitor(mockHolder); + + IncompatibleDependencyCheck mockIncompatibleDependencyCheck = new IncompatibleDependencyCheck(); + mockIncompatibleDependencyVisitor = mockIncompatibleDependencyCheck.new IncompatibleDependencyVisitor(mockHolder); + } + + /** + * Test the UpgradeLibraryVersionVisitor with a library that is out of date. + * The test checks that the visitor registers a problem when the version of the library is out of date. + */ + @Test + public void testUpgradeCheckOutOfDate() { + String groupIDValue = "com.azure"; + String artifactIDValue = "azure-messaging-servicebus"; + String versionValue = "7.0.0"; + int numOfInvocations = 1; + String recommendedVersion = "7.17"; + + verifyRegisterProblem(mockUpgradeLibraryVersionVisitor, groupIDValue, artifactIDValue, versionValue, numOfInvocations, recommendedVersion); + } + + /** + * Test the UpgradeLibraryVersionVisitor with a library that is up to date. + * The test checks that the visitor does not register a problem when the version of the library is up to date. + */ + @Test + public void testUpgradeCheckUpToDate() { + String groupIDValue = "com.azure"; + String artifactIDValue = "azure-messaging-servicebus"; + String versionValue = "7.17.1"; + int numOfInvocations = 0; + String recommendedVersion = "7.17"; + + verifyRegisterProblem(mockUpgradeLibraryVersionVisitor, groupIDValue, artifactIDValue, versionValue, numOfInvocations, recommendedVersion); + } + + /** + * Test the UpgradeLibraryVersionVisitor with a library that has a missing version. + * The test checks that the visitor does not register a problem when the version of the library is missing. + * A missing version is considered out of scope for this inspection. + */ + @Test + public void testUpgradeCheckMissingVersion() { + String groupIDValue = "com.example"; + String artifactIDValue = "example-lib"; + String versionValue = ""; // Empty version + int numOfInvocations = 0; // No problems will be registered by this checker because its out of scope + String recommendedVersion = null; + + verifyRegisterProblem(mockUpgradeLibraryVersionVisitor, groupIDValue, artifactIDValue, versionValue, numOfInvocations, recommendedVersion); + } + + /** + * Test the IncompatibleDependencyVisitor with a library that has a different minor version. + * The test checks that the visitor registers a problem when the version of the library is different from the recommended version. + */ + @Test + public void testIncompatibleDifferentMinorGroup() { + String groupIDValue = "com.google.code.gson"; + String artifactIDValue = "gson"; + String versionValue = "2.9.0"; + int numOfInvocations = 1; + String recommendedVersion = "2.10"; + + verifyRegisterProblem(mockIncompatibleDependencyVisitor, groupIDValue, artifactIDValue, versionValue, numOfInvocations, recommendedVersion); + } + + /** + * Test the IncompatibleDependencyVisitor with a library that has the same minor version. + * The test checks that the visitor does not register a problem when the version of the library is the same as the recommended version. + */ + @Test + public void testIncompatibleCheckSameMinorGroup() { + String groupIDValue = "com.fasterxml.jackson.core"; + String artifactIDValue = "jackson-databind"; + String versionValue = "2.10.0"; + int numOfInvocations = 0; + String recommendedVersion = "2.10"; + + verifyRegisterProblem(mockIncompatibleDependencyVisitor, groupIDValue, artifactIDValue, versionValue, numOfInvocations, recommendedVersion); + } + + /** + * Test the IncompatibleDependencyVisitor with a library that is not in the database. + * The test checks that the visitor does not register a problem when the library is not in the database. + * This is because the visitor only checks for libraries that are in the database. + */ + @Test + public void testIncompatibleCheckDifferentMajorVersion() { + String groupIDValue = "com.fasterxml.jackson.core"; + String artifactIDValue = "jackson-databind"; + String versionValue = "3.0.0"; // Different major version + int numOfInvocations = 0; + String recommendedVersion = null; + + verifyRegisterProblem(mockIncompatibleDependencyVisitor, groupIDValue, artifactIDValue, versionValue, numOfInvocations, recommendedVersion); + } + + /** + * Helper method to verify the registration of a problem by the visitor. + * + * @param visitor The visitor to check the pom.xml file + * @param groupIDValue The group ID of the library + * @param artifactIDValue The artifact ID of the library + * @param versionIDValue The version of the library + * @param numOfInvocations The number of times the visitor should register a problem + * @param recommendedVersion The recommended version of the library + */ + private void verifyRegisterProblem(PsiElementVisitor visitor, String groupIDValue, String artifactIDValue, String versionIDValue, int numOfInvocations, String recommendedVersion) { + + 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); + + visitor.visitFile(mockFile); + + if (visitor instanceof UpgradeLibraryVersionVisitor) { + verify(mockHolder, times(numOfInvocations)).registerProblem(versionTag, "A newer stable minor version of " + groupIDValue + ":" + artifactIDValue + " is available. We recommend you update to version " + recommendedVersion + ".x"); + } else if (visitor instanceof IncompatibleDependencyVisitor) { + verify(mockHolder, times(numOfInvocations)).registerProblem(versionTag, "The version of " + groupIDValue + ":" + artifactIDValue + " is not compatible with other dependencies of the same library defined in the pom.xml. Please use versions of the same library release group " + recommendedVersion + ".x to ensure proper functionality."); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractUpdateCheckpointAsyncCheckerTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractUpdateCheckpointAsyncCheckerTest.java new file mode 100644 index 00000000000..ab3e503a9ff --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/AbstractUpdateCheckpointAsyncCheckerTest.java @@ -0,0 +1,193 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.PsiExpressionList; +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.azure.sdk.buildtool.UpdateCheckpointAsyncBlockChecker.BlockVisitor; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.UpdateCheckpointAsyncSubscribeChecker.SubscribeVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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 is used to test the AbstractUpdateCheckpointChecker class. + * The test methods test the visitMethodCallExpression method of the classes that extend the AbstractUpdateCheckpointChecker class. + * These are the UpdateCheckpointBlockChecker and UpdateCheckpointSubscribeChecker classes. + */ +public class AbstractUpdateCheckpointAsyncCheckerTest { + + // Create a mock ProblemsHolder to be used in the test + @Mock + private ProblemsHolder mockHolder; + + // Create a mock PsiMethodCallExpression. + @Mock + private PsiMethodCallExpression mockMethodCallExpression; + + + @BeforeEach + public void setUp() { + // Set up the test + mockHolder = mock(ProblemsHolder.class); + mockMethodCallExpression = mock(PsiMethodCallExpression.class); + } + + /** + * This test method tests the visitMethodCallExpression method of the UpdateCheckpointAsyncSubscribeVisitor class. + * The test checks if the problem is registered when the following method is subscribe. + */ + @Test + public void testWithSubscribe() { + + SubscribeVisitor mockSubscribeVisitor = new SubscribeVisitor(mockHolder, true); + + String packageName = "com.azure"; + String followingMethod = "subscribe"; + int numOfInvocations = 1; + String mainMethodFound = "updateCheckpointAsync"; + String objectType = "EventBatchContext"; + + verifyProblemRegistered(mockSubscribeVisitor, packageName, mainMethodFound, numOfInvocations, followingMethod, objectType); + } + + /** + * This test method tests the visitMethodCallExpression method of the UpdateCheckpointAsyncBlockVisitor class. + * The test checks if the problem is registered when the following method is block. + * A problem should NOT be registered in this case. + */ + @Test + public void testWithBlock() { + + BlockVisitor mockBlockVisitor = new BlockVisitor(mockHolder, true); + + String packageName = "com.azure"; + String followingMethod = "block"; + int numOfInvocations = 0; + String mainMethodFound = "updateCheckpointAsync"; + String objectType = "EventBatchContext"; + + verifyProblemRegistered(mockBlockVisitor, packageName, mainMethodFound, numOfInvocations, followingMethod, objectType); + } + + /** + * This test method tests the visitMethodCallExpression method of the UpdateCheckpointAsyncBlockVisitor class. + * The test checks if the problem is registered when the following method is null. + * A problem should be registered in this case -- there is no 'block' or 'block(timeout)' method following the 'updateCheckpointAsync' method. + */ + @Test + public void testWithNullFollowingMethod() { + + BlockVisitor mockBlockVisitor = new BlockVisitor(mockHolder, true); + + String packageName = "com.azure"; + String followingMethod = null; + int numOfInvocations = 1; + String mainMethodFound = "updateCheckpointAsync"; + String objectType = "EventBatchContext"; + + verifyProblemRegistered(mockBlockVisitor, packageName, mainMethodFound, numOfInvocations, followingMethod, objectType); + } + + /** + * This test method tests the visitMethodCallExpression method of the UpdateCheckpointAsyncSubscribeVisitor class. + * The test checks if the problem is registered when the following method is null. + * A problem should NOT be registered in this case because the 'subscribe' method is not called + */ + @Test + public void testSubscribeCheckerWithNullFollowingMethod() { + + SubscribeVisitor mockSubscribeVisitor = new SubscribeVisitor(mockHolder, true); + + String packageName = "com.azure"; + String followingMethod = null; + int numOfInvocations = 0; + String mainMethodFound = "updateCheckpointAsync"; + String objectType = "EventBatchContext"; + + verifyProblemRegistered(mockSubscribeVisitor, packageName, mainMethodFound, numOfInvocations, followingMethod, objectType); + } + + /** + * This test method tests the visitMethodCallExpression method of the UpdateCheckpointAsyncBlockVisitor class. + * The test checks if the problem is registered when the package name is not com.azure.... + * A problem should NOT be registered in this case. + */ + @Test + public void testWithWrongPackage() { + + BlockVisitor mockBlockVisitor = new BlockVisitor(mockHolder, true); + + String packageName = "com.microsoft.azure"; + String followingMethod = "block"; + int numOfInvocations = 0; + String mainMethodFound = "updateCheckpointAsync"; + String objectType = "EventBatchContext"; + + verifyProblemRegistered(mockBlockVisitor, packageName, mainMethodFound, numOfInvocations, followingMethod, objectType); + } + + /** + * This method verifies if the problem is registered with the ProblemsHolder. + */ + private void verifyProblemRegistered(JavaElementVisitor mockVisitor, String packageName, String mainMethodFound, int numOfInvocations, String followingMethod, String objectType) { + + PsiReferenceExpression mockReferenceExpression = mock(PsiReferenceExpression.class); + + // getFollowingMethodName mocking + PsiReferenceExpression parentReferenceExpression = mock(PsiReferenceExpression.class); + PsiMethodCallExpression grandParentMethodCalLExpression = mock(PsiMethodCallExpression.class); + + // isCalledOnEventBatchContext + PsiReferenceExpression mockQualifier = mock(PsiReferenceExpression.class); + PsiParameter mockParameter = mock(PsiParameter.class); + PsiClassType parameterType = mock(PsiClassType.class); + PsiClass psiClass = mock(PsiClass.class); + PsiExpression duration = mock(PsiExpression.class); + PsiExpressionList arguments = mock(PsiExpressionList.class); + PsiExpression[] expressions = new PsiExpression[]{duration}; + PsiType durationType = mock(PsiType.class); + + when(mockMethodCallExpression.getMethodExpression()).thenReturn(mockReferenceExpression); + when(mockReferenceExpression.getReferenceName()).thenReturn(mainMethodFound); + + // getFollowingMethodName mocking + when(mockMethodCallExpression.getParent()).thenReturn(mockReferenceExpression); + when(mockReferenceExpression.getParent()).thenReturn(grandParentMethodCalLExpression); + when(grandParentMethodCalLExpression.getMethodExpression()).thenReturn(parentReferenceExpression); + when(parentReferenceExpression.getReferenceName()).thenReturn(followingMethod); + when(grandParentMethodCalLExpression.getArgumentList()).thenReturn(arguments); + when(arguments.getExpressions()).thenReturn(expressions); + when(expressions[0].getType()).thenReturn(durationType); + when(durationType.getPresentableText()).thenReturn("java.time.Duration"); + + // isCalledOnEventBatchContext + when(mockReferenceExpression.getQualifierExpression()).thenReturn(mockQualifier); + when(mockQualifier.resolve()).thenReturn(mockParameter); + when(mockParameter.getType()).thenReturn(parameterType); + when(parameterType.getPresentableText()).thenReturn(objectType); + when(parameterType.resolve()).thenReturn(psiClass); + when(psiClass.getQualifiedName()).thenReturn(packageName); + + mockVisitor.visitMethodCallExpression(mockMethodCallExpression); + + if (followingMethod != null && followingMethod.equals("subscribe")) { + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(mockMethodCallExpression), contains("Instead of `subscribe()`, call `block()` or `block()` with timeout, or use the synchronous version `updateCheckpoint()`")); + } else { + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(mockMethodCallExpression), contains("Calling updateCheckpointAsync() without block() will not do anything, use `block()` or `block` operator with a timeout, or consider using the synchronous version `updateCheckpoint().")); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedAPIUsageCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedAPIUsageCheckTest.java new file mode 100644 index 00000000000..5a77f1215ff --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedAPIUsageCheckTest.java @@ -0,0 +1,147 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.ProblemsHolder; +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.azure.sdk.buildtool.DetectDiscouragedAPIUsageCheck.DetectDiscouragedAPIUsageVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +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 tests the DetectDiscouragedAPIUsageCheck class. + * It tests the buildVisitor method and the visitElement method. + * It tests the check for the usage of discouraged APIs in the code. + * The test checks for the use of the following discouraged APIs: + * 1. connectionString + * 2. getCompletions + *

+ * This is an example of a problem that would be raised: + * 1. ServiceBusSenderClient senderClient2 = new ServiceBusClientBuilder().connectionString(connectionString2).sender().queueName("myQueueName2").buildClient(); + * * 2. BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder().connectionString(connectionString2).buildClient(); + * 3. Completions completions = client.getCompletions(deploymentOrModelId, new CompletionsOptions(prompt)); + */ +public class DetectDiscouragedAPIUsageCheckTest { + + @Mock + private ProblemsHolder mockHolder; + + @Mock + private PsiElementVisitor mockVisitor; + + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + } + + /** + * This test checks the buildVisitor method. + * It checks if the visitor is created and if it is an instance of JavaElementVisitor. + * It also verifies that a warning is raised when a discouraged API is detected. + */ + @ParameterizedTest + @CsvSource({"connectionString, com.azure, Connection String detected. Use DefaultAzureCredential for Azure service client authentication instead if the service client supports Token Credential (Entra ID Authentication)", "getCompletions, com.azure.ai.openai, getCompletions API detected. Use the getChatCompletions API instead."}) + public void testDetectDiscouragedAPIUsageCheck(String methodToCheck, String packageName, String suggestionMessage) { + + int numOfInvocations = 1; + + verifyRegisterProblem(mockVisitor, methodToCheck, numOfInvocations, packageName, suggestionMessage); + } + + /** + * Problem isn't registered because the method to check is different + * from the methods that should be flagged + */ + @Test + public void differentMethodCheck() { + String methodToCheck = "differentMethod"; + int numOfInvocations = 0; + String packageName = "com.azure"; + String suggestionMessage = ""; + + verifyRegisterProblem(mockVisitor, methodToCheck, numOfInvocations, packageName, suggestionMessage); + } + + /** + * Problem isn't registered because the package name is different + * from the package that should be flagged + */ + @Test + public void differentClassCheck() { + String methodToCheck = "connectionString"; + int numOfInvocations = 0; + String packageName = "com.microsoft.azure"; + String suggestionMessage = ""; + + verifyRegisterProblem(mockVisitor, methodToCheck, numOfInvocations, packageName, suggestionMessage); + } + + /** + * Problem isn't registered because the package name is null + */ + @Test + public void nullClassCheck() { + String methodToCheck = "getCompletions"; + int numOfInvocations = 0; + String packageName = null; + String suggestionMessage = ""; + + verifyRegisterProblem(mockVisitor, methodToCheck, numOfInvocations, packageName, suggestionMessage); + } + + /** + * This method creates a visitor for the DetectDiscouragedAPIUsageCheck class. + * + * @return PsiElementVisitor visitor + */ + private PsiElementVisitor createVisitor() { + boolean isOnTheFly = true; + DetectDiscouragedAPIUsageVisitor visitor = new DetectDiscouragedAPIUsageVisitor(mockHolder, isOnTheFly); + return visitor; + } + + /** + * Verifies that a warning is raised when a discouraged API is detected. + * + * @param visitor PsiElementVisitor visitor to inspect elements in the code + * @param methodToCheck String method to check for in the code + * @param numOfInvocations int number of times registerProblem should be called + * @param packageName String package name of the class + */ + private void verifyRegisterProblem(PsiElementVisitor visitor, String methodToCheck, int numOfInvocations, String packageName, String suggestionMessage) { + + PsiMethodCallExpression methodCallExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiMethod resolvedMethod = mock(PsiMethod.class); + PsiClass containingClass = mock(PsiClass.class); + PsiElement 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); + + (visitor).visitElement(methodCallExpression); + + // Verify problem is registered + verify(mockHolder, times(numOfInvocations)).registerProblem(Mockito.eq(problemElement), Mockito.contains(suggestionMessage)); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedClientCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedClientCheckTest.java new file mode 100644 index 00000000000..287cf1115cb --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DetectDiscouragedClientCheckTest.java @@ -0,0 +1,157 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +// Import necessary libraries + +import com.intellij.psi.JavaElementVisitor; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiTypeElement; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiType; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.DetectDiscouragedClientCheck.DetectDiscouragedClientVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +/** + * 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 { + + // Create a mock ProblemsHolder to be used in the test + @Mock + private ProblemsHolder mockHolder; + + // Create a mock PsiElementVisitor for visiting the PsiTypeElement + @Mock + private PsiElementVisitor mockVisitor; + + // Create a mock PsiTypeElement. + @Mock + private PsiTypeElement mockTypeElement; + + + @BeforeEach + public void setUp() { + // Set up the test + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockTypeElement = mock(PsiTypeElement.class); + } + + /** + * Test that a problem is registered when the client name is ServiceBusReceiverAsyncClient. + *

+ * This test is important because it verifies that the code registers a problem + * when the client name is ServiceBusReceiverAsyncClient. + */ + @Test + public void testProblemRegisteredWhenUsingServiceBusReceiverAsyncClient() { + + String clientToCheck = "ServiceBusReceiverAsyncClient"; + String suggestionMessage = "Use of ServiceBusReceiverAsyncClient detected. Use ServiceBusProcessorClient instead."; + + // Visit Type Element + int numberOfInvocations = 1; // Number of times registerProblem should be called + visitTypeElement(mockTypeElement, numberOfInvocations, clientToCheck, suggestionMessage); + } + + /** + * Test that a problem is registered when the client name is EventHubConsumerAsyncClient. + *

+ * This test is important because it verifies that the code registers a problem + * when the client name is EventHubConsumerAsyncClient. + */ + @Test + public void testProblemRegisteredWhenUsingEventHubConsumerAsyncClientCheck() { + + String clientToCheck = "EventHubConsumerAsyncClient"; + String suggestionMessage = "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."; + + // Visit Type Element + int numberOfInvocations = 1; // Number of times registerProblem should be called + visitTypeElement(mockTypeElement, numberOfInvocations, clientToCheck, suggestionMessage); + } + + /** + * Test that a problem is not registered when the client name is not ServiceBusReceiverAsyncClient. + *

+ * This test is important because it verifies that the code does not + * register a problem when the client name is not ServiceBusReceiverAsyncClient. + */ + + @Test + public void testProblemNotRegisteredWhenCheckingForDiifferentClient() { + + String clientToCheck = "ServiceBusProcessorClient"; + + // Visit Type Element + int numberOfInvocations = 0; // Number of times registerProblem should be called + + String suggestionMessage = ""; + visitTypeElement(mockTypeElement, numberOfInvocations, clientToCheck, suggestionMessage); + + } + + /** + * Test that a problem is not registered when the PsiTypeElement is null and the client name is empty. + */ + @Test + public void testProblemNotRegisteredWhenClientIsEmpty() { + // Visit Type Element + int numberOfInvocations = 0; // Number of times registerProblem should be called + String clientToCheck = ""; + String suggestionMessage = ""; + visitTypeElement(null, numberOfInvocations, clientToCheck, suggestionMessage); + } + + /** + * Create a visitor by calling the buildVisitor method of ServiceBusReceiverAsyncClientCheck + * and return the visitor. + * + * @return PsiElementVisitor + */ + private PsiElementVisitor createVisitor() { + + DetectDiscouragedClientVisitor mockVisitor = new DetectDiscouragedClientVisitor(mockHolder); + return mockVisitor; + } + + /** + * Visit a Type Element and verify that a problem was registered + * when the ServiceBusReceiverAsyncClient is used. + * + * @param typeElement The PsiTypeElement to visit + * @param numberOfInvocations The number of times registerProblem should be called + */ + private void visitTypeElement(PsiTypeElement typeElement, int numberOfInvocations, String clientToCheck, String suggestionMessage) { + + PsiType mockType = mock(PsiType.class); + + when(mockTypeElement.getType()).thenReturn(mockType); + when(mockType.getPresentableText()).thenReturn(clientToCheck); + + ((JavaElementVisitor) mockVisitor).visitTypeElement(mockTypeElement); + verify(mockHolder, times(numberOfInvocations)).registerProblem((Mockito.eq(mockTypeElement)), Mockito.contains(suggestionMessage)); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DisableAutoCompleteCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DisableAutoCompleteCheckTest.java new file mode 100644 index 00000000000..5b1e2e44e28 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DisableAutoCompleteCheckTest.java @@ -0,0 +1,204 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.azure.sdk.buildtool.DisableAutoCompleteCheck.DisableAutoCompleteVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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 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 { + + // Create a mock ProblemsHolder to be used in the test + @Mock + private ProblemsHolder mockHolder; + + // Create a mock JavaElementVisitor for visiting the PsiDeclarationStatement + @Mock + private JavaElementVisitor mockVisitor; + + // Create a mock PsiDeclarationStatement. + @Mock + private PsiDeclarationStatement mockDeclarationStatement; + + + @BeforeEach + public void setUp() { + // Set up the test + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockDeclarationStatement = mock(PsiDeclarationStatement.class); + } + + /** + * Test the visitDeclarationStatement method of the DisableAutoCompleteVisitor class. + * This test checks if the auto-complete feature is disabled for the ServiceBusReceiverClient. + * If the auto-complete feature is not disabled, a problem is registered with the ProblemsHolder. + */ + @Test + public void testACNotDisabledForReceiver() { + + String packageName = "com.azure"; + String clientName = "ServiceBusReceiverClient"; + int numOfInvocations = 1; + String methodFound = "notDisableAutoComplete"; + + + // Assert + verifyProblemRegistered(packageName, clientName, numOfInvocations, methodFound); + } + + /** + * Test the visitDeclarationStatement method of the DisableAutoCompleteVisitor class. + * This test checks if the auto-complete feature is disabled for the ServiceBusProcessorClient. + * If the auto-complete feature is not disabled, a problem is registered with the ProblemsHolder. + */ + @Test + public void testACNotDisabledForProcessor() { + + String packageName = "com.azure"; + String clientName = "ServiceBusProcessorClient"; + int numOfInvocations = 1; + String methodFound = "notDisableAutoComplete"; + + // Assert + verifyProblemRegistered(packageName, clientName, numOfInvocations, methodFound); + } + + /** + * Test the visitDeclarationStatement method of the DisableAutoCompleteVisitor class. + * This test checks if the auto-complete feature is disabled for the ServiceBusRuleManagerClient. + * If the auto-complete feature is not disabled, a problem will NOT be registered with the ProblemsHolder + * because the ServiceBusRuleManagerClient is not the correct client to check. + */ + @Test + public void testNoProblemRegisteredWithWrongClient() { + + String packageName = "com.azure"; + String clientName = "ServiceBusRuleManagerClient"; + int numOfInvocations = 0; + String methodFound = "notDisableAutoComplete"; + + // Assert + verifyProblemRegistered(packageName, clientName, numOfInvocations, methodFound); + } + + /** + * Test the visitDeclarationStatement method of the DisableAutoCompleteVisitor class. + * This test checks if the auto-complete feature is disabled for the ServiceBusReceiverClient. + * A problem will NOT be registered with the ProblemsHolder + * because the auto-complete feature is disabled. + */ + @Test + public void testACDisabledForReceiver() { + + String packageName = "com.azure"; + String clientName = "ServiceBusReceiverClient"; + int numOfInvocations = 0; + String methodFound = "disableAutoComplete"; + + + // Assert + verifyProblemRegistered(packageName, clientName, numOfInvocations, methodFound); + } + + /** + * Test the visitDeclarationStatement method of the DisableAutoCompleteVisitor class. + * This test checks if the auto-complete feature is disabled for the ServiceBusReceiverClient. + * A problem will NOT be registered with the ProblemsHolder + * because the package name is different -- not an Azure SDK client. + */ + @Test + public void testNoproblemRegisteredDifferentPackage() { + + String packageName = "com.microsoft.azure"; + String clientName = "ServiceBusReceiverClient"; + int numOfInvocations = 0; + String methodFound = "disableAutoComplete"; + + + // Assert + verifyProblemRegistered(packageName, clientName, numOfInvocations, methodFound); + } + + + /** + * Create a visitor by calling the buildVisitor method of the DisableAutoCompleteCheck class. + * + * @return The visitor created + */ + private JavaElementVisitor createVisitor() { + DisableAutoCompleteVisitor mockVisitor = new DisableAutoCompleteVisitor(mockHolder, true); + return mockVisitor; + } + + + /** + * Verify that a problem is registered with the ProblemsHolder. + * + * @param packageName The package name of the client + * @param clientName The name of the client + * @param numOfInvocations The number of times the registerProblem method should be called + * @param methodFound The method found in the initializer + */ + private void verifyProblemRegistered(String packageName, String clientName, int numOfInvocations, String methodFound) { + + PsiVariable declaredElement = mock(PsiVariable.class); + PsiElement[] declaredElements = new PsiElement[]{declaredElement}; + + // processVariableDeclaration + PsiType clientType = mock(PsiType.class); + PsiMethodCallExpression initializer = mock(PsiMethodCallExpression.class); + + // isAutoCompleteDisabled method + PsiReferenceExpression expression = mock(PsiReferenceExpression.class); + PsiMethodCallExpression qualifier = mock(PsiMethodCallExpression.class); + PsiMethodCallExpression finalExpression = mock(PsiMethodCallExpression.class); + + when(mockDeclarationStatement.getDeclaredElements()).thenReturn(declaredElements); + + // processVariableDeclaration method + when(declaredElement.getType()).thenReturn(clientType); + when(declaredElement.getInitializer()).thenReturn(initializer); + when(clientType.getCanonicalText()).thenReturn(packageName); + when(clientType.getPresentableText()).thenReturn(clientName); + + // isAutoCompleteDisabled method + when(initializer.getMethodExpression()).thenReturn(expression); + when(expression.getQualifierExpression()).thenReturn(qualifier); + + // First level qualifier method call + when(qualifier.getMethodExpression()).thenReturn(expression); + when(expression.getQualifierExpression()).thenReturn(finalExpression); + when(expression.getReferenceName()).thenReturn(methodFound); + + // Final expression should return null to break the loop if the method is not disableAutoComplete + when(finalExpression.getMethodExpression()).thenReturn(expression); + + if (methodFound != "disableAutoComplete") { + when(expression.getQualifierExpression()).thenReturn(null); + } + + mockVisitor.visitDeclarationStatement(mockDeclarationStatement); + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(initializer), contains("Auto-complete enabled by default. Use the disableAutoComplete() API call to prevent automatic message completion.")); + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DynamicClientCreationCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DynamicClientCreationCheckTest.java new file mode 100644 index 00000000000..9a84f00e02b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/DynamicClientCreationCheckTest.java @@ -0,0 +1,214 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.azure.sdk.buildtool.DynamicClientCreationCheck.DynamicClientCreationVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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; + +public class DynamicClientCreationCheckTest { + + // Declare as instance variables + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiForStatement mockElement; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockElement = mock(PsiForStatement.class); + } + + /** + * This is the main test method that tests the KustoQueriesWithTimeIntervalInQueryStringCheck class. + * + * This tests blocks of code that are a PsiExpressionStatement in the checkClientCreation method. + */ + @Test + void testDynamicClientCreationWithExpressionStatement() { + + int numOfInvocations = 1; + String methodName = "buildClient"; + String packageName = "com.azure."; + + // verify register problem with assignment expression + verifyRegisterProblemWithAssignmentExpression(methodName, packageName, numOfInvocations); + + // verify register problem with declaration statement + verifyRegisterProblemWithDeclarationStatement(methodName, packageName, numOfInvocations); + } + + /** + * This is a test case that verifies the behavior of the DynamicClientCreationCheck when + * it encounters a code block that does not match the criteria for dynamic client creation. + * This involves a method call on an object that is not part of the com.azure package + */ + @Test + // unhappy path + void testDynamicClientCreationWithNonAzurePackage() { + + int numOfInvocations = 0; + String methodName = "buildClient"; + String packageName = "com.Notazure."; + + // verify register problem with assignment expression + verifyRegisterProblemWithAssignmentExpression(methodName, packageName, numOfInvocations); + + // verify register problem with declaration statement + verifyRegisterProblemWithDeclarationStatement(methodName, packageName, numOfInvocations); + } + + /** + * This is a test case that verifies the behavior of the DynamicClientCreationCheck when + * it encounters a code block that does not match the criteria for dynamic client creation. + * This involves a method call that is not part of the METHODS_TO_CHECK list. + */ + @Test + void testDynamicClientCreationWithNonBuildMethod() { + + int numOfInvocations = 0; + String methodName = "NotbuildClient"; + String packageName = "com.azure."; + + // verify register problem with assignment expression + verifyRegisterProblemWithAssignmentExpression(methodName, packageName, numOfInvocations); + + // verify register problem with declaration statement + verifyRegisterProblemWithDeclarationStatement(methodName, packageName, numOfInvocations); + } + + /** + * This helper method creates a new instance of the DynamicClientCreationVisitor class. + */ + private JavaElementVisitor createVisitor() { + DynamicClientCreationVisitor mockVisitor = new DynamicClientCreationVisitor(mockHolder); + return mockVisitor; + } + + /** + * This method verifies that a problem is registered when a client creation method is found + * building a client from the com.azure package in an assignment expression. + * + * @param methodName this is the method name that is being checked for + * @param packageName this is the package name that is being checked for + * @param numOfInvocations this is the number of times the registerProblem method should be called + */ + private void verifyRegisterProblemWithAssignmentExpression(String methodName, String packageName, int numOfInvocations) { + + // main method + 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}; + + // checkClientCreation method + PsiAssignmentExpression expression = mock(PsiAssignmentExpression.class); + PsiMethodCallExpression rhs = mock(PsiMethodCallExpression.class); + + // isClientCreationMethod + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiExpression qualifierExpression = mock(PsiExpression.class); + PsiType type = mock(PsiType.class); + + // main method + when(mockElement.getBody()).thenReturn(body); + when(body.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(blockStatements); + + // checkClientCreation method + when(blockChild.getExpression()).thenReturn(expression); + when(expression.getRExpression()).thenReturn(rhs); + + // isClientCreationMethod + when(rhs.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.getReferenceName()).thenReturn(methodName); + when(methodExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(qualifierExpression.getType()).thenReturn(type); + when(qualifierExpression.getType().getCanonicalText()).thenReturn(packageName); + + mockVisitor.visitForStatement(mockElement); + + // Verify problem is registered + verify(mockHolder, + times(numOfInvocations)).registerProblem(Mockito.eq(rhs), + Mockito.contains("Dynamic client creation detected. Create a single client instance and reuse it instead.")); + } + + /** + * This method verifies that a problem is registered when a client creation method is found + * building a client from the com.azure package in a declaration statement. + * + * @param methodName this is the method name that is being checked for + * @param packageName this is the package name that is being checked for + * @param numOfInvocations this is the number of times the registerProblem method should be called + */ + private void verifyRegisterProblemWithDeclarationStatement(String methodName, String packageName, int numOfInvocations) { + + // main method + 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}; + + // checkClientCreation method + PsiLocalVariable declaredElement = mock(PsiLocalVariable.class); + PsiElement[] declaredElements = new PsiElement[]{declaredElement}; + PsiMethodCallExpression initializer = mock(PsiMethodCallExpression.class); + + // isClientCreationMethod + PsiReferenceExpression methodExpression = mock(PsiReferenceExpression.class); + PsiExpression qualifierExpression = mock(PsiExpression.class); + PsiType type = mock(PsiType.class); + + // main method + when(mockElement.getBody()).thenReturn(body); + when(body.getCodeBlock()).thenReturn(codeBlock); + when(codeBlock.getStatements()).thenReturn(blockStatements); + + // checkClientCreation method + when(blockChild.getDeclaredElements()).thenReturn(declaredElements); + when(declaredElement.getInitializer()).thenReturn(initializer); + + // isClientCreationMethod + when(initializer.getMethodExpression()).thenReturn(methodExpression); + when(methodExpression.getReferenceName()).thenReturn(methodName); + when(methodExpression.getQualifierExpression()).thenReturn(qualifierExpression); + when(qualifierExpression.getType()).thenReturn(type); + when(qualifierExpression.getType().getCanonicalText()).thenReturn(packageName); + + mockVisitor.visitForStatement(mockElement); + + // Verify problem is registered + verify(mockHolder, + times(numOfInvocations)).registerProblem(Mockito.eq(initializer), + Mockito.contains("Dynamic client creation detected. Create a single client instance and reuse it instead.")); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/EndpointOnNonAzureOpenAIAuthCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/EndpointOnNonAzureOpenAIAuthCheckTest.java new file mode 100644 index 00000000000..abfa8021ddb --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/EndpointOnNonAzureOpenAIAuthCheckTest.java @@ -0,0 +1,169 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaElementVisitor; +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.EndpointOnNonAzureOpenAIAuthCheck.EndpointOnNonAzureOpenAIAuthVisitor; + +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 { + + // Declare as instance variables + @Mock + private ProblemsHolder mockHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @Mock + private PsiMethodCallExpression mockMethodCall; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockMethodCall = mock(PsiMethodCallExpression.class); + } + + /** + * This test 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. + */ + @Test + public void testEndpointOnNonAzureOpenAIAuthCheck() { + + int numOfInvocation = 1; + String endpoint = "endpoint"; + String credential = "credential"; + String keyCredentialPackageName = "KeyCredential"; + String azurePackageName = "com.azure.ai.openai"; + + verifyRegisterProblem(numOfInvocation, endpoint, credential, keyCredentialPackageName, azurePackageName); + } + + /** + * This test checks if the endpoint method is not used with KeyCredential for non-Azure OpenAI clients. + * If the endpoint method is not used with KeyCredential for non-Azure OpenAI clients, no warning is registered. + */ + @Test + public void testNoEndpoint() { + + int numOfInvocation = 0; + String endpoint = "notEndpoint"; + String credential = "credential"; + String keyCredentialPackageName = "KeyCredential"; + String azurePackageName = "com.azure.ai.openai"; + + verifyRegisterProblem(numOfInvocation, endpoint, credential, keyCredentialPackageName, azurePackageName); + } + + /** + * This test checks if the endpoint method is used but credential is not used. + * If the endpoint method is used but credential is not used, no warning is registered. + */ + @Test + public void testNoCredential() { + + int numOfInvocation = 0; + String endpoint = "endpoint"; + String credential = "notCredential"; + String keyCredentialPackageName = "KeyCredential"; + String azurePackageName = "com.azure.ai.openai"; + + verifyRegisterProblem(numOfInvocation, endpoint, credential, keyCredentialPackageName, azurePackageName); + } + + /** + * This test checks if the endpoint method is used with KeyCredential but for Azure OpenAI clients. + * If the endpoint method is used with KeyCredential but for Azure OpenAI clients, no warning is registered. + */ + @Test + public void testWithAzureKeyCredential() { + + int numOfInvocation = 0; + String endpoint = "endpoint"; + String credential = "notCredential"; + String keyCredentialPackageName = "com.azure.core.credential.AzureKeyCredential"; + String azurePackageName = "com.azure.ai.openai"; + + verifyRegisterProblem(numOfInvocation, endpoint, credential, keyCredentialPackageName, azurePackageName); + } + + /** + * creates a JavaElementVisitor object for visiting method call expressions + */ + private JavaElementVisitor createVisitor() { + return new EndpointOnNonAzureOpenAIAuthVisitor(mockHolder); + } + + /** + * This method verifies if the registerProblem method is called with the correct parameters + */ + private void verifyRegisterProblem(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); + + // isUsingKeyCredential + when(methodExpression.getQualifierExpression()).thenReturn(qualifierOne); + when(qualifierOne.getMethodExpression()).thenReturn(methodExpressionOne); + when(methodExpressionOne.getReferenceName()).thenReturn(credential); + when(methodExpressionOne.getQualifierExpression()).thenReturn(null); + + when(qualifierOne.getArgumentList()).thenReturn(argumentList); + when(argumentList.getExpressions()).thenReturn(arguments); + + // isKeyCredential + + when(newExpression.getClassReference()).thenReturn(classReference); + when(classReference.getReferenceName()).thenReturn(keyCredentialPackageName); + + // isNonAzureOpenAIClient + when(qualifierOne.getParent()).thenReturn(parent); + when(parent.getType()).thenReturn(qualifierTYpe); + when(qualifierTYpe.getCanonicalText()).thenReturn(azurePackageName); + + mockVisitor.visitMethodCallExpression(mockMethodCall); + + verify(mockHolder, times(numOfInvocation)).registerProblem(mockMethodCall, "Endpoint should not be used with KeyCredential for non-Azure OpenAI clients"); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/GetSyncPollerOnPollerFluxCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/GetSyncPollerOnPollerFluxCheckTest.java new file mode 100644 index 00000000000..fa0dbdd5c1d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/GetSyncPollerOnPollerFluxCheckTest.java @@ -0,0 +1,157 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.GetSyncPollerOnPollerFluxCheck.GetSyncPollerOnPollerFluxVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +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 class for the GetSyncPollerOnPollerFluxCheck inspection tool. + * The test class will test the visitor's ability to detect the use of getSyncPoller() on a PollerFlux + * and register a problem with the suggestion message. + *

+ * 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 GetSyncPollerOnPollerFluxCheckTest { + + // Declare as instance variables + @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); + } + + /** + * Test to verify if the visitor is able to detect the use of getSyncPoller() on a PollerFlux + * and register a problem with the suggestion message. + */ + @Test + public void testGetSyncPollerOnPollerFluxCheck() { + assertVisitor(); + + String methodName = "getSyncPoller"; + String className = "com.azure.core.util.polling.PollerFlux"; + int numberOfInvocations = 1; + verifyRegisterProblem(methodName, className, numberOfInvocations); + } + + /** + * Test to verify if the visitor is not flagged when a different method name is used. + * The visitor should not register a problem in this case. + */ + @Test + public void testGetSyncPollerOnPollerFluxCheckWithDifferentMethodName() { + + String methodName = "getAnotherMethod"; + String className = "com.azure.core.util.polling.PollerFlux"; + int numberOfInvocations = 0; + verifyRegisterProblem(methodName, className, numberOfInvocations); + } + + /** + * Test to verify if the visitor is not flagged when a different package name is used. + * The visitor should not register a problem in this case. + */ + @Test + public void testGetSyncPollerOnPollerFluxCheckWithDifferentClassName() { + + String methodName = "getSyncPoller"; + String className = "com.azure.core.util.polling.DifferentClassName"; + int numberOfInvocations = 0; + verifyRegisterProblem(methodName, className, numberOfInvocations); + } + + /** + * Test to verify if the visitor is not flagged when the package name is null. + * The visitor should not register a problem in this case. + */ + @Test + public void testGetSyncPollerOnPollerFluxCheckWithNullClassName() { + + String methodName = "getSyncPoller"; + String className = null; + int numberOfInvocations = 0; + verifyRegisterProblem(methodName, className, numberOfInvocations); + } + + /** + * A helper method to create the visitor for the test. + * + * @return JavaElementVisitor + */ + private JavaElementVisitor createVisitor() { + boolean isOnTheFly = true; + GetSyncPollerOnPollerFluxVisitor visitor = new GetSyncPollerOnPollerFluxVisitor(mockHolder, isOnTheFly); + return visitor; + } + + /** + * A helper method to assert the visitor is not null and is an instance of JavaElementVisitor. + */ + private void assertVisitor() { + assertNotNull(mockVisitor); + assertTrue(mockVisitor instanceof JavaElementVisitor); + } + + /** + * A helper method to verify if the visitor is able to detect the use of getSyncPoller() on a PollerFlux + * and register a problem with the suggestion message. + * + * @param methodName The method name to be used in the test + * @param className The package name to be used in the test + * @param numberOfInvocations The number of times registerProblem should be called + */ + private void verifyRegisterProblem(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); + + // Act + mockVisitor.visitMethodCallExpression(mockElement); + + verify(mockHolder, times(numberOfInvocations)).registerProblem(Mockito.eq(mockElement), Mockito.contains("Use of getSyncPoller() on a PollerFlux detected. Directly use SyncPoller to handle synchronous polling tasks")); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/HardcodedAPIKeysAndTokensCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/HardcodedAPIKeysAndTokensCheckTest.java new file mode 100644 index 00000000000..9bb8749b633 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/HardcodedAPIKeysAndTokensCheckTest.java @@ -0,0 +1,109 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiNewExpression; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElementVisitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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 { + + // Declare as instance variables + @Mock + private ProblemsHolder mockHolder; + + @Mock + private PsiElementVisitor mockVisitor; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + } + + + /** + * Test the HardcodedAPIKeysAndTokensCheck class for hardcoded API keys and tokens. + * When a client is authenticated with AzurekeyCredentials and AccessToken, a problem is registered. + */ + @ParameterizedTest + @ValueSource(strings = {"AzureKeyCredential", "AccessToken", "KeyCredential", "AzureNamedKeyCredential", "AzureSasCredential", "AzureNamedKey", "ClientSecretCredentialBuilder", "UsernamePasswordCredentialBuilder", "BasicAuthenticationCredential"}) + public void testHardcodedAPIKeysAndTokensCheck(String clientType) { + + int numOfInvocations = 1; // number of times registerProblem is called + verifyRegisterProblem(clientType, numOfInvocations); + } + + /** + * Test for non-auth client use. + * These are other Azure clients that are not AzureKeyCredential or AccessToken. + */ + @Test + public void testNonAuthAzureClientUse() { + + String authServiceToCheck = "SomeOtherClient"; + int numOfInvocations = 0; + verifyRegisterProblem(authServiceToCheck, numOfInvocations); + } + + /** + * Test for null class reference. + */ + @Test + public void testNullClassReference() { + + String authServiceToCheck = ""; // null class reference + int numOfInvocations = 0; + verifyRegisterProblem(authServiceToCheck, numOfInvocations); + } + + // Helper method to create visitor. + PsiElementVisitor createVisitor() { + boolean isOnTheFly = true; + APIKeysAndTokensVisitor visitor = new HardcodedAPIKeysAndTokensCheck.APIKeysAndTokensVisitor(mockHolder, isOnTheFly); + return visitor; + } + + // Helper method to verify registerProblem is called + private void verifyRegisterProblem(String authServiceToCheck, int numOfInvocations) { + + PsiNewExpression newExpression = mock(PsiNewExpression.class); + PsiJavaCodeReferenceElement javaCodeReferenceElement = mock(PsiJavaCodeReferenceElement.class); + + when(newExpression.getClassReference()).thenReturn(javaCodeReferenceElement); + when(javaCodeReferenceElement.getReferenceName()).thenReturn(authServiceToCheck); + + when(javaCodeReferenceElement.getQualifiedName()).thenReturn("com.azure"); + + mockVisitor.visitElement(newExpression); + + // Verify registerProblem is called + verify(mockHolder, times(numOfInvocations)).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.")); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java new file mode 100644 index 00000000000..4e2ccddedad --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/KustoQueriesWithTimeIntervalInQueryStringCheckTest.java @@ -0,0 +1,222 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiLiteralExpression; +import com.intellij.psi.impl.source.tree.java.PsiMethodCallExpressionImpl; +import com.intellij.psi.util.PsiTreeUtil; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.KustoQueriesWithTimeIntervalInQueryStringCheck.KustoQueriesVisitor; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiPolyadicExpression; +import com.intellij.psi.PsiReferenceExpression; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiVariable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * This class tests the KustoQueriesWithTimeIntervalInQueryStringCheck class. + *

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

+ * If these queries are used in a method call to an Azure client, they should be flagged. + *

+ * eg BlobClient blobAsyncClient = new BlobClientBuilder().buildClient(); + *

+ * String kqlQueryOne = "ExampleTable\n" + + * "| where TimeGenerated > ago(1h)" + + * "| summarize count() by bin(TimeGenerated, 1h);"; + *

+ * String result = blobAsyncClient.query(kqlQueryOne); + */ +class KustoQueriesWithTimeIntervalInQueryStringCheckTest { + + // Declare as instance variables + @Mock + private ProblemsHolder mockHolder; + private KustoQueriesVisitor mockVisitor; + private PsiElement mockElement; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockElement = mock(PsiElement.class); + } + + /** + * This is the main test method that tests the KustoQueriesWithTimeIntervalInQueryStringCheck class. + * It tests the KustoQueriesVisitor class by calling the visitElement method with a local variable and a polyadic expression. + * The local variable is used in a query string or the polyadic expression is used in a query string. + * The method then checks if a problem is registered when a local variable or a polyadic expression is used in a query string + * and if the method call is to an Azure client. + */ + @ParameterizedTest + @ValueSource(strings = {"datetime(startDate)", "filter.datetime(2022-01-01)", "time.now()", "time.startofday()", "time.startofmonth()", "range.between(datetime(2022-01-01), datetime(2022-02-01)"}) + public void testKustoQueriesWithTimeIntervalInQueryStringCheck(String queryString) { + + String packageName = "com.azure"; + + int numOfInvocations = 1; + + // verify register problem with local variable as the query string + verifyRegisterProblemWithLocalVariable(queryString, packageName, numOfInvocations); + + // verify register problem with polyadic expression as the query string + verifyRegisterProblemWithPolyadicExpression(queryString, packageName, numOfInvocations); + } + + @Test + public void testWithWrongPackageName() { + + String packageName = "com.microsoft.azure"; + String queryString = "datetime(startDate)"; + + int numOfInvocations = 0; + + // verify register problem with local variable as the query string + verifyRegisterProblemWithLocalVariable(queryString, packageName, numOfInvocations); + + // verify register problem with polyadic expression as the query string + verifyRegisterProblemWithPolyadicExpression(queryString, packageName, numOfInvocations); + } + + /** + * This test checks the checkExpression method with a null expression as input + */ + @Test + public void testCheckExpressionWithNullExpression() { + // Arrange + PsiExpression nullExpression = null; + + // Act + mockVisitor.checkExpression(nullExpression, mockElement); + } + + + /** + * Create a visitor by calling the buildVisitor method of KustoQueriesWithTimeIntervalInQueryStringCheck + * + * @return PsiElementVisitor + */ + private KustoQueriesVisitor createVisitor() { + KustoQueriesVisitor visitor = new KustoQueriesVisitor(mockHolder); + return visitor; + } + + + /** + * This method tests the registerProblem method with a local variable as the query string + */ + private void verifyRegisterProblemWithLocalVariable(String queryString, String packageName, int numOfInvocations) { + + PsiLocalVariable variable = mock(PsiLocalVariable.class); + PsiLiteralExpression initializer = mock(PsiLiteralExpression.class); + PsiLocalVariable parentElement = mock(PsiLocalVariable.class); + PsiClass containingClass = mock(PsiClass.class); + + PsiMethodCallExpressionImpl 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(variable.getInitializer()).thenReturn(initializer); + when(variable.getName()).thenReturn("stringQuery"); + + // stubs for checkExpression method + when(initializer.getText()).thenReturn(queryString); + when(variable.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); + + // Visit the variable to store its name if it's a query string + mockVisitor.visitElement(variable); + + // 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(numOfInvocations)).registerProblem(eq(methodCall), contains("KQL queries with time intervals in the query string detected.")); + } + + void verifyRegisterProblemWithPolyadicExpression(String queryString, String packageName, int numOfInvocations) { + + PsiPolyadicExpression polyadicExpression = mock(PsiPolyadicExpression.class); + PsiExpression initializer = mock(PsiExpression.class); + PsiLocalVariable parentElement = mock(PsiLocalVariable.class); + PsiClass containingClass = mock(PsiClass.class); + + // stubs for handlePolyadicExpression method + when(polyadicExpression.getText()).thenReturn(queryString); + + // stubs for checkExpression method + when(initializer.getText()).thenReturn(queryString); + when(polyadicExpression.getParent()).thenReturn(parentElement); + when(parentElement.getName()).thenReturn("stringQuery"); + + PsiMethodCallExpressionImpl 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); + + // Visit the variable to store its name if it's a query string + mockVisitor.visitElement(polyadicExpression); + + // 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(numOfInvocations)).registerProblem(eq(methodCall), contains("KQL queries with time intervals in the query string detected.")); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiveModeCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiveModeCheckTest.java new file mode 100644 index 00000000000..e2cefe1cc4e --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/ServiceBusReceiveModeCheckTest.java @@ -0,0 +1,243 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; + +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.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 { + + // Create a mock ProblemsHolder to be used in the test + @Mock + private ProblemsHolder mockHolder; + + // Create a mock JavaElementVisitor for visiting the PsiDeclarationStatement + @Mock + private JavaElementVisitor mockVisitor; + + // Create a mock PsiDeclarationStatement. + @Mock + private PsiDeclarationStatement mockDeclarationStatement; + + + @BeforeEach + public void setUp() { + // Set up the test + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + mockDeclarationStatement = mock(PsiDeclarationStatement.class); + } + + + /** + * This tests the visitor with receiveMode as PEEK_LOCK and prefetchCount as 100. + * The test verifies that a problem is registered with the ProblemsHolder. + */ + @ParameterizedTest + @ValueSource(strings = {"ServiceBusReceiverClient", "ServiceBusReceiverAsyncClient", "ServiceBusProcessorClient"}) + public void testWithPeekLockAndHighPrefetchCount(String clientName) { + + String methodFoundOne = "receiveMode"; + String methodFoundTwo = "prefetchCount"; + int numOfInvocations = 1; + String prefetchCountValue = "100"; + + verifyProblemRegistered(clientName, methodFoundOne, methodFoundTwo, numOfInvocations, prefetchCountValue); + } + + /** + * This tests the visitor with receiveMode as PEEK_LOCK and prefetchCount as 1. + * The test verifies that a problem is not registered with the ProblemsHolder. + */ + @Test + public void testWithPeekLockAndLowPrefetchCount() { + + String clientName = "ServiceBusReceiverClient"; + String methodFoundOne = "receiveMode"; + String methodFoundTwo = "prefetchCount"; + int numOfInvocations = 0; + String prefetchCountValue = "1"; + + verifyProblemRegistered(clientName, methodFoundOne, methodFoundTwo, numOfInvocations, prefetchCountValue); + } + + /** + * This tests the visitor without a receiveMode. + * The test verifies that a problem is not registered with the ProblemsHolder. + */ + @Test + public void testWithoutPeekLockAndHighPrefetchCount() { + + String clientName = "ServiceBusReceiverClient"; + String methodFoundOne = "notreceiveMode"; + String methodFoundTwo = "prefetchCount"; + int numOfInvocations = 0; + String prefetchCountValue = "100"; + + verifyProblemRegistered(clientName, methodFoundOne, methodFoundTwo, numOfInvocations, prefetchCountValue); + } + + /** + * This tests the visitor without a receiveMode and with a prefetchCount of 100. + * The test verifies that a problem is not registered with the ProblemsHolder. + */ + @Test + public void testWithPeekLockAndNoPrefetchCount() { + + String clientName = "ServiceBusReceiverClient"; + String methodFoundOne = "receiveMode"; + String methodFoundTwo = "noprefetchCount"; + int numOfInvocations = 0; + String prefetchCountValue = "100"; + + verifyProblemRegistered(clientName, methodFoundOne, methodFoundTwo, numOfInvocations, prefetchCountValue); + } + + /** + * This tests the visitor without a receiveMode and without a prefetchCount. + * The test verifies that a problem is not registered with the ProblemsHolder. + */ + @Test + public void testWithoutPeekLockAndNoPrefetchCount() { + + String clientName = "servicebus"; + String methodFoundOne = "notreceiveMode"; + String methodFoundTwo = "noprefetchCount"; + int numOfInvocations = 0; + String prefetchCountValue = "100"; + + verifyProblemRegistered(clientName, methodFoundOne, methodFoundTwo, numOfInvocations, prefetchCountValue); + } + + /** + * This tests the visitor with a different Azure service. + * The test verifies that a problem is not registered with the ProblemsHolder. + */ + @Test + public void testDifferentAzureService() { + + String clientName = "notservicebus"; + String methodFoundOne = "notreceiveMode"; + String methodFoundTwo = "noprefetchCount"; + int numOfInvocations = 0; + String prefetchCountValue = "100"; + + verifyProblemRegistered(clientName, methodFoundOne, methodFoundTwo, numOfInvocations, prefetchCountValue); + } + + + /** + * Create a visitor by calling the buildVisitor method of the ServiceBusReceiveModeCheck class. + * + * @return The visitor created + */ + private JavaElementVisitor createVisitor() { + ServiceBusReceiveModeVisitor mockVisitor = new ServiceBusReceiveModeVisitor(mockHolder, true); + return mockVisitor; + } + + + /** + * This method verifies that a problem is registered with the ProblemsHolder. + * + * @param clientName The name of the client eg. ServiceBusReceiverClient + * @param methodFoundOne The first method found, receiveMode + * @param methodFoundTwo The second method found, prefetchCount + * @param numOfInvocations The number of times the registerProblem method is called + * @param prefetchCountValue The value of the prefetchCount + */ + private void verifyProblemRegistered(String clientName, String methodFoundOne, String methodFoundTwo, int numOfInvocations, String prefetchCountValue) { + + String azurePackageName = "com.azure"; + + PsiVariable declaredElement = mock(PsiVariable.class); + PsiElement[] declaredElements = new PsiElement[]{declaredElement}; + + // processVariableDeclaration method + PsiType clientType = mock(PsiType.class); + PsiMethodCallExpression initializer = mock(PsiMethodCallExpression.class); + + // determineReceiveMode method + // First level qualifier method call + PsiReferenceExpression expressionOne = mock(PsiReferenceExpression.class); + PsiMethodCallExpression qualifierOne = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpressionOne = mock(PsiReferenceExpression.class); + + // receiveModePeekLockCheck + PsiExpression receiveModePeekLockParameter = mock(PsiExpression.class); + PsiReferenceExpression prefetchCountParameter = mock(PsiReferenceExpression.class); + PsiExpressionList methodArgumentList = mock(PsiExpressionList.class); + PsiExpression[] methodArguments = new PsiExpression[]{prefetchCountParameter, receiveModePeekLockParameter}; + + // Second level qualifier method call + PsiMethodCallExpression qualifierTwo = mock(PsiMethodCallExpression.class); + PsiReferenceExpression methodExpressionTwo = mock(PsiReferenceExpression.class); + + PsiElement prefetchCountMethod = mock(PsiElement.class); + + + when(mockDeclarationStatement.getDeclaredElements()).thenReturn(declaredElements); + + // processVariableDeclaration method + when(declaredElement.getType()).thenReturn(clientType); + when(declaredElement.getInitializer()).thenReturn(initializer); + when(clientType.getCanonicalText()).thenReturn(azurePackageName); + when(clientType.getPresentableText()).thenReturn(clientName); + + // determineReceiveMode method + // First level qualifier method call + when(initializer.getMethodExpression()).thenReturn(expressionOne); + when(expressionOne.getQualifierExpression()).thenReturn(qualifierOne); + when(qualifierOne.getMethodExpression()).thenReturn(methodExpressionOne); + when(methodExpressionOne.getReferenceName()).thenReturn(methodFoundOne); + + // receiveModePeekLockCheck + when(qualifierOne.getArgumentList()).thenReturn(methodArgumentList); + when(methodArgumentList.getExpressions()).thenReturn(methodArguments); + when(receiveModePeekLockParameter.getText()).thenReturn("PEEK_LOCK"); + + // Second level qualifier method call + when(methodExpressionOne.getQualifierExpression()).thenReturn(qualifierTwo); + when(qualifierTwo.getMethodExpression()).thenReturn(methodExpressionTwo); + when(methodExpressionTwo.getReferenceName()).thenReturn(methodFoundTwo); + + // getPrefetchCount method + when(qualifierTwo.getArgumentList()).thenReturn(methodArgumentList); + when(methodArgumentList.getExpressions()).thenReturn(methodArguments); + when(prefetchCountParameter.getText()).thenReturn(prefetchCountValue); + + when(methodExpressionTwo.getReferenceNameElement()).thenReturn(prefetchCountMethod); + + if (methodFoundOne != "receiveMode" || methodFoundTwo != "prefetchCount") { + when(methodExpressionTwo.getQualifierExpression()).thenReturn(null); + } + + mockVisitor.visitDeclarationStatement(mockDeclarationStatement); + verify(mockHolder, times(numOfInvocations)).registerProblem(eq(prefetchCountMethod), contains("A high prefetch value in PEEK_LOCK detected. We recommend a prefetch value of 0 or 1 for efficient message retrieval.")); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/SingleOperationInLoopCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/SingleOperationInLoopCheckTest.java new file mode 100644 index 00000000000..52cd6df1fc7 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/SingleOperationInLoopCheckTest.java @@ -0,0 +1,318 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.azure.sdk.buildtool.SingleOperationInLoopCheck.SingleOperationInLoopVisitor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElement; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +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 { + + // Declare as instance variables + @Mock + private ProblemsHolder mockHolder; + private JavaElementVisitor mockVisitor; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockVisitor = createVisitor(); + } + + /** + * This test is used to verify a problem is registered when a + * single PsiExpressionStatement operation is found in a for loop. + */ + @ParameterizedTest + @ValueSource(strings = {"detectLanguage", "recognizeEntities", "recognizePiiEntities", "recognizeLinkedEntities", "extractKeyPhrases", "analyzeSentiment"}) + public void testPsiExpressionStatementInForStatement(String methodName) { + + PsiForStatement statement = mock(PsiForStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + verifyRegisterProblemWithSinglePsiExpressionStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a + * single PsiExpressionStatement operation is found in a for each loop. + */ + @Test + public void testPsiExpressionStatementInForEachStatement() { + + PsiForeachStatement statement = mock(PsiForeachStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiExpressionStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a + * single PsiExpressionStatement operation is found in a while loop. + */ + @Test + public void testPsiExpressionStatementInWhileStatement() { + + PsiWhileStatement statement = mock(PsiWhileStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiExpressionStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a + * single PsiExpressionStatement operation is found in a do while loop. + */ + @Test + public void testPsiExpressionStatementInDoWhileStatement() { + + PsiDoWhileStatement statement = mock(PsiDoWhileStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiExpressionStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a single + * PsiExpressionStatement operation is found in a for each loop. + */ + @Test + public void testPsiDeclarationStatementInForStatement() { + + PsiForStatement statement = mock(PsiForStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiDeclarationStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a single + * PsiDeclarationStatement operation is found in a do while loop. + */ + @Test + public void testPsiDeclarationStatementInForEachStatement() { + + PsiForeachStatement statement = mock(PsiForeachStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiDeclarationStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a single + * PsiDeclarationStatement operation is found in a while loop. + */ + @Test + public void testPsiDeclarationStatementInWhileStatement() { + + PsiWhileStatement statement = mock(PsiWhileStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiDeclarationStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is registered when a single + * PsiDeclarationStatement operation is found in a do while loop. + */ + @Test + public void testPsiDeclarationStatementInDoWhileStatement() { + + PsiDoWhileStatement statement = mock(PsiDoWhileStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 1; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiDeclarationStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is NOT registered when a different package name + * is used in the PsiExpressionStatement operation in a for loop. + */ + @Test + public void testWithDifferentPackageName() { + + PsiForStatement statement = mock(PsiForStatement.class); + String packageName = "com.microsoft.azure.storage.blob"; + int numberOfInvocations = 0; + String methodName = "detectLanguage"; + verifyRegisterProblemWithSinglePsiDeclarationStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This test is used to verify a problem is NOT registered when a different method name + * is used in the PsiExpressionStatement operation in a for loop. + */ + @Test + public void testWithDifferentMethodName() { + + PsiForStatement statement = mock(PsiForStatement.class); + String packageName = "com.azure.ai.textanalytics"; + int numberOfInvocations = 0; + String methodName = "differentMethodName"; + verifyRegisterProblemWithSinglePsiDeclarationStatement(statement, packageName, numberOfInvocations, methodName); + } + + /** + * This helper method is used to create a new SingleOperationInLoopVisitor object. + */ + private JavaElementVisitor createVisitor() { + boolean isOnTheFly = true; + SingleOperationInLoopVisitor visitor = new SingleOperationInLoopVisitor(mockHolder, isOnTheFly); + return visitor; + } + + /** + * This helper method is used to verify a problem is registered when a + * PsiExpressionStatement operation is found in a loop. + */ + private void verifyRegisterProblemWithSinglePsiExpressionStatement(PsiStatement loopStatement, String packageName, int numberOfInvocations, String methodName) { + + // Arrange + PsiBlockStatement loopBody = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + PsiMethodCallExpression 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); + + + // Visitor invocation based on the type of loopStatement + if (loopStatement instanceof PsiForStatement) { + + // .getBody() is specific to objects of PsiForStatement, PsiForeachStatement, PsiWhileStatement, and PsiDoWhileStatement + 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); + } + + // Verify problem is registered + verify(mockHolder, times(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: " + methodName + "Batch")); + } + + /** + * This helper method is used to verify a problem is registered when a + * PsiDeclarationStatement operation is found in a loop. + */ + private void verifyRegisterProblemWithSinglePsiDeclarationStatement(PsiStatement loopStatement, String packageName, int numberOfInvocations, String methodName) { + + // Arrange + PsiBlockStatement loopBody = mock(PsiBlockStatement.class); + PsiCodeBlock codeBlock = mock(PsiCodeBlock.class); + PsiVariable element = mock(PsiVariable.class); + PsiElement[] elements = new PsiElement[]{element}; + PsiMethodCallExpression 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); + + // Visitor invocation based on the type of loopStatement + 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); + } + + // Verify problem is registered + verify(mockHolder, times(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: " + methodName + "Batch")); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StorageUploadWithoutLengthCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StorageUploadWithoutLengthCheckTest.java new file mode 100644 index 00000000000..9d92008e7b0 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/StorageUploadWithoutLengthCheckTest.java @@ -0,0 +1,142 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.JavaRecursiveElementWalkingVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiExpressionList; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiNewExpression; +import com.intellij.psi.PsiType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.Mock; + +import com.intellij.psi.PsiExpression; +import com.intellij.psi.PsiReferenceExpression; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +/** + * 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 { + + // Declare as instance variables + @Mock + private ProblemsHolder mockHolder; + private PsiElementVisitor mockVisitor; + private PsiElement mockElement; + + @BeforeEach + public void setup() { + mockHolder = mock(ProblemsHolder.class); + mockElement = mock(PsiElement.class); + mockVisitor = createVisitor(); + } + + /** + * assertVisitor(mockVisitor) asserts that it the visitor not null and is an instance of JavaRecursiveElementWalkingVisitor. + * verifyRegisterProblem(mockVisitor, METHOD_TO_CHECK, NUMBER_OF_INVOCATIONS) verifies that the registerProblem method is called + * when the visitor visits a method call expression with the method name METHOD_TO_CHECK. + */ + @Test + public void testStorageUploadWithoutLengthCheck() { + assertVisitor(mockVisitor); + + String METHOD_TO_CHECK = "upload"; + int NUMBER_OF_INVOCATIONS = 1; // Number of times registerProblem should be called + verifyRegisterProblem(mockVisitor, METHOD_TO_CHECK, NUMBER_OF_INVOCATIONS); + + } + + // Create a visitor for the class under test + private PsiElementVisitor createVisitor() { + boolean isOnTheFly = false; + + // Create an instance of the class under test + StorageUploadWithoutLengthCheck check = new StorageUploadWithoutLengthCheck(); + return check.buildVisitor(mockHolder, isOnTheFly); + } + + // Assert that the visitor is not null and is an instance of JavaRecursiveElementWalkingVisitor + void assertVisitor(PsiElementVisitor visitor) { + assertNotNull(visitor); + assertTrue(visitor instanceof JavaRecursiveElementWalkingVisitor); + } + + // Verify that the registerProblem method is called when the visitor visits a method call expression with the method name METHOD_TO_CHECK. + private void verifyRegisterProblem(PsiElementVisitor visitor, String METHOD_TO_CHECK, int NUMBER_OF_INVOCATIONS) { + + // Arrange + PsiMethodCallExpression mockExpression = mock(PsiMethodCallExpression.class); + PsiExpressionList mockArgList = mock(PsiExpressionList.class); + + PsiExpression mockArgExpression = mock(PsiExpression.class); + + PsiMethodCallExpression mockArgMethodCall = mock(PsiMethodCallExpression.class); + PsiReferenceExpression mockMethodExpression = mock(PsiReferenceExpression.class); + PsiExpression mockQualifier = mock(PsiExpression.class); + PsiNewExpression nextMockQualifier = mock(PsiNewExpression.class); + PsiType mockType = mock(PsiType.class); + PsiMethodCallExpression mockQualifierMethodCall = mock(PsiMethodCallExpression.class); + + + // Act + when(mockExpression.getMethodExpression()).thenReturn(mockMethodExpression); + when(mockMethodExpression.getReferenceName()).thenReturn(METHOD_TO_CHECK); + + when(mockExpression.getArgumentList()).thenReturn(mockArgList); + when(mockArgList.getExpressions()).thenReturn(new PsiExpression[0]); + + when(mockArgExpression.getType()).thenReturn(mockType); + when(mockType.toString()).thenReturn("long"); + + when(mockArgMethodCall.getMethodExpression()).thenReturn(mockMethodExpression); + when(mockMethodExpression.getQualifierExpression()).thenReturn(mockQualifier); + + when(mockQualifierMethodCall.getMethodExpression()).thenReturn(mockMethodExpression); + when(mockMethodExpression.getQualifierExpression()).thenReturn(nextMockQualifier); + + when(nextMockQualifier.getArgumentList()).thenReturn(mockArgList); + when(mockArgList.getExpressions()).thenReturn(new PsiExpression[0]); + + when(nextMockQualifier.getType()).thenReturn(mockType); + when(mockType.toString()).thenReturn("long"); + + ((JavaRecursiveElementWalkingVisitor) visitor).visitMethodCallExpression(mockExpression); + + // Verify registerProblem is called + verify(mockHolder, times(NUMBER_OF_INVOCATIONS)).registerProblem(Mockito.eq(mockExpression), Mockito.contains("Azure Storage upload API without length parameter detected")); + } + + /** + * Test the visitor method when the method call expression is not in the list of methods to check. + * This should not call registerProblem. + */ + @Test + public void testMethodCallNotInList() { + String METHOD_TO_CHECK = "notInList"; + int NUMBER_OF_INVOCATIONS = 0; // Number of times registerProblem should be called + verifyRegisterProblem(mockVisitor, METHOD_TO_CHECK, NUMBER_OF_INVOCATIONS); + } +} + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryClientProviderTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryClientProviderTest.java new file mode 100644 index 00000000000..78333c046a4 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/TelemetryClientProviderTest.java @@ -0,0 +1,174 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +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.PsiReferenceExpression; +import com.intellij.psi.PsiVariable; +import com.microsoft.applicationinsights.TelemetryClient; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.TelemetryClientProvider.TelemetryClientProviderVisitor; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyMap; +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 TelemetryClientProvider class. + * It tests the buildVisitor, visitMethodCallExpression, and sendTelemetryData methods. + */ +public class TelemetryClientProviderTest { + + @Mock + private TelemetryClient mockTelemetryClient; + + @Mock + private ProblemsHolder mockProblemsHolder; + + @Mock + private JavaElementVisitor mockVisitor; + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + mockVisitor = createVisitor(); + TelemetryClientProviderVisitor.methodCounts = new HashMap<>(); + } + + /** + * This test method tests the visitMethodCallExpression method. + * It tests the method with an Azure package to ensure the method count is incremented correctly. + */ + @Test + public void testVisitMethodCallExpressionWithAzurePackage() { + + String packageName = "com.azure.testClient"; + String className = "testClient"; + String methodName = "upsertMethod"; + int numCountIncrease = 1; + + // Mock necessary objects + PsiMethodCallExpression mockExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression mockMethodExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression mockQualifier = mock(PsiReferenceExpression.class); + PsiVariable mockElement = mock(PsiVariable.class); + PsiClassType mockType = mock(PsiClassType.class); + PsiClass mockClass = mock(PsiClass.class); + + + // Mock method name and client name + when(mockExpression.getMethodExpression()).thenReturn(mockMethodExpression); + when(mockMethodExpression.getReferenceName()).thenReturn(methodName); + when(mockMethodExpression.getQualifierExpression()).thenReturn(mockQualifier); + when(mockQualifier.resolve()).thenReturn(mockElement); + when(mockElement.getType()).thenReturn(mockType); + when(mockType.resolve()).thenReturn(mockClass); + when(mockClass.getQualifiedName()).thenReturn(packageName); + when(mockType.getPresentableText()).thenReturn(className); + + // Build the visitor and visit the method call expression + mockVisitor.visitElement(mockExpression); + + // Assert method name starts with one of the prefixes + boolean startsWithPrefix = false; + for (String prefix : TelemetryClientProviderVisitor.AZURE_METHOD_PREFIXES) { + if (methodName.startsWith(prefix)) { + startsWithPrefix = true; + break; + } + } + assertTrue(startsWithPrefix); + + // Assert class name ends with "Client" + assertTrue(className.endsWith("Client")); + + + assertTrue(TelemetryClientProviderVisitor.methodCounts.containsKey(className)); + + // Verify the method count increment + if (numCountIncrease > 0) { + assertEquals(numCountIncrease, TelemetryClientProviderVisitor.methodCounts.get(className).get(methodName).intValue()); + } + } + + /** + * This test method tests the visitMethodCallExpression method. + * It tests the method with a non-Azure package to ensure the methodcount list is not incremented. + */ + @Test + public void testVisitMethodCallExpressionWithNonAzurePackage() { + + String packageName = "com.nonazure"; + String className = null; + String methodName = null; + + PsiMethodCallExpression mockExpression = mock(PsiMethodCallExpression.class); + PsiReferenceExpression mockMethodExpression = mock(PsiReferenceExpression.class); + PsiReferenceExpression mockQualifier = mock(PsiReferenceExpression.class); + PsiVariable mockElement = mock(PsiVariable.class); + PsiClassType mockType = mock(PsiClassType.class); + PsiClass mockClass = mock(PsiClass.class); + + // Mock method name and client name + when(mockExpression.getMethodExpression()).thenReturn(mockMethodExpression); + when(mockMethodExpression.getReferenceName()).thenReturn(methodName); + when(mockMethodExpression.getQualifierExpression()).thenReturn(mockQualifier); + when(mockQualifier.resolve()).thenReturn(mockElement); + when(mockElement.getType()).thenReturn(mockType); + when(mockType.resolve()).thenReturn(mockClass); + when(mockClass.getQualifiedName()).thenReturn(packageName); + when(mockType.getPresentableText()).thenReturn(className); + + + // Build the visitor and visit the method call expression + mockVisitor.visitMethodCallExpression(mockExpression); + + // assert that the methodCounts map is empty + assertTrue(TelemetryClientProviderVisitor.methodCounts.isEmpty()); + + } + + /** + * This test method tests the sendTelemetryData method. + * It tests that the telemetry data is sent correctly. + */ + @Test + public void testSendTelemetryData() { + // Populate methodCounts with test data + Map methodMap = new HashMap<>(); + methodMap.put("upsertMethod", 1); + TelemetryClientProviderVisitor.methodCounts.put("testClient", methodMap); + + // Inject the mock telemetry client + TelemetryClientProviderVisitor.telemetryClient = mockTelemetryClient; + + // Call the method to send telemetry data + TelemetryClientProviderVisitor.sendTelemetryData(); + + // Verify that trackMetric and trackEvent were called + verify(mockTelemetryClient, times(1)).trackEvent(eq("azure_sdk_usage_frequency"), anyMap(), anyMap()); + verify(mockTelemetryClient, times(1)).flush(); + } + + // Helper method to create a visitor + private JavaElementVisitor createVisitor() { + boolean isOnTheFly = false; + TelemetryClientProviderVisitor visitor = new TelemetryClientProviderVisitor(mockProblemsHolder, false); + + return visitor; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UseOfBlockOnAsyncClientsCheckTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UseOfBlockOnAsyncClientsCheckTest.java new file mode 100644 index 00000000000..f94f1471752 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-azure-sdk/src/test/java/com/microsoft/azure/toolkit/intellij/azure/sdk/buildtool/UseOfBlockOnAsyncClientsCheckTest.java @@ -0,0 +1,214 @@ +package com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool; + +import com.intellij.psi.PsiClassType; +import com.microsoft.azure.toolkit.intellij.azure.sdk.buildtool.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.api.Test; +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 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 { + + // Declare as instance variables + @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); + } + + /** + * This is the main test method that tests the use of blocking method on async clients. + * This method should be flagged by the inspection tool as it is a blocking method calls on an async client. + */ + @Test + public void testUseOfBlockOnAsyncClient() { + + int numberOfInvocations = 1; + String methodName = "block"; + String clientPackageName = "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient"; + String reactivePackageName = "reactor.core.publisher.Flux"; + + // verify register problem + verifyRegisterProblem(methodName, clientPackageName, numberOfInvocations, reactivePackageName); + } + + /** + * This test method tests the use of blockOptional() method on async clients. + * This method should be flagged by the inspection tool as it is a blockOptional() method call on an async client. + */ + @Test + public void testUseOfDifferentBlockOnAsyncClient() { + + int numberOfInvocations = 1; + String methodName = "blockOptional"; + String clientPackageName = "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient"; + String reactivePackageName = "reactor.core.publisher.Mono"; + + // verify register problem + verifyRegisterProblem(methodName, clientPackageName, numberOfInvocations, reactivePackageName); + } + + /** + * This test method tests the use of blockFirst() method on non-azure async clients. + * This method should not be flagged by the inspection tool + */ + @Test + public void testBlockOnAsyncClientsWithNonAzureClient() { + + int numberOfInvocations = 0; + String methodName = "blockFirst"; + String clientPackageName = "com.notAzure."; + String reactivePackageName = "reactor.core.publisher.Flux"; + + // verify register problem + verifyRegisterProblem(methodName, clientPackageName, numberOfInvocations, reactivePackageName); + } + + /** + * This test method tests the use of a different method call on async clients. + * This method should not be flagged by the inspection tool. + */ + @Test + public void testVisitOnDifferentMethodCall() { + + int numberOfInvocations = 0; + String methodName = "nonBlockingMethod"; + String clientPackageName = "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient"; + String reactivePackageName = "reactor.core.publisher.Flux"; + + // verify register problem + verifyRegisterProblem(methodName, clientPackageName, numberOfInvocations, reactivePackageName); + } + + /** + * This test method tests the use of blocking method on a non-reactive type. + * This method should not be flagged by the inspection tool. + */ + @Test + public void testBlockOnNonReactiveType() { + int numberOfInvocations = 0; + String methodName = "block"; + String clientPackageName = "com.azure.messaging.servicebus.ServiceBusReceiverAsyncClient"; + String nonReactivePackageName = "java.util.List"; + + // verify register problem + verifyRegisterProblem(methodName, clientPackageName, numberOfInvocations, nonReactivePackageName); + } + + /** + * This test method tests the use of blocking method on a non-async client. + * This method should not be flagged by the inspection tool. + */ + @Test + public void testBlockOnAzureNonAsyncClient() { + int numberOfInvocations = 0; + String methodName = "block"; + String nonAsyncClientPackageName = "com.azure.messaging.servicebus.ServiceBusReceiverClient"; + String reactivePackageName = "reactor.core.publisher.Mono"; + + // verify register problem + verifyRegisterProblem(methodName, nonAsyncClientPackageName, numberOfInvocations, reactivePackageName); + } + + /** + * Create a visitor object for the test + * + * @return JavaElementVisitor + */ + private JavaElementVisitor createVisitor() { + return new UseOfBlockOnAsyncClientsVisitor(mockHolder); + } + + /** + * This method is used to verify the registerProblem method is called when the method call is a blocking method call on an async client. + * + * @param methodName String - the name of the method called + * @param clientPackageName String - the package name of the async client + * @param numberOfInvocations int - the number of times registerProblem should be called + * @param reactivePackageName String - the package name of the reactive type + */ + private void verifyRegisterProblem(String methodName, String clientPackageName, int numberOfInvocations, String reactivePackageName) { + + // Arrange + 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); + + // visitMethodCallExpression method + when(mockElement.getMethodExpression()).thenReturn(referenceExpression); + when(referenceExpression.getReferenceName()).thenReturn(methodName); + + // checkIfAsyncContext method + when(referenceExpression.getQualifierExpression()).thenReturn(expression); + when(expression.getType()).thenReturn(type); + when(type.resolve()).thenReturn(qualifierReturnTypeClass); + + // isReactiveType method + when(qualifierReturnTypeClass.getQualifiedName()).thenReturn(reactivePackageName); + + // isAzureAsyncClient method + 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); + + // Act + mockVisitor.visitMethodCallExpression(mockElement); + + // Verify registerProblem is not called + verify(mockHolder, times(numberOfInvocations)).registerProblem(Mockito.eq(mockElement), Mockito.contains("Use of block methods on asynchronous clients detected. Switch to synchronous APIs instead.")); + } +}