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 Results window:
+
+
## 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 Results window:
+
]]>