Skip to content

Commit 20297a3

Browse files
authored
#65 Support for @InheritConfiguration in UnmappedTargetPropertiesInspection
1 parent 830a34c commit 20297a3

12 files changed

+594
-17
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.mapstruct.intellij.settings.ProjectSettings;
3737
import org.mapstruct.intellij.util.MapStructVersion;
3838
import org.mapstruct.intellij.util.MapstructUtil;
39-
import org.mapstruct.intellij.util.TargetUtils;
4039

4140
import static com.intellij.codeInsight.AnnotationUtil.findAnnotation;
4241
import static com.intellij.codeInsight.AnnotationUtil.getBooleanAttributeValue;
@@ -45,8 +44,11 @@
4544
import static org.mapstruct.intellij.util.MapstructUtil.isMapper;
4645
import static org.mapstruct.intellij.util.MapstructUtil.isMapperConfig;
4746
import static org.mapstruct.intellij.util.SourceUtils.findAllSourceProperties;
47+
import static org.mapstruct.intellij.util.TargetUtils.findAllDefinedMappingTargets;
4848
import static org.mapstruct.intellij.util.TargetUtils.findAllSourcePropertiesForCurrentTarget;
4949
import static org.mapstruct.intellij.util.TargetUtils.findAllTargetProperties;
50+
import static org.mapstruct.intellij.util.TargetUtils.findInheritedTargetProperties;
51+
import static org.mapstruct.intellij.util.TargetUtils.getRelevantType;
5052

