Skip to content

Commit d6cdabb

Browse files
fix: make CtRecordComponent#toMethod return a proper model (#5801)
Before, the method body was just a code snippet Co-authored-by: I-Al-Istannen <[email protected]>
1 parent 2719e53 commit d6cdabb

File tree

7 files changed

+146
-28
lines changed

7 files changed

+146
-28
lines changed

src/main/java/spoon/reflect/factory/CodeFactory.java

+14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import spoon.reflect.code.CtLocalVariable;
2828
import spoon.reflect.code.CtNewArray;
2929
import spoon.reflect.code.CtNewClass;
30+
import spoon.reflect.code.CtReturn;
3031
import spoon.reflect.code.CtStatement;
3132
import spoon.reflect.code.CtStatementList;
3233
import spoon.reflect.code.CtTextBlock;
@@ -647,6 +648,19 @@ public <A extends Annotation> CtAnnotation<A> createAnnotation(CtTypeReference<A
647648
return a;
648649
}
649650

651+
/**
652+
* Creates a return statement.
653+
*
654+
* @param expression the expression to be returned.
655+
* @param <T> the type of the expression
656+
* @return a return.
657+
*/
658+
public <T> CtReturn<T> createCtReturn(CtExpression<T> expression) {
659+
final CtReturn<T> result = factory.Core().createReturn();
660+
result.setReturnedExpression(expression);
661+
return result;
662+
}
663+
650664
/**
651665
* Gets a list of references from a list of elements.
652666
*

src/main/java/spoon/reflect/factory/Factory.java

+8
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,14 @@ public interface Factory {
321321
*/
322322
<T> CtLocalVariableReference<T> createLocalVariableReference(CtLocalVariable<T> localVariable);
323323

324+
/**
325+
* @param expression the expression to return
326+
* @param <T> the type of the expression
327+
* @return a return statement
328+
* @see CodeFactory#createCtReturn(CtExpression)
329+
*/
330+
<T> CtReturn<T> createCtReturn(CtExpression<T> expression);
331+
324332
/**
325333
* @see CodeFactory#createLocalVariableReference(CtTypeReference,String)
326334
*/

src/main/java/spoon/reflect/factory/FactoryImpl.java

+5
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,11 @@ public <T> CtLocalVariable<T> createLocalVariable(CtTypeReference<T> type, Strin
509509
return Code().createLocalVariable(type, name, defaultExpression);
510510
}
511511

