Skip to content

Commit 9cb04f3

Browse files
authored
Quickfixes for removing unnecessary whitespace before and after Java expressions (#164)
1 parent e1fbabf commit 9cb04f3

12 files changed

+497
-4
lines changed

src/main/java/org/mapstruct/intellij/expression/JavaExpressionInjector.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
*/
5050
public class JavaExpressionInjector implements MultiHostInjector {
5151

52-
private static final Pattern JAVA_EXPRESSION = Pattern.compile( "\"java\\(.*\\)\"" );
52+
public static final Pattern JAVA_EXPRESSION = Pattern.compile( "\" *java\\(.*\\) *\"" );
5353

5454
private static final ElementPattern<PsiElement> PATTERN =
5555
StandardPatterns.or(
@@ -280,12 +280,23 @@ else if ( importValue instanceof PsiClassObjectAccessExpression ) {
280280
+ prefixBuilder,
281281
";\n }\n}",
282282
(PsiLanguageInjectionHost) context,
283-
new TextRange( "\"java(".length(), context.getTextRange().getLength() - ")\"".length() )
283+
getTextRange( context )
284284
)
285285
.doneInjecting();
286286
}
287287
}
288288

289+
@NotNull
290+
private static TextRange getTextRange(@NotNull PsiElement context) {
291+
String text = context.getText();
292+
return new TextRange(text.indexOf( "java(" ) + 5,
293+
context.getTextRange().getLength() - getEndOfExpression( text ) );
294+
}
295+
296+
private static int getEndOfExpression(@NotNull String text) {
297+
return text.length() - text.lastIndexOf( ')' );
298+
}
299+
289300
@NotNull
290301
@Override
291302
public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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.ProblemHighlightType;
10+
import com.intellij.codeInspection.ProblemsHolder;
11+
import com.intellij.codeInspection.util.IntentionFamilyName;
12+
import com.intellij.codeInspection.util.IntentionName;
13+
import com.intellij.openapi.project.Project;
14+
import com.intellij.psi.PsiAnnotation;
15+
import com.intellij.psi.PsiAnnotationMemberValue;
16+
import com.intellij.psi.PsiElement;
17+
import com.intellij.psi.PsiFile;
18+
import com.intellij.psi.PsiNameValuePair;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.mapstruct.intellij.MapStructBundle;
21+
22+
import static com.intellij.psi.PsiElementFactory.getInstance;
23+
import static org.mapstruct.intellij.expression.JavaExpressionInjector.JAVA_EXPRESSION;
24+
25+
public class JavaExpressionUnnecessaryWhitespacesInspector extends MappingAnnotationInspectionBase {
26+
27+
@Override
28+
void visitMappingAnnotation(@NotNull ProblemsHolder problemsHolder, @NotNull PsiAnnotation psiAnnotation,
29+
@NotNull MappingAnnotation mappingAnnotation) {
30+
inspectUnnecessaryWhitespaces( problemsHolder, mappingAnnotation.getExpressionProperty() );
31+
inspectUnnecessaryWhitespaces( problemsHolder, mappingAnnotation.getDefaultExpressionProperty() );
32+
inspectUnnecessaryWhitespaces( problemsHolder, mappingAnnotation.getConditionExpression() );
33+
}
34+
35+
private void inspectUnnecessaryWhitespaces(@NotNull ProblemsHolder problemsHolder, PsiNameValuePair property) {
36+
if ( property == null ) {
37+
return;
38+
}
39+
PsiAnnotationMemberValue value = property.getValue();
40+
if ( value == null ) {
41+
return;
42+
}
43+
String text = value.getText();
44+
if ( !JAVA_EXPRESSION.matcher( text ).matches() ) {
45+
return;
46+
}
47+
if ( text.indexOf( "java(" ) > 1 ) {
48+
problemsHolder.registerProblem( property,
49+
MapStructBundle.message( "inspection.java.expression.unnecessary.whitespace",
50+
"before", property.getAttributeName() ),
51+
ProblemHighlightType.WEAK_WARNING, new RemoveWhitespacesBefore(property) );
52+
}
53+
if ( text.lastIndexOf( ')' ) < text.length() - 2) {
54+
problemsHolder.registerProblem( property,
55+
MapStructBundle.message( "inspection.java.expression.unnecessary.whitespace",
56+
"after", property.getAttributeName() ),
57+
ProblemHighlightType.WEAK_WARNING, new RemoveWhitespacesAfter(property) );
58+
}
59+
}
60+
61+
private static class RemoveWhitespacesBefore extends LocalQuickFixOnPsiElement {
62+
63+
private final String name;
64+
65+
private RemoveWhitespacesBefore(@NotNull PsiNameValuePair element) {
66+
super( element );
67+
this.name = element.getName();
68+
}
69+
70+
@Override
71+
public @IntentionName @NotNull String getText() {
72+
return MapStructBundle.message( "inspection.java.expression.remove.unnecessary.whitespace",
73+
"before", name );
74+
}
75+
76+
@Override
77+
public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement,
78+
@NotNull PsiElement psiElement1) {
79+
if (psiElement instanceof PsiNameValuePair) {
80+
PsiNameValuePair psiNameValuePair = (PsiNameValuePair) psiElement;
81+
PsiAnnotationMemberValue value = psiNameValuePair.getValue();
82+
if (value != null) {
83+
String text = value.getText();
84+
psiNameValuePair.setValue( getInstance( project )
85+
.createExpressionFromText( "\"" + text.substring( text.indexOf( "java(" ) ), value ) );
86+
}
87+
}
88+
}
89+
90+
@Override
91+
public @IntentionFamilyName @NotNull String getFamilyName() {
92+
return MapStructBundle.message( "intention.java.expression.remove.unnecessary.whitespace" );
93+
}
94+
95+
@Override
96+
public boolean isAvailable(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
97+
@NotNull PsiElement endElement) {
98+
if ( !super.isAvailable( project, file, startElement, endElement ) ) {
99+
return false;
100+
}
101+
if ( !(startElement instanceof PsiNameValuePair ) ) {
102+
return false;
103+
}
104+
return ((PsiNameValuePair) startElement).getValue() != null;
105+
}
106+
}
107+
108+
private static class RemoveWhitespacesAfter extends LocalQuickFixOnPsiElement {
109+
110+
private final String name;
111+
112+
private RemoveWhitespacesAfter(@NotNull PsiNameValuePair element) {
113+
super( element );
114+
this.name = element.getName();
115+
}
116+
117+
@Override
118+
public @IntentionName @NotNull String getText() {
119+
return MapStructBundle.message( "inspection.java.expression.remove.unnecessary.whitespace", "after", name );
120+
}
121+
122+
@Override
123+
public void invoke(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull PsiElement psiElement,
124+
@NotNull PsiElement psiElement1) {
125+
if (psiElement instanceof PsiNameValuePair) {
126+
PsiNameValuePair psiNameValuePair = (PsiNameValuePair) psiElement;
127+
PsiAnnotationMemberValue value = psiNameValuePair.getValue();
128+
if (value != null) {
129+
String text = value.getText();
130+
psiNameValuePair.setValue( getInstance( project ).createExpressionFromText(
131+
text.substring( 0, text.lastIndexOf( ')' ) + 1 ) + "\"", value ) );
132+
}
133+
}
134+
}
135+
136+
@Override
137+
public @IntentionFamilyName @NotNull String getFamilyName() {
138+
return MapStructBundle.message( "intention.java.expression.remove.unnecessary.whitespace" );
139+
}
140+
141+
@Override
142+
public boolean isAvailable(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement,
143+
@NotNull PsiElement endElement) {
144+
if ( !super.isAvailable( project, file, startElement, endElement ) ) {
145+
return false;
146+
}
147+
if ( !(startElement instanceof PsiNameValuePair ) ) {
148+
return false;
149+
}
150+
return ((PsiNameValuePair) startElement).getValue() != null;
151+
}
152+
}
153+
}

