Skip to content

Commit 40769e3

Browse files
committed
#220: Support multi level inheritance generic builders as fluent builder (like lomboks @SuperBuilder)
1 parent f178b56 commit 40769e3

File tree

4 files changed

+253
-2
lines changed

4 files changed

+253
-2
lines changed

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.HashMap;
1111
import java.util.List;
1212
import java.util.Map;
13+
import java.util.Optional;
1314
import java.util.function.Function;
1415
import java.util.stream.Stream;
1516
import javax.swing.Icon;
@@ -39,6 +40,7 @@
3940
import com.intellij.psi.PsiRecordComponent;
4041
import com.intellij.psi.PsiSubstitutor;
4142
import com.intellij.psi.PsiType;
43+
import com.intellij.psi.PsiTypeParameter;
4244
import com.intellij.psi.impl.PsiClassImplUtil;
4345
import com.intellij.psi.search.GlobalSearchScope;
4446
import com.intellij.psi.util.CachedValueProvider;
@@ -229,10 +231,12 @@ public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
229231
return !psiType.getCanonicalText().startsWith( "java.lang" ) &&
230232
method.getReturnType() != null &&
231233
!isAdderWithUpperCase4thCharacter( method ) &&
232-
isAssignableFromReturnTypeOrSuperTypes( psiType, method.getReturnType() );
234+
isAssignableFromReturnTypeOrSuperTypesOrGenericBuilder( psiType, method );
233235
}
234236

