Skip to content

Commit 14393d1

Browse files
authored
#153 additional inspection for @mapping annotation (e.g.multiple or no sources)
1 parent f82fc92 commit 14393d1

File tree

33 files changed

+1751
-3
lines changed

33 files changed

+1751
-3
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ To learn more about MapStruct have a look at the [mapstruct](https://github.com/
3434
* `@Mapper` or `@MapperConfig` annotation missing
3535
* Unmapped target properties with quick fixes: Add unmapped target property and Ignore unmapped target property.
3636
Uses `unmappedTargetPolicy` to determine the severity that should be used
37-
37+
* No `source` defined in `@Mapping` annotation
38+
* 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`.
39+
* More than one default source in `@Mapping` annotation defined with quick fixes: Remove `defaultValue`. Remove `defaultExpression`.
40+
3841
## Requirements
3942

4043
The MapStruct plugin requires Java 11 or later

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ task libs(type: Sync) {
111111
rename 'mapstruct-1.5.3.Final.jar', 'mapstruct.jar'
112112
}
113113

114-
def mockJdkLocation = "https://github.com/JetBrains/intellij-community/raw/master/java/mock"
114+
def mockJdkLocation = "https://github.com/JetBrains/intellij-community/raw/212.5712/java/mock"
115115
def mockJdkDest = "$buildDir/mock"
116116
def downloadMockJdk(mockJdkLocation, mockJdkDest, mockJdkVersion) {
117117
def location = mockJdkLocation + mockJdkVersion

description.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
<li><code>@Mapper</code> or <code>@MapperConfig</code> annotation missing</li>
3939
<li>Unmapped target properties with quick fixes: Add unmapped target property and Ignore unmapped target property.
4040
Uses <code>unmappedTargetPolicy</code> to determine the severity that should be used</li>
41+
<li>No <code>source</code> defined in <code>@Mapping</code> annotation</li>
42+
<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>
43+
<li>More than one default source in <code>@Mapping</code> annotation defined with quick fixes: Remove <code>defaultValue</code>. Remove <code>defaultExpression</code>.</li>
4144
</ul>
4245
</li>
4346
</ul>
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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.codeInspection.LocalQuickFixOnPsiElement;
9+
import com.intellij.codeInspection.ProblemsHolder;
10+
import com.intellij.codeInspection.util.IntentionFamilyName;
11+
import com.intellij.codeInspection.util.IntentionName;
12+
import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute;
13+
import com.intellij.openapi.project.Project;
14+
import com.intellij.psi.JavaElementVisitor;
15+
import com.intellij.psi.PsiAnnotation;
16+
import com.intellij.psi.PsiElement;
17+
import com.intellij.psi.PsiElementVisitor;
18+
import com.intellij.psi.PsiFile;
19+
import com.intellij.psi.PsiNameValuePair;
20+
import com.intellij.psi.impl.source.tree.java.PsiAnnotationParamListImpl;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.mapstruct.intellij.util.MapstructUtil;
23+
24+
import static com.intellij.psi.PsiElementFactory.getInstance;
25+
26+
public abstract class MappingAnnotationInspectionBase extends InspectionBase {
27+
28+
@Override
29+
@NotNull PsiElementVisitor buildVisitorInternal( @NotNull ProblemsHolder holder, boolean isOnTheFly ) {
30+
return new MappingAnnotationInspectionBase.MyJavaElementVisitor( holder );
31+
}
32+
33+
private class MyJavaElementVisitor extends JavaElementVisitor {
34+
private final ProblemsHolder problemsHolder;
35+
36+
private MyJavaElementVisitor( ProblemsHolder problemsHolder ) {
37+
this.problemsHolder = problemsHolder;
38+
}
39+
40+
@Override
41+
public void visitAnnotation( PsiAnnotation annotation ) {
42+
super.visitAnnotation( annotation );
43+
if (annotation.hasQualifiedName( MapstructUtil.MAPPING_ANNOTATION_FQN )) {
44+
MappingAnnotation mappingAnnotation = new MappingAnnotation();
45+
for (JvmAnnotationAttribute annotationAttribute : annotation.getAttributes()) {
46+
// exclude not written attributes. They result in a syntax error
47+
if (annotationAttribute instanceof PsiNameValuePair
48+
&& annotationAttribute.getAttributeValue() != null) {
49+
PsiNameValuePair nameValuePair = (PsiNameValuePair) annotationAttribute;
50+
switch (nameValuePair.getAttributeName()) {
51+
case "source":
52+
mappingAnnotation.setSourceProperty( nameValuePair );
53+
break;
54+
case "constant":
55+
mappingAnnotation.setConstantProperty( nameValuePair );
56+
break;
57+
case "expression":
58+
mappingAnnotation.setExpressionProperty( nameValuePair );
59+
break;
60+
case "defaultValue":
61+
mappingAnnotation.setDefaultValueProperty( nameValuePair );
62+
break;
63+
case "defaultExpression":
64+
mappingAnnotation.setDefaultExpressionProperty( nameValuePair );
65+
break;
66+
case "ignore":
67+
mappingAnnotation.setIgnoreProperty( nameValuePair );
68+
break;
69+
case "dependsOn":
70+
mappingAnnotation.setDependsOnProperty( nameValuePair );
71+
break;
72+
case "qualifiedByName":
73+
mappingAnnotation.setQualifiedByNameProperty( nameValuePair );
74+
break;
75+
default:
76+
break;
77+
}
78+
}
79+
}
80+
81+
visitMappingAnnotation( problemsHolder, annotation, mappingAnnotation );
82+
}
83+
}
84+
85+
}
86+
87+
abstract void visitMappingAnnotation( @NotNull ProblemsHolder problemsHolder, @NotNull PsiAnnotation psiAnnotation,
88+
@NotNull MappingAnnotation mappingAnnotation );
89+
90+
protected static class MappingAnnotation {
91+
private PsiNameValuePair sourceProperty;
92+
private PsiNameValuePair constantProperty;
93+
private PsiNameValuePair defaultValueProperty;
94+
private PsiNameValuePair expressionProperty;
95+
private PsiNameValuePair defaultExpressionProperty;
96+
private PsiNameValuePair ignoreProperty;
97+
private PsiNameValuePair dependsOnProperty;
98+
private PsiNameValuePair qualifiedByNameProperty;
99+
100+
public PsiNameValuePair getSourceProperty() {
101+
return sourceProperty;
102+
}
103+
104+
public void setSourceProperty( PsiNameValuePair sourceProperty ) {
105+
this.sourceProperty = sourceProperty;
106+
}
107+
108+
public PsiNameValuePair getConstantProperty() {
109+
return constantProperty;
110+
}
111+
112+
public void setConstantProperty( PsiNameValuePair constantProperty ) {
113+
this.constantProperty = constantProperty;
114+
}
115+
116+
public PsiNameValuePair getDefaultValueProperty() {
117+
return defaultValueProperty;
118+
}
119+
120+
public void setDefaultValueProperty( PsiNameValuePair defaultValueProperty ) {
121+
this.defaultValueProperty = defaultValueProperty;
122+
}
123+
124+
public PsiNameValuePair getExpressionProperty() {
125+
return expressionProperty;
126+
}
127+
128+
public void setExpressionProperty( PsiNameValuePair expressionProperty ) {
129+
this.expressionProperty = expressionProperty;
130+
}
131+
132+
public PsiNameValuePair getDefaultExpressionProperty() {
133+
return defaultExpressionProperty;
134+
}
135+
136+
public void setDefaultExpressionProperty( PsiNameValuePair defaultExpressionProperty ) {
137+
this.defaultExpressionProperty = defaultExpressionProperty;
138+
}
139+
140+
public PsiNameValuePair getIgnoreProperty() {
141+
return ignoreProperty;
142+
}
143+
144+
public void setIgnoreProperty( PsiNameValuePair ignoreProperty ) {
145+
this.ignoreProperty = ignoreProperty;
146+
}
147+
148+
public boolean hasNoSourceProperties() {
149+
return sourceProperty == null && defaultValueProperty == null && expressionProperty == null
150+
&& ignoreProperty == null && constantProperty == null && dependsOnProperty == null
151+
&& qualifiedByNameProperty == null;
152+
}
153+
154+
public boolean hasNoDefaultProperties() {
155+
return defaultValueProperty == null && defaultExpressionProperty == null;
156+
}
157+
158+
public PsiNameValuePair getDependsOnProperty() {
159+
return dependsOnProperty;
160+
}
161+
162+
public void setDependsOnProperty(PsiNameValuePair dependsOnProperty) {
163+
this.dependsOnProperty = dependsOnProperty;
164+
}
165+
166+
public PsiNameValuePair getQualifiedByNameProperty() {
167+
return qualifiedByNameProperty;
168+
}
169+
170+
public void setQualifiedByNameProperty(PsiNameValuePair qualifiedByNameProperty) {
171+
this.qualifiedByNameProperty = qualifiedByNameProperty;
172+
}
173+
}
174+
175+
protected static RemoveAnnotationAttributeQuickFix createRemoveAnnotationAttributeQuickFix(
176+
@NotNull PsiNameValuePair annotationAttribute, @NotNull String text, @NotNull String family ) {
177+
return new RemoveAnnotationAttributeQuickFix( annotationAttribute, text, family );
178+
}
179+
180+
protected static ReplaceAsDefaultValueQuickFix createReplaceAsDefaultValueQuickFix(
181+
@NotNull PsiNameValuePair annotationAttribute, @NotNull String source,
182+
@NotNull String target, @NotNull String text, @NotNull String family ) {
183+
return new ReplaceAsDefaultValueQuickFix( annotationAttribute, source, target, text, family );
184+
}
185+
186+
protected static class RemoveAnnotationAttributeQuickFix extends LocalQuickFixOnPsiElement {
187+
private final String text;
188+
private final String family;
189+
190+
private RemoveAnnotationAttributeQuickFix( @NotNull PsiNameValuePair element, @NotNull String text,
191+
@NotNull String family) {
192+
super( element );
193+
this.text = text;
194+
this.family = family;
195+
}
196+
197+
@Override
198+
public boolean isAvailable( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
199+
@NotNull PsiElement endElement ) {
200+
return startElement.isValid();
201+
}
202+
203+
@Override
204+
public @IntentionName @NotNull String getText() {
205+
return text;
206+
}
207+
208+
@Override
209+
public void invoke( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
210+
@NotNull PsiElement endElement ) {
211+
startElement.delete();
212+
}
213+
214+
@Override
215+
public @IntentionFamilyName @NotNull String getFamilyName() {
216+
return family;
217+
}
218+
219+
@Override
220+
public boolean availableInBatchMode() {
221+
return false;
222+
}
223+
}
224+
225+
protected static class ReplaceAsDefaultValueQuickFix extends LocalQuickFixOnPsiElement {
226+
227+
private final String source;
228+
private final String target;
229+
private final String text;
230+
private final String family;
231+
232+
private ReplaceAsDefaultValueQuickFix( @NotNull PsiNameValuePair element, @NotNull String source,
233+
@NotNull String target, @NotNull String text,
234+
@NotNull String family) {
235+
super( element );
236+
this.source = source;
237+
this.target = target;
238+
this.text = text;
239+
this.family = family;
240+
}
241+
242+
@Override
243+
public boolean isAvailable( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
244+
@NotNull PsiElement endElement ) {
245+
if ( !endElement.isValid() ) {
246+
return false;
247+
}
248+
PsiElement parent = endElement.getParent();
249+
return parent.isValid() && parent instanceof PsiAnnotationParamListImpl;
250+
}
251+
252+
@Override
253+
public @IntentionName @NotNull String getText() {
254+
return text;
255+
}
256+
257+
@Override
258+
public void invoke( @NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
259+
@NotNull PsiElement endElement ) {
260+
if (endElement instanceof PsiNameValuePair) {
261+
PsiNameValuePair end = (PsiNameValuePair) endElement;
262+
PsiAnnotationParamListImpl parent = (PsiAnnotationParamListImpl) end.getParent();
263+
PsiElement parent1 = parent.getParent();
264+
265+
// don't replace inside of strings. Only the constant value name
266+
String annotationText = parent1.getText().replaceFirst( "(?<!\")\\s*,?\\s*" + source + "\\s*=\\s*",
267+
target + " = " );
268+
parent1.replace( getInstance( project ).createAnnotationFromText( annotationText, parent1 ) );
269+
}
270+
}
271+
272+
@Override
273+
public @IntentionFamilyName @NotNull String getFamilyName() {
274+
return family;
275+
}
276+
277+
@Override
278+
public boolean availableInBatchMode() {
279+
return false;
280+
}
281+
}
282+
283+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.codeInspection.ProblemsHolder;
9+
import com.intellij.psi.PsiAnnotation;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.mapstruct.intellij.MapStructBundle;
12+
13+
/**
14+
* Inspection that checks if inside a @Mapping annotation more than one default source property is defined
15+
*
16+
* @author hduelme
17+
*/
18+
public class MoreThanOneDefaultSourcePropertyDefinedInspection extends MappingAnnotationInspectionBase {
19+
20+
@Override
21+
void visitMappingAnnotation( @NotNull ProblemsHolder problemsHolder, @NotNull PsiAnnotation psiAnnotation,
22+
@NotNull MappingAnnotation mappingAnnotation ) {
23+
// only apply if source property is defined
24+
if (mappingAnnotation.getSourceProperty() != null && mappingAnnotation.getDefaultValueProperty() != null
25+
&& mappingAnnotation.getDefaultExpressionProperty() != null) {
26+
String family = MapStructBundle.message( "intention.more.than.one.default.source.property" );
27+
problemsHolder.registerProblem( psiAnnotation,
28+
MapStructBundle.message( "inspection.more.than.one.default.source.property" ),
29+
createRemoveAnnotationAttributeQuickFix( mappingAnnotation.getDefaultValueProperty(),
30+
"Remove default value", family ),
31+
createRemoveAnnotationAttributeQuickFix( mappingAnnotation.getDefaultExpressionProperty(),
32+
"Remove default expression", family ) );
33+
34+
}
35+
}
36+
37+
}

0 commit comments

Comments
 (0)