From 51f436cefe957bd82d21a557a67735cca067c572 Mon Sep 17 00:00:00 2001
From: hduelme
Date: Sun, 7 Jul 2024 01:42:14 +0200
Subject: [PATCH 1/9] Target property mapped more than once inspection
---
README.md | 1 +
description.html | 1 +
...tPropertyMappedMoreThanOnceInspection.java | 113 ++++++++++++++++++
.../util/MapstructAnnotationUtils.java | 34 +++---
.../intellij/util/MapstructUtil.java | 2 +-
.../mapstruct/intellij/util/TargetUtils.java | 7 +-
src/main/resources/META-INF/plugin.xml | 8 ++
...tPropertyMappedMoreThanOnceInspection.html | 29 +++++
.../messages/MapStructBundle.properties | 2 +
...pertyMappedMoreThanOnceInspectionTest.java | 24 ++++
.../TargetPropertyMappedMoreThanOnce.java | 97 +++++++++++++++
11 files changed, 300 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
diff --git a/README.md b/README.md
index 47f98315..76d9936a 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
## Requirements
diff --git a/description.html b/description.html
index cf79fbad..380d4a64 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
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..5cdcad2c
--- /dev/null
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -0,0 +1,113 @@
+/*
+ * 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.codeInspection.ProblemsHolder;
+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.PsiMethod;
+import com.intellij.psi.PsiType;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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 );
+ }
+ }
+ for (Map.Entry> problem : problemMap.entrySet()) {
+ List problemElements = problem.getValue();
+ if (problemElements.size() > 1) {
+ for (PsiElement problemElement : problemElements) {
+ holder.registerProblem( problemElement,
+ MapStructBundle.message( "inspection.target.property.mapped.more.than.once",
+ problem.getKey() ) );
+ }
+ }
+ }
+ }
+
+ 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 );
+ }
+ }
+
+ }
+ }
+}
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..1af750e1 100644
--- a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
+++ b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
@@ -93,7 +93,7 @@ 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..a06f82d1 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
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..a2849fdb
--- /dev/null
+++ b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
@@ -0,0 +1,24 @@
+/*
+ * 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.codeInspection.LocalInspectionTool;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author hduelme
+ */
+public class TargetPropertyMappedMoreThanOnceInspectionTest extends BaseInspectionTest {
+
+ @Override
+ protected @NotNull Class extends LocalInspectionTool> getInspection() {
+ return TargetPropertyMappedMoreThanOnceInspection.class;
+ }
+
+ public void testTargetPropertyMappedMoreThanOnce() {
+ doTest();
+ }
+}
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
From f5dc8a55458177e1f05378f26a8541582f90159a Mon Sep 17 00:00:00 2001
From: hduelme
Date: Sun, 7 Jul 2024 17:15:58 +0200
Subject: [PATCH 2/9] add change target quick fix and remove annotation quick
fix
---
...tPropertyMappedMoreThanOnceInspection.java | 101 +++++++++++++++++-
.../messages/MapStructBundle.properties | 2 +
...pertyMappedMoreThanOnceInspectionTest.java | 44 ++++++++
...argetPropertyMappedMoreThanOnce_after.java | 93 ++++++++++++++++
4 files changed, 238 insertions(+), 2 deletions(-)
create mode 100644 testData/inspection/TargetPropertyMappedMoreThanOnce_after.java
diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
index 5cdcad2c..461daedf 100644
--- a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -5,13 +5,30 @@
*/
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 org.jetbrains.annotations.NotNull;
@@ -21,9 +38,12 @@
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.Objects;
+import java.util.Optional;
import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.extractMappingAnnotationsFromMappings;
@@ -75,18 +95,44 @@ else if (MAPPINGS_ANNOTATION_FQN.equals( qualifiedName )) {
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) {
- holder.registerProblem( problemElement,
+ LocalQuickFix[] quickFixes = getLocalQuickFixes( problemElement, quickFixFactory );
+ holder.registerProblem( problemElement,
MapStructBundle.message( "inspection.target.property.mapped.more.than.once",
- problem.getKey() ) );
+ 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) {
+ 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( (PsiAnnotationMemberValue) problemElement) );
+ }
+ return quickFixes.toArray( new LocalQuickFix[]{} );
+ }
+
+ private static @NotNull LocalQuickFixAndIntentionActionOnPsiElement getDeleteFix(
+ PsiElement problemElement, QuickFixFactory quickFixFactory) {
+ String qualifiedName = Objects.requireNonNullElse(
+ ( (PsiAnnotation) problemElement).getQualifiedName(), "unknown" );
+ return quickFixFactory.createDeleteFix( problemElement,
+ MapStructBundle.message( "intention.remove.annotation", qualifiedName ) );
+ }
+
private void handleAnnotationWithMappingAnnotation(PsiAnnotation psiAnnotation,
Map> problemMap) {
PsiClass annotationClass = psiAnnotation.resolveAnnotationType();
@@ -107,7 +153,58 @@ private static void handleMappingAnnotation(PsiAnnotation psiAnnotation,
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) {
+ Editor editor = ( (TextEditor) selectedEditor ).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/resources/org/mapstruct/intellij/messages/MapStructBundle.properties b/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties
index a06f82d1..8664108a 100644
--- a/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties
+++ b/src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties
@@ -36,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
index a2849fdb..cf4ced0a 100644
--- a/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
+++ b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
@@ -5,9 +5,15 @@
*/
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
*/
@@ -20,5 +26,43 @@ public class TargetPropertyMappedMoreThanOnceInspectionTest extends BaseInspecti
public void testTargetPropertyMappedMoreThanOnce() {
doTest();
+ String testName = getTestName( false );
+ List allQuickFixes = myFixture.getAllQuickFixes();
+
+ assertThat( allQuickFixes )
+ .extracting( IntentionAction::getText )
+ .as( "Intent Text" )
+ .containsExactly(
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove org.mapstruct.Mapping annotation",
+ "Change target property",
+ "Remove MyMappingAnnotation annotation",
+ "Remove org.mapstruct.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_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
From 8ff979bcc71e5f1b3c3ae22240ce88026ddea99a Mon Sep 17 00:00:00 2001
From: hduelme
Date: Sun, 7 Jul 2024 17:19:03 +0200
Subject: [PATCH 3/9] formatting
---
...etPropertyMappedMoreThanOnceInspectionTest.java | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
index cf4ced0a..142693c9 100644
--- a/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
+++ b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
@@ -54,15 +54,15 @@ public void testTargetPropertyMappedMoreThanOnce() {
);
// 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));
+ 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.launchAction( allQuickFixes.get( 16 ) );
myFixture.checkResultByFile( testName + "_after.java" );
Caret currentCaret = myFixture.getEditor().getCaretModel().getCurrentCaret();
- assertThat(currentCaret.getSelectedText()).isEqualTo("testName");
+ assertThat( currentCaret.getSelectedText( ) ).isEqualTo( "testName" );
}
}
From 4a13b8341a61c561846b8271d720803ea77fa5ee Mon Sep 17 00:00:00 2001
From: hduelme
Date: Sun, 7 Jul 2024 17:28:28 +0200
Subject: [PATCH 4/9] documentation
---
README.md | 2 +-
description.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 76d9936a..57f246d3 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +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
+ * `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 380d4a64..e658e17a 100644
--- a/description.html
+++ b/description.html
@@ -41,7 +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
+ target mapped more than once by @Mapping annotations with quick fixes: Remove annotation and change target property.
From ea778cc8b52f64c2582012e161a14aef4641074f Mon Sep 17 00:00:00 2001
From: hduelme
Date: Mon, 15 Jul 2024 17:25:38 +0200
Subject: [PATCH 5/9] formatting
---
.../inspection/TargetPropertyMappedMoreThanOnceInspection.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
index 461daedf..1b38fd32 100644
--- a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -81,7 +81,7 @@ public void visitMethod(PsiMethod method) {
return;
}
Map> problemMap = new HashMap<>();
- for (PsiAnnotation psiAnnotation :method.getAnnotations()) {
+ for (PsiAnnotation psiAnnotation : method.getAnnotations()) {
String qualifiedName = psiAnnotation.getQualifiedName();
if ( MAPPING_ANNOTATION_FQN.equals( qualifiedName ) ) {
handleMappingAnnotation( psiAnnotation, problemMap );
From c422a205b89926352cf6c092160f17f66c92e86f Mon Sep 17 00:00:00 2001
From: hduelme
Date: Mon, 15 Jul 2024 18:20:43 +0200
Subject: [PATCH 6/9] use simple annotation name
---
...rgetPropertyMappedMoreThanOnceInspection.java | 11 ++++++-----
...PropertyMappedMoreThanOnceInspectionTest.java | 16 ++++++++--------
2 files changed, 14 insertions(+), 13 deletions(-)
diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
index 1b38fd32..7f973b2f 100644
--- a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -29,8 +29,10 @@
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiJavaCodeReferenceElement;
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;
@@ -42,7 +44,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
@@ -126,11 +127,11 @@ else if (problemElement instanceof PsiAnnotationMemberValue) {
}
private static @NotNull LocalQuickFixAndIntentionActionOnPsiElement getDeleteFix(
- PsiElement problemElement, QuickFixFactory quickFixFactory) {
- String qualifiedName = Objects.requireNonNullElse(
- ( (PsiAnnotation) problemElement).getQualifiedName(), "unknown" );
+ @NotNull PsiElement problemElement, @NotNull QuickFixFactory quickFixFactory) {
+
+ String annotationName = PsiAnnotationImpl.getAnnotationShortName( problemElement.getText() );
return quickFixFactory.createDeleteFix( problemElement,
- MapStructBundle.message( "intention.remove.annotation", qualifiedName ) );
+ MapStructBundle.message( "intention.remove.annotation", annotationName ) );
}
private void handleAnnotationWithMappingAnnotation(PsiAnnotation psiAnnotation,
diff --git a/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
index 142693c9..486f524b 100644
--- a/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
+++ b/src/test/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspectionTest.java
@@ -33,22 +33,22 @@ public void testTargetPropertyMappedMoreThanOnce() {
.extracting( IntentionAction::getText )
.as( "Intent Text" )
.containsExactly(
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
"Remove MyMappingAnnotation annotation",
- "Remove org.mapstruct.Mapping annotation",
+ "Remove Mapping annotation",
"Change target property",
"Remove MyMappingAnnotation annotation"
);
From 5181d7d227b74e27133e5674b221bd4a3526de54 Mon Sep 17 00:00:00 2001
From: hduelme
Date: Mon, 15 Jul 2024 18:25:44 +0200
Subject: [PATCH 7/9] formatting
---
src/main/java/org/mapstruct/intellij/util/MapstructUtil.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
index 1af750e1..e78be940 100644
--- a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
+++ b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
@@ -94,6 +94,7 @@ public final class MapstructUtil {
public static final String INHERIT_INVERSE_CONFIGURATION_FQN = InheritInverseConfiguration.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();
From 371ae588692a9cea7b6a1ac2e824c2e71c5864c5 Mon Sep 17 00:00:00 2001
From: hduelme
Date: Mon, 15 Jul 2024 18:31:31 +0200
Subject: [PATCH 8/9] formatting
---
.../inspection/TargetPropertyMappedMoreThanOnceInspection.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
index 7f973b2f..cba75876 100644
--- a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -29,7 +29,6 @@
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
-import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import com.intellij.psi.impl.source.tree.java.PsiAnnotationImpl;
From 05521ae6d7c8867d3ffcc22838677786704ff7e4 Mon Sep 17 00:00:00 2001
From: hduelme
Date: Sun, 21 Jul 2024 14:25:27 +0200
Subject: [PATCH 9/9] use Java 17 features
---
.../TargetPropertyMappedMoreThanOnceInspection.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
index cba75876..72a59816 100644
--- a/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
+++ b/src/main/java/org/mapstruct/intellij/inspection/TargetPropertyMappedMoreThanOnceInspection.java
@@ -115,12 +115,12 @@ else if (MAPPINGS_ANNOTATION_FQN.equals( qualifiedName )) {
if (problemElement instanceof PsiAnnotation) {
quickFixes.add( getDeleteFix( problemElement, quickFixFactory ) );
}
- else if (problemElement instanceof PsiAnnotationMemberValue) {
+ 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( (PsiAnnotationMemberValue) problemElement) );
+ quickFixes.add( new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ) );
}
return quickFixes.toArray( new LocalQuickFix[]{} );
}
@@ -176,8 +176,8 @@ private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) {
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) {
- Editor editor = ( (TextEditor) selectedEditor ).getEditor();
+ if ( selectedEditor instanceof TextEditor textEditor) {
+ Editor editor = textEditor.getEditor();
TextRange textRange = psiElement.getTextRange();
String textOfElement = String.valueOf( editor.getDocument()