Skip to content

Commit aefc7b8

Browse files
committed
Support for java code completion in expression and defaultExpression
closes #30
1 parent f768a0b commit aefc7b8

File tree

6 files changed

+446
-0
lines changed

6 files changed

+446
-0
lines changed

change-notes.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<html>
2+
<h2>1.2.1</h2>
3+
<ul>
4+
<li>Support code completion in Mapping#expression and Mapping#defaultExpression</li>
5+
</ul>
26
<h2>1.2.0</h2>
37
<ul>
48
<li>Support for public fields (auto completion and unmapped target / source inspection warnings)</li>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.intellij.expression;
7+
8+
import java.util.Collections;
9+
import java.util.List;
10+
import java.util.regex.Pattern;
11+
12+
import com.intellij.codeInsight.AnnotationUtil;
13+
import com.intellij.lang.injection.MultiHostInjector;
14+
import com.intellij.lang.injection.MultiHostRegistrar;
15+
import com.intellij.lang.java.JavaLanguage;
16+
import com.intellij.openapi.util.TextRange;
17+
import com.intellij.patterns.ElementPattern;
18+
import com.intellij.patterns.StandardPatterns;
19+
import com.intellij.psi.PsiAnnotation;
20+
import com.intellij.psi.PsiAnnotationMemberValue;
21+
import com.intellij.psi.PsiAnnotationParameterList;
22+
import com.intellij.psi.PsiClass;
23+
import com.intellij.psi.PsiClassType;
24+
import com.intellij.psi.PsiElement;
25+
import com.intellij.psi.PsiJavaCodeReferenceElement;
26+
import com.intellij.psi.PsiLanguageInjectionHost;
27+
import com.intellij.psi.PsiLiteralExpression;
28+
import com.intellij.psi.PsiMethod;
29+
import com.intellij.psi.PsiNameValuePair;
30+
import com.intellij.psi.PsiParameter;
31+
import com.intellij.psi.PsiReference;
32+
import com.intellij.psi.PsiType;
33+
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
34+
import com.intellij.psi.util.PsiUtil;
35+
import org.jetbrains.annotations.NotNull;
36+
import org.mapstruct.intellij.util.MapstructElementUtils;
37+
import org.mapstruct.intellij.util.MapstructUtil;
38+
39+
/**
40+
* @author Filip Hrisafov
41+
*/
42+
public class JavaExpressionInjector implements MultiHostInjector {
43+
44+
private static final Pattern JAVA_EXPRESSION = Pattern.compile( "\"java\\(.*\\)\"" );
45+
46+
private static final ElementPattern<PsiElement> PATTERN =
47+
StandardPatterns.or(
48+
MapstructElementUtils.mappingElementPattern( "expression" ),
49+
MapstructElementUtils.mappingElementPattern( "defaultExpression" )
50+
);
51+
52+
@Override
53+
public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
54+
55+
if ( PATTERN.accepts( context ) && context instanceof PsiLiteralExpression &&
56+
JAVA_EXPRESSION.matcher( context.getText() ).matches() ) {
57+
58+
// Context is the PsiLiteralExpression
59+
// In order to reach the method have the following steps to do:
60+
// PsiLiteralExpression - "java(something)"
61+
// PsiNameValuePair - expression = "java(something)"
62+
// PsiAnnotationParameterList - target = "", expression = "java(something)"
63+
// PsiAnnotation - @Mapping(target = "", expression = "java(something)")
64+
// PsiModifierList
65+
// PsiMethod
66+
67+
PsiAnnotationParameterList annotationParameterList = (PsiAnnotationParameterList) context.getParent()
68+
.getParent();
69+
PsiType targetType = null;
70+
for ( PsiNameValuePair attribute : annotationParameterList.getAttributes() ) {
71+
if ( "target" .equals( attribute.getAttributeName() ) ) {
72+
PsiAnnotationMemberValue attributeValue = attribute.getValue();
73+
if ( attributeValue != null ) {
74+
PsiReference[] references = ReferenceProvidersRegistry.getReferencesFromProviders(
75+
attributeValue );
76+
if ( references.length > 0 ) {
77+
PsiElement resolved = references[0].resolve();
78+
if ( resolved instanceof PsiMethod ) {
79+
targetType = ( (PsiMethod) resolved ).getParameterList().getParameters()[0].getType();
80+
}
81+
else if ( resolved instanceof PsiParameter ) {
82+
targetType = ( (PsiParameter) resolved ).getType();
83+
}
84+
}
85+
}
86+
break;
87+
}
88+
}
89+
90+
if ( targetType == null ) {
91+
return;
92+
}
93+
94+
PsiMethod method = (PsiMethod) annotationParameterList.getParent().getParent().getParent();
95+
PsiClass mapperClass = (PsiClass) method.getParent();
96+
StringBuilder importsBuilder = new StringBuilder();
97+
StringBuilder prefixBuilder = new StringBuilder();
98+
99+
prefixBuilder.append( "public class " )
100+
.append( mapperClass.getName() ).append( "Impl" )
101+
.append( " implements " ).append( mapperClass.getQualifiedName() ).append( "{ " )
102+
.append( "public " ).append( method.getReturnType().getCanonicalText() ).append( " " )
103+
.append( method.getName() ).append( "(" );
104+
105+
PsiParameter[] parameters = method.getParameterList().getParameters();
106+
for ( int i = 0; i < parameters.length; i++ ) {
107+
if ( i != 0 ) {
108+
prefixBuilder.append( "," );
109+
}
110+
111+
PsiParameter parameter = parameters[i];
112+
PsiType parameterType = parameter.getType();
113+
PsiClass parameterClass = PsiUtil.resolveClassInType( parameterType );
114+
115+
if ( parameterClass == null ) {
116+
return;
117+
}
118+
119+
importsBuilder.append( "import " ).append( parameterClass.getQualifiedName() ).append( ";\n" );
120+
121+
prefixBuilder.append( parameterType.getCanonicalText() ).append( " " ).append( parameter.getName() );
122+
}
123+
124+
prefixBuilder.append( ") {" )
125+
.append( targetType.getCanonicalText() ).append( " __target__ =" );
126+
127+
128+
PsiClass targetClass = PsiUtil.resolveClassInType( targetType );
129+
if ( targetClass != null ) {
130+
importsBuilder.append( "import " ).append( targetClass.getQualifiedName() ).append( ";\n" );
131+
}
132+
if ( targetType instanceof PsiClassType ) {
133+
for ( PsiType typeParameter : ( (PsiClassType) targetType ).getParameters() ) {
134+
PsiClass typeClass = PsiUtil.resolveClassInType( typeParameter );
135+
if ( typeClass != null ) {
136+
importsBuilder.append( "import " ).append( typeClass.getQualifiedName() ).append( ";\n" );
137+
}
138+
}
139+
}
140+
141+
PsiAnnotation mapper = mapperClass.getAnnotation( MapstructUtil.MAPPER_ANNOTATION_FQN );
142+
if ( mapper != null ) {
143+
for ( PsiNameValuePair attribute : mapper.getParameterList().getAttributes() ) {
144+
if ( "imports" .equals( attribute.getName() ) ) {
145+
for ( PsiAnnotationMemberValue importValue : AnnotationUtil.arrayAttributeValues(
146+
attribute.getValue() ) ) {
147+
148+
if ( importValue instanceof PsiJavaCodeReferenceElement ) {
149+
importsBuilder.append( "import " )
150+
.append( ( (PsiJavaCodeReferenceElement) importValue ).getQualifiedName() )
151+
.append( ";" );
152+
}
153+
}
154+
}
155+
}
156+
}
157+
158+
registrar.startInjecting( JavaLanguage.INSTANCE )
159+
.addPlace(
160+
importsBuilder.toString() + prefixBuilder.toString(),
161+
";} }",
162+
(PsiLanguageInjectionHost) context,
163+
new TextRange( 6, context.getTextRange().getLength() - 2 )
164+
)
165+
.doneInjecting();
166+
}
167+
}
168+
169+
@NotNull
170+
@Override
171+
public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
172+
return Collections.singletonList( PsiAnnotationMemberValue.class );
173+
}
174+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<psi.referenceContributor language="JAVA" implementation="org.mapstruct.intellij.codeinsight.references.MapstructReferenceContributor" />
3838
<methodReferencesSearch implementation="org.mapstruct.intellij.search.MappingMethodUsagesSearcher" />
3939
<renameHandler implementation="org.mapstruct.intellij.rename.MapstructSourceTargetParameterRenameHandler"/>
40+
<multiHostInjector implementation="org.mapstruct.intellij.expression.JavaExpressionInjector"/>
4041