512+
@Override
513+
public <T> CtReturn<T> createCtReturn(CtExpression<T> expression) {
514+
return Code().createCtReturn(expression);
515+
}
516+
512517
@SuppressWarnings(value = "unchecked")
513518
@Override
514519
public <T> CtNewArray<T[]> createLiteralArray(T[] value) {

src/main/java/spoon/support/compiler/jdt/ParentExiter.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,13 @@ public <T> void scanCtType(CtType<T> type) {
251251
return;
252252
} else if (child instanceof CtEnumValue && type instanceof CtEnum) {
253253
((CtEnum) type).addEnumValue((CtEnumValue) child);
254-
} else if (child instanceof CtField) {
255-
type.addField((CtField<?>) child);
254+
} else if (child instanceof CtField<?> field) {
255+
// We add the field in addRecordComponent. Afterward, however, JDT visits the Field itself -> Duplication.
256+
// To combat this, we delete the existing field and trust JDTs version.
257+
if (type instanceof CtRecord record) {
258+
record.removeField(record.getField(field.getSimpleName()));
259+
}
260+
type.addField(field);
256261
return;
257262
} else if (child instanceof CtConstructor) {
258263
return;

src/main/java/spoon/support/reflect/declaration/CtRecordComponentImpl.java

+52-11
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,24 @@
1010
import java.util.Collections;
1111
import java.util.HashSet;
1212
import java.util.Set;
13+
14+
import org.jspecify.annotations.Nullable;
1315
import spoon.JLSViolation;
1416
import spoon.reflect.annotations.MetamodelPropertyField;
17+
import spoon.reflect.code.CtFieldAccess;
18+
import spoon.reflect.declaration.CtElement;
1519
import spoon.reflect.declaration.CtField;
1620
import spoon.reflect.declaration.CtMethod;
1721
import spoon.reflect.declaration.CtNamedElement;
22+
import spoon.reflect.declaration.CtRecord;
1823
import spoon.reflect.declaration.CtRecordComponent;
1924
import spoon.reflect.declaration.CtShadowable;
2025
import spoon.reflect.declaration.CtTypedElement;
2126
import spoon.reflect.declaration.ModifierKind;
2227
import spoon.reflect.path.CtRole;
28+
import spoon.reflect.reference.CtFieldReference;
2329
import spoon.reflect.reference.CtTypeReference;
30+
import spoon.reflect.visitor.CtScanner;
2431
import spoon.reflect.visitor.CtVisitor;
2532
import spoon.support.reflect.CtExtendedModifier;
2633

@@ -36,31 +43,58 @@ public class CtRecordComponentImpl extends CtNamedElementImpl implements CtRecor
3643
public CtMethod<?> toMethod() {
3744
CtMethod<?> method = this.getFactory().createMethod();
3845
method.setSimpleName(getSimpleName());
39-
method.setType((CtTypeReference) getType());
46+
method.setType(getClonedType());
4047
method.setExtendedModifiers(Collections.singleton(new CtExtendedModifier(ModifierKind.PUBLIC, true)));
41-
method.setImplicit(true);
42-
method.setBody(getFactory().createCodeSnippetStatement("return " + getSimpleName()));
43-
return method;
48+
49+
CtFieldAccess<?> ctVariableAccess = (CtFieldAccess<?>) getFactory().Code()
50+
.createVariableRead(getRecordFieldReference(), false);
51+
52+
method.setBody(getFactory().Code().createCtReturn(ctVariableAccess));
53+
54+
return makeTreeImplicit(method);
55+
}
56+
57+
private CtFieldReference<?> getRecordFieldReference() {
58+
CtRecord parent = isParentInitialized() ? (CtRecord) getParent() : null;
59+
60+
// Reference the field we think should exist. It might be added to the record later on, so do not directly
61+
// query for it.
62+
CtFieldReference<?> reference = getFactory().createFieldReference()
63+
.setFinal(true)
64+
.setStatic(false)
65+
.setType(getClonedType())
66+
.setSimpleName(getSimpleName());
67+
68+
// We have a parent record, make the field refer to it. Ideally we could do this all the time, but if we
69+
// do not yet have a parent that doesn't work.
70+
if (parent != null) {
71+
reference.setDeclaringType(parent.getReference());
72+
}
73+
74+
return reference;
4475
}
4576

4677
@Override
4778
public CtField<?> toField() {
4879
CtField<?> field = this.getFactory().createField();
4980
field.setSimpleName(getSimpleName());
50-
field.setType((CtTypeReference) getType());
81+
field.setType(getClonedType());
5182
Set<CtExtendedModifier> modifiers = new HashSet<>();
5283
modifiers.add(new CtExtendedModifier(ModifierKind.PRIVATE, true));
5384
modifiers.add(new CtExtendedModifier(ModifierKind.FINAL, true));
5485
field.setExtendedModifiers(modifiers);
55-
field.setImplicit(true);
56-
return field;
86+
return makeTreeImplicit(field);
5787
}
5888

5989
@Override
6090
public boolean isImplicit() {
6191
return true;
6292
}
6393

94+
private @Nullable CtTypeReference<?> getClonedType() {
95+
return getType() != null ? getType().clone() : null;
96+
}
97+
6498
@Override
6599
public CtTypeReference<Object> getType() {
66100
return type;
@@ -92,17 +126,15 @@ private void checkName(String simpleName) {
92126
JLSViolation.throwIfSyntaxErrorsAreNotIgnored(this, "The name '" + simpleName + "' is not allowed as record component name.");
93127
}
94128
}
129+
95130
private static Set<String> createForbiddenNames() {
96131
return Set.of("clone", "finalize", "getClass", "notify", "notifyAll", "equals", "hashCode", "toString", "wait");
97132
}
98-
99133
@Override
100134
public CtRecordComponent clone() {
101135
return (CtRecordComponent) super.clone();
102136
}
103137

104-
105-
106138
@Override
107139
public boolean isShadow() {
108140
return isShadow;
@@ -114,5 +146,14 @@ public <E extends CtShadowable> E setShadow(boolean isShadow) {
114146
this.isShadow = isShadow;
115147
return (E) this;
116148
}
117-
}
118149

150+
private static <T extends CtElement> T makeTreeImplicit(T element) {
151+
element.accept(new CtScanner() {
152+
@Override
153+
protected void enter(CtElement e) {
154+
e.setImplicit(true);
155+
}
156+
});
157+
return element;
158+
}
159+
}

src/main/java/spoon/support/reflect/declaration/CtRecordImpl.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public CtRecord addRecordComponent(CtRecordComponent component) {
6666
component.setParent(this);
6767
getFactory().getEnvironment().getModelChangeListener().onSetAdd(this, CtRole.RECORD_COMPONENT, components, component);
6868
components.add(component);
69+
70+
if (getField(component.getSimpleName()) == null) {
71+
addField(component.toField());
72+
}
6973
if (!hasMethodWithSameNameAndNoParameter(component)) {
7074
addMethod(component.toMethod());
7175
}
@@ -100,12 +104,14 @@ public void accept(CtVisitor v) {
100104
public <C extends CtType<Object>> C addTypeMemberAt(int position, CtTypeMember member) {
101105
// a record can have only implicit instance fields and this is the best point to preserve the invariant
102106
// because there are multiple ways to add a field to a record
107+
String memberName = member.getSimpleName();
108+
103109
if (member instanceof CtField && !member.isStatic()) {
104110
member.setImplicit(true);
105-
getAnnotationsWithName(member.getSimpleName(), ElementType.FIELD).forEach(member::addAnnotation);
111+
getAnnotationsWithName(memberName, ElementType.FIELD).forEach(member::addAnnotation);
106112
}
107113
if (member instanceof CtMethod && member.isImplicit()) {
108-
getAnnotationsWithName(member.getSimpleName(), ElementType.METHOD).forEach(member::addAnnotation);
114+
getAnnotationsWithName(memberName, ElementType.METHOD).forEach(member::addAnnotation);
109115
}
110116
if (member instanceof CtConstructor && member.isImplicit()) {
111117
for (CtParameter<?> parameter : ((CtConstructor<?>) member).getParameters()) {
@@ -115,7 +121,7 @@ public <C extends CtType<Object>> C addTypeMemberAt(int position, CtTypeMember m
115121
}
116122
if (member instanceof CtMethod && (member.isAbstract() || member.isNative())) {
117123
JLSViolation.throwIfSyntaxErrorsAreNotIgnored(this, String.format("%s method is native or abstract, both is not allowed",
118-
member.getSimpleName()));
124+
memberName));
119125
}
120126
if (member instanceof CtAnonymousExecutable && !member.isStatic()) {
121127
JLSViolation.throwIfSyntaxErrorsAreNotIgnored(this, "Instance initializer is not allowed in a record (JLS 17 $8.10.2)");

src/test/java/spoon/test/record/CtRecordTest.java

+51-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package spoon.test.record;
22

33
import static java.lang.System.lineSeparator;
4-
import static org.hamcrest.CoreMatchers.equalTo;
5-
import static org.hamcrest.MatcherAssert.assertThat;
64
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
75
import static org.junit.jupiter.api.Assertions.assertEquals;
86
import static org.junit.jupiter.api.Assertions.assertFalse;
97
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
import static spoon.testing.assertions.SpoonAssertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
1011
import java.util.Arrays;
1112
import java.util.Collection;
1213
import java.util.Comparator;
@@ -15,17 +16,25 @@
1516
import java.util.stream.Collectors;
1617
import javax.validation.constraints.NotNull;
1718

19+
import org.assertj.core.api.InstanceOfAssertFactory;
1820
import org.junit.jupiter.api.Test;
1921
import spoon.Launcher;
2022
import spoon.reflect.CtModel;
23+
import spoon.reflect.code.CtFieldRead;
24+
import spoon.reflect.code.CtReturn;
25+
import spoon.reflect.code.CtStatement;
2126
import spoon.reflect.declaration.CtAnonymousExecutable;
2227
import spoon.reflect.declaration.CtConstructor;
28+
import spoon.reflect.declaration.CtElement;
2329
import spoon.reflect.declaration.CtField;
2430
import spoon.reflect.declaration.CtMethod;
2531
import spoon.reflect.declaration.CtRecord;
32+
import spoon.reflect.declaration.CtRecordComponent;
2633
import spoon.reflect.declaration.CtType;
2734
import spoon.reflect.factory.Factory;
35+
import spoon.reflect.visitor.CtScanner;
2836
import spoon.reflect.visitor.filter.TypeFilter;
37+
import spoon.testing.assertions.SpoonAssertions;
2938
import spoon.testing.utils.ModelTest;
3039

3140
public class CtRecordTest {
@@ -76,24 +85,33 @@ public void testMultipleParameterRecord() {
7685

7786
assertEquals(1, records.size());
7887
assertEquals("public record MultiParameter(int first, float second) {}", head(records).toString());
79-
88+
8089
// test fields
8190
assertEquals(
8291
Arrays.asList(
83-
"private final int first;",
92+
"private final int first;",
8493
"private final float second;"
85-
),
94+
),
8695
head(records).getFields().stream().map(String::valueOf).collect(Collectors.toList())
8796
);
88-
97+
98+
// Make them explicit so we can print them (but assert they were implicit initially)
99+
assertThat(head(records)).getMethods().allSatisfy(CtElement::isImplicit);
100+
head(records).getMethods().forEach(it -> it.accept(new CtScanner() {
101+
@Override
102+
protected void enter(CtElement e) {
103+
e.setImplicit(false);
104+
}
105+
}));
106+
89107
// test methods
90108
assertEquals(
91109
Arrays.asList(
92110
"int first() {\n" +
93-
" return first;\n" +
94-
"}",
111+
" return this.first;\n" +
112+
"}",
95113
"float second() {\n" +
96-
" return second;\n" +
114+
" return this.second;\n" +
97115
"}"
98116
),
99117
head(records).getMethods().stream()
@@ -211,7 +229,7 @@ private CtModel createModelFromPath(String code) {
211229

212230
@Test
213231
void testGenericTypeParametersArePrintedBeforeTheFunctionParameters() {
214-
// contract: a record with generic type arguments should be printed correctly
232+
// contract: a record with generic type arguments should be printed correctly
215233
String code = "src/test/resources/records/GenericRecord.java";
216234
CtModel model = createModelFromPath(code);
217235
Collection<CtRecord> records = model.getElements(new TypeFilter<>(CtRecord.class));
@@ -225,7 +243,7 @@ void testBuildRecordModelWithStaticInitializer() {
225243
String code = "src/test/resources/records/WithStaticInitializer.java";
226244
CtModel model = assertDoesNotThrow(() -> createModelFromPath(code));
227245
List<CtAnonymousExecutable> execs = model.getElements(new TypeFilter<>(CtAnonymousExecutable.class));
228-
assertThat(execs.size(), equalTo(2));
246+
assertThat(execs).hasSize(2);
229247
}
230248

231249
@ModelTest(value = "./src/test/resources/records/MultipleConstructors.java", complianceLevel = 16)
@@ -275,8 +293,29 @@ void testNonCompactCanonicalConstructor(Factory factory) {
275293
assertEquals(constructor.getParameters().get(0).getSimpleName(), "x");
276294
}
277295

296+
@ModelTest(value = "./src/test/resources/records/GenericRecord.java", complianceLevel = 16)
297+
void testProperReturnInRecordAccessor(Factory factory) {
298+
// contract: the return statement in the accessor method should return a field read expression to the correct
299+
// field
300+
CtRecord record = head(factory.getModel().getElements(new TypeFilter<>(CtRecord.class)));
301+
302+
assertThat(record.getRecordComponents()).isNotEmpty();
303+
for (CtRecordComponent component : record.getRecordComponents()) {
304+
CtMethod<?> method = component.toMethod();
305+
306+
assertThat(method.getBody().<CtStatement>getLastStatement())
307+
.asInstanceOf(new InstanceOfAssertFactory<>(CtReturn.class, SpoonAssertions::assertThat))
308+
.getReturnedExpression()
309+
.self()
310+
.asInstanceOf(new InstanceOfAssertFactory<>(CtFieldRead.class, SpoonAssertions::assertThat))
311+
.getVariable()
312+
.getDeclaringType()
313+
.getSimpleName()
314+
.isEqualTo(record.getSimpleName());
315+
}
316+
}
317+
278318
private <T> T head(Collection<T> collection) {
279319
return collection.iterator().next();
280320
}
281-
282321
}

0 commit comments

Comments
 (0)