Skip to content

Commit 48f4d6a

Browse files
authored
Add inspect for target property mapped more than once (#200)
1 parent e0586d7 commit 48f4d6a

File tree

12 files changed

+537
-18
lines changed

12 files changed

+537
-18
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ To learn more about MapStruct have a look at the [mapstruct](https://github.com/
3737
* No `source` defined in `@Mapping` annotation
3838
* 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`.
3939
* More than one default source in `@Mapping` annotation defined with quick fixes: Remove `defaultValue`. Remove `defaultExpression`.
40+
* `target` mapped more than once by `@Mapping` annotations with quick fixes: Remove annotation and change target property.
4041

4142
## Requirements
4243

description.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<li>No <code>source</code> defined in <code>@Mapping</code> annotation</li>
4242
<li>More than one <code>source</code> in <code>@Mapping</code> annotation defined with quick fixes: Remove <code>source</code>. Remove <code>constant</code>. Remove <code>expression</code>. Use <code>constant</code> as <code>defaultValue</code>. Use <code>expression</code> as <code>defaultExpression</code>.</li>
4343
<li>More than one default source in <code>@Mapping</code> annotation defined with quick fixes: Remove <code>defaultValue</code>. Remove <code>defaultExpression</code>.</li>
44+
<li><code>target</code> mapped more than once by <code>@Mapping</code> annotations with quick fixes: Remove annotation and change target property.</li>
4445
</ul>
4546
</li>
4647
</ul>
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.intellij.inspection;
7+
8+
import com.intellij.codeInsight.intention.QuickFixFactory;
9+
import com.intellij.codeInspection.LocalQuickFix;
10+
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
11+
import com.intellij.codeInspection.LocalQuickFixOnPsiElement;
12+
import com.intellij.codeInspection.ProblemsHolder;
13+
import com.intellij.codeInspection.util.IntentionFamilyName;
14+
import com.intellij.codeInspection.util.IntentionName;
15+
import com.intellij.openapi.editor.CaretState;
16+
import com.intellij.openapi.editor.Editor;
17+
import com.intellij.openapi.editor.LogicalPosition;
18+
import com.intellij.openapi.editor.ScrollType;
19+
import com.intellij.openapi.fileEditor.FileEditor;
20+
import com.intellij.openapi.fileEditor.FileEditorManager;
21+
import com.intellij.openapi.fileEditor.TextEditor;
22+
import com.intellij.openapi.project.Project;
23+
import com.intellij.openapi.util.TextRange;
24+
import com.intellij.openapi.util.text.Strings;
25+
import com.intellij.psi.JavaElementVisitor;
26+
import com.intellij.psi.PsiAnnotation;
27+
import com.intellij.psi.PsiAnnotationMemberValue;
28+
import com.intellij.psi.PsiClass;
29+
import com.intellij.psi.PsiElement;
30+
import com.intellij.psi.PsiElementVisitor;
31+
import com.intellij.psi.PsiFile;
32+
import com.intellij.psi.PsiMethod;
33+
import com.intellij.psi.PsiType;
34+
import com.intellij.psi.impl.source.tree.java.PsiAnnotationImpl;
35+
import org.jetbrains.annotations.NotNull;
36+
import org.mapstruct.intellij.MapStructBundle;
37+
import org.mapstruct.intellij.util.MapStructVersion;
38+
import org.mapstruct.intellij.util.MapstructUtil;
39+
import org.mapstruct.intellij.util.TargetUtils;
40+
41+
import java.util.ArrayList;
42+
import java.util.Collections;
43+
import java.util.HashMap;
44+
import java.util.List;
45+
import java.util.Map;
46+
import java.util.Optional;
47+
48+
import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
49+
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.extractMappingAnnotationsFromMappings;
50+
import static org.mapstruct.intellij.util.MapstructUtil.MAPPINGS_ANNOTATION_FQN;
51+
import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN;
52+
import static org.mapstruct.intellij.util.TargetUtils.getTargetType;
53+
54+
/**
55+
* @author hduelme
56+
*/
57+
public class TargetPropertyMappedMoreThanOnceInspection extends InspectionBase {
58+
@NotNull
59+
@Override
60+
PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
61+
return new TargetPropertyMappedMoreThanOnceInspection.MyJavaElementVisitor( holder,
62+
MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) );
63+
}
64+
65+
private static class MyJavaElementVisitor extends JavaElementVisitor {
66+
private final ProblemsHolder holder;
67+
private final MapStructVersion mapStructVersion;
68+
69+
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) {
70+
this.holder = holder;
71+
this.mapStructVersion = mapStructVersion;
72+
}
73+
74+
@Override
75+
public void visitMethod(PsiMethod method) {
76+
if ( !MapstructUtil.isMapper( method.getContainingClass() ) ) {
77+
return;
78+
}
79+
PsiType targetType = getTargetType( method );
80+
if ( targetType == null ) {
81+
return;
82+
}
83+
Map<String, List<PsiElement>> problemMap = new HashMap<>();
84+
for (PsiAnnotation psiAnnotation : method.getAnnotations()) {
85+
String qualifiedName = psiAnnotation.getQualifiedName();
86+
if ( MAPPING_ANNOTATION_FQN.equals( qualifiedName ) ) {
87+
handleMappingAnnotation( psiAnnotation, problemMap );
88+
}
89+
else if (MAPPINGS_ANNOTATION_FQN.equals( qualifiedName )) {
90+
extractMappingAnnotationsFromMappings( psiAnnotation )
91+
.forEach( a -> handleMappingAnnotation( a, problemMap ) );
92+
}
93+
else {
94+
// Handle annotations containing at least one Mapping annotation
95+
handleAnnotationWithMappingAnnotation( psiAnnotation, problemMap );
96+
}
97+
}
98+
QuickFixFactory quickFixFactory = QuickFixFactory.getInstance();
99+
for (Map.Entry<String, List<PsiElement>> problem : problemMap.entrySet()) {
100+
List<PsiElement> problemElements = problem.getValue();
101+
if (problemElements.size() > 1) {
102+
for (PsiElement problemElement : problemElements) {
103+
LocalQuickFix[] quickFixes = getLocalQuickFixes( problemElement, quickFixFactory );
104+
holder.registerProblem( problemElement,
105+
MapStructBundle.message( "inspection.target.property.mapped.more.than.once",
106+
problem.getKey() ), quickFixes );
107+
}
108+
}
109+
}
110+
}
111+
112+
private static @NotNull LocalQuickFix[] getLocalQuickFixes(PsiElement problemElement,
113+
QuickFixFactory quickFixFactory) {
114+
List<LocalQuickFix> quickFixes = new ArrayList<>(2);
115+
if (problemElement instanceof PsiAnnotation) {
116+
quickFixes.add( getDeleteFix( problemElement, quickFixFactory ) );
117+
}
118+
else if (problemElement instanceof PsiAnnotationMemberValue problemPsiAnnotationMemberValue) {
119+
Optional.ofNullable( problemElement.getParent() ).map( PsiElement::getParent )
120+
.map( PsiElement::getParent ).filter( PsiAnnotation.class::isInstance )
121+
.ifPresent( annotation -> quickFixes.add(
122+
getDeleteFix( annotation, quickFixFactory ) ) );
123+
quickFixes.add( new ChangeTargetQuickFix( problemPsiAnnotationMemberValue ) );
124+
}
125+
return quickFixes.toArray( new LocalQuickFix[]{} );
126+
}
127+
128+
private static @NotNull LocalQuickFixAndIntentionActionOnPsiElement getDeleteFix(
129+
@NotNull PsiElement problemElement, @NotNull QuickFixFactory quickFixFactory) {
130+
131+
String annotationName = PsiAnnotationImpl.getAnnotationShortName( problemElement.getText() );
132+
return quickFixFactory.createDeleteFix( problemElement,
133+
MapStructBundle.message( "intention.remove.annotation", annotationName ) );
134+
}
135+
136+
private void handleAnnotationWithMappingAnnotation(PsiAnnotation psiAnnotation,
137+
Map<String, List<PsiElement>> problemMap) {
138+
PsiClass annotationClass = psiAnnotation.resolveAnnotationType();
139+
if (annotationClass == null) {
140+
return;
141+
}
142+
TargetUtils.findAllDefinedMappingTargets( annotationClass, mapStructVersion )
143+
.forEach( target ->
144+
problemMap.computeIfAbsent( target, k -> new ArrayList<>() ).add( psiAnnotation ) );
145+
}
146+
147+
private static void handleMappingAnnotation(PsiAnnotation psiAnnotation,
148+
Map<String, List<PsiElement>> problemMap) {
149+
PsiAnnotationMemberValue value = psiAnnotation.findDeclaredAttributeValue( "target" );
150+
if (value != null) {
151+
String target = getStringAttributeValue( value );
152+
if (target != null) {
153+
problemMap.computeIfAbsent( target, k -> new ArrayList<>() ).add( value );
154+
}
155+
}
156+
}
157+
158+
private static class ChangeTargetQuickFix extends LocalQuickFixOnPsiElement {
159+
160+
private final String myText;
161+
private final String myFamilyName;
162+
163+
private ChangeTargetQuickFix(@NotNull PsiAnnotationMemberValue element) {
164+
super( element );
165+
myText = MapStructBundle.message( "intention.change.target.property" );
166+
myFamilyName = MapStructBundle.message( "inspection.target.property.mapped.more.than.once",
167+
element.getText() );
168+
}
169+
170+
@Override
171+
public @IntentionName @NotNull String getText() {
172+
return myText;
173+
}
174+
175+
@Override
176+
public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement,
177+
@NotNull PsiElement psiElement1) {
178+
FileEditor selectedEditor = FileEditorManager.getInstance( project ).getSelectedEditor();
179+
if ( selectedEditor instanceof TextEditor textEditor) {
180+
Editor editor = textEditor.getEditor();
181+
182+
TextRange textRange = psiElement.getTextRange();
183+
String textOfElement = String.valueOf( editor.getDocument()
184+
.getCharsSequence()
185+
.subSequence( textRange.getStartOffset(), textRange.getEndOffset() ) );
186+
int targetStart = Strings.indexOf( textOfElement, "\"" ) + 1;
187+
int targetEnd = textOfElement.lastIndexOf( "\"" );
188+
189+
editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetStart );
190+
LogicalPosition startPosition = editor.getCaretModel().getLogicalPosition();
191+
editor.getCaretModel().moveToOffset( textRange.getStartOffset() + targetEnd );
192+
editor.getCaretModel().setCaretsAndSelections(
193+
Collections.singletonList( new CaretState(startPosition, startPosition,
194+
editor.getCaretModel().getLogicalPosition() ) ) );
195+
editor.getScrollingModel().scrollToCaret( ScrollType.MAKE_VISIBLE );
196+
}
197+
}
198+
199+
@Override
200+
public @IntentionFamilyName @NotNull String getFamilyName() {
201+
return myFamilyName;
202+
}
203+
204+
@Override
205+
public boolean availableInBatchMode() {
206+
return false;
207+
}
208+
}
209+
}
210+
}

src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -296,26 +296,32 @@ public static Stream<PsiAnnotation> findAllDefinedMappingAnnotations(@NotNull Ps
296296
private static Stream<PsiAnnotation> findAllDefinedMappingAnnotations(@NotNull PsiModifierListOwner owner,
297297
boolean includeMetaAnnotations) {
298298
//TODO cache
299-
Stream<PsiAnnotation> mappingsAnnotations = Stream.empty();
300299
PsiAnnotation mappings = findAnnotation( owner, true, MapstructUtil.MAPPINGS_ANNOTATION_FQN );
301-
if ( mappings != null ) {
302-
//TODO maybe there is a better way to do this, but currently I don't have that much knowledge
303-
PsiAnnotationMemberValue mappingsValue = mappings.findDeclaredAttributeValue( null );
304-
if ( mappingsValue instanceof PsiArrayInitializerMemberValue mappingsArrayInitializerMemberValue ) {
305-
mappingsAnnotations = Stream.of( mappingsArrayInitializerMemberValue.getInitializers() )
306-
.filter( MapstructAnnotationUtils::isMappingPsiAnnotation )
307-
.map( PsiAnnotation.class::cast );
308-
}
309-
else if ( mappingsValue instanceof PsiAnnotation mappingsAnnotation ) {
310-
mappingsAnnotations = Stream.of( mappingsAnnotation );
311-
}
312-
}
313-
300+
Stream<PsiAnnotation> mappingsAnnotations = extractMappingAnnotationsFromMappings( mappings );
314301
Stream<PsiAnnotation> mappingAnnotations = findMappingAnnotations( owner, includeMetaAnnotations );
315302

316303
return Stream.concat( mappingAnnotations, mappingsAnnotations );
317304
}
318305

306+
@NotNull
307+
public static Stream<PsiAnnotation> extractMappingAnnotationsFromMappings(@Nullable PsiAnnotation mappings) {
308+
if (mappings == null) {
309+
return Stream.empty();
310+
}
311+
//TODO maybe there is a better way to do this, but currently I don't have that much knowledge
312+
PsiAnnotationMemberValue mappingsValue = mappings.findDeclaredAttributeValue( null );
313+
if ( mappingsValue instanceof PsiArrayInitializerMemberValue mappingsArrayInitializerMemberValue) {
314+
return Stream.of( mappingsArrayInitializerMemberValue
315+
.getInitializers() )
316+
.filter( MapstructAnnotationUtils::isMappingPsiAnnotation )
317+
.map( PsiAnnotation.class::cast );
318+
}
319+
else if ( mappingsValue instanceof PsiAnnotation mappingsAnnotation ) {
320+
return Stream.of( mappingsAnnotation );
321+
}
322+
return Stream.empty();
323+
}
324+
319325
private static Stream<PsiAnnotation> findMappingAnnotations(@NotNull PsiModifierListOwner method,
320326
boolean includeMetaAnnotations) {
321327

src/main/java/org/mapstruct/intellij/util/MapstructUtil.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ public final class MapstructUtil {
9393
public static final String INHERIT_CONFIGURATION_FQN = InheritConfiguration.class.getName();
9494
public static final String INHERIT_INVERSE_CONFIGURATION_FQN = InheritInverseConfiguration.class.getName();
9595

96-
static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
96+
public static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
97+
9798
static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName();
9899
static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName();
99100
private static final String MAPPING_TARGET_ANNOTATION_FQN = MappingTarget.class.getName();

src/main/java/org/mapstruct/intellij/util/TargetUtils.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,14 +387,15 @@ private static boolean hasBuildMethod(@Nullable PsiType builderType, @NotNull Ps
387387
/**
388388
* Find all defined {@link org.mapstruct.Mapping#target()} for the given method
389389
*
390-
* @param method that needs to be checked
390+
* @param owner that needs to be checked
391391
* @param mapStructVersion the MapStruct project version
392392
*
393393
* @return see description
394394
*/
395-
public static Stream<String> findAllDefinedMappingTargets(@NotNull PsiMethod method,
395+
@NotNull
396+
public static Stream<String> findAllDefinedMappingTargets(@NotNull PsiModifierListOwner owner,
396397
MapStructVersion mapStructVersion) {
397-
return findAllDefinedMappingAnnotations( method, mapStructVersion )
398+
return findAllDefinedMappingAnnotations( owner, mapStructVersion )
398399
.map( psiAnnotation -> AnnotationUtil.getDeclaredStringAttributeValue( psiAnnotation, "target" ) )
399400
.filter( Objects::nonNull )
400401
.filter( s -> !s.isEmpty() );

src/main/resources/META-INF/plugin.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@
128128
key="inspection.this.target.mapping.no.source.property"
129129
shortName="TargetThisMappingNoSourcePropertyInspection"
130130
implementationClass="org.mapstruct.intellij.inspection.TargetThisMappingNoSourcePropertyInspection"/>
131+
<localInspection
132+
language="JAVA"
133+
enabledByDefault="true"
134+
level="ERROR"
135+
bundle="org.mapstruct.intellij.messages.MapStructBundle"
136+
key="inspection.target.property.mapped.more.than.once.title"
137+
shortName="TargetPropertyMappedMoreThanOnceInspection"
138+
implementationClass="org.mapstruct.intellij.inspection.TargetPropertyMappedMoreThanOnceInspection"/>
131139
</extensions>
132140

133141
<actions>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<html>
2+
<body>
3+
<p>
4+
This inspection reports when a target property is explicit mapped more than once
5+
</p>
6+
<p>
7+
<pre><code>
8+
//wrong
9+
@Mapper
10+
public interface EmployeeMapper {
11+
@Mapping(source = "employeeName", target = "name")
12+
@Mapping(source = "employeeNameLast", target = "name")
13+
Employee toEmployee(EmployeeDto employeeDto, @Context CycleAvoidingMappingContext context);
14+
}
15+
</code></pre>
16+
</p>
17+
<p>
18+
<pre><code>
19+
//correct
20+
@Mapper
21+
public interface EmployeeMapper {
22+
@Mapping(source = "employeeName", target = "name")
23+
Employee toEmployee(EmployeeDto employeeDto, @Context CycleAvoidingMappingContext context);
24+
}
25+
</code></pre>
26+
</p>
27+
<!-- tooltip end -->
28+
</body>
29+
</html>

src/main/resources/org/mapstruct/intellij/messages/MapStructBundle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ inspection.wrong.map.mapping.map.type.raw=Raw map used for mapping Map to Bean
2525
inspection.wrong.map.mapping.map.type.raw.set.default=Replace {0} with {0}<String, String>
2626
inspection.wrong.map.mapping.map.key=Key must be of type String for mapping Map to Bean
2727
inspection.wrong.map.mapping.map.key.change.to.string=Change key type to String
28+
inspection.target.property.mapped.more.than.once=Target property ''{0}'' must not be mapped more than once.
29+
inspection.target.property.mapped.more.than.once.title=Target properties must not be mapped more than once.
2830
intention.add.ignore.all.unmapped.target.properties=Add ignore all unmapped target properties
2931
intention.add.ignore.unmapped.target.property=Add ignore unmapped target property
3032
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
3436
intention.java.expression.remove.unnecessary.whitespace=Remove unnecessary whitespaces
3537
intention.wrong.map.mapping.map.type.raw=Add type to Map for mapping Map to Bean
3638
intention.wrong.map.mapping.map.key=Use Map with key of type String for mapping Map to Bean
39+
intention.remove.annotation=Remove {0} annotation
40+
intention.change.target.property=Change target property
3741
plugin.settings.title=MapStruct
3842
plugin.settings.quickFix.title=Quick fix properties
3943
plugin.settings.quickFix.preferSourceBeforeTargetInMapping=Prefer source before target in @Mapping

0 commit comments

Comments
 (0)