diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc9c8d4..606070a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,8 @@ jobs: build: name: "Build & Publish" runs-on: windows-2022 + permissions: + contents: write steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -31,12 +33,32 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Get Version + id: get_version + shell: pwsh + run: | + $version = Select-String -Path gradle.properties -Pattern "PluginVersion=(.*)" | ForEach-Object { $_.Matches.Groups[1].Value } + echo "VERSION=$version" >> $env:GITHUB_OUTPUT + - name: Build Plugin run: ./gradlew buildPlugin - name: Run Tests run: ./gradlew testDotNet + - name: Create GitHub Release + if: github.event_name == 'workflow_dispatch' && inputs.publish == true + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.get_version.outputs.VERSION }} + name: v${{ steps.get_version.outputs.VERSION }} + files: | + output/*.zip + output/*.nupkg + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish Plugin if: github.event_name == 'workflow_dispatch' && inputs.publish == true shell: pwsh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6236e97 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + name: .NET Tests + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run Tests + run: ./gradlew testDotNet \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..45e816e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +# AI Project Context: AutoMapper.FindUsage + +This file provides essential information for AI agents working on the AutoMapper.FindUsage project. + +## Project Overview +AutoMapper.FindUsage is a JetBrains Rider and ReSharper plugin that enables navigation between DTOs and Models based on AutoMapper configurations. It helps developers jump from a property in a destination class to its source in the original model and vice versa. + +## Technology Stack +- **C# / .NET**: Main logic for ReSharper/Rider backend. +- **Gradle**: Build system for the entire project. +- **IntelliJ Platform SDK**: For Rider frontend integration. +- **ReSharper SDK**: For static analysis and navigation logic. +- **Kotlin/Java**: Currently minimal or none, as the plugin relies on the ReSharper backend host in Rider. + +## Project Structure +- `src/dotnet`: Contains the ReSharper plugin code. + - `ReSharperPlugin.AutoMapper.FindUsage`: Main project containing mapping analysis and navigation logic. + - `Registrations/`: Core logic for finding and caching AutoMapper mappings. + - `ReSharperPlugin.AutoMapper.FindUsage.Tests`: Unit and integration tests. + - `test/data/Navigation/`: C# files used as input for navigation tests. +- `src/rider`: Contains Rider-specific resources and potential frontend code. + - `main/resources/META-INF/plugin.xml`: Plugin metadata and registration of components. +- `build.gradle.kts`: Main Gradle build script. +- `ReSharperPlugin.AutoMapper.FindUsage.sln`: .NET solution file. + +## Key Concepts +- **Context Actions**: The plugin provides actions in the `Alt+Enter` menu on property setters (`set`) or `init` accessors. +- **Mapping Analysis**: The plugin scans the codebase for `CreateMap()` calls and handles `.ReverseMap()`. +- **Zones**: ReSharper uses "Zones" for component isolation. See `IAutoMapper.FindUsageZone.cs` and `ZoneMarker.cs`. +- **Navigation**: Implements custom navigation to jump between mapped properties. + +## Build and Run +- **Build .NET part**: `./gradlew compileDotNet` +- **Run Rider with plugin**: `./gradlew runIde` (Starts an experimental instance of Rider). +- **Run Tests**: `dotnet test` or via Gradle if configured. + +## Development Guidelines for AI +- **Adding new mapping patterns**: Modify `AutoMapperRegistrationFinder.cs` in the `Registrations` folder to support more complex AutoMapper configurations. +- **Extending Navigation**: Look at how `AutoMapperSearchFactory.cs` and `AutoMapperMapping.cs` are implemented. +- **Tests**: When adding features, add a corresponding test case in `src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation`. +- **Code Style**: Adhere to the existing C# coding standards (ReSharper style). + +## Useful Documentation +- [ReSharper SDK Documentation](https://www.jetbrains.com/help/resharper/sdk/Welcome.html) +- [Rider SDK Documentation](https://plugins.jetbrains.com/docs/rider/introduction.html) +- [AutoMapper Documentation](https://docs.automapper.org/) diff --git a/CHANGELOG.md b/CHANGELOG.md index ced012e..6ae2bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.2] - 2026-06-30 +### Changed +- **Breaking Change**: Removed support for Context Actions (`Alt+Enter`). The plugin now integrates exclusively with the standard "Find Usages" action. +- Updated version to 0.2. + +## [0.1.3] - 2026-06-30 +### Added +- Deep integration with standard "Find Usages" action. +- Support for `init` accessors in navigation. +- Smart grouping of results in the "Find Results" window for properties with multiple mappings. +- New tests for multiple source mappings and `init` property navigation. + +### Changed +- Navigation from Source to Destination now requires `.ReverseMap()` to be defined, while Destination to Source always works. + +### Fixed +- Prevented self-mapping navigation (e.g., when a type is mapped to itself). +- Improved performance of mapping discovery by deduplicating project files during analysis. + ## [0.1.2] - 2026-05-24 ### Added - Support for `Ignore()` method for properties. Navigation to the source property is not suggested if the destination property is marked as ignored in the AutoMapper configuration. @@ -21,6 +40,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Automatic use of full type names in the menu when multiple mappings are present. - Integration with ReSharper 2025.3 SDK. +[0.2]: https://github.com/Backs/AutoMapper.FindUsage/compare/v0.1.3...v0.2 +[0.1.3]: https://github.com/Backs/AutoMapper.FindUsage/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/Backs/AutoMapper.FindUsage/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/Backs/AutoMapper.FindUsage/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/Backs/AutoMapper.FindUsage/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 4488168..79c6110 100644 --- a/README.md +++ b/README.md @@ -7,32 +7,38 @@ **AutoMapper.FindUsage** is a plugin for JetBrains Rider and ReSharper that provides seamless navigation between DTOs and Models based on your AutoMapper configurations. ## Features - -- **Quick Navigation**: Jump from a property in your destination class directly to its source in the original model. +- **Find Usages**: Deep integration with the standard Find Usages action in Rider and ReSharper. +- **Quick Navigation**: Seamlessly jump between source and destination properties. - **Fluent API Support**: Automatically recognizes mappings configured via `CreateMap()`. -- **Reverse Mapping**: Full support for `.ReverseMap()`, allowing you to navigate in both directions. -- **Clean UI**: Context actions appear only on `set` or `init` accessors to minimize visual noise in your `Alt+Enter` menu. -- **Smart Grouping**: If a property is mapped from/to multiple types, the plugin groups them and uses full type names for easy identification. +- **Bidirectional Mapping**: Full support for `.ReverseMap()`. Navigation from Destination to Source always works, while Source to Destination navigation is available when a reverse mapping is defined. +- **Smart Grouping**: Mappings are grouped by type in the Find Results window for properties with multiple sources or destinations. ## Usage 1. Open a C# file containing a class used in AutoMapper mappings. 2. Place the caret on the `set` or `init` accessor of a property. -3. Press `Alt + Enter` to open Context Actions. -4. Select **AutoMapper. Navigate to source: .** to jump to the corresponding member. +3. Run `Find Usages` action. +4. View the search result for source properties or jump to the corresponding member. + +![Find Usages Demo](find_usages.gif) -![Navigate to source](navigate_3.gif) +Find Results window: + +![Search Results](find_usages.png) ## Supported Configurations The plugin analyzes your codebase to find various mapping patterns: ```csharp -// Standard mapping +// Standard mapping (Destination -> Source navigation) CreateMap(); -// Reverse mapping support +// Reverse mapping support (Bidirectional navigation) CreateMap().ReverseMap(); + +// Support for init accessors +public class UserDto { public string Name { get; init; } } ``` ## Installation @@ -45,21 +51,23 @@ To test the plugin in a sandbox environment: 1. **Build the .NET part**: ```bash - .\gradlew.bat compileDotNet + ./gradlew compileDotNet ``` + *(On Windows, use `.\gradlew.bat compileDotNet`)* 2. **Launch Rider with the plugin**: ```bash - .\gradlew.bat runIde + ./gradlew runIde ``` + *(On Windows, use `.\gradlew.bat runIde`)* *This will start an experimental instance of Rider with the plugin installed.* 3. **Verify functionality**: - Open any solution with AutoMapper configurations and test the `Alt+Enter` menu on property setters. + Open any solution with AutoMapper configurations and test the `Find Usages` action. ## License -This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License 2.0 – see the [LICENSE](LICENSE) file for details. ## Author diff --git a/ReSharperPlugin.AutoMapper.FindUsage.sln b/ReSharperPlugin.AutoMapper.FindUsage.sln index b9a6f9b..6914270 100644 --- a/ReSharperPlugin.AutoMapper.FindUsage.sln +++ b/ReSharperPlugin.AutoMapper.FindUsage.sln @@ -14,9 +14,8 @@ ProjectSection(SolutionItems) = preProject src\rider\main\resources\META-INF\plugin.xml = src\rider\main\resources\META-INF\plugin.xml build.gradle.kts = build.gradle.kts gradle.properties = gradle.properties - navigate_1.png = navigate_1.png - navigate_2.png = navigate_2.png - navigate_3.gif = navigate_3.gif + find_usages.gif = find_usages.gif + find_usages.png = find_usages.png EndProjectSection EndProject Global diff --git a/build.gradle.kts b/build.gradle.kts index 70d1936..589b83b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,8 @@ import com.jetbrains.plugin.structure.base.utils.isFile import groovy.ant.FileNameFinder import org.apache.tools.ant.taskdefs.condition.Os import org.jetbrains.intellij.platform.gradle.Constants +import org.gradle.kotlin.dsl.support.serviceOf +import org.gradle.process.ExecOperations import java.io.ByteArrayOutputStream plugins { @@ -35,7 +37,7 @@ repositories { } tasks.wrapper { - gradleVersion = "8.8" + gradleVersion = "8.13" distributionType = Wrapper.DistributionType.ALL distributionUrl = "https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-${gradleVersion}-all.zip" } @@ -54,8 +56,15 @@ sourceSets { } } -tasks.compileKotlin { - kotlinOptions { jvmTarget = "17" } +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +tasks.withType { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } } val setBuildTool by tasks.registering { @@ -65,7 +74,7 @@ val setBuildTool by tasks.registering { if (isWindows) { val stdout = ByteArrayOutputStream() - exec { + serviceOf().exec { executable("${rootDir}\\tools\\vswhere.exe") args("-latest", "-property", "installationPath", "-products", "*") standardOutput = stdout @@ -91,9 +100,10 @@ val compileDotNet by tasks.registering { dependsOn(setBuildTool) doLast { val executable: String by setBuildTool.get().extra + @Suppress("UNCHECKED_CAST") val arguments = (setBuildTool.get().extra["args"] as List).toMutableList() arguments.add("/t:Restore;Rebuild") - exec { + serviceOf().exec { executable(executable) args(arguments) workingDir(rootDir) @@ -103,7 +113,7 @@ val compileDotNet by tasks.registering { val testDotNet by tasks.registering { doLast { - exec { + serviceOf().exec { executable("dotnet") args("test","${DotnetSolution}","--logger","GitHubActions") workingDir(rootDir) @@ -114,7 +124,7 @@ val testDotNet by tasks.registering { tasks.buildPlugin { doLast { copy { - from("${buildDir}/distributions/${rootProject.name}-${version}.zip") + from(layout.buildDirectory.file("distributions/${rootProject.name}-${version}.zip")) into("${rootDir}/output") } @@ -128,12 +138,13 @@ tasks.buildPlugin { }.joinToString("\u2022 ", prefix = "\u2022 ") val executable: String by setBuildTool.get().extra + @Suppress("UNCHECKED_CAST") val arguments = (setBuildTool.get().extra["args"] as List).toMutableList() arguments.add("/t:Pack") arguments.add("/p:PackageOutputPath=${rootDir}/output") arguments.add("/p:PackageReleaseNotes=${changeNotes}") arguments.add("/p:PackageVersion=${version}") - exec { + serviceOf().exec { executable(executable) args(arguments) workingDir(rootDir) @@ -143,7 +154,9 @@ tasks.buildPlugin { dependencies { intellijPlatform { - rider(ProductVersion, useInstaller = false) + rider(ProductVersion) { + useInstaller = false + } jetbrainsRuntime() // TODO: add plugins @@ -196,7 +209,7 @@ tasks.publishPlugin { token.set("${PublishToken}") doLast { - exec { + serviceOf().exec { executable("dotnet") args("nuget","push","output/${DotnetPluginId}.${version}.nupkg","--api-key","${PublishToken}","--source","https://plugins.jetbrains.com") workingDir(rootDir) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..996fb7a --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Exit on error +set -e + +VERSION=${1:-"9999.0.0"} +SOLUTION="ReSharperPlugin.AutoMapper.FindUsage.sln" +OUTPUT_DIR="$(pwd)/output" + +echo "Building solution $SOLUTION with version $VERSION..." + +dotnet build "$SOLUTION" \ + -c Release \ + -p:PackageVersion="$VERSION" \ + -p:PackageOutputPath="$OUTPUT_DIR" \ + /t:Restore,Rebuild,Pack \ + -v minimal + +echo "Build finished. Artifacts are in $OUTPUT_DIR" diff --git a/find_usages.gif b/find_usages.gif new file mode 100644 index 0000000..4b1c8a2 Binary files /dev/null and b/find_usages.gif differ diff --git a/find_usages.png b/find_usages.png new file mode 100644 index 0000000..8433aa3 Binary files /dev/null and b/find_usages.png differ diff --git a/gradle.properties b/gradle.properties index f580a59..a2f25d5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ DotnetPluginId=ReSharperPlugin.AutoMapper.FindUsage DotnetSolution=ReSharperPlugin.AutoMapper.FindUsage.sln RiderPluginId=me.rogatnev.automapper.findusage -PluginVersion=0.1.2 +PluginVersion=0.2 BuildConfiguration=Debug diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..9bbc975 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 57b7436..39e374c --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -129,7 +131,7 @@ if [ "$darwin" = "true" ]; then JVM_URL=https://download.oracle.com/java/17/archive/jdk-17.0.3.1_macos-aarch64_bin.tar.gz JVM_TARGET_DIR=$BUILD_DIR/jdk-17.0.3.1_macos-aarch64_bin-297fa2 ;; - *) + *) die "Unknown architecture $JVM_ARCH" ;; esac @@ -147,7 +149,7 @@ else JVM_URL=https://download.oracle.com/java/17/archive/jdk-17.0.3.1_linux-aarch64_bin.tar.gz JVM_TARGET_DIR=$BUILD_DIR/jdk-17.0.3.1_linux-aarch64_bin-319da6 ;; - *) + *) die "Unknown architecture $JVM_ARCH" ;; esac @@ -182,7 +184,7 @@ else *".zip") unzip "$JVM_TEMP_FILE" -d "$JVM_TARGET_DIR" ;; *) tar -x -f "$JVM_TEMP_FILE" -C "$JVM_TARGET_DIR" ;; esac - + rm -f "$JVM_TEMP_FILE" echo "$JVM_URL" >"$JVM_TARGET_DIR/.flag" @@ -294,7 +296,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/gradlew.bat b/gradlew.bat index 8adb778..c0f5943 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/navigate_1.png b/navigate_1.png deleted file mode 100644 index 9582310..0000000 Binary files a/navigate_1.png and /dev/null differ diff --git a/navigate_2.png b/navigate_2.png deleted file mode 100644 index 39b5b9e..0000000 Binary files a/navigate_2.png and /dev/null differ diff --git a/navigate_3.gif b/navigate_3.gif deleted file mode 100644 index 1d68b41..0000000 Binary files a/navigate_3.gif and /dev/null differ diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts index 8aee0f6..3052800 100644 --- a/protocol/build.gradle.kts +++ b/protocol/build.gradle.kts @@ -5,6 +5,17 @@ plugins { id("com.jetbrains.rdgen") version libs.versions.rdGen } +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +tasks.withType { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } +} + dependencies { implementation(libs.kotlinStdLib) implementation(libs.rdGen) diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..14b032b --- /dev/null +++ b/publish.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Exit on error +set -e + +if [ "$#" -lt 2 ]; then + echo "Usage: ./publish.sh [Configuration]" + exit 1 +fi + +VERSION=$1 +API_KEY=$2 +CONFIGURATION=${3:-"Release"} +PLUGIN_ID="ReSharperPlugin.AutoMapper.FindUsage" +OUTPUT_DIR="$(pwd)/output" +SOLUTION="ReSharperPlugin.AutoMapper.FindUsage.sln" + +# Extract latest changelog section (simplistic version) +CHANGELOG=$(sed -n '/^## \[/,/^## \[/{ /^## \[/!p; }' CHANGELOG.md | head -n 20) + +echo "Publishing $PLUGIN_ID version $VERSION..." + +dotnet build "$SOLUTION" \ + -c "$CONFIGURATION" \ + -p:PackageVersion="$VERSION" \ + -p:PackageOutputPath="$OUTPUT_DIR" \ + -p:PackageReleaseNotes="$CHANGELOG" \ + /t:Restore,Rebuild,Pack \ + -v minimal + +PACKAGE_FILE=$(ls $OUTPUT_DIR/$PLUGIN_ID.$VERSION.nupkg) + +if [ -f "$PACKAGE_FILE" ]; then + echo "Pushing $PACKAGE_FILE..." + dotnet nuget push "$PACKAGE_FILE" -s "https://plugins.jetbrains.com/api/v2/package" -k "$API_KEY" +else + echo "Error: Package file $PACKAGE_FILE not found." + exit 1 +fi diff --git a/settings.gradle.kts b/settings.gradle.kts index 0d6d102..ce0f233 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,7 +3,6 @@ pluginManagement { repositories { maven { setUrl("https://cache-redirector.jetbrains.com/plugins.gradle.org") } maven { setUrl("https://cache-redirector.jetbrains.com/maven-central") } - maven { setUrl("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-eap") } } resolutionStrategy { eachPlugin { diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/AutoMapperCacheTests.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/AutoMapperCacheTests.cs new file mode 100644 index 0000000..c292032 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/AutoMapperCacheTests.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using FluentAssertions; +using JetBrains.Application.Threading; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.TestFramework; +using JetBrains.TestFramework.Projects; +using NUnit.Framework; +using ReSharperPlugin.AutoMapper.FindUsage.Registrations; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Tests; + +[ReuseSolution(false)] +public class AutoMapperCacheTests : BaseTestWithSingleProject +{ + protected override string RelativeTestDataPath => "Navigation"; + + [Test] + public void BuildOneWayMappingTest() + { + WithSingleProject(["OneWayMapping.cs"], (_, solution, project) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var cache = solution.GetComponent(); + var sourceFile = GetProjectSourceFile(project, "OneWayMapping.cs"); + + var built = cache.Build(sourceFile, isPreParent: false) as List; + + built.Should().BeEquivalentTo([ + new SerializableMapping + { + DestinationTypeClrName = "TestNamespace.Destination", + HasReverseMap = false, + IgnoredProperties = [], + SourceTypeClrName = "TestNamespace.Source" + } + ] + ); + }); + }); + } + + [Test] + public void BuildReverseMappingTest() + { + WithSingleProject(["ReverseMapping.cs"], (_, solution, project) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var cache = solution.GetComponent(); + var sourceFile = GetProjectSourceFile(project, "ReverseMapping.cs"); + + var built = cache.Build(sourceFile, isPreParent: false) as List; + built.Should().BeEquivalentTo([ + new SerializableMapping + { + DestinationTypeClrName = "TestNamespaceReverse.Destination", + HasReverseMap = true, + IgnoredProperties = [], + SourceTypeClrName = "TestNamespaceReverse.Source" + } + ] + ); + }); + }); + } + + [Test] + public void BuildsIgnoredPropertiesMappiingTest() + { + WithSingleProject(["IgnoredMapping.cs"], (_, solution, project) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var cache = solution.GetComponent(); + var sourceFile = GetProjectSourceFile(project, "IgnoredMapping.cs"); + + var built = cache.Build(sourceFile, isPreParent: false) as List; + + built.Should().BeEquivalentTo([ + new SerializableMapping + { + DestinationTypeClrName = "TestNamespaceIgnored.Destination", + HasReverseMap = false, + IgnoredProperties = ["Ignored"], + SourceTypeClrName = "TestNamespaceIgnored.Source" + } + ] + ); + }); + }); + } + + private static IPsiSourceFile GetProjectSourceFile(IProject project, string fileName) + { + foreach (var pf in project.GetAllProjectFiles()) + { + if (pf.Name == fileName) + return pf.ToSourceFile(); + } + + return null; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/AutoMapperSearchTests.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/AutoMapperSearchTests.cs new file mode 100644 index 0000000..b7615dc --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/AutoMapperSearchTests.cs @@ -0,0 +1,281 @@ +using System.Linq; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp.Impl.Occurrences; +using JetBrains.ReSharper.TestFramework; +using JetBrains.TestFramework.Projects; +using NUnit.Framework; +using JetBrains.ReSharper.Psi.Search; +using ReSharperPlugin.AutoMapper.FindUsage.Registrations; +using JetBrains.Application.Threading; +using FluentAssertions; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Tests; + +[ReuseSolution(false)] +public class AutoMapperSearchTests : BaseTestWithSingleProject +{ + protected override string RelativeTestDataPath => "Navigation"; + + [Test] + public void PropertyNavigationFromSourceTest() + { + WithSingleProject(["OneWayMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var source = PsiTestHelper.GetTypeElement(solution, "TestNamespace.Source"); + source.Should().NotBeNull("Source type not found"); + solution.GetComponent().EnsureProjectMappings(source); + + var results = searchFactory.GetRelatedFindResults(source).ToList(); + results.Should().BeEmpty("Should NOT find Destination when searching for Source in one-way mapping (no ReverseMap)"); + + var destination = PsiTestHelper.GetTypeElement(solution, "TestNamespace.Destination"); + destination.Should().NotBeNull("Destination type not found"); + + var resultsForDest = searchFactory.GetRelatedFindResults(destination).ToList(); + resultsForDest.Should().NotBeEmpty("Should find Source when searching for Destination (one-way mapping)"); + + var sourceDecls = source.GetDeclarations().ToList(); + var foundSource = resultsForDest + .OfType() + .Any(r => sourceDecls.Any(d => d.Contains(r.Declaration))); + foundSource.Should().BeTrue("Should find Source when searching for Destination (one-way mapping)"); + }); + }); + } + + [Test] + public void PropertyNavigationFromDestinationTest() + { + WithSingleProject(["OneWayMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespace.Destination"); + dest.Should().NotBeNull("Destination type not found"); + solution.GetComponent().EnsureProjectMappings(dest); + + var nameProp = dest.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Destination.Name not found"); + + var results = searchFactory.GetRelatedFindResults(nameProp).ToList(); + results.Should().NotBeEmpty("Expected related results for Destination.Name (one-way mapping)"); + + var source = PsiTestHelper.GetTypeElement(solution, "TestNamespace.Source"); + var sourceNameProp = source.Properties.FirstOrDefault(p => p.ShortName == "Name"); + + var foundSource = results.Any(r => + r is FindResultDeclaredElement { DeclaredElement: var de } && de.Equals(sourceNameProp) || + r is FindResultInitializer { Declaration: var node } && sourceNameProp.GetDeclarations().Any(d => d.Contains(node))); + + foundSource.Should().BeTrue("Should find Source.Name when searching for Destination.Name (one-way mapping)"); + }); + }); + } + + [Test] + public void PropertyNavigationFromSourcePropertyTest() + { + WithSingleProject(["OneWayMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var source = PsiTestHelper.GetTypeElement(solution, "TestNamespace.Source"); + source.Should().NotBeNull("Source type not found"); + solution.GetComponent().EnsureProjectMappings(source); + + var nameProp = source.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Source.Name not found"); + + var results = searchFactory.GetRelatedFindResults(nameProp).ToList(); + results.Should().BeEmpty("Expected no results for Source.Name in one-way mapping (no ReverseMap)"); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespace.Destination"); + var foundDest = results.Any(r => IsRelatedToProperty(r, dest, "Name")); + + foundDest.Should().BeFalse("Should NOT find Destination.Name when searching for Source.Name in one-way mapping"); + }); + }); + } + + [Test] + public void PropertyNavigationNoDuplicatesWithReverseMapTest() + { + WithSingleProject(["ReverseMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var source = PsiTestHelper.GetTypeElement(solution, "TestNamespaceReverse.Source"); + source.Should().NotBeNull("Source type not found"); + solution.GetComponent().EnsureProjectMappings(source); + + var nameProp = source.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Source.Name not found"); + + var results = searchFactory.GetRelatedFindResults(nameProp).ToList(); + results.Should().HaveCount(1, "Should not have duplicate results for ReverseMap"); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespaceReverse.Destination"); + var foundDest = results.Any(r => IsRelatedToProperty(r, dest, "Name")); + + foundDest.Should().BeTrue("Should find Destination.Name when searching for Source.Name via ReverseMap"); + }); + }); + } + + [Test] + public void PropertyNavigationViiaReverseMapTest() + { + WithSingleProject(["ReverseMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespaceReverse.Destination"); + dest.Should().NotBeNull("Destination type not found"); + solution.GetComponent().EnsureProjectMappings(dest); + + var nameProp = dest.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Destination.Name not found"); + + var nameResults = searchFactory.GetRelatedFindResults(nameProp).ToList(); + nameResults.Should().NotBeEmpty("Expected related results for Destination.Name via ReverseMap"); + }); + }); + } + + [Test] + public void PropertyNavigationDoesNotFindIgnoredPropertyTest() + { + WithSingleProject(["IgnoredMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespaceIgnored.Destination"); + dest.Should().NotBeNull("Destination type not found"); + solution.GetComponent().EnsureProjectMappings(dest); + + var ignoredProp = dest.Properties.FirstOrDefault(p => p.ShortName == "Ignored"); + ignoredProp.Should().NotBeNull("Destination.Ignored not found"); + + var ignoredResults = searchFactory.GetRelatedFindResults(ignoredProp).ToList(); + ignoredResults.Should().BeEmpty("Ignored property should produce no related results"); + }); + }); + } + + [Test] + public void PropertyNavigationDoesNotFindMissingPropertyTest() + { + WithSingleProject(["MissingProperty.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespaceMissing.Destination"); + dest.Should().NotBeNull("Destination type not found"); + solution.GetComponent().EnsureProjectMappings(dest); + + var missingProp = dest.Properties.FirstOrDefault(p => p.ShortName == "Missing"); + missingProp.Should().NotBeNull("Destination.Missing not found"); + + var results = searchFactory.GetRelatedFindResults(missingProp).ToList(); + results.Should().BeEmpty("Missing property should produce no related results"); + }); + }); + } + + [Test] + public void MultipleSourcesMappingTest() + { + WithSingleProject(["MultipleSourcesMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespaceMultiple.Destination"); + dest.Should().NotBeNull("Destination type not found"); + solution.GetComponent().EnsureProjectMappings(dest); + + var nameProp = dest.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Destination.Name not found"); + + var results = searchFactory.GetRelatedFindResults(nameProp).ToList(); + results.Should().HaveCount(2, "Expected results from both Source1 and Source2"); + + var source1 = PsiTestHelper.GetTypeElement(solution, "TestNamespaceMultiple.Source1"); + var source2 = PsiTestHelper.GetTypeElement(solution, "TestNamespaceMultiple.Source2"); + + var foundSource1 = results.Any(r => IsRelatedToProperty(r, source1, "Name")); + var foundSource2 = results.Any(r => IsRelatedToProperty(r, source2, "Name")); + + foundSource1.Should().BeTrue("Should find Source1.Name"); + foundSource2.Should().BeTrue("Should find Source2.Name"); + }); + }); + } + + [Test] + public void InitPropertyNavigationTest() + { + WithSingleProject(["InitPropertyMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var dest = PsiTestHelper.GetTypeElement(solution, "TestNamespaceInit.Destination"); + dest.Should().NotBeNull("Destination type not found"); + solution.GetComponent().EnsureProjectMappings(dest); + + var nameProp = dest.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Destination.Name not found"); + + // Get the init accessor + var initAccessor = nameProp.Setter; + initAccessor.Should().NotBeNull("Destination.Name should have an init accessor"); + + var results = searchFactory.GetRelatedFindResults(initAccessor).ToList(); + results.Should().NotBeEmpty("Should find Source.Name when searching from init accessor"); + + var source = PsiTestHelper.GetTypeElement(solution, "TestNamespaceInit.Source"); + var foundSource = results.Any(r => IsRelatedToProperty(r, source, "Name")); + + foundSource.Should().BeTrue("Should find Source.Name when searching from init accessor"); + }); + }); + } + + private static bool IsRelatedToProperty(FindResult result, ITypeElement type, string propertyName) + { + var property = type.Properties.FirstOrDefault(p => p.ShortName == propertyName); + if (property == null) return false; + + return result is FindResultDeclaredElement { DeclaredElement: var de } && de.Equals(property) || + result is FindResultInitializer { Declaration: var node } && property.GetDeclarations().Any(d => d.Contains(node)); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/Navigation/AutoMapperNavigationActionAvailabilityTest.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/Navigation/AutoMapperNavigationActionAvailabilityTest.cs deleted file mode 100644 index 05c4812..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/Navigation/AutoMapperNavigationActionAvailabilityTest.cs +++ /dev/null @@ -1,35 +0,0 @@ -using JetBrains.ReSharper.FeaturesTestFramework.Intentions; -using JetBrains.ReSharper.TestFramework; -using NUnit.Framework; -using ReSharperPlugin.AutoMapper.FindUsage.Navigation; - -namespace ReSharperPlugin.AutoMapper.FindUsage.Tests.Navigation; - -[TestFixture] -[TestPackages("AutoMapper")] -public class AutoMapperNavigationActionAvailabilityTest - : CSharpContextActionAvailabilityTestBase -{ - protected override string RelativeTestDataPath => "Navigation"; - protected override string ExtraPath => ""; - - [Test] public void TestAvailableOnSetter() => DoNamedTest(); - - [Test] public void TestAvailableOnInit() => DoNamedTest(); - - [Test] public void TestReverseMap() => DoNamedTest(); - - [Test] public void TestReverseMapChain() => DoNamedTest(); - - [Test] public void TestMultipleMappings() => DoNamedTest(); - - [Test] public void TestConfigurationExpression() => DoNamedTest(); - - [Test] public void TestIgnoreProperty() => DoNamedTest(); - - [Test] public void TestNotAvailableOnGetter() => DoNamedTest(); - - [Test] public void TestNotAvailableWithoutMapping() => DoNamedTest(); - - [Test] public void TestNotAvailableOnUnmappedType() => DoNamedTest(); -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/Navigation/AutoMapperNavigationActionWithoutAutoMapperAvailabilityTest.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/Navigation/AutoMapperNavigationActionWithoutAutoMapperAvailabilityTest.cs deleted file mode 100644 index cc73689..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/Navigation/AutoMapperNavigationActionWithoutAutoMapperAvailabilityTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using JetBrains.ReSharper.FeaturesTestFramework.Intentions; -using JetBrains.ReSharper.TestFramework; -using NUnit.Framework; -using ReSharperPlugin.AutoMapper.FindUsage.Navigation; - -namespace ReSharperPlugin.AutoMapper.FindUsage.Tests.Navigation; - -[TestFixture] -public class AutoMapperNavigationActionWithoutAutoMapperAvailabilityTest - : CSharpContextActionAvailabilityTestBase -{ - protected override string RelativeTestDataPath => "Navigation"; - protected override string ExtraPath => ""; - - [Test] public void TestNotAvailableWithoutAutoMapper() => DoNamedTest(); -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/PsiTestHelper.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/PsiTestHelper.cs new file mode 100644 index 0000000..0b0f2a0 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/PsiTestHelper.cs @@ -0,0 +1,19 @@ +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Tests; + +public static class PsiTestHelper +{ + public static ITypeElement GetTypeElement(ISolution solution, string clrName) + { + var psiServices = solution.GetPsiServices(); + foreach (var module in psiServices.Modules.GetModules()) + { + var scope = psiServices.Symbols.GetSymbolScope(module, true, true); + var type = scope.GetTypeElementByCLRName(clrName); + if (type != null) return type; + } + return null; + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/ReSharperPlugin.AutoMapper.FindUsage.Tests.csproj b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/ReSharperPlugin.AutoMapper.FindUsage.Tests.csproj index 1c02610..ad2d027 100644 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/ReSharperPlugin.AutoMapper.FindUsage.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/ReSharperPlugin.AutoMapper.FindUsage.Tests.csproj @@ -6,6 +6,7 @@ + @@ -21,4 +22,9 @@ + + + + + diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/SelfMappingTests.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/SelfMappingTests.cs new file mode 100644 index 0000000..c0e49bd --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/SelfMappingTests.cs @@ -0,0 +1,46 @@ +using System.Linq; +using JetBrains.Application.Threading; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.TestFramework; +using JetBrains.TestFramework.Projects; +using NUnit.Framework; +using ReSharperPlugin.AutoMapper.FindUsage.Registrations; +using FluentAssertions; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Tests; + +[ReuseSolution(false)] +public class SelfMappingTests : BaseTestWithSingleProject +{ + protected override string RelativeTestDataPath => "Navigation"; + + [Test] + public void PropertyNavigationFromSelfMappingTest() + { + WithSingleProject(["SelfMapping.cs"], (_, solution, _) => + { + var locks = solution.GetComponent(); + locks.ExecuteWithReadLock(() => + { + var searchFactory = solution.GetComponent(); + + var source = PsiTestHelper.GetTypeElement(solution, "TestNamespaceSelf.Source"); + source.Should().NotBeNull("Source type not found"); + var registrationFinder = solution.GetComponent(); + registrationFinder.EnsureProjectMappings(source); + + var mappings = registrationFinder.FindMappingsForType(source); + mappings.Should().BeEmpty("Should NOT have mappings to itself in RegistrationFinder"); + + var results = searchFactory.GetRelatedFindResults(source).ToList(); + results.Should().BeEmpty("Should NOT find itself when searching for mappings"); + + var nameProp = source.Properties.FirstOrDefault(p => p.ShortName == "Name"); + nameProp.Should().NotBeNull("Source.Name not found"); + + var nameResults = searchFactory.GetRelatedFindResults(nameProp).ToList(); + nameResults.Should().BeEmpty("Should NOT find itself for property when searching for mappings"); + }); + }); + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/IgnoredMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/IgnoredMapping.cs new file mode 100644 index 0000000..fa4cb9c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/IgnoredMapping.cs @@ -0,0 +1,23 @@ +using AutoMapper; + +namespace TestNamespaceIgnored +{ + public class Source + { + public string Ignored { get; set; } + } + + public class Destination + { + public string Ignored { get; set; } + } + + public class IgnoredProfile : Profile + { + public IgnoredProfile() + { + CreateMap() + .ForMember(d => d.Ignored, opt => opt.Ignore()); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/InitPropertyMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/InitPropertyMapping.cs new file mode 100644 index 0000000..920dc21 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/InitPropertyMapping.cs @@ -0,0 +1,22 @@ +using AutoMapper; + +namespace TestNamespaceInit +{ + public class Source + { + public string Name { get; set; } + } + + public class Destination + { + public string Name { get; init; } + } + + public class InitProfile : Profile + { + public InitProfile() + { + CreateMap(); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/MissingProperty.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/MissingProperty.cs new file mode 100644 index 0000000..c526e57 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/MissingProperty.cs @@ -0,0 +1,23 @@ +using AutoMapper; + +namespace TestNamespaceMissing +{ + public class Source + { + public string Name { get; set; } + } + + public class Destination + { + public string Name { get; set; } + public string Missing { get; set; } + } + + public class MissingProfile : Profile + { + public MissingProfile() + { + CreateMap(); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/MultipleSourcesMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/MultipleSourcesMapping.cs new file mode 100644 index 0000000..914db75 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/MultipleSourcesMapping.cs @@ -0,0 +1,28 @@ +using AutoMapper; + +namespace TestNamespaceMultiple +{ + public class Source1 + { + public string Name { get; set; } + } + + public class Source2 + { + public string Name { get; set; } + } + + public class Destination + { + public string Name { get; set; } + } + + public class MultipleProfile : Profile + { + public MultipleProfile() + { + CreateMap(); + CreateMap(); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/OneWayMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/OneWayMapping.cs new file mode 100644 index 0000000..8b85abd --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/OneWayMapping.cs @@ -0,0 +1,22 @@ +using AutoMapper; + +namespace TestNamespace +{ + public class Source + { + public string Name { get; set; } + } + + public class Destination + { + public string Name { get; set; } + } + + public class OneWayProfile : Profile + { + public OneWayProfile() + { + CreateMap(); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/ReverseMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/ReverseMapping.cs new file mode 100644 index 0000000..10f40ba --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/ReverseMapping.cs @@ -0,0 +1,22 @@ +using AutoMapper; + +namespace TestNamespaceReverse +{ + public class Source + { + public string Name { get; set; } + } + + public class Destination + { + public string Name { get; set; } + } + + public class ReverseProfile : Profile + { + public ReverseProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/SelfMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/SelfMapping.cs new file mode 100644 index 0000000..05760b2 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/SelfMapping.cs @@ -0,0 +1,17 @@ +using AutoMapper; + +namespace TestNamespaceSelf +{ + public class Source + { + public string Name { get; set; } + } + + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestAvailableOnInit.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestAvailableOnInit.cs deleted file mode 100644 index 1ece040..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestAvailableOnInit.cs +++ /dev/null @@ -1,21 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set; } -} - -public class DestinationDto -{ - public string Name { get; init{on}; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestAvailableOnSetter.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestAvailableOnSetter.cs deleted file mode 100644 index 348c0ea..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestAvailableOnSetter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set; } -} - -public class DestinationDto -{ - public string Name { get; set{on}; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestConfigurationExpression.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestConfigurationExpression.cs deleted file mode 100644 index 22d312e..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestConfigurationExpression.cs +++ /dev/null @@ -1,17 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto { public string Name { get; set; } } -public class DestinationDto { public string Name { get; set{on}; } } - -public class Startup -{ - public void Configure() - { - var config = new MapperConfiguration(cfg => - { - cfg.CreateMap(); - }); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestIgnoreProperty.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestIgnoreProperty.cs deleted file mode 100644 index 51f2958..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestIgnoreProperty.cs +++ /dev/null @@ -1,22 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set; } -} - -public class DestinationDto -{ - public string Name { get; set{off}; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap() - .ForMember(it => it.Name, exp => exp.Ignore()); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestMultipleMappings.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestMultipleMappings.cs deleted file mode 100644 index 61b7636..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestMultipleMappings.cs +++ /dev/null @@ -1,20 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class Source1Dto { public string Name { get; set; } } -public class Source2Dto { public string Name { get; set; } } - -public class DestinationDto -{ - public string Name { get; set{on}; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap(); - CreateMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableOnGetter.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableOnGetter.cs deleted file mode 100644 index a6444f9..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableOnGetter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set; } -} - -public class DestinationDto -{ - public string Name { get{off}; set; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableOnUnmappedType.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableOnUnmappedType.cs deleted file mode 100644 index af72e79..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableOnUnmappedType.cs +++ /dev/null @@ -1,19 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto { public string Name { get; set; } } -public class DestinationDto { public string Name { get; set; } } - -public class UnmappedDto -{ - public string Name { get; set{off}; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableWithoutAutoMapper.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableWithoutAutoMapper.cs deleted file mode 100644 index a6d403c..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableWithoutAutoMapper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set; } -} - -public class DestinationDto -{ - public string Name { get; set{off}; } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableWithoutMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableWithoutMapping.cs deleted file mode 100644 index 7130250..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestNotAvailableWithoutMapping.cs +++ /dev/null @@ -1,21 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set; } -} - -public class DestinationDto -{ - public string Name { get; set{off}; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - // No CreateMap here - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestReverseMap.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestReverseMap.cs deleted file mode 100644 index cf802ad..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestReverseMap.cs +++ /dev/null @@ -1,21 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto -{ - public string Name { get; set{on}; } -} - -public class DestinationDto -{ - public string Name { get; set; } -} - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap().ReverseMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestReverseMapChain.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestReverseMapChain.cs deleted file mode 100644 index b3691e1..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/TestReverseMapChain.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoMapper; - -namespace TestProject; - -public class SourceDto { public string Name { get; set{on}; } } -public class DestinationDto { public string Name { get; set; } } - -public class TestProfile : Profile -{ - public TestProfile() - { - CreateMap().MaxDepth(5).ReverseMap(); - } -} diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/packages.config b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/packages.config new file mode 100644 index 0000000..b29f3d5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/Navigation/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock new file mode 100644 index 0000000..8b9d669 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock @@ -0,0 +1,15 @@ +# Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. +# Input (NuGetFramework=net35): +# JetBrains.Tests.Platform.NETFrameWork [2.0.0.0, 2.0.0.2147483647] +# JetBrains.Tests.Platform.NETFrameWork [3.0.0.0, 3.0.0.2147483647] +# JetBrains.Tests.Platform.NETFrameWork [3.5.0.0, 3.5.0.2147483647] +JetBrains.Tests.Microsoft.Gac 1.1 +JetBrains.Tests.Platform.NETFrameWork 2.0 +JetBrains.Tests.Platform.NETFrameWork 3.0 +JetBrains.Tests.Platform.NETFrameWork 3.5 +JetBrains.Tests.Platform.NetFramework.Binaries 2.0 +JetBrains.Tests.Platform.NetFramework.Binaries 3.0 +JetBrains.Tests.Platform.NetFramework.Binaries 3.5 +JetBrains.Tests.Platform.NetFramework.Profiles 3.5 +JetBrains.Tests.Platform.NetFramework.ReferenceAssemblies 3.0 +JetBrains.Tests.Platform.NetFramework.ReferenceAssemblies 3.5 \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/F1E85CB459CEDD759BDA4F44439771A90968306FF233FECEEC12839A5C161619.lock b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/3E42A6AD41D44E3BEC2AEC6B499CA648AF56E1DF8A971AA5F437D7AC7157A95B.lock similarity index 56% rename from src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/F1E85CB459CEDD759BDA4F44439771A90968306FF233FECEEC12839A5C161619.lock rename to src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/3E42A6AD41D44E3BEC2AEC6B499CA648AF56E1DF8A971AA5F437D7AC7157A95B.lock index f4161e6..8089620 100644 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/F1E85CB459CEDD759BDA4F44439771A90968306FF233FECEEC12839A5C161619.lock +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/3E42A6AD41D44E3BEC2AEC6B499CA648AF56E1DF8A971AA5F437D7AC7157A95B.lock @@ -1,4 +1,4 @@ # Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. -# Input (NuGetFramework=net9.0): -# Microsoft.NETCore.App.Ref [9.0.0] -Microsoft.NETCore.App.Ref 9.0.0 \ No newline at end of file +# Input (NuGetFramework=net35): +# AutoMapper (, ) +AutoMapper 16.1.1 \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/A714BD8C6AAB995E590847AA474991BBC5450C60428A22B91BDAB0D3DA3469C7.lock b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/A714BD8C6AAB995E590847AA474991BBC5450C60428A22B91BDAB0D3DA3469C7.lock deleted file mode 100644 index beaaa69..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/A714BD8C6AAB995E590847AA474991BBC5450C60428A22B91BDAB0D3DA3469C7.lock +++ /dev/null @@ -1,6 +0,0 @@ -# Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. -# Input (NuGetFramework=net9.0): -# JetBrains.Annotations [2025.2.0] -# Microsoft.NETCore.App.Ref [9.0.0] -JetBrains.Annotations 2025.2.0 -Microsoft.NETCore.App.Ref 9.0.0 \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/C57A061EA9C7F3875D28DF0F1248AC3453B0C7741443AEB2C239278A72BF8406.lock b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/C57A061EA9C7F3875D28DF0F1248AC3453B0C7741443AEB2C239278A72BF8406.lock deleted file mode 100644 index 79fb6c5..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage.Tests/test/data/NuGetLocks/C57A061EA9C7F3875D28DF0F1248AC3453B0C7741443AEB2C239278A72BF8406.lock +++ /dev/null @@ -1,15 +0,0 @@ -# Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. -# Input (NuGetFramework=net9.0): -# AutoMapper (, ) -# Microsoft.NETCore.App.Ref [9.0.0] -AutoMapper 16.1.1 -Microsoft.Extensions.DependencyInjection.Abstractions 10.0.0 -Microsoft.Extensions.Logging.Abstractions 10.0.0 -Microsoft.Extensions.Options 10.0.0 -Microsoft.Extensions.Primitives 10.0.0 -Microsoft.IdentityModel.Abstractions 8.14.0 -Microsoft.IdentityModel.JsonWebTokens 8.14.0 -Microsoft.IdentityModel.Logging 8.14.0 -Microsoft.IdentityModel.Tokens 8.14.0 -Microsoft.NETCore.App.Ref 9.0.0 -System.Diagnostics.DiagnosticSource 10.0.0 \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/IAutoMapper.FindUsageZone.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/IAutoMapper.FindUsageZone.cs index 32c66e7..6304abe 100644 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/IAutoMapper.FindUsageZone.cs +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/IAutoMapper.FindUsageZone.cs @@ -1,9 +1,6 @@ using JetBrains.Application.BuildScript.Application.Zones; -namespace ReSharperPlugin.AutoMapper.FindUsage -{ - [ZoneDefinition] - public interface IAutoMapperFindUsageZone : IZone - { - } -} +namespace ReSharperPlugin.AutoMapper.FindUsage; + +[ZoneDefinition] +public interface IAutoMapperFindUsageZone : IZone; \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Navigation/AutoMapperNavigationAction.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Navigation/AutoMapperNavigationAction.cs deleted file mode 100644 index c07f7e9..0000000 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Navigation/AutoMapperNavigationAction.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Application.Progress; -using JetBrains.ProjectModel; -using JetBrains.ReSharper.Feature.Services.Bulbs; -using JetBrains.ReSharper.Feature.Services.ContextActions; -using JetBrains.ReSharper.Feature.Services.CSharp.ContextActions; -using JetBrains.ReSharper.Feature.Services.Intentions; -using JetBrains.ReSharper.Feature.Services.Navigation; -using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Psi.CSharp.Tree; -using JetBrains.ReSharper.Psi.Modules; -using JetBrains.ReSharper.Psi.Tree; -using JetBrains.TextControl; -using JetBrains.ReSharper.Psi.Resources; -using JetBrains.Util; -using ReSharperPlugin.AutoMapper.FindUsage.Registrations; - -namespace ReSharperPlugin.AutoMapper.FindUsage.Navigation; - -[ContextAction(Name = "AutoMapper Navigation", Description = "Navigate to AutoMapper source", - GroupType = typeof(CSharpContextActions))] -public class AutoMapperNavigationAction : IContextAction -{ - private readonly ICSharpContextActionDataProvider _dataProvider; - private readonly AutoMapperRegistrationFinder _registrationFinder; - - public AutoMapperNavigationAction(ICSharpContextActionDataProvider dataProvider) - { - _dataProvider = dataProvider; - _registrationFinder = dataProvider.Solution.GetComponent(); - } - - public IEnumerable CreateBulbItems() - { - return CreateBulbItemsInternal(); - } - - private IEnumerable CreateBulbItemsInternal() - { - var property = GetSelectedProperty(); - if (property == null) return EmptyList.Instance; - - var containingType = property.ContainingType; - if (containingType == null) return EmptyList.Instance; - - var mappings = _registrationFinder.FindMappingsForType(containingType); - if (mappings.Count == 0) return EmptyList.Instance; - - var itemsToCreate = new List<(IProperty property, IType type)>(); - var processedProperties = new HashSet(); - - foreach (var mapping in mappings) - { - if (mapping.IgnoredProperties.Contains(property.ShortName)) - continue; - - var otherType = mapping.Source; - if (otherType == null) continue; - - var otherProperty = FindCorrespondingProperty(otherType, property.ShortName); - if (otherProperty != null && processedProperties.Add(otherProperty)) - { - itemsToCreate.Add((otherProperty, otherType)); - } - } - - if (itemsToCreate.Count == 0) return EmptyList.Instance; - - var result = new List(); - var useFullName = itemsToCreate.Count > 1; - - foreach (var item in itemsToCreate) - { - var typeName = useFullName - ? GetFullName(item.type, property.PresentationLanguage) - : item.type.GetPresentableName(property.PresentationLanguage); - - var text = $"AutoMapper. Navigate to source: {typeName}.{property.ShortName}"; - result.Add(new IntentionAction(new NavigateToPropertyBulbAction(item.property, text), - PsiSymbolsThemedIcons.Other.Id, IntentionsAnchors.ContextActionsAnchor)); - } - - return result; - } - - public bool IsAvailable(IUserDataHolder cache) - { - if (!IsAutoMapperAvailable(_dataProvider.PsiModule)) - return false; - - var property = GetSelectedProperty(); - if (property == null) - return false; - - return true; - } - - private static bool IsAutoMapperAvailable(IPsiModule module) - { - return TypeFactory.CreateTypeByCLRName("AutoMapper.Profile", module).GetTypeElement() != null; - } - - private IProperty GetSelectedProperty() - { - var node = _dataProvider.GetSelectedTreeNode(); - if (node == null) - return null; - - var accessorDeclaration = node.GetContainingNode(true); - if (accessorDeclaration is { Kind: AccessorKind.SETTER }) - { - var propertyDeclaration = accessorDeclaration.GetContainingNode(); - return propertyDeclaration?.DeclaredElement; - } - - return null; - } - - private static IProperty FindCorrespondingProperty(IType type, string propertyName) - { - var typeElement = (type as IDeclaredType)?.GetTypeElement(); - - return typeElement?.Properties.FirstOrDefault(p => p.ShortName == propertyName); - } - - private static string GetFullName(IType type, PsiLanguageType language) - { - if (type is IDeclaredType declaredType) - { - var typeElement = declaredType.GetTypeElement(); - if (typeElement != null) - { - return typeElement.GetClrName().FullName; - } - } - - return type.GetPresentableName(language); - } -} - -public class NavigateToPropertyBulbAction : BulbActionBase -{ - private readonly IProperty _property; - public override string Text { get; } - - public NavigateToPropertyBulbAction(IProperty property, string text) - { - _property = property; - Text = text; - } - - public override void Execute(ISolution solution, ITextControl textControl) - { - _property.Navigate(true); - } - - protected override Action - ExecutePsiTransaction(ISolution solution, IProgressIndicator progress) => null; -} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/ReSharperPlugin.AutoMapper.FindUsage.csproj b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/ReSharperPlugin.AutoMapper.FindUsage.csproj index 11db438..6c974ca 100644 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/ReSharperPlugin.AutoMapper.FindUsage.csproj +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/ReSharperPlugin.AutoMapper.FindUsage.csproj @@ -16,8 +16,11 @@ - - + + + + <_Parameter1>ReSharperPlugin.AutoMapper.FindUsage.Tests + diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperCache.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperCache.cs new file mode 100644 index 0000000..cd9ee84 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperCache.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Application.Parts; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Caches; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.Serialization; +using JetBrains.Util; +using JetBrains.Util.PersistentMap; +using JetBrains.Application.Threading; +using JetBrains.Collections; +using JetBrains.Lifetimes; +using JetBrains.ReSharper.Psi.Files; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Registrations; + +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] +public class AutoMapperCache : SimpleICache> +{ + public AutoMapperCache(Lifetime lifetime, IShellLocks shellLocks, IPersistentIndexManager persistentIndexManager) + : base(lifetime, shellLocks, persistentIndexManager, Marshaller.Instance) + { + } + + private class Marshaller : IUnsafeMarshaller> + { + public static readonly Marshaller Instance = new Marshaller(); + + public void Marshal(UnsafeWriter writer, List value) + { + writer.WriteInt32(value.Count); + foreach (var mapping in value) + mapping.Write(writer); + } + + public List Unmarshal(UnsafeReader reader) + { + var count = reader.ReadInt32(); + var result = new List(count); + for (var i = 0; i < count; i++) + result.Add(SerializableMapping.Read(reader)); + return result; + } + } + + public override string Version => "7"; + + protected override bool IsApplicable(IPsiSourceFile sourceFile) + { + return true; + } + + public override object Build(IPsiSourceFile sourceFile, bool isPreParent) + { + var results = new List(); + foreach (var file in sourceFile.GetPsiFiles()) + { + if (file is ICSharpFile sharpFile) + { + sharpFile.ProcessDescendants(new AutoMapperProcessor(results)); + } + } + + results.TrimExcess(); + return results.Count > 0 ? results : null; + } + + public IEnumerable<(IPsiSourceFile, SerializableMapping)> GetMappingsForType(string typeClrName) + { + foreach (var (sourceFile, mappings) in Map) + { + if (mappings == null) continue; + foreach (var mapping in mappings) + { + if (mapping.SourceTypeClrName == typeClrName || mapping.DestinationTypeClrName == typeClrName) + { + yield return (sourceFile, mapping); + } + } + } + } + + public void EnsureMappingsBuilt(IEnumerable sourceFiles) + { + foreach (var sourceFile in sourceFiles) + { + if (sourceFile == null || Map.ContainsKey(sourceFile)) + continue; + + if (Build(sourceFile, isPreParent: false) is List built && built.Count > 0) + Map[sourceFile] = built; + } + } + + private class AutoMapperProcessor : IRecursiveElementProcessor + { + private readonly List _results; + + public AutoMapperProcessor(List results) + { + _results = results; + } + + public bool ProcessingIsFinished => false; + public bool InteriorShouldBeProcessed(ITreeNode element) => true; + + public void ProcessBeforeInterior(ITreeNode element) + { + if (element is IInvocationExpression invocation) + { + var reference = invocation.Reference; + if (reference.GetName() != "CreateMap") return; + + var resolveResult = reference.Resolve(); + var method = resolveResult.DeclaredElement as IMethod; + + if (method != null && !IsAutoMapperMethod(method)) + return; + + if (TryGetMappingTypes(resolveResult.Substitution, method, invocation, out var tSource, out var tDest)) + { + var sourceName = GetTypeClrName(tSource); + var destName = GetTypeClrName(tDest); + + if (sourceName != null && destName != null && sourceName != destName) + { + var ignoredProperties = GetIgnoredProperties(invocation); + _results.Add(new SerializableMapping + { + SourceTypeClrName = sourceName, + DestinationTypeClrName = destName, + IgnoredProperties = ignoredProperties.ToList(), + HasReverseMap = HasReverseMap(invocation) + }); + } + } + } + } + + private static string GetTypeClrName(IType type) + { + var scalarType = type.GetScalarType(); + if (scalarType == null) return null; + + var clrName = scalarType.GetClrName().FullName; + if (!string.IsNullOrEmpty(clrName)) return clrName; + + return null; + } + + public void ProcessAfterInterior(ITreeNode element) + { + } + + private static bool IsAutoMapperMethod(IMethod method) + { + var type = method.ContainingType; + if (type == null) return false; + var clrName = type.GetClrName().FullName; + return clrName is "AutoMapper.ProfileExtensions" or + "AutoMapper.Profile" or + "AutoMapper.IProfileExpression" or + "AutoMapper.IMapperConfigurationExpression"; + } + + private static bool TryGetMappingTypes(ISubstitution substitution, IMethod method, + IInvocationExpression invocation, + out IType sourceType, out IType destinationType) + { + sourceType = null; + destinationType = null; + + if (method != null) + { + if (method.TypeParametersCount >= 2) + { + sourceType = substitution[method.TypeParameters[0]]; + destinationType = substitution[method.TypeParameters[1]]; + } + + sourceType ??= FindSubstitutedTypeByNames(substitution, "TSource"); + destinationType ??= FindSubstitutedTypeByNames(substitution, "TDestination", "TDest"); + } + + if ((sourceType == null || destinationType == null) && invocation.TypeArguments.Count >= 2) + { + sourceType = invocation.TypeArguments[0]; + destinationType = invocation.TypeArguments[1]; + } + + return sourceType != null && destinationType != null; + } + + private static IType FindSubstitutedTypeByNames(ISubstitution substitution, params string[] names) + { + return substitution.Domain.Where(typeParameter => names.Contains(typeParameter.ShortName)) + .Select(typeParameter => substitution[typeParameter]).FirstOrDefault(); + } + + private static bool HasReverseMap(IInvocationExpression invocation) + { + var current = invocation.Parent; + while (current != null) + { + if (current is IReferenceExpression refExp && refExp.Reference.GetName() == "ReverseMap") + return true; + + if (current is IInvocationExpression { InvokedExpression: IReferenceExpression re } && + re.Reference.GetName() == "ReverseMap") + return true; + + if (current is IExpressionStatement) break; + current = current.Parent; + } + + return false; + } + + private static ISet GetIgnoredProperties(IInvocationExpression invocation) + { + var ignoredProperties = new HashSet(); + var current = invocation.Parent; + while (current != null) + { + if (current is IInvocationExpression forMemberInvocation && + forMemberInvocation.InvokedExpression is IReferenceExpression { Reference: var reference } && + reference.GetName() == "ForMember") + { + if (IsIgnore(forMemberInvocation)) + { + var propertyName = GetPropertyName(forMemberInvocation); + if (propertyName != null) + ignoredProperties.Add(propertyName); + } + } + + if (current is IExpressionStatement) break; + current = current.Parent; + } + + return ignoredProperties; + } + + private static bool IsIgnore(IInvocationExpression forMemberInvocation) + { + if (forMemberInvocation.Arguments.Count < 2) return false; + var optArg = forMemberInvocation.Arguments[1].Expression; + if (optArg is ILambdaExpression lambda) + { + var body = lambda.BodyExpression; + if (body is IInvocationExpression + { + InvokedExpression: IReferenceExpression { Reference: var reference } + } && + reference.GetName() == "Ignore") + return true; + } + + return false; + } + + private static string GetPropertyName(IInvocationExpression forMemberInvocation) + { + if (forMemberInvocation.Arguments.Count < 1) return null; + var propArg = forMemberInvocation.Arguments[0].Expression; + if (propArg is ILambdaExpression { BodyExpression: IReferenceExpression refExp }) + return refExp.Reference.GetName(); + return null; + } + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperMapping.cs index 5c2524f..f819a45 100644 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperMapping.cs +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperMapping.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Psi.Tree; namespace ReSharperPlugin.AutoMapper.FindUsage.Registrations; @@ -9,15 +8,14 @@ public sealed class AutoMapperMapping { public IType Source { get; } public IType Destination { get; } - public ITreeNode Registration { get; } - public ISet IgnoredProperties { get; } + public ICollection IgnoredProperties { get; } + public bool HasReverseMap { get; } - public AutoMapperMapping(IType source, IType destination, ITreeNode registration, - ISet ignoredProperties = null) + public AutoMapperMapping(IType source, IType destination, bool hasReverseMap, ICollection ignoredProperties = null) { Source = source; Destination = destination; - Registration = registration; + HasReverseMap = hasReverseMap; IgnoredProperties = ignoredProperties ?? ImmutableHashSet.Empty; } } \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperRegistrationFinder.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperRegistrationFinder.cs index c150b93..f5abcc5 100644 --- a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperRegistrationFinder.cs +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperRegistrationFinder.cs @@ -1,239 +1,85 @@ using System.Collections.Generic; -using System.Linq; using JetBrains.Application.Parts; using JetBrains.ProjectModel; using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Psi.Caches; -using JetBrains.ReSharper.Psi.CSharp.Tree; -using JetBrains.ReSharper.Psi.Resolve; -using JetBrains.ReSharper.Psi.Search; -using JetBrains.Util; namespace ReSharperPlugin.AutoMapper.FindUsage.Registrations; [SolutionComponent(Instantiation.DemandAnyThreadSafe)] public class AutoMapperRegistrationFinder { - private readonly ISolution _solution; + private readonly AutoMapperCache _cache; - public AutoMapperRegistrationFinder(ISolution solution) + public AutoMapperRegistrationFinder(AutoMapperCache cache) { - _solution = solution; + _cache = cache; } public IReadOnlyCollection FindMappingsForType(ITypeElement typeElement) { - var psiServices = _solution.GetPsiServices(); - var symbolScope = psiServices.Symbols.GetSymbolScope(LibrarySymbolScope.FULL, true); + var typeClrName = typeElement.GetClrName().FullName; + var fromIndex = new List(); + var seenFromIndex = new HashSet(); - var mappings = new List(); - var processedTypes = new HashSet(); - - // CreateMap is defined as an extension method in ProfileExtensions - // and also directly on Profile base class - TryFindIn("AutoMapper.ProfileExtensions"); - TryFindIn("AutoMapper.Profile"); - TryFindIn("AutoMapper.IProfileExpression"); - TryFindIn("AutoMapper.IMapperConfigurationExpression"); - - return mappings; - - void TryFindIn(string clrName) + foreach (var (_, serializableMapping) in _cache.GetMappingsForType(typeClrName)) { - var type = symbolScope.GetTypeElementByCLRName(clrName); - if (type != null && processedTypes.Add(type)) - { - mappings.AddRange(FindMappingsInType(type, typeElement)); - } + AddIfMatches(typeElement, typeClrName, serializableMapping, fromIndex, seenFromIndex); } + + return fromIndex; } - private IReadOnlyCollection FindMappingsInType(ITypeElement ownerType, ITypeElement targetType) + internal void EnsureProjectMappings(ITypeElement typeElement) { - var psiServices = _solution.GetPsiServices(); - - var createMapMethods = ownerType.Methods - .Where(m => m.ShortName == "CreateMap") - .ToList(); - - var searchDomain = psiServices.SearchDomainFactory.CreateSearchDomain(_solution, includeLibraries: false); - var results = new List(4); - - foreach (var method in createMapMethods) - { - psiServices.SingleThreadedFinder.FindReferences(method, searchDomain, - new ReferenceConsumer(results, targetType), - JetBrains.Application.Progress.NullProgressIndicator.Create()); - } - - return results; + _cache.EnsureMappingsBuilt(GetProjectSourceFiles(typeElement)); } - private class ReferenceConsumer : IFindResultConsumer + private static void AddIfMatches(ITypeElement contextTypeElement, string typeClrName, + SerializableMapping serializableMapping, + List results, HashSet seen) { - private readonly List _results; - private readonly ITypeElement _targetType; + if (serializableMapping.SourceTypeClrName == serializableMapping.DestinationTypeClrName) + return; - public ReferenceConsumer(List results, ITypeElement targetType) - { - _results = results; - _targetType = targetType; - } + if (serializableMapping.DestinationTypeClrName != typeClrName && + serializableMapping.SourceTypeClrName != typeClrName) + return; - public IReference Build(FindResult result) - { - return (result as FindResultReference)?.Reference; - } + var key = serializableMapping.SourceTypeClrName + "->" + serializableMapping.DestinationTypeClrName; + if (!seen.Add(key)) + return; - public FindExecution Merge(IReference result) - { - if (result == null) return FindExecution.Continue; - var invocation = result.GetTreeNode().GetContainingNode(); - if (invocation != null) - { - var resolveResult = invocation.Reference.Resolve(); - if (resolveResult.DeclaredElement is IMethod m) - { - if (TryGetMappingTypes(resolveResult.Substitution, m, out var tSource, out var tDest)) - { - if (IsTargetType(tDest, _targetType)) - { - var ignoredProperties = GetIgnoredProperties(invocation); - var mapping = new AutoMapperMapping(tSource, tDest, invocation, ignoredProperties); - _results.Add(mapping); - } - - if (HasReverseMap(invocation) && IsTargetType(tSource, _targetType)) - { - var mapping = new AutoMapperMapping(tDest, tSource, invocation); - _results.Add(mapping); - } - } - } - } - - return FindExecution.Continue; - } + var sourceType = + TypeFactory.CreateTypeByCLRName(serializableMapping.SourceTypeClrName, contextTypeElement.Module); + var destType = + TypeFactory.CreateTypeByCLRName(serializableMapping.DestinationTypeClrName, contextTypeElement.Module); - private static bool TryGetMappingTypes(ISubstitution substitution, IMethod method, out IType sourceType, - out IType destinationType) + if (sourceType.GetTypeElement() == null || destType.GetTypeElement() == null) { - sourceType = null; - destinationType = null; - - // Standard generic method case: CreateMap - if (method.TypeParametersCount >= 2) - { - sourceType = substitution[method.TypeParameters[0]]; - destinationType = substitution[method.TypeParameters[1]]; - } - - // Reduced extension method/fallback case: look up by generic parameter names - sourceType ??= FindSubstitutedTypeByNames(substitution, "TSource"); - destinationType ??= FindSubstitutedTypeByNames(substitution, "TDestination", "TDest"); - - return sourceType != null && destinationType != null; + return; } - private static IType FindSubstitutedTypeByNames(ISubstitution substitution, params string[] names) - { - return substitution.Domain.Where(typeParameter => names.Contains(typeParameter.ShortName)) - .Select(typeParameter => substitution[typeParameter]).FirstOrDefault(); - } + results.Add(new AutoMapperMapping(sourceType, destType, serializableMapping.HasReverseMap, + serializableMapping.IgnoredProperties)); } - private static bool IsTargetType(IType type, ITypeElement targetType) - { - return (type as IDeclaredType)?.GetTypeElement()?.Equals(targetType) == true; - } - private static bool HasReverseMap(IInvocationExpression invocation) + private static IEnumerable GetProjectSourceFiles(ITypeElement typeElement) { - // Usually it's .CreateMap().ReverseMap() - // In PSI it looks like ReverseMap(CreateMap(S, D)) - - var current = invocation.Parent; - while (current != null) + foreach (var declaration in typeElement.GetDeclarations()) { - if (current is IReferenceExpression refExp && refExp.Reference.GetName() == "ReverseMap") - { - return true; - } - - if (current is IInvocationExpression { InvokedExpression: IReferenceExpression re } && - re.Reference.GetName() == "ReverseMap") - { - return true; - } - - if (current is IExpressionStatement) break; - current = current.Parent; - } - - return false; - } - - private static ISet GetIgnoredProperties(IInvocationExpression invocation) - { - var ignoredProperties = new HashSet(); - - var current = invocation.Parent; - while (current != null) - { - if (current is IInvocationExpression forMemberInvocation && - forMemberInvocation.InvokedExpression is IReferenceExpression { Reference: var reference } && - reference.GetName() == "ForMember") - { - if (IsIgnore(forMemberInvocation)) - { - var propertyName = GetPropertyName(forMemberInvocation); - if (propertyName != null) - { - ignoredProperties.Add(propertyName); - } - } - } + var sourceFile = declaration.GetSourceFile(); + var projectFile = sourceFile?.ToProjectFile(); + var project = projectFile?.GetProject(); + if (project == null) + continue; - if (current is IExpressionStatement) break; - current = current.Parent; - } - - return ignoredProperties; - } - - private static bool IsIgnore(IInvocationExpression forMemberInvocation) - { - // ForMember(it => it.Prop, opt => opt.Ignore()) - if (forMemberInvocation.Arguments.Count < 2) return false; - - var optArg = forMemberInvocation.Arguments[1].Expression; - if (optArg is ILambdaExpression lambda) - { - var body = lambda.BodyExpression; - // Handle opt => opt.Ignore() - if (body is IInvocationExpression - { - InvokedExpression: IReferenceExpression { Reference: var reference } - } && reference.GetName() == "Ignore") + foreach (var pf in project.GetAllProjectFiles()) { - return true; + var candidate = pf.ToSourceFile(); + if (candidate != null) + yield return candidate; } } - - return false; - } - - private static string GetPropertyName(IInvocationExpression forMemberInvocation) - { - // ForMember(it => it.Prop, ...) - if (forMemberInvocation.Arguments.Count < 1) return null; - - var propArg = forMemberInvocation.Arguments[0].Expression; - if (propArg is ILambdaExpression { BodyExpression: IReferenceExpression refExp }) - { - return refExp.Reference.GetName(); - } - - return null; } } \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperSearchFactory.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperSearchFactory.cs new file mode 100644 index 0000000..118fcc2 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/AutoMapperSearchFactory.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Application.Parts; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.CSharp.Impl.Occurrences; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.ExtensionsAPI; +using JetBrains.ReSharper.Psi.Search; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Registrations; + +[PsiComponent(Instantiation.DemandAnyThreadSafe)] +public class AutoMapperSearchFactory : DomainSpecificSearcherFactoryBase +{ + private readonly AutoMapperRegistrationFinder _registrationFinder; + + public AutoMapperSearchFactory(AutoMapperRegistrationFinder registrationFinder) + { + _registrationFinder = registrationFinder; + } + + public override bool IsCompatibleWithLanguage(PsiLanguageType languageType) + { + return languageType is CSharpLanguage; + } + + public override IEnumerable GetRelatedFindResults(IDeclaredElement element) + { + if (element is IMethod method && GetPropertyFromAccessor(method) is { } ownerProperty) + element = ownerProperty; + + var seenResults = new HashSet(); + + return element switch + { + ITypeElement typeElement => GetTypeResults(typeElement, seenResults), + IProperty { ContainingType: { } containingType } property => GetPropertyResults(containingType, property, + seenResults), + _ => [] + }; + } + + private IEnumerable GetPropertyResults(ITypeElement containingType, IProperty property, + HashSet seenResults) + { + var mappings = _registrationFinder.FindMappingsForType(containingType); + + foreach (var mapping in mappings) + { + if (mapping.IgnoredProperties.Contains(property.ShortName)) + { + continue; + } + + var isSource = mapping.Source is IDeclaredType s && s.GetTypeElement()?.Equals(containingType) == true; + var isDestination = mapping.Destination is IDeclaredType d && d.GetTypeElement()?.Equals(containingType) == true; + + if (isSource && mapping.HasReverseMap) + { + if (mapping.Destination is IDeclaredType destType && destType.GetTypeElement() is { } destElement && !destElement.Equals(containingType)) + { + foreach (var result in CreatePropertyRelatedFindResults(destElement, property.ShortName)) + if (seenResults.Add(result)) yield return result; + } + } + + if (isDestination) + { + if (mapping.Source is IDeclaredType sourceType && sourceType.GetTypeElement() is { } sourceElement && !sourceElement.Equals(containingType)) + { + foreach (var result in CreatePropertyRelatedFindResults(sourceElement, property.ShortName)) + if (seenResults.Add(result)) yield return result; + } + } + } + } + + private IEnumerable GetTypeResults(ITypeElement typeElement, HashSet seenResults) + { + var mappings = _registrationFinder.FindMappingsForType(typeElement); + + foreach (var mapping in mappings) + { + var isSource = mapping.Source is IDeclaredType s && s.GetTypeElement()?.Equals(typeElement) == true; + var isDestination = mapping.Destination is IDeclaredType d && d.GetTypeElement()?.Equals(typeElement) == true; + + if (isSource && mapping.HasReverseMap) + { + if (mapping.Destination is IDeclaredType destType && destType.GetTypeElement() is { } destElement && !destElement.Equals(typeElement)) + { + foreach (var result in CreateFindResults(destElement)) + if (seenResults.Add(result)) yield return result; + } + } + + if (isDestination) + { + if (mapping.Source is IDeclaredType sourceType && sourceType.GetTypeElement() is { } sourceElement && !sourceElement.Equals(typeElement)) + { + foreach (var result in CreateFindResults(sourceElement)) + if (seenResults.Add(result)) yield return result; + } + } + } + } + + private static IEnumerable CreateFindResults(IDeclaredElement element) + { + foreach (var declaration in element.GetDeclarations()) + { + yield return new FindResultInitializer(declaration); + } + } + + private static IEnumerable CreatePropertyRelatedFindResults(ITypeElement typeElement, string propertyName) + { + var property = typeElement.Properties.FirstOrDefault(p => p.ShortName == propertyName); + if (property != null) + { + foreach (var declaration in property.GetDeclarations()) + { + if (declaration is IPropertyDeclaration propertyDeclaration) + { + var nameNode = propertyDeclaration.NameIdentifier; + if (nameNode != null) + { + yield return new FindResultInitializer(nameNode); + yield break; + } + } + } + + yield return new FindResultDeclaredElement(property); + } + } + + private static IProperty GetPropertyFromAccessor(IDeclaredElement element) + { + foreach (var declaration in element.GetDeclarations()) + { + var propertyDeclaration = declaration.GetContainingNode(true); + if (propertyDeclaration?.DeclaredElement is { } property) + return property; + } + + return null; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/SerializableMapping.cs b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/SerializableMapping.cs new file mode 100644 index 0000000..43d829f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.AutoMapper.FindUsage/Registrations/SerializableMapping.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using JetBrains.Serialization; + +namespace ReSharperPlugin.AutoMapper.FindUsage.Registrations; + +public class SerializableMapping +{ + public string SourceTypeClrName; + public string DestinationTypeClrName; + public List IgnoredProperties; + public bool HasReverseMap; + + public void Write(UnsafeWriter writer) + { + writer.WriteString(SourceTypeClrName); + writer.WriteString(DestinationTypeClrName); + writer.WriteInt32(IgnoredProperties.Count); + foreach (var prop in IgnoredProperties) + writer.WriteString(prop); + writer.WriteBoolean(HasReverseMap); + } + + public static SerializableMapping Read(UnsafeReader reader) + { + var mapping = new SerializableMapping + { + SourceTypeClrName = reader.ReadString(), + DestinationTypeClrName = reader.ReadString() + }; + var count = reader.ReadInt32(); + mapping.IgnoredProperties = new List(count); + for (var i = 0; i < count; i++) + mapping.IgnoredProperties.Add(reader.ReadString()); + mapping.HasReverseMap = reader.ReadBoolean(); + return mapping; + } +} diff --git a/src/rider/main/resources/META-INF/plugin.xml b/src/rider/main/resources/META-INF/plugin.xml index 220987f..55a313a 100644 --- a/src/rider/main/resources/META-INF/plugin.xml +++ b/src/rider/main/resources/META-INF/plugin.xml @@ -1,14 +1,25 @@ me.rogatnev.automapper.findusage AutoMapper.FindUsage - 0.1.2 + 0.2 Sergey Rogatnev com.intellij.modules.rider ReSharper plugin for easy navigation from destination properties to source properties in AutoMapper mappings.

+

Plugin for JetBrains Rider and ReSharper that provides seamless navigation between DTOs and Models based on your AutoMapper configurations.

+

Features:

+
    +
  • Find Usages integration for properties and types.
  • +
  • Bidirectional mapping support (including .ReverseMap()).
  • +
  • Support for set and init accessors.
  • +
  • Automatic recognition of CreateMap<TSource, TDest>() configurations.
  • +
  • Smart grouping in the Find Results window.
  • +
+

Find Usages Demo

+

Find Results window:

+

Search Results

]]>