235-
private static boolean isAssignableFromReturnTypeOrSuperTypes(PsiType psiType, PsiType returnType) {
237+
private static boolean isAssignableFromReturnTypeOrSuperTypesOrGenericBuilder(PsiType psiType, PsiMethod method) {
238+
239+
PsiType returnType = method.getReturnType();
236240

237241
if ( isAssignableFrom( psiType, returnType ) ) {
238242
return true;
@@ -243,8 +247,43 @@ private static boolean isAssignableFromReturnTypeOrSuperTypes(PsiType psiType, P
243247
return true;
244248
}
245249
}
250+
251+
return isAssignableFromGenericBuilder( returnType, method.getContainingClass() );
252+
}
253+
254+
private static boolean isAssignableFromGenericBuilder(PsiType returnType, PsiClass containingClass) {
255+
256+
if ( returnType == null || containingClass == null ) {
257+
return false;
258+
}
259+
260+
// Generic types (FooBuilder<C, B>)
261+
PsiTypeParameter[] typeParameters = containingClass.getTypeParameters();
262+
if ( typeParameters.length < 2 ) {
263+
return false;
264+
}
265+
266+
for ( PsiTypeParameter typeParameter : typeParameters ) {
267+
PsiClassType[] superTypes = typeParameter.getExtendsListTypes();
268+
for ( PsiClassType superType : superTypes ) {
269+
// Check if the return value is derivable from a builder type
270+
if ( classNameEndsWithBuilder( superType )
271+
&& superType.isAssignableFrom( returnType ) ) {
272+
return true;
273+
}
274+
}
275+
}
276+
246277
return false;
247278
}
279+
280+
private static boolean classNameEndsWithBuilder(PsiClassType psiClassType) {
281+
282+
return Optional.ofNullable( psiClassType.resolve() )
283+
.map( PsiClass::getQualifiedName )
284+
.map( name -> name.endsWith( "Builder" ) )
285+
.orElse( false );
286+
}
248287

249288
private static boolean isAssignableFrom(PsiType psiType, @Nullable PsiType returnType) {
250289
return TypeConversionUtil.isAssignable(
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 java.util.List;
9+
10+
import com.intellij.codeInsight.intention.IntentionAction;
11+
import org.jetbrains.annotations.NotNull;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
/**
16+
* @author Oliver Erhart
17+
*/
18+
public class UnmappedSuperBuilderMultiLevelTargetPropertiesInspectionTest extends BaseInspectionTest {
19+
20+
@NotNull
21+
@Override
22+
protected Class<UnmappedTargetPropertiesInspection> getInspection() {
23+
return UnmappedTargetPropertiesInspection.class;
24+
}
25+
26+
@Override
27+
protected void setUp() throws Exception {
28+
super.setUp();
29+
myFixture.copyFileToProject(
30+
"UnmappedSuperBuilderMultiLevelTargetPropertiesData.java",
31+
"org/example/data/UnmappedSuperBuilderMultiLevelTargetPropertiesData.java"
32+
);
33+
}
34+
35+
public void testUnmappedSuperBuilderMultiLevelTargetProperties() {
36+
doTest();
37+
List<IntentionAction> allQuickFixes = myFixture.getAllQuickFixes();
38+
39+
assertThat( allQuickFixes )
40+
.extracting( IntentionAction::getText )
41+
.as( "Intent Text" )
42+
.containsExactly(
43+
"Ignore unmapped target property: 'age'",
44+
"Add unmapped target property: 'age'",
45+
"Ignore unmapped target property: 'id'",
46+
"Add unmapped target property: 'id'",
47+
"Ignore unmapped target property: 'name'",
48+
"Add unmapped target property: 'name'",
49+
"Ignore all unmapped target properties"
50+
);
51+
}
52+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
7+
import org.mapstruct.Mapper;
8+
import org.example.data.UnmappedSuperBuilderMultiLevelTargetPropertiesData.PersonEntity;
9+
10+
@Mapper
11+
interface MultiLevelMapper {
12+
13+
PersonEntity <warning descr="Unmapped target properties: age, id, name">map</warning>(String input);
14+
15+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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.example.data;
7+
8+
public class UnmappedSuperBuilderMultiLevelTargetPropertiesData {
9+
10+
public static class BaseEntity {
11+
Long id;
12+
13+
protected BaseEntity(BaseEntityBuilder<?, ?> b) {
14+
this.id = b.id;
15+
}
16+
17+
public static BaseEntityBuilder<?, ?> builder() {
18+
return new BaseEntityBuilderImpl();
19+
}
20+
21+
public static abstract class BaseEntityBuilder<C extends BaseEntity, B extends BaseEntityBuilder<C, B>> {
22+
private Long id;
23+
24+
public B id(Long id) {
25+
this.id = id;
26+
return self();
27+
}
28+
29+
protected abstract B self();
30+
31+
public abstract C build();
32+
33+
public String toString() {
34+
return "BaseEntity.BaseEntityBuilder(id=" + this.id + ")";
35+
}
36+
}
37+
38+
private static final class BaseEntityBuilderImpl extends BaseEntityBuilder<BaseEntity, BaseEntityBuilderImpl> {
39+
private BaseEntityBuilderImpl() {
40+
}
41+
42+
protected BaseEntityBuilderImpl self() {
43+
return this;
44+
}
45+
46+
public BaseEntity build() {
47+
return new BaseEntity( this );
48+
}
49+
}
50+
}
51+
52+
public static class NamedEntity extends BaseEntity {
53+
String name;
54+
55+
protected NamedEntity(NamedEntityBuilder<?, ?> b) {
56+
super( b );
57+
this.name = b.name;
58+
}
59+
60+
public static NamedEntityBuilder<?, ?> builder() {
61+
return new NamedEntityBuilderImpl();
62+
}
63+
64+
public static abstract class NamedEntityBuilder<C extends NamedEntity, B extends NamedEntityBuilder<C, B>>
65+
extends BaseEntityBuilder<C, B> {
66+
private String name;
67+
68+
public B name(String name) {
69+
this.name = name;
70+
return self();
71+
}
72+
73+
protected abstract B self();
74+
75+
public abstract C build();
76+
77+
public String toString() {
78+
return "NamedEntity.NamedEntityBuilder(super=" + super.toString() + ", name=" + this.name + ")";
79+
}
80+
}
81+
82+
private static final class NamedEntityBuilderImpl extends NamedEntityBuilder<NamedEntity, NamedEntityBuilderImpl> {
83+
private NamedEntityBuilderImpl() {
84+
}
85+
86+
protected NamedEntityBuilderImpl self() {
87+
return this;
88+
}
89+
90+
public NamedEntity build() {
91+
return new NamedEntity( this );
92+
}
93+
}
94+
}
95+
96+
public static class PersonEntity extends NamedEntity {
97+
int age;
98+
99+
protected PersonEntity(PersonEntityBuilder<?, ?> b) {
100+
super( b );
101+
this.age = b.age;
102+
}
103+
104+
public static PersonEntityBuilder<?, ?> builder() {
105+
return new PersonEntityBuilderImpl();
106+
}
107+
108+
public static abstract class PersonEntityBuilder<C extends PersonEntity, B extends PersonEntityBuilder<C, B>>
109+
extends NamedEntityBuilder<C, B> {
110+
private int age;
111+
112+
public B age(int age) {
113+
this.age = age;
114+
return self();
115+
}
116+
117+
public C someIrrelevantMethod(int someParameter) {
118+
return null;
119+
}
120+
121+
protected abstract B self();
122+
123+
public abstract C build();
124+
125+
public String toString() {
126+
return "PersonEntity.PersonEntityBuilder(super=" + super.toString() + ", age=" + this.age + ")";
127+
}
128+
}
129+
130+
private static final class PersonEntityBuilderImpl
131+
extends PersonEntityBuilder<PersonEntity, PersonEntityBuilderImpl> {
132+
private PersonEntityBuilderImpl() {
133+
}
134+
135+
protected PersonEntityBuilderImpl self() {
136+
return this;
137+
}
138+
139+
public PersonEntity build() {
140+
return new PersonEntity( this );
141+
}
142+
}
143+
}
144+
145+
}

0 commit comments

Comments
 (0)