src/main/java/org/mapstruct/intellij/inspection/MappingAnnotationInspectionBase.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private MyJavaElementVisitor( ProblemsHolder problemsHolder ) {
3838
}
3939

4040
@Override
41-
public void visitAnnotation( PsiAnnotation annotation ) {
41+
public void visitAnnotation(@NotNull PsiAnnotation annotation ) {
4242
super.visitAnnotation( annotation );
4343
if (annotation.hasQualifiedName( MapstructUtil.MAPPING_ANNOTATION_FQN )) {
4444
MappingAnnotation mappingAnnotation = new MappingAnnotation();
@@ -72,6 +72,9 @@ public void visitAnnotation( PsiAnnotation annotation ) {
7272
case "qualifiedByName":
7373
mappingAnnotation.setQualifiedByNameProperty( nameValuePair );
7474
break;
75+
case "conditionExpression":
76+
mappingAnnotation.setConditionExpression( nameValuePair );
77+
break;
7578
default:
7679
break;
7780
}
@@ -96,6 +99,7 @@ protected static class MappingAnnotation {
9699
private PsiNameValuePair ignoreProperty;
97100
private PsiNameValuePair dependsOnProperty;
98101
private PsiNameValuePair qualifiedByNameProperty;
102+
private PsiNameValuePair conditionExpression;
99103

100104
public PsiNameValuePair getSourceProperty() {
101105
return sourceProperty;
@@ -170,6 +174,14 @@ public PsiNameValuePair getQualifiedByNameProperty() {
170174
public void setQualifiedByNameProperty(PsiNameValuePair qualifiedByNameProperty) {
171175
this.qualifiedByNameProperty = qualifiedByNameProperty;
172176
}
177+
178+
public PsiNameValuePair getConditionExpression() {
179+
return conditionExpression;
180+
}
181+
182+
public void setConditionExpression(PsiNameValuePair conditionExpression) {
183+
this.conditionExpression = conditionExpression;
184+
}
173185
}
174186

175187
protected static RemoveAnnotationAttributeQuickFix createRemoveAnnotationAttributeQuickFix(

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,17 @@
101101
enabledByDefault="true"
102102
level="ERROR"
103103
bundle="org.mapstruct.intellij.messages.MapStructBundle"
104-
key="inspection.not.null.checkable.property.source.used.with.default.property"
104+
key="inspection.not.null.checkable.property.source.used.with.default.property.title"
105105
shortName="NotNullCheckableSourcePropertyUsedWithDefaultValue"
106106
implementationClass="org.mapstruct.intellij.inspection.NotNullCheckableSourcePropertyUsedWithDefaultValueInspection"/>
107+
<localInspection
108+
language="JAVA"
109+
enabledByDefault="true"
110+
level="WARNING"
111+
bundle="org.mapstruct.intellij.messages.MapStructBundle"
112+
key="inspection.java.expression.unnecessary.whitespace.title"
113+
shortName="JavaExpressionUnnecessaryWhitespaces"
114+
implementationClass="org.mapstruct.intellij.inspection.JavaExpressionUnnecessaryWhitespacesInspector"/>
107115
</extensions>
108116

109117
<actions>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<html>
2+
<body>
3+
<p>
4+
This inspection reports when a java expression has whitespaces before or after the <code>java()</code> block.
5+
</p>
6+
<pre><code>
7+
//wrong
8+
@Mapper
9+
public interface EmployeeMapper {
10+
@Mapping(source = "employeeName", expression = " java(\"Name\")")
11+
Employee toEmployee(EmployeeDto employeeDto, @Context CycleAvoidingMappingContext context);
12+
}
13+
</code></pre>
14+
</p>
15+
<p>
16+
<pre><code>
17+
//correct
18+
@Mapper
19+
public interface EmployeeMapper {
20+
@Mapping(source = "employeeName", expression = "java(\"Name\")")
21+
Employee toEmployee(EmployeeDto employeeDto, @Context CycleAvoidingMappingContext context);
22+
}
23+
</code></pre>
24+
</p>
25+
<!-- tooltip end -->
26+
</body>
27+
</html>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ inspection.no.source.property=No source property defined
1515
inspection.more.than.one.source.property=More than one source property defined
1616
inspection.more.than.one.default.source.property=More than one default source property defined
1717
inspection.not.null.checkable.property.source.used.with.default.property={0} property used with {1}
18+
inspection.not.null.checkable.property.source.used.with.default.property.title=Constant or expression source property used with a default source property
19+
inspection.java.expression.unnecessary.whitespace=Unnecessary whitespaces {0} {1}
20+
inspection.java.expression.remove.unnecessary.whitespace=Remove unnecessary whitespaces {0} {1}
21+
inspection.java.expression.unnecessary.whitespace.title=Unnecessary whitespaces before or after Java expression
1822
intention.add.ignore.all.unmapped.target.properties=Add ignore all unmapped target properties
1923
intention.add.ignore.unmapped.target.property=Add ignore unmapped target property
2024
intention.add.unmapped.target.property=Add unmapped target property
2125
intention.no.source.property=Add one source property
2226
intention.more.than.one.source.property=Only use one source property
2327
intention.more.than.one.default.source.property=Only use one default source property
2428
intention.not.null.checkable.property.source.used.with.default.property=Remove default properties
29+
intention.java.expression.remove.unnecessary.whitespace=Remove unnecessary whitespaces
2530
plugin.settings.title=MapStruct
2631
plugin.settings.quickFix.title=Quick fix properties
2732
plugin.settings.quickFix.preferSourceBeforeTargetInMapping=Prefer source before target in @Mapping

src/test/java/org/mapstruct/intellij/expression/JavaExpressionInjectionTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,42 @@ protected void withTargetDefinedMapper(String attribute) {
268268
assertThat( elementAt.getText() ).isEqualTo( ";" );
269269
}
270270

271+
public void testExpressionWithTargetDefinedAndWhitespacesMapper() {
272+
withTargetDefinedAndWhitespaceMapper( "expression" );
273+
withTargetDefinedAndWhitespaceMapper( "defaultExpression" );
274+
withTargetDefinedAndWhitespaceMapper( "conditionExpression" );
275+
}
276+
277+
protected void withTargetDefinedAndWhitespaceMapper(String attribute) {
278+
String mapping = "@Mapping(target = \"manufacturingYear\", " + attribute + " = \" java(car.<caret>) \")\n";
279+
@Language("java")
280+
String mapper = formatMapper( CAR_MAPPER, mapping );
281+
PsiFile file = configureMapperByText( mapper );
282+
283+
assertThat( myFixture.completeBasic() )
284+
.extracting( LookupElementPresentation::renderElement )
285+
.extracting( LookupElementPresentation::getItemText )
286+
.contains(
287+
"getMake",
288+
"setMake",
289+
"getManufacturingDate",
290+
"setManufacturingDate",
291+
"getNumberOfSeats",
292+
"setNumberOfSeats"
293+
);
294+
295+
assertThat( myFixture.complete( CompletionType.SMART ) )
296+
.extracting( LookupElementPresentation::renderElement )
297+
.extracting( LookupElementPresentation::getItemText )
298+
.containsExactlyInAnyOrder( "getMake", "toString" );
299+
300+
PsiElement elementAt = file.findElementAt( myFixture.getCaretOffset() );
301+
assertThat( elementAt )
302+
.isNotNull()
303+
.isInstanceOf( PsiJavaToken.class );
304+
assertThat( elementAt.getText() ).isEqualTo( ";" );
305+
}
306+
271307
public void testExpressionWithTargetDefinedMapperInMappings() {
272308
withTargetDefinedMapperInMappings( "expression" );
273309
withTargetDefinedMapperInMappings( "defaultExpression" );
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.IntentionAction;
9+
import com.intellij.codeInspection.LocalInspectionTool;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import java.util.List;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
public class JavaExpressionUnnecessaryWhitespacesInspectorTest extends BaseInspectionTest {
17+
@Override
18+
protected @NotNull Class<? extends LocalInspectionTool> getInspection() {
19+
return JavaExpressionUnnecessaryWhitespacesInspector.class;
20+
}
21+
22+
public void testJavaExpressionUnnecessaryWhitespacesInspectorWhitespaceBefore() {
23+
doTest();
24+
String testName = getTestName( false );
25+
List<IntentionAction> allQuickFixes = myFixture.getAllQuickFixes();
26+
27+
assertThat( allQuickFixes )
28+
.extracting( IntentionAction::getText )
29+
.as( "Intent Text" )
30+
.containsExactly(
31+
"Remove unnecessary whitespaces before conditionExpression",
32+
"Remove unnecessary whitespaces before defaultExpression",
33+
"Remove unnecessary whitespaces before expression"
34+
);
35+
36+
allQuickFixes.forEach( myFixture::launchAction );
37+
myFixture.checkResultByFile( testName + "_after.java" );
38+
}
39+
40+
public void testJavaExpressionUnnecessaryWhitespacesInspectorWhitespaceAfter() {
41+
doTest();
42+
String testName = getTestName( false );
43+
List<IntentionAction> allQuickFixes = myFixture.getAllQuickFixes();
44+
45+
assertThat( allQuickFixes )
46+
.extracting( IntentionAction::getText )
47+
.as( "Intent Text" )
48+
.containsExactly(
49+
"Remove unnecessary whitespaces after conditionExpression",
50+
"Remove unnecessary whitespaces after defaultExpression",
51+
"Remove unnecessary whitespaces after expression"
52+
);
53+
54+
allQuickFixes.forEach( myFixture::launchAction );
55+
myFixture.checkResultByFile( testName + "_after.java" );
56+
}
57+
}

0 commit comments

Comments
 (0)