5153
/**
5254
* Inspection that checks if there are unmapped target properties.
@@ -85,11 +87,17 @@ public void visitMethod(PsiMethod method) {
8587
Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion, method );
8688

8789
// find and remove all defined mapping targets
88-
Set<String> definedTargets = TargetUtils.findAllDefinedMappingTargets( method, mapStructVersion )
89-
.map( MyJavaElementVisitor::getBaseTarget )
90-
.collect( Collectors.toSet() );
90+
Set<String> definedTargets = findAllDefinedMappingTargets( method, mapStructVersion )
91+
.map( MyJavaElementVisitor::getBaseTarget )
92+
.collect( Collectors.toSet() );
9193
allTargetProperties.removeAll( definedTargets );
9294

95+
// find and remove all inherited target properties
96+
Set<String> inheritedTargetProperties = findInheritedTargetProperties( method, mapStructVersion )
97+
.map( MyJavaElementVisitor::getBaseTarget )
98+
.collect( Collectors.toSet() );
99+
allTargetProperties.removeAll( inheritedTargetProperties );
100+
93101
if ( definedTargets.contains( "." ) ) {
94102
// If there is a defined current target then we need to remove all implicit mapped properties for
95103
// the target source
@@ -184,7 +192,7 @@ private static PsiType getTargetType(PsiMethod method) {
184192
|| !( isMapper( containingClass ) || isMapperConfig( containingClass ) ) ) {
185193
return null;
186194
}
187-
return TargetUtils.getRelevantType( method );
195+
return getRelevantType( method );
188196
}
189197
}
190198

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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.util;
7+
8+
import java.util.Arrays;
9+
import java.util.HashSet;
10+
import java.util.List;
11+
import java.util.Optional;
12+
import java.util.Set;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
import com.intellij.psi.PsiAnnotation;
17+
import com.intellij.psi.PsiClass;
18+
import com.intellij.psi.PsiMethod;
19+
import com.intellij.psi.PsiModifierListOwner;
20+
import com.intellij.psi.PsiParameter;
21+
import com.intellij.psi.PsiParameterList;
22+
import com.intellij.psi.PsiType;
23+
import org.jetbrains.annotations.NotNull;
24+
import org.jetbrains.annotations.Nullable;
25+
26+
import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
27+
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findMapperConfigReference;
28+
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findReferencedMapperClasses;
29+
30+
/**
31+
* Utils for working with inherited configurations based on {@link org.mapstruct.InheritConfiguration}
32+
*/
33+
public class InheritConfigurationUtils {
34+
35+
private InheritConfigurationUtils() {
36+
}
37+
38+
/**
39+
* Find all mapping methods (regardless of their type and parameter) that are in the scope to be inherited from:
40+
* <ul>
41+
* <li>local methods</li>
42+
* <li>methods of mappers extending from</li>
43+
* <li>methods in referenced mappers by {@link org.mapstruct.Mapper#uses()}</li>
44+
* <li>methods from {@link org.mapstruct.MapperConfig}</li>
45+
* <li>methods from referenced mappers from {@link org.mapstruct.MapperConfig#uses()}</li>
46+
* </ul>
47+
*
48+
* @param containingClass containing class of the mapping method to check
49+
* @param mapperAnnotation the mapper annotation of the containing class
50+
* @return possible mapping methods to be inherited from
51+
*/
52+
public static Stream<PsiMethod> findMappingMethodsFromInheritScope(@NotNull PsiClass containingClass,
53+
@NotNull PsiAnnotation mapperAnnotation) {
54+
55+
Stream<PsiMethod> localAndParentMethods = Arrays.stream( containingClass.getAllMethods() );
56+
57+
Stream<PsiMethod> referencedMethods = findReferencedMapperClasses( mapperAnnotation )
58+
.flatMap( c -> Arrays.stream( c.getMethods() ) );
59+
60+
Stream<PsiMethod> mapperConfigMethods = findMapperConfigMethods( mapperAnnotation );
61+
62+
return Stream.concat( Stream.concat( localAndParentMethods, referencedMethods ), mapperConfigMethods );
63+
}
64+
65+
private static Stream<PsiMethod> findMapperConfigMethods(@NotNull PsiAnnotation mapperAnnotation) {
66+
67+
PsiModifierListOwner mapperConfigReference = findMapperConfigReference( mapperAnnotation );
68+
69+
if ( !( mapperConfigReference instanceof PsiClass ) ) {
70+
return Stream.empty();
71+
}
72+
73+
return Arrays.stream( ( (PsiClass) mapperConfigReference ).getMethods() );
74+
}
75+
76+
/**
77+
* Find a candidate that this mapping method can inherit from, only if it is the only one possible.
78+
*
79+
* @param mappingMethod the mapping method to find a belonging method
80+
* @param candidates mapping methods that possibly hold the method to inherit from
81+
* @param inheritConfigurationAnnotation the {@link org.mapstruct.InheritConfiguration} annotation
82+
* @return a mapping method to inherit from, only if it is the only possible one found.
83+
*/
84+
public static Optional<PsiMethod> findSingleMatchingInheritMappingMethod(
85+
@NotNull PsiMethod mappingMethod,
86+
@NotNull Stream<PsiMethod> candidates,
87+
@NotNull PsiAnnotation inheritConfigurationAnnotation) {
88+
89+
String inheritConfigurationName = getStringAttributeValue( inheritConfigurationAnnotation, "name" );
90+
91+
PsiType targetType = findTargetTypeOfMappingMethod( mappingMethod );
92+
93+
List<PsiMethod> matchingCandidates = candidates
94+
.filter( candidate -> isNotTheSameMethod( mappingMethod, candidate ) )
95+
.filter( candidate -> matchesNameWhenNameIsDefined( inheritConfigurationName, candidate ) )
96+
.filter( candidate -> canInheritFrom( mappingMethod, targetType, candidate ) )
97+
.collect( Collectors.toList() );
98+
99+
if ( matchingCandidates.size() == 1 ) {
100+
return Optional.of( matchingCandidates.get( 0 ) );
101+
}
102+
103+
return Optional.empty();
104+
105+
}
106+
107+
private static boolean isNotTheSameMethod(PsiMethod mappingMethod, PsiMethod candidate) {
108+
109+
return !( mappingMethod.equals( candidate ) );
110+
}
111+
112+
private static boolean matchesNameWhenNameIsDefined(String inheritConfigurationName, PsiMethod candidate) {
113+
114+
if ( inheritConfigurationName == null || inheritConfigurationName.isEmpty() ) {
115+
return true;
116+
}
117+
118+
return candidate.getName().equals( inheritConfigurationName );
119+
}
120+
121+
/**
122+
* simplified version of <code>org.mapstruct.ap.internal.model.source.SourceMethod#canInheritFrom</code>
123+
*/
124+
public static boolean canInheritFrom(PsiMethod mappingMethod, PsiType targetType, PsiMethod candidate) {
125+
126+
PsiType targetTypeOfCandidate = findTargetTypeOfMappingMethod( candidate );
127+
128+
return targetType != null
129+
&& candidate.getBody() == null
130+
&& targetTypeOfCandidate != null
131+
&& targetTypeOfCandidate.isAssignableFrom( targetType )
132+
&& allParametersAreAssignable( mappingMethod.getParameterList(), candidate.getParameterList() );
133+
}
134+
135+
@Nullable
136+
private static PsiType findTargetTypeOfMappingMethod(PsiMethod mappingMethod) {
137+
138+
PsiType targetType = mappingMethod.getReturnType();
139+
140+
if ( !PsiType.VOID.equals( targetType ) ) {
141+
return targetType;
142+
}
143+
144+
return Stream.of( mappingMethod.getParameterList().getParameters() )
145+
.filter( MapstructUtil::isMappingTarget )
146+
.findFirst()
147+
.map( PsiParameter::getType )
148+
.orElse( null );
149+
}
150+
151+
private static boolean allParametersAreAssignable(PsiParameterList inheritParameters,
152+
PsiParameterList candidateParameters) {
153+
154+
if ( inheritParameters == null || candidateParameters == null || inheritParameters.isEmpty() ||
155+
candidateParameters.isEmpty() ) {
156+
return false;
157+
}
158+
159+
List<PsiParameter> fromParams = Arrays.stream( inheritParameters.getParameters() )
160+
.filter( MapstructUtil::isValidSourceParameter )
161+
.collect( Collectors.toList() );
162+
163+
List<PsiParameter> toParams = Arrays.stream( candidateParameters.getParameters() )
164+
.filter( MapstructUtil::isValidSourceParameter )
165+
.collect( Collectors.toList() );
166+
167+
return allParametersAreAssignable( fromParams, toParams );
168+
}
169+
170+
/**
171+
* psi-modified copy of <code>org.mapstruct.ap.internal.model.source.SourceMethod#allParametersAreAssignable</code>
172+
*/
173+
private static boolean allParametersAreAssignable(List<PsiParameter> fromParams, List<PsiParameter> toParams) {
174+
if ( fromParams.size() == toParams.size() ) {
175+
Set<PsiParameter> unaccountedToParams = new HashSet<>( toParams );
176+
177+
for ( PsiParameter fromParam : fromParams ) {
178+
// each fromParam needs at least one match, and all toParam need to be accounted for at the end
179+
boolean hasMatch = false;
180+
for ( PsiParameter toParam : toParams ) {
181+
if ( toParam.getType().isAssignableFrom( fromParam.getType() ) ) {
182+
unaccountedToParams.remove( toParam );
183+
hasMatch = true;
184+
}
185+
}
186+
187+
if ( !hasMatch ) {
188+
return false;
189+
}
190+
}
191+
192+
return unaccountedToParams.isEmpty();
193+
}
194+
195+
return false;
196+
}
197+
}

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
import org.jetbrains.annotations.NotNull;
5050
import org.jetbrains.annotations.Nullable;
5151
import org.mapstruct.BeanMapping;
52+
import org.mapstruct.Builder;
53+
import org.mapstruct.Context;
54+
import org.mapstruct.EnumMapping;
55+
import org.mapstruct.InheritConfiguration;
5256
import org.mapstruct.InheritInverseConfiguration;
5357
import org.mapstruct.Mapper;
5458
import org.mapstruct.MapperConfig;
@@ -86,15 +90,16 @@ public final class MapstructUtil {
8690

8791
public static final String NAMED_ANNOTATION_FQN = Named.class.getName();
8892

93+
public static final String INHERIT_CONFIGURATION_FQN = InheritConfiguration.class.getName();
94+
8995
static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
9096
static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName();
9197
static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName();
9298
private static final String MAPPING_TARGET_ANNOTATION_FQN = MappingTarget.class.getName();
93-
//TODO maybe we need to include the 1.2.0-RC1 here
94-
private static final String CONTEXT_ANNOTATION_FQN = "org.mapstruct.Context";
95-
private static final String INHERIT_INVERSE_CONFIGURATION = InheritInverseConfiguration.class.getName();
96-
private static final String BUILDER_ANNOTATION_FQN = "org.mapstruct.Builder";
97-
private static final String ENUM_MAPPING_ANNOTATION_FQN = "org.mapstruct.EnumMapping";
99+
private static final String CONTEXT_ANNOTATION_FQN = Context.class.getName();
100+
private static final String INHERIT_INVERSE_CONFIGURATION_FQN = InheritInverseConfiguration.class.getName();
101+
private static final String BUILDER_ANNOTATION_FQN = Builder.class.getName();
102+
private static final String ENUM_MAPPING_ANNOTATION_FQN = EnumMapping.class.getName();
98103

99104
/**
100105
* Hide constructor.
@@ -569,7 +574,7 @@ static boolean isMapStructJdk8Present(@NotNull Module module) {
569574
* {@code false} otherwise
570575
*/
571576
public static boolean isInheritInverseConfiguration(PsiMethod method) {
572-
return isAnnotated( method, INHERIT_INVERSE_CONFIGURATION, AnnotationUtil.CHECK_TYPE );
577+
return isAnnotated( method, INHERIT_INVERSE_CONFIGURATION_FQN, AnnotationUtil.CHECK_TYPE );
573578
}
574579

575580
}

0 commit comments

Comments
 (0)