4142
<localInspection language="JAVA"
4243
enabledByDefault="true"
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.intellij.expression;
7+
8+
import com.intellij.codeInsight.completion.CompletionType;
9+
import com.intellij.codeInsight.lookup.LookupElementPresentation;
10+
import com.intellij.ide.highlighter.JavaFileType;
11+
import com.intellij.psi.PsiElement;
12+
import com.intellij.psi.PsiFile;
13+
import com.intellij.psi.PsiIdentifier;
14+
import com.intellij.psi.PsiJavaToken;
15+
import com.intellij.psi.PsiReference;
16+
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
17+
import org.intellij.lang.annotations.Language;
18+
import org.mapstruct.intellij.MapstructBaseCompletionTestCase;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
/**
23+
* @author Filip Hrisafov
24+
*/
25+
public class JavaExpressionInjectionTest extends MapstructBaseCompletionTestCase {
26+
27+
private static final String CAR_MAPPER = "import java.util.List;\n" +
28+
"\n" +
29+
"import org.mapstruct.Mapper;\n" +
30+
"import org.mapstruct.Mapping;\n" +
31+
"import org.example.dto.CarDto;\n" +
32+
"import org.example.dto.Car;\n" +
33+
"\n" +
34+
"@Mapper(%s)\n" +
35+
"public interface CarMapper {\n" +
36+
"\n" +
37+
" %s" +
38+
" CarDto carToCarDto(Car car);\n" +
39+
"}";
40+
41+
@Override
42+
protected String getTestDataPath() {
43+
return "testData/expression";
44+
}
45+
46+
@Override
47+
protected void setUp() throws Exception {
48+
super.setUp();
49+
50+
addDirectoryToProject( "dto" );
51+
}
52+
53+
public void testExpressionWithNoTargetDefinedMapper() {
54+
noTargetDefinedMapper( "expression" );
55+
noTargetDefinedMapper( "defaultExpression" );
56+
}
57+
58+
protected void noTargetDefinedMapper(String attribute) {
59+
String mapping = "@Mapping(target = \"\", " + attribute + " = \"java(car.<caret>)\")\n";
60+
@Language("java")
61+
String mapper = String.format( CAR_MAPPER, "", mapping );
62+
PsiFile file = configureMapperByText( mapper );
63+
64+
assertThat( myFixture.completeBasic() )
65+
.extracting( LookupElementPresentation::renderElement )
66+
.extracting( LookupElementPresentation::getItemText )
67+
.isEmpty();
68+
69+
PsiElement elementAt = file.findElementAt( myFixture.getCaretOffset() );
70+
assertThat( elementAt )
71+
.isNotNull()
72+
.isInstanceOf( PsiJavaToken.class );
73+
assertThat( elementAt.getText() ).isEqualTo( "\"java(car.)\"" );
74+
}
75+
76+
public void testExpressionWithoutJavaExpression() {
77+
withoutJavaExpresion( "expression" );
78+
withoutJavaExpresion( "defaultExpression" );
79+
}
80+
81+
protected void withoutJavaExpresion(String attribute) {
82+
String mapping = "@Mapping(target = \"manufacturingYear\", " + attribute + " = \"car<caret>\")\n";
83+
@Language("java")
84+
String mapper = String.format( CAR_MAPPER, "", mapping );
85+
PsiFile file = configureMapperByText( mapper );
86+
87+
PsiElement elementAt = file.findElementAt( myFixture.getCaretOffset() );
88+
assertThat( elementAt )
89+
.isNotNull()
90+
.isInstanceOf( PsiJavaToken.class );
91+
assertThat( elementAt.getText() ).isEqualTo( "\"car\"" );
92+
}
93+
94+
public void testExpressionWithTargetDefinedMapper() {
95+
withTargetDefinedMapper( "expression" );
96+
withTargetDefinedMapper( "defaultExpression" );
97+
}
98+
99+
protected void withTargetDefinedMapper(String attribute) {
100+
String mapping = "@Mapping(target = \"manufacturingYear\", " + attribute + " = \"java(car.<caret>)\")\n";
101+
@Language("java")
102+
String mapper = String.format( CAR_MAPPER, "", mapping );
103+
PsiFile file = configureMapperByText( mapper );
104+
105+
assertThat( myFixture.completeBasic() )
106+
.extracting( LookupElementPresentation::renderElement )
107+
.extracting( LookupElementPresentation::getItemText )
108+
.contains(
109+
"getMake",
110+
"setMake",
111+
"getManufacturingDate",
112+
"setManufacturingDate",
113+
"getNumberOfSeats",
114+
"setNumberOfSeats"
115+
);
116+
117+
assertThat( myFixture.complete( CompletionType.SMART ) )
118+
.extracting( LookupElementPresentation::renderElement )
119+
.extracting( LookupElementPresentation::getItemText )
120+
.containsExactlyInAnyOrder( "getMake", "toString" );
121+
122+
PsiElement elementAt = file.findElementAt( myFixture.getCaretOffset() );
123+
assertThat( elementAt )
124+
.isNotNull()
125+
.isInstanceOf( PsiJavaToken.class );
126+
assertThat( elementAt.getText() ).isEqualTo( ";" );
127+
}
128+
129+
public void testExpressionWithMapperWithImports() {
130+
withMapperWithImports( "expression" );
131+
withMapperWithImports( "defaultExpression" );
132+
}
133+
134+
protected void withMapperWithImports(String attribute) {
135+
String mapping = "@Mapping(target = \"manufacturingYear\", " + attribute + " = \"java(Collections<caret>)\")\n";
136+
@Language("java")
137+
String mapper = String.format( CAR_MAPPER, "imports = Collections.class", mapping );
138+
PsiFile file = configureMapperByText( mapper );
139+
140+
assertThat( myFixture.completeBasic() )
141+
.extracting( LookupElementPresentation::renderElement )
142+
.extracting( LookupElementPresentation::getItemText )
143+
.contains(
144+
"Collections"
145+
);
146+
147+
PsiElement elementAt = file.findElementAt( myFixture.getCaretOffset() - 1 );
148+
assertThat( elementAt )
149+
.isNotNull()
150+
.isInstanceOf( PsiIdentifier.class );
151+
assertThat( elementAt.getText() ).isEqualTo( "Collections" );
152+
153+
PsiReference[] references = ReferenceProvidersRegistry.getReferencesFromProviders( elementAt );
154+
assertThat( references ).isEmpty();
155+
156+
}
157+
158+
public void testExpressionWithMapperWithoutImports() {
159+
withMapperWithoutImports( "expression" );
160+
withMapperWithoutImports( "defaultExpression" );
161+
}
162+
163+
protected void withMapperWithoutImports(String attribute) {
164+
String mapping = "@Mapping(target = \"manufacturingYear\", " + attribute + " = \"java(Collections<caret>)\")\n";
165+
@Language("java")
166+
String mapper = String.format( CAR_MAPPER, "", mapping );
167+
PsiFile file = configureMapperByText( mapper );
168+
169+
assertThat( myFixture.completeBasic() )
170+
.extracting( LookupElementPresentation::renderElement )
171+
.extracting( LookupElementPresentation::getItemText )
172+
.contains(
173+
"Collections"
174+
);
175+
176+
PsiElement elementAt = file.findElementAt( myFixture.getCaretOffset() - 1 );
177+
assertThat( elementAt )
178+
.isNotNull()
179+
.isInstanceOf( PsiIdentifier.class );
180+
assertThat( elementAt.getText() ).isEqualTo( "Collections" );
181+
182+
PsiReference[] references = ReferenceProvidersRegistry.getReferencesFromProviders( elementAt );
183+
assertThat( references ).isEmpty();
184+
185+
}
186+
187+
private PsiFile configureMapperByText(@Language("java") String text) {
188+
return myFixture.configureByText( JavaFileType.INSTANCE, text );
189+
}
190+
}

0 commit comments

Comments
 (0)