From 48f4d6af2baf6d07932c54df8a05cddabc49ae67 Mon Sep 17 00:00:00 2001
From: hduelme <46139144+hduelme@users.noreply.github.com>
Date: Sun, 21 Jul 2024 17:25:30 +0200
Subject: [PATCH] Add inspect for target property mapped more than once (#200)
---
README.md | 1 +
description.html | 1 +
...tPropertyMappedMoreThanOnceInspection.java | 210 ++++++++++++++++++
.../util/MapstructAnnotationUtils.java | 34 +--
.../intellij/util/MapstructUtil.java | 3 +-
.../mapstruct/intellij/util/TargetUtils.java | 7 +-
src/main/resources/META-INF/plugin.xml | 8 +
...tPropertyMappedMoreThanOnceInspection.html | 29 +++
.../messages/MapStructBundle.properties | 4 +
...pertyMappedMoreThanOnceInspectionTest.java | 68 ++++++
.../TargetPropertyMappedMoreThanOnce.java | 97 ++++++++
...argetPropertyMappedMoreThanOnce_after.java | 93 ++++++++
12 files changed, 537 insertions(+), 18 deletions(-)
create mode 100644 src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
create mode 100644 src/main/resources/inspectionDescriptions/TargetPropertyMappedMoreThanOnceInspection.html
create mode 100644 src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
create mode 100644 testData/inspection/TargetPropertyMappedMoreThanOnce.java
create mode 100644 testData/inspection/TargetPropertyMappedMoreThanOnce_after.java
diff --git a/README.md b/README.md
index 47f98315..57f246d3 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ To learn more about MapStruct have a look at the [mapstruct](https://github.com/
* No `source` defined in `@Mapping` annotation
* More than one `source` in `@Mapping` annotation defined with quick fixes: Remove `source`. Remove `constant`. Remove `expression`. Use `constant` as `defaultValue`. Use `expression` as `defaultExpression`.
* More than one default source in `@Mapping` annotation defined with quick fixes: Remove `defaultValue`. Remove `defaultExpression`.
+ * `target` mapped more than once by `@Mapping` annotations with quick fixes: Remove annotation and change target property.
## Requirements
diff --git a/description.html b/description.html
index cf79fbad..e658e17a 100644
--- a/description.html
+++ b/description.html
@@ -41,6 +41,7 @@
No source
defined in @Mapping
annotation
More than one source
in @Mapping
annotation defined with quick fixes: Remove source
. Remove constant
. Remove expression
. Use constant
as defaultValue
. Use expression
as defaultExpression
.
More than one default source in @Mapping
annotation defined with quick fixes: Remove defaultValue
. Remove defaultExpression
.
+ target
mapped more than once by @Mapping
annotations with quick fixes: Remove annotation and change target property.
diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
new file mode 100644
index 00000000..72a59816
--- /dev/null
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.intellij.inspection;
+
+import com.intellij.codeInsight.intention.QuickFixFactory;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
+import com.intellij.codeInspection.LocalQuickFixOnPsiElement;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.codeInspection.util.IntentionFamilyName;
+import com.intellij.codeInspection.util.IntentionName;
+import com.intellij.openapi.editor.CaretState;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.ScrollType;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.TextEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.Strings;
+import com.intellij.psi.JavaElementVisitor;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiAnnotationMemberValue;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.impl.source.tree.java.PsiAnnotationImpl;
+import org.jetbrains.annotations.NotNull;
+import org.mapstruct.intellij.MapStructBundle;
+import org.mapstruct.intellij.util.MapStructVersion;
+import org.mapstruct.intellij.util.MapstructUtil;
+import org.mapstruct.intellij.util.TargetUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
+import static org.mapstruct.intellij.util.MapstructAnnotationUtils.extractMappingAnnotationsFromMappings;
+import static org.mapstruct.intellij.util.MapstructUtil.MAPPINGS_ANNOTATION_FQN;
+import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN;
+import static org.mapstruct.intellij.util.TargetUtils.getTargetType;
+
+/**
+ * @author hduelme
+ */
+public class TargetPropertyMappedMoreThanOnceInspection extends InspectionBase {
+ @NotNull
+ @Override
+ PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
+ return new TargetPropertyMappedMoreThanOnceInspection.MyJavaElementVisitor( holder,
+ MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) );
+ }
+
+ private static class MyJavaElementVisitor extends JavaElementVisitor {
+ private final ProblemsHolder holder;
+ private final MapStructVersion mapStructVersion;
+
+ private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) {
+ this.holder = holder;
+ this.mapStructVersion = mapStructVersion;
+ }
+
+ @Override
+ public void visitMethod(PsiMethod method) {
+ if ( !MapstructUtil.isMapper( method.getContainingClass() ) ) {
+ return;
+ }
+ PsiType targetType = getTargetType( method );
+ if ( targetType == null ) {
+ return;
+ }
+ Map> problemMap = new HashMap<>();
+ for (PsiAnnotation psiAnnotation : method.getAnnotations()) {
+ String qualifiedName = psiAnnotation.getQualifiedName();
+ if ( MAPPING_ANNOTATION_FQN.equals( qualifiedName ) ) {
+ handleMappingAnnotation( psiAnnotation, problemMap );
+ }
+ else if (MAPPINGS_ANNOTATION_FQN.equals( qualifiedName )) {
+ extractMappingAnnotationsFromMappings( psiAnnotation )
+ .forEach( a -> handleMappingAnnotation( a, problemMap ) );
+ }
+ else {
+ // Handle annotations containing at least one Mapping annotation
+ handleAnnotationWithMappingAnnotation( psiAnnotation, problemMap );
+ }
+ }
+ QuickFixFactory quickFixFactory = QuickFixFactory.getInstance();
+ for (Map.Entry> problem : problemMap.entrySet()) {
+ List problemElements = problem.getValue();
+ if (problemElements.size() > 1) {
+ for (PsiElement problemElement : problemElements) {
+ LocalQuickFix[] quickFixes = getLocalQuickFixes( problemElement, quickFixFactory );
+ holder.registerProblem( problemElement,
+ MapStructBundle.message( "inspection.target.property.mapped.more.than.once",
+ problem.getKey() ), quickFixes );
+ }
+ }
+ }
+ }
+
+ private static @NotNull LocalQuickFix[] getLocalQuickFixes(PsiElement problemElement,
+ QuickFixFactory quickFixFactory) {
+ List quickFixes = new ArrayList<>(2);
+ if (problemElement instanceof PsiAnnotation) {
+ quickFixes.add( getDeleteFix( problemElement, quickFixFactory ) );
+ }
+ else if (problemElement instanceof PsiAnnotationMemberValue problemPsiAnnotationMemberValue) {
+ Optional.ofNullable( problemElement.getParent() ).map( PsiElement::getParent )
+ .map( PsiElement::getParent ).filter( PsiAnnotation.class::isInstance )
+ .ifPresent( annotation -> quickFixes.add(
+ getDeleteFix( annotation, quickFixFactory ) ) );
+ quickFixes.add( new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ) );
+ }
+ return quickFixes.toArray( new LocalQuickFix[]{} );
+ }
+
+ private static @NotNull LocalQuickFixAndIntentionActionOnPsiElement getDeleteFix(
+ @NotNull PsiElement problemElement, @NotNull QuickFixFactory quickFixFactory) {
+
+ String annotationName = PsiAnnotationImpl.getAnnotationShortName( problemElement.getText() );
+ return quickFixFactory.createDeleteFix( problemElement,
+ MapStructBundle.message( "intention.remove.annotation", annotationName ) );
+ }
+
+ private void handleAnnotationWithMappingAnnotation(PsiAnnotation psiAnnotation,
+ Map> problemMap) {
+ PsiClass annotationClass = psiAnnotation.resolveAnnotationType();
+ if (annotationClass == null) {
+ return;
+ }
+ TargetUtils.findAllDefinedMappingTargets( annotationClass, mapStructVersion )
+ .forEach( target ->
+ problemMap.computeIfAbsent( target, k -> new ArrayList<>() ).add( psiAnnotation ) );
+ }
+
+ private static void handleMappingAnnotation(PsiAnnotation psiAnnotation,
+ Map> problemMap) {
+ PsiAnnotationMemberValue value = psiAnnotation.findDeclaredAttributeValue( "target" );
+ if (value != null) {
+ String target = getStringAttributeValue( value );
+ if (target != null) {
+ problemMap.computeIfAbsent( target, k -> new ArrayList<>() ).add( value );
+ }
+ }
+ }
+
+ private static class ChangeTargetQuickFix extends LocalQuickFixOnPsiElement {
+
+ private final String myText;
+ private final String myFamilyName;
+
+ private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) {
+ super( element );
+ myText = MapStructBundle.message( "intention.change.target.property" );
+ myFamilyName = MapStructBundle.message( "inspection.target.property.mapped.more.than.once",
+ element.getText() );
+ }
+
+ @Override
+ public @IntentionName @NotNull String getText() {
+ return myText;
+ }
+
+ @Override
+ public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement,
+ @NotNull PsiElement psiElement1) {
+ FileEditor selectedEditor = FileEditorManager.getInstance( project ).getSelectedEditor();
+ if ( selectedEditor instanceof TextEditor textEditor) {
+ Editor editor = textEditor.getEditor();
+
+ TextRange textRange = psiElement.getTextRange();
+ String textOfElement = String.valueOf( editor.getDocument()
+ .getCharsSequence()
+ .subSequence( textRange.getStartOffset(), textRange.getEndOffset() ) );
+ int targetStart = Strings.indexOf( textOfElement, "\"" ) + 1;
+ int targetEnd = textOfElement.lastIndexOf( "\"" );
+
+ editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetStart );
+ LogicalPosition startPosition = editor.getCaretModel().getLogicalPosition();
+ editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetEnd );
+ editor.getCaretModel().setCaretsAndSelections(
+ Collections.singletonList( new CaretState(startPosition, startPosition,
+ editor.getCaretModel().getLogicalPosition() ) ) );
+ editor.getScrollingModel().scrollToCaret( ScrollType.MAKE_VISIBLE );
+ }
+ }
+
+ @Override
+ public @IntentionFamilyName @NotNull String getFamilyName() {
+ return myFamilyName;
+ }
+
+ @Override
+ public boolean availableInBatchMode() {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java b/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java
index ba9c2b1f..caba7770 100644
--- a/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java
+++ b/src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java
@@ -296,26 +296,32 @@ public static Stream findAllDefinedMappingAnnotations(@NotNull Ps
private static Stream findAllDefinedMappingAnnotations(@NotNull PsiModifierListOwner owner,
boolean includeMetaAnnotations) {
//TODO cache
- Stream mappingsAnnotations = Stream.empty();
PsiAnnotation mappings = findAnnotation( owner, true, MapstructUtil.MAPPINGS_ANNOTATION_FQN );
- if ( mappings != null ) {
- //TODO maybe there is a better way to do this, but currently I don't have that much knowledge
- PsiAnnotationMemberValue mappingsValue = mappings.findDeclaredAttributeValue( null );
- if ( mappingsValue instanceof PsiArrayInitializerMemberValue mappingsArrayInitializerMemberValue ) {
- mappingsAnnotations = Stream.of( mappingsArrayInitializerMemberValue.getInitializers() )
- .filter( MapstructAnnotationUtils::isMappingPsiAnnotation )
- .map( PsiAnnotation.class::cast );
- }
- else if ( mappingsValue instanceof PsiAnnotation mappingsAnnotation ) {
- mappingsAnnotations = Stream.of( mappingsAnnotation );
- }
- }
-
+ Stream mappingsAnnotations = extractMappingAnnotationsFromMappings( mappings );
Stream mappingAnnotations = findMappingAnnotations( owner, includeMetaAnnotations );
return Stream.concat( mappingAnnotations, mappingsAnnotations );
}
+ @NotNull
+ public static Stream extractMappingAnnotationsFromMappings(@Nullable PsiAnnotation mappings) {
+ if (mappings == null) {
+ return Stream.empty();
+ }
+ //TODO maybe there is a better way to do this, but currently I don't have that much knowledge
+ PsiAnnotationMemberValue mappingsValue = mappings.findDeclaredAttributeValue( null );
+ if ( mappingsValue instanceof PsiArrayInitializerMemberValue mappingsArrayInitializerMemberValue) {
+ return Stream.of( mappingsArrayInitializerMemberValue
+ .getInitializers() )
+ .filter( MapstructAnnotationUtils::isMappingPsiAnnotation )
+ .map( PsiAnnotation.class::cast );
+ }
+ else if ( mappingsValue instanceof PsiAnnotation mappingsAnnotation ) {
+ return Stream.of( mappingsAnnotation );
+ }
+ return Stream.empty();
+ }
+
private static Stream findMappingAnnotations(@NotNull PsiModifierListOwner method,
boolean includeMetaAnnotations) {
diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
index ceb4559a..e78be940 100644
--- a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
+++ b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
@@ -93,7 +93,8 @@ public final class MapstructUtil {
public static final String INHERIT_CONFIGURATION_FQN = InheritConfiguration.class.getName();
public static final String INHERIT_INVERSE_CONFIGURATION_FQN = InheritInverseConfiguration.class.getName();
- static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
+ public static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
+
static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName();
static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName();
private static final String MAPPING_TARGET_ANNOTATION_FQN = MappingTarget.class.getName();
diff --git a/src/main/java/org/mapstruct/intellij/util/TargetUtils.java b/src/main/java/org/mapstruct/intellij/util/TargetUtils.java
index 0432fd1b..4193356d 100644
--- a/src/main/java/org/mapstruct/intellij/util/TargetUtils.java
+++ b/src/main/java/org/mapstruct/intellij/util/TargetUtils.java
@@ -387,14 +387,15 @@ private static boolean hasBuildMethod(@Nullable PsiType builderType, @NotNull Ps
/**
* Find all defined {@link org.mapstruct.Mapping#target()} for the given method
*
- * @param method that needs to be checked
+ * @param owner that needs to be checked
* @param mapStructVersion the MapStruct project version
*
* @return see description
*/
- public static Stream findAllDefinedMappingTargets(@NotNull PsiMethod method,
+ @NotNull
+ public static Stream findAllDefinedMappingTargets(@NotNull PsiModifierListOwner owner,
MapStructVersion mapStructVersion) {
- return findAllDefinedMappingAnnotations( method, mapStructVersion )
+ return findAllDefinedMappingAnnotations( owner, mapStructVersion )
.map( psiAnnotation -> AnnotationUtil.getDeclaredStringAttributeValue( psiAnnotation, "target" ) )
.filter( Objects::nonNull )
.filter( s -> !s.isEmpty() );
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 3a561998..cef1b5a4 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -128,6 +128,14 @@
key="inspection.this.target.mapping.no.source.property"
shortName="TargetThisMappingNoSourcePropertyInspection"
implementationClass="org.mapstruct.intellij.inspection.TargetThisMappingNoSourcePropertyInspection"/>
+
diff --git a/src/main/resources/inspectionDescriptions/TargetPropertyMappedMoreThanOnceInspection.html b/src/main/resources/inspectionDescriptions/TargetPropertyMappedMoreThanOnceInspection.html
new file mode 100644
index 00000000..43578fb0
--- /dev/null
+++ b/src/main/resources/inspectionDescriptions/TargetPropertyMappedMoreThanOnceInspection.html
@@ -0,0 +1,29 @@
+
+
+
+ This inspection reports when a target property is explicit mapped more than once
+
+
+
+//wrong
+@Mapper
+public interface EmployeeMapper {
+ @Mapping(source = "employeeName", target = "name")
+ @Mapping(source = "employeeNameLast", target = "name")
+ Employee toEmployee(EmployeeDto employeeDto, @Context CycleAvoidingMappingContext context);
+}
+
+
+
+
+//correct
+@Mapper
+public interface EmployeeMapper {
+ @Mapping(source = "employeeName", target = "name")
+ Employee toEmployee(EmployeeDto employeeDto, @Context CycleAvoidingMappingContext context);
+}
+
+
+
+
+
diff --git a/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties b/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties
index 5732d130..8664108a 100644
--- a/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties
+++ b/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties
@@ -25,6 +25,8 @@ inspection.wrong.map.mapping.map.type.raw=Raw map used for mapping Map to Bean
inspection.wrong.map.mapping.map.type.raw.set.default=Replace {0} with {0}
inspection.wrong.map.mapping.map.key=Key must be of type String for mapping Map to Bean
inspection.wrong.map.mapping.map.key.change.to.string=Change key type to String
+inspection.target.property.mapped.more.than.once=Target property ''{0}'' must not be mapped more than once.
+inspection.target.property.mapped.more.than.once.title=Target properties must not be mapped more than once.
intention.add.ignore.all.unmapped.target.properties=Add ignore all unmapped target properties
intention.add.ignore.unmapped.target.property=Add ignore unmapped target property
intention.add.unmapped.target.property=Add unmapped target property
@@ -34,6 +36,8 @@ intention.not.null.checkable.property.source.used.with.default.property=Remove d
intention.java.expression.remove.unnecessary.whitespace=Remove unnecessary whitespaces
intention.wrong.map.mapping.map.type.raw=Add type to Map for mapping Map to Bean
intention.wrong.map.mapping.map.key=Use Map with key of type String for mapping Map to Bean
+intention.remove.annotation=Remove {0} annotation
+intention.change.target.property=Change target property
plugin.settings.title=MapStruct
plugin.settings.quickFix.title=Quick fix properties
plugin.settings.quickFix.preferSourceBeforeTargetInMapping=Prefer source before target in @Mapping
diff --git a/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
new file mode 100644
index 00000000..486f524b
--- /dev/null
+++ b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.intellij.inspection;
+
+import com.intellij.codeInsight.intention.IntentionAction;
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.openapi.editor.Caret;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author hduelme
+ */
+public class TargetPropertyMappedMoreThanOnceInspectionTest extends BaseInspectionTest {
+
+ @Override
+ protected @NotNull Class extends LocalInspectionTool> getInspection() {
+ return TargetPropertyMappedMoreThanOnceInspection.class;
+ }
+
+ public void testTargetPropertyMappedMoreThanOnce() {
+ doTest();
+ String testName = getTestName( false );
+ List allQuickFixes = myFixture.getAllQuickFixes();
+
+ assertThat( allQuickFixes )
+ .extracting( IntentionAction::getText )
+ .as( "Intent Text" )
+ .containsExactly(
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove MyMappingAnnotation annotation",
+ "Remove Mapping annotation",
+ "Change target property",
+ "Remove MyMappingAnnotation annotation"
+ );
+
+ // Delete annotations
+ myFixture.launchAction( allQuickFixes.get( 0 ) );
+ myFixture.launchAction( allQuickFixes.get( 3 ) );
+ myFixture.launchAction( allQuickFixes.get( 6 ) );
+ myFixture.launchAction( allQuickFixes.get( 8 ) );
+ myFixture.launchAction( allQuickFixes.get( 14 ) );
+ // Set cursor
+ myFixture.launchAction( allQuickFixes.get( 16 ) );
+ myFixture.checkResultByFile( testName + "_after.java" );
+ Caret currentCaret = myFixture.getEditor().getCaretModel().getCurrentCaret();
+ assertThat( currentCaret.getSelectedText( ) ).isEqualTo( "testName" );
+ }
+}
diff --git a/testData/inspection/TargetPropertyMappedMoreThanOnce.java b/testData/inspection/TargetPropertyMappedMoreThanOnce.java
new file mode 100644
index 00000000..2437a335
--- /dev/null
+++ b/testData/inspection/TargetPropertyMappedMoreThanOnce.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
+ */
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+class Target {
+ private String testName;
+
+ public String getTestName() {
+ return testName;
+ }
+
+ public void setTestName(String testName) {
+ this.testName = testName;
+ }
+}
+
+class Source {
+ private String name;
+ private String lastName;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+}
+
+@Retention(RetentionPolicy.CLASS)
+@Mapping(target = "testName", source = "name")
+@interface MyMappingAnnotation {
+}
+
+
+@Mapper
+interface TargetMappedMoreThanOnceByMappingAnnotationMapper {
+
+ @Mapping(target = "testName", source = "name")
+ @Mapping(target = "testName", source = "lastName")
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMappingsAnnotationsMapper {
+
+ @Mappings({
+ @Mapping(target = "testName", source = "name"),
+ @Mapping(target = "testName", source = "lastName")
+ })
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMappingsAnnotationsAndMappingAnnotationMapper {
+
+ @Mapping(target = "testName", source = "name")
+ @Mappings({
+ @Mapping(target = "testName", source = "lastName")
+ })
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMyMappingAnnotationAndMappingAnnotationMapper {
+
+ @Mapping(target = "testName", source = "lastName")
+ @MyMappingAnnotation
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMyMappingAnnotationAndMappingsAnnotationMapper {
+
+ @Mappings({
+ @Mapping(target = "testName", source = "lastName")
+ })
+ @MyMappingAnnotation
+ Target map(Source source);
+}
\ No newline at end of file
diff --git a/testData/inspection/TargetPropertyMappedMoreThanOnce_after.java b/testData/inspection/TargetPropertyMappedMoreThanOnce_after.java
new file mode 100644
index 00000000..669c79b9
--- /dev/null
+++ b/testData/inspection/TargetPropertyMappedMoreThanOnce_after.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
+ */
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+class Target {
+ private String testName;
+
+ public String getTestName() {
+ return testName;
+ }
+
+ public void setTestName(String testName) {
+ this.testName = testName;
+ }
+}
+
+class Source {
+ private String name;
+ private String lastName;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+}
+
+@Retention(RetentionPolicy.CLASS)
+@Mapping(target = "testName", source = "name")
+@interface MyMappingAnnotation {
+}
+
+
+@Mapper
+interface TargetMappedMoreThanOnceByMappingAnnotationMapper {
+
+ @Mapping(target = "testName", source = "lastName")
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMappingsAnnotationsMapper {
+
+ @Mappings({
+ @Mapping(target = "testName", source = "name")
+ })
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMappingsAnnotationsAndMappingAnnotationMapper {
+
+ @Mappings({
+ @Mapping(target = "testName", source = "lastName")
+ })
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMyMappingAnnotationAndMappingAnnotationMapper {
+
+ @Mapping(target = "testName", source = "lastName")
+ Target map(Source source);
+}
+
+@Mapper
+interface TargetMappedMoreThanOnceByMyMappingAnnotationAndMappingsAnnotationMapper {
+
+ @Mappings({
+ @Mapping(target = "testName", source = "lastName")
+ })
+ @MyMappingAnnotation
+ Target map(Source source);
+}
\ No newline at end of file