diff --git a/doc/code_elements.md b/doc/code_elements.md index 35833fb1d57..13427ccaa61 100644 --- a/doc/code_elements.md +++ b/doc/code_elements.md @@ -474,6 +474,19 @@ int x = switch(i) { // <-- switch expression int x=3; --x; // <-- unary -- +``` +### CtUnnamedPattern +[(javadoc)](http://spoon.gforge.inria.fr/mvnsites/spoon-core/apidocs/spoon/reflect/code/CtUnnamedPattern.html) + +```java + + Object obj = new Object(); + record X(int i) {} + int i = switch (obj) { + case X(_) -> 0; // an unnamed pattern does neither mention a type nor a name + case null, default -> -1; + }; + ``` ### CtVariableRead [(javadoc)](http://spoon.gforge.inria.fr/mvnsites/spoon-core/apidocs/spoon/reflect/code/CtVariableRead.html) diff --git a/qodana.yaml b/qodana.yaml index 8abc4de68ed..33065fc35d2 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -29,3 +29,11 @@ include: - name: PointlessBooleanExpression exclude: - name: UseOfClone + # Do not check generated code + - name: All + paths: + - src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java + - src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java + - src/main/java/spoon/support/visitor/clone/CloneBuilder.java + - src/main/java/spoon/support/visitor/clone/CloneVisitor.java + - src/main/java/spoon/reflect/meta/impl/ModelRoleHandlers.java diff --git a/spoon-smpl/src/main/java/spoon/smpl/Substitutor.java b/spoon-smpl/src/main/java/spoon/smpl/Substitutor.java index 62eb8b0036a..8c2fcee6436 100644 --- a/spoon-smpl/src/main/java/spoon/smpl/Substitutor.java +++ b/spoon-smpl/src/main/java/spoon/smpl/Substitutor.java @@ -74,6 +74,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -712,6 +713,11 @@ public void visitCtRecordPattern(CtRecordPattern recordPattern) { throw new NotImplementedException("Not implemented"); } + @Override + public void visitCtUnnamedPattern(CtUnnamedPattern unnamedPattern) { + throw new NotImplementedException("Not implemented"); + } + @Override public void visitCtReceiverParameter(CtReceiverParameter receiverParameter) { throw new NotImplementedException("Not implemented"); diff --git a/spoon-smpl/src/main/java/spoon/smpl/pattern/PatternBuilder.java b/spoon-smpl/src/main/java/spoon/smpl/pattern/PatternBuilder.java index 75b47bdf18c..d5f373a2ca1 100644 --- a/spoon-smpl/src/main/java/spoon/smpl/pattern/PatternBuilder.java +++ b/spoon-smpl/src/main/java/spoon/smpl/pattern/PatternBuilder.java @@ -72,6 +72,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -852,6 +853,11 @@ public void visitCtRecordPattern(CtRecordPattern recordPattern) { throw new NotImplementedException("Not implemented"); } + @Override + public void visitCtUnnamedPattern(CtUnnamedPattern unnamedPattern) { + throw new NotImplementedException("Not implemented"); + } + @Override public void visitCtReceiverParameter(CtReceiverParameter receiverParameter) { throw new NotImplementedException("Not implemented"); diff --git a/src/main/java/spoon/metamodel/Metamodel.java b/src/main/java/spoon/metamodel/Metamodel.java index 57f84aaa231..146b129c117 100644 --- a/src/main/java/spoon/metamodel/Metamodel.java +++ b/src/main/java/spoon/metamodel/Metamodel.java @@ -128,6 +128,7 @@ public static Set> getAllMetamodelInterfaces() { result.add(factory.Type().get(spoon.reflect.code.CtTypeAccess.class)); result.add(factory.Type().get(spoon.reflect.code.CtTypePattern.class)); result.add(factory.Type().get(spoon.reflect.code.CtUnaryOperator.class)); + result.add(factory.Type().get(spoon.reflect.code.CtUnnamedPattern.class)); result.add(factory.Type().get(spoon.reflect.code.CtVariableAccess.class)); result.add(factory.Type().get(spoon.reflect.code.CtVariableRead.class)); result.add(factory.Type().get(spoon.reflect.code.CtVariableWrite.class)); diff --git a/src/main/java/spoon/reflect/code/CtCatchVariable.java b/src/main/java/spoon/reflect/code/CtCatchVariable.java index e29c512f803..8394d1b15b3 100644 --- a/src/main/java/spoon/reflect/code/CtCatchVariable.java +++ b/src/main/java/spoon/reflect/code/CtCatchVariable.java @@ -39,6 +39,12 @@ public interface CtCatchVariable extends CtVariable, CtMultiTypedElement, @UnsettableProperty > C setDefaultExpression(CtExpression assignedExpression); + /** + * {@return whether this catch variable is unnamed} + */ + @DerivedProperty + boolean isUnnamed(); + /** * Returns type reference of the exception variable in a catch. * If type is unknown, or any of the types in a multi-catch is unknown, returns null. diff --git a/src/main/java/spoon/reflect/code/CtLocalVariable.java b/src/main/java/spoon/reflect/code/CtLocalVariable.java index 72feb853003..5253f8b4ecc 100644 --- a/src/main/java/spoon/reflect/code/CtLocalVariable.java +++ b/src/main/java/spoon/reflect/code/CtLocalVariable.java @@ -38,15 +38,32 @@ * @see spoon.reflect.declaration.CtExecutable */ public interface CtLocalVariable extends CtStatement, CtVariable, CtRHSReceiver, CtResource { + + /** + * The {@link #getSimpleName() simple name} of a local variable that is + * unnamed. + */ + String UNNAMED_VARIABLE_NAME = "_"; + /* * (non-Javadoc) * * @see spoon.reflect.declaration.CtNamedElement#getReference() */ + /** + * {@inheritDoc} + * If the variable is unnamed, {@code null} is returned. + */ @Override @DerivedProperty CtLocalVariableReference getReference(); + /** + * {@return whether this local variable is unnamed} + */ + @DerivedProperty + boolean isUnnamed(); + /** * Useful proxy to {@link #getDefaultExpression()}. */ diff --git a/src/main/java/spoon/reflect/code/CtUnnamedPattern.java b/src/main/java/spoon/reflect/code/CtUnnamedPattern.java new file mode 100644 index 00000000000..a12717413a6 --- /dev/null +++ b/src/main/java/spoon/reflect/code/CtUnnamedPattern.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.reflect.code; + +import spoon.reflect.reference.CtTypeReference; +import spoon.support.UnsettableProperty; + +import java.util.List; + +/** + * This code element defines an unnamed pattern, introduced in Java 2 + * by JEP 456. + *

+ * Example: + *

+ *     Object obj = new Object();
+ *     record X(int i) {}
+ *     int i = switch (obj) {
+ *         case X(_) -> 0; // an unnamed pattern does neither mention a type nor a name
+ *         case null, default -> -1;
+ *     };
+ * 
+ */ +public interface CtUnnamedPattern extends CtPattern, CtExpression { + + @Override + CtUnnamedPattern clone(); + + @Override + @UnsettableProperty + List> getTypeCasts(); + + @Override + @UnsettableProperty + > C setTypeCasts(List> types); + + @Override + @UnsettableProperty + > C addTypeCast(CtTypeReference type); + +} diff --git a/src/main/java/spoon/reflect/declaration/CtNamedElement.java b/src/main/java/spoon/reflect/declaration/CtNamedElement.java index 930d81c7296..ce87cd03ea0 100644 --- a/src/main/java/spoon/reflect/declaration/CtNamedElement.java +++ b/src/main/java/spoon/reflect/declaration/CtNamedElement.java @@ -34,7 +34,7 @@ public interface CtNamedElement extends CtElement { T setSimpleName(String simpleName); /** - * Returns the corresponding reference. + * {@return the corresponding reference} */ @DerivedProperty CtReference getReference(); diff --git a/src/main/java/spoon/reflect/declaration/CtParameter.java b/src/main/java/spoon/reflect/declaration/CtParameter.java index 7a08fbba41f..69423c09917 100644 --- a/src/main/java/spoon/reflect/declaration/CtParameter.java +++ b/src/main/java/spoon/reflect/declaration/CtParameter.java @@ -60,6 +60,13 @@ public interface CtParameter extends CtVariable, CtShadowable { @UnsettableProperty > C setDefaultExpression(CtExpression assignedExpression); + /** + * {@return whether this parameter is unnamed} + * Unnamed parameters are always lambda parameters. + */ + @DerivedProperty + boolean isUnnamed(); + /** * Returns true if this parameter is a lambda parameter with type defined using the `var` keyword (since Java 11). */ diff --git a/src/main/java/spoon/reflect/factory/CoreFactory.java b/src/main/java/spoon/reflect/factory/CoreFactory.java index 19f27026089..3a854111e07 100644 --- a/src/main/java/spoon/reflect/factory/CoreFactory.java +++ b/src/main/java/spoon/reflect/factory/CoreFactory.java @@ -59,6 +59,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -660,4 +661,9 @@ BodyHolderSourcePosition createBodyHolderSourcePosition( * @return the created receiver parameter. */ CtReceiverParameter createReceiverParameter(); + + /** + * {@return an unnamed pattern} + */ + CtUnnamedPattern createUnnamedPattern(); } diff --git a/src/main/java/spoon/reflect/visitor/CtAbstractVisitor.java b/src/main/java/spoon/reflect/visitor/CtAbstractVisitor.java index de90a697751..99d361e36c8 100644 --- a/src/main/java/spoon/reflect/visitor/CtAbstractVisitor.java +++ b/src/main/java/spoon/reflect/visitor/CtAbstractVisitor.java @@ -59,6 +59,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -570,4 +571,9 @@ public void visitCtReceiverParameter(CtReceiverParameter receiverParameter) { public void visitCtRecordPattern(CtRecordPattern recordPattern) { } + + @Override + public void visitCtUnnamedPattern(CtUnnamedPattern unnamedPattern) { + + } } diff --git a/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java b/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java index 896b6b31661..e6da81b7b58 100644 --- a/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java +++ b/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java @@ -1119,4 +1119,15 @@ public void visitCtRecordPattern(spoon.reflect.code.CtRecordPattern recordPatter biScan(spoon.reflect.path.CtRole.COMMENT, recordPattern.getComments(), other.getComments()); exit(recordPattern); } + + // autogenerated by CtBiScannerGenerator + @java.lang.Override + public void visitCtUnnamedPattern(spoon.reflect.code.CtUnnamedPattern unnamedPattern) { + spoon.reflect.code.CtUnnamedPattern other = ((spoon.reflect.code.CtUnnamedPattern) (this.stack.peek())); + enter(unnamedPattern); + biScan(spoon.reflect.path.CtRole.ANNOTATION, unnamedPattern.getAnnotations(), other.getAnnotations()); + biScan(spoon.reflect.path.CtRole.TYPE, unnamedPattern.getType(), other.getType()); + biScan(spoon.reflect.path.CtRole.COMMENT, unnamedPattern.getComments(), other.getComments()); + exit(unnamedPattern); + } } diff --git a/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java b/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java index 764aa0e3697..d0a8941cf08 100644 --- a/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java +++ b/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java @@ -71,6 +71,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableAccess; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; @@ -1132,4 +1133,13 @@ public void visitCtReceiverParameter(CtReceiverParameter e) { scanCtShadowable(e); } + @Override + public void visitCtUnnamedPattern(CtUnnamedPattern pattern) { + scanCtPattern(pattern); + scanCtExpression(pattern); + scanCtTypedElement(pattern); + scanCtCodeElement(pattern); + scanCtElement(pattern); + scanCtVisitable(pattern); + } } diff --git a/src/main/java/spoon/reflect/visitor/CtScanner.java b/src/main/java/spoon/reflect/visitor/CtScanner.java index d90c36e46b9..ee221b9208a 100644 --- a/src/main/java/spoon/reflect/visitor/CtScanner.java +++ b/src/main/java/spoon/reflect/visitor/CtScanner.java @@ -64,6 +64,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -1115,5 +1116,14 @@ public void visitCtRecordPattern(CtRecordPattern recordPattern) { scan(CtRole.COMMENT, recordPattern.getComments()); exit(recordPattern); } + + @Override + public void visitCtUnnamedPattern(CtUnnamedPattern unnamedPattern) { + enter(unnamedPattern); + scan(CtRole.ANNOTATION, unnamedPattern.getAnnotations()); + scan(CtRole.TYPE, unnamedPattern.getType()); + scan(CtRole.COMMENT, unnamedPattern.getComments()); + exit(unnamedPattern); + } } diff --git a/src/main/java/spoon/reflect/visitor/CtVisitor.java b/src/main/java/spoon/reflect/visitor/CtVisitor.java index 74c5a376e58..e38e946f935 100644 --- a/src/main/java/spoon/reflect/visitor/CtVisitor.java +++ b/src/main/java/spoon/reflect/visitor/CtVisitor.java @@ -59,6 +59,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -577,4 +578,10 @@ void visitCtOperatorAssignment( * @param receiverParameter the receiver parameter to visit. */ void visitCtReceiverParameter(CtReceiverParameter receiverParameter); + + /** + * Visits an unnamed pattern. + * @param unnamedPattern the unnamed pattern to visit. + */ + void visitCtUnnamedPattern(CtUnnamedPattern unnamedPattern); } diff --git a/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java b/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java index 19be75b642c..76af0925e80 100644 --- a/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java +++ b/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java @@ -70,6 +70,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableAccess; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; @@ -2383,4 +2384,9 @@ public void visitCtReceiverParameter(CtReceiverParameter receiverParameter) { printer.writeSeparator("this"); } } + + @Override + public void visitCtUnnamedPattern(CtUnnamedPattern unnamedPattern) { + printer.writeKeyword(CtLocalVariable.UNNAMED_VARIABLE_NAME); + } } diff --git a/src/main/java/spoon/support/DefaultCoreFactory.java b/src/main/java/spoon/support/DefaultCoreFactory.java index ec468d1915b..7be746e00b3 100644 --- a/src/main/java/spoon/support/DefaultCoreFactory.java +++ b/src/main/java/spoon/support/DefaultCoreFactory.java @@ -63,6 +63,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; import spoon.reflect.code.CtWhile; @@ -165,6 +166,7 @@ import spoon.support.reflect.code.CtTypeAccessImpl; import spoon.support.reflect.code.CtTypePatternImpl; import spoon.support.reflect.code.CtUnaryOperatorImpl; +import spoon.support.reflect.code.CtUnnamedPatternImpl; import spoon.support.reflect.code.CtVariableReadImpl; import spoon.support.reflect.code.CtVariableWriteImpl; import spoon.support.reflect.code.CtWhileImpl; @@ -1126,6 +1128,9 @@ public CtElement create(Class klass) { if (klass.equals(spoon.reflect.declaration.CtReceiverParameter.class)) { return createReceiverParameter(); } + if (klass.equals(spoon.reflect.code.CtUnnamedPattern.class)) { + return createUnnamedPattern(); + } throw new IllegalArgumentException("not instantiable by CoreFactory(): " + klass); } @@ -1230,4 +1235,11 @@ public CtReceiverParameter createReceiverParameter() { receiverParameter.setFactory(getMainFactory()); return receiverParameter; } + + @Override + public CtUnnamedPattern createUnnamedPattern() { + CtUnnamedPattern unnamedPattern = new CtUnnamedPatternImpl(); + unnamedPattern.setFactory(getMainFactory()); + return unnamedPattern; + } } diff --git a/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilder.java b/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilder.java index 050dc02cfaa..a042f1046ba 100644 --- a/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilder.java @@ -141,10 +141,10 @@ import spoon.reflect.code.CtLiteral; import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtOperatorAssignment; +import spoon.reflect.code.CtPattern; import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtTry; import spoon.reflect.code.CtTypeAccess; -import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; import spoon.reflect.code.LiteralBase; import spoon.reflect.code.UnaryOperatorKind; @@ -1360,7 +1360,7 @@ public boolean visit(LabeledStatement labeledStatement, BlockScope scope) { public boolean visit(LocalDeclaration localDeclaration, BlockScope scope) { CtLocalVariable v = factory.Core().createLocalVariable(); - boolean isVar = localDeclaration.type.isTypeNameVar(scope); + boolean isVar = localDeclaration.type != null && localDeclaration.type.isTypeNameVar(scope); if (isVar) { v.setInferred(true); @@ -1754,9 +1754,23 @@ public boolean visit(GuardedPattern guardedPattern, BlockScope scope) { @Override public boolean visit(TypePattern anyPattern, BlockScope scope) { - CtTypePattern typePattern = factory.Core().createTypePattern(); - context.enter(typePattern, anyPattern); - return true; + boolean unnamedPattern = isUnnamedPattern(anyPattern); + CtPattern pattern; + if (unnamedPattern) { + CtTypeReference type = references.getTypeReference(anyPattern.local.binding.type).setImplicit(true); + pattern = factory.Core().createUnnamedPattern().setType(type); + } else { + pattern = factory.Core().createTypePattern(); + } + context.enter(pattern, anyPattern); + // when we have an unnamed pattern, we don't want to visit the local variable declaration anymore + return !unnamedPattern; + } + + // only '_', NOT 'Type _' + private boolean isUnnamedPattern(TypePattern pattern) { + // an unnamed pattern does not have a type in the source, but the binding holds the inferred type + return pattern.isUnnamed() && pattern.local.type == null && pattern.local.binding != null; } @Override diff --git a/src/main/java/spoon/support/compiler/jdt/ParentExiter.java b/src/main/java/spoon/support/compiler/jdt/ParentExiter.java index aeadd6ad802..85e4b9b675d 100644 --- a/src/main/java/spoon/support/compiler/jdt/ParentExiter.java +++ b/src/main/java/spoon/support/compiler/jdt/ParentExiter.java @@ -16,6 +16,7 @@ import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer; import org.eclipse.jdt.internal.compiler.ast.CaseStatement; import org.eclipse.jdt.internal.compiler.ast.CastExpression; +import org.eclipse.jdt.internal.compiler.ast.EitherOrMultiPattern; import org.eclipse.jdt.internal.compiler.ast.ExplicitConstructorCall; import org.eclipse.jdt.internal.compiler.ast.Expression; import org.eclipse.jdt.internal.compiler.ast.ForStatement; @@ -500,8 +501,7 @@ public void visitCtCase(CtCase caseStatement) { if (node instanceof CaseStatement) { caseStatement.setCaseKind(((CaseStatement) node).isExpr ? CaseKind.ARROW : CaseKind.COLON); } - if (node instanceof CaseStatement && ((CaseStatement) node).constantExpressions != null && child instanceof CtExpression - && caseStatement.getCaseExpressions().size() < ((CaseStatement) node).constantExpressions.length) { + if (shouldAddAsCaseExpression(caseStatement, node)) { if (child instanceof CtPattern pattern) { caseStatement.addCaseExpression((CtExpression) jdtTreeBuilder.getFactory().Core().createCasePattern().setPattern(pattern)); } else { @@ -517,6 +517,26 @@ public void visitCtCase(CtCase caseStatement) { super.visitCtCase(caseStatement); } + private boolean shouldAddAsCaseExpression(CtCase caseStatement, ASTNode node) { + if (!(node instanceof CaseStatement cs)) { + return false; + } + if (cs.constantExpressions == null) { + return false; + } + if (child instanceof CtExpression + && caseStatement.getCaseExpressions().size() < cs.constantExpressions.length) { + return true; + } + // case A _, B _ -> {} is only one constantExpression in JDT, but an EitherOrMultiPattern + // so we need to unpack it and see how many case expressions it actually is + if (cs.constantExpressions.length == 1 && cs.constantExpressions[0] instanceof EitherOrMultiPattern eomp) { + // returns true if we still expect more case expressions to be added + return caseStatement.getCaseExpressions().size() < eomp.getAlternatives().length; + } + return false; + } + @Override public void visitCtCatch(CtCatch catchBlock) { if (child instanceof CtBlock) { diff --git a/src/main/java/spoon/support/reflect/code/CtCatchVariableImpl.java b/src/main/java/spoon/support/reflect/code/CtCatchVariableImpl.java index 3137122dcc6..7003a8d779f 100644 --- a/src/main/java/spoon/support/reflect/code/CtCatchVariableImpl.java +++ b/src/main/java/spoon/support/reflect/code/CtCatchVariableImpl.java @@ -107,6 +107,11 @@ public > C setDefaultExpression(CtExpression defaultE return (C) this; } + @Override + public boolean isUnnamed() { + return CtLocalVariableImpl.isUnnamed(this); + } + @Override public boolean isPartOfJointDeclaration() { return false; diff --git a/src/main/java/spoon/support/reflect/code/CtLocalVariableImpl.java b/src/main/java/spoon/support/reflect/code/CtLocalVariableImpl.java index dd9c14b3540..177bd2e109a 100644 --- a/src/main/java/spoon/support/reflect/code/CtLocalVariableImpl.java +++ b/src/main/java/spoon/support/reflect/code/CtLocalVariableImpl.java @@ -7,6 +7,7 @@ */ package spoon.support.reflect.code; +import org.jspecify.annotations.Nullable; import spoon.reflect.annotations.MetamodelPropertyField; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtLocalVariable; @@ -22,6 +23,7 @@ import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.CtVisitor; import spoon.support.DerivedProperty; +import spoon.support.Internal; import spoon.support.UnsettableProperty; import spoon.support.reflect.CtExtendedModifier; import spoon.support.reflect.CtModifierHandler; @@ -57,10 +59,24 @@ public CtExpression getDefaultExpression() { } @Override - public CtLocalVariableReference getReference() { + public @Nullable CtLocalVariableReference getReference() { + if (isUnnamed()) { + return null; + } return getFactory().Code().createLocalVariableReference(this); } + @Override + public boolean isUnnamed() { + return isUnnamed(this); + } + + @Internal + public static boolean isUnnamed(CtVariable variable) { + return variable.getSimpleName().equals(UNNAMED_VARIABLE_NAME) + && variable.getFactory().getEnvironment().getComplianceLevel() >= 22; + } + @Override public String getSimpleName() { return name; diff --git a/src/main/java/spoon/support/reflect/code/CtUnnamedPatternImpl.java b/src/main/java/spoon/support/reflect/code/CtUnnamedPatternImpl.java new file mode 100644 index 00000000000..e7fc67e9879 --- /dev/null +++ b/src/main/java/spoon/support/reflect/code/CtUnnamedPatternImpl.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.reflect.code; + +import spoon.SpoonException; +import spoon.reflect.code.CtRecordPattern; +import spoon.reflect.code.CtUnnamedPattern; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.visitor.CtVisitor; + +public class CtUnnamedPatternImpl extends CtExpressionImpl implements CtUnnamedPattern { + + + @Override + public void accept(CtVisitor visitor) { + visitor.visitCtUnnamedPattern(this); + } + + @Override + public E setParent(CtElement parent) { + if (parent != null && !(parent instanceof CtRecordPattern)) { + throw new SpoonException("unnamed pattern can only be used in a record pattern"); + } + return super.setParent(parent); + } + + @Override + public CtUnnamedPattern clone() { + return (CtUnnamedPattern) super.clone(); + } +} diff --git a/src/main/java/spoon/support/reflect/declaration/CtParameterImpl.java b/src/main/java/spoon/support/reflect/declaration/CtParameterImpl.java index 17bead6fe54..62d0a04eef9 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtParameterImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtParameterImpl.java @@ -24,6 +24,7 @@ import spoon.support.UnsettableProperty; import spoon.support.reflect.CtExtendedModifier; import spoon.support.reflect.CtModifierHandler; +import spoon.support.reflect.code.CtLocalVariableImpl; import java.util.Set; @@ -78,6 +79,11 @@ public > C setDefaultExpression(CtExpression defaultE return (C) this; } + @Override + public boolean isUnnamed() { + return CtLocalVariableImpl.isUnnamed(this); + } + @Override public boolean isPartOfJointDeclaration() { // a parameter can never be part of a joint declaration diff --git a/src/main/java/spoon/support/visitor/clone/CloneVisitor.java b/src/main/java/spoon/support/visitor/clone/CloneVisitor.java index 8c5aeb08858..7508c7aad9d 100644 --- a/src/main/java/spoon/support/visitor/clone/CloneVisitor.java +++ b/src/main/java/spoon/support/visitor/clone/CloneVisitor.java @@ -1188,4 +1188,16 @@ public void visitCtRecordPattern(spoon.reflect.code.CtRecordPattern recordPatter this.cloneHelper.tailor(recordPattern, aCtRecordPattern); this.other = aCtRecordPattern; } + + // auto-generated, see spoon.generating.CloneVisitorGenerator + @java.lang.Override + public void visitCtUnnamedPattern(spoon.reflect.code.CtUnnamedPattern unnamedPattern) { + spoon.reflect.code.CtUnnamedPattern aCtUnnamedPattern = unnamedPattern.getFactory().Core().createUnnamedPattern(); + this.builder.copy(unnamedPattern, aCtUnnamedPattern); + aCtUnnamedPattern.setAnnotations(this.cloneHelper.clone(unnamedPattern.getAnnotations())); + aCtUnnamedPattern.setType(this.cloneHelper.clone(unnamedPattern.getType())); + aCtUnnamedPattern.setComments(this.cloneHelper.clone(unnamedPattern.getComments())); + this.cloneHelper.tailor(unnamedPattern, aCtUnnamedPattern); + this.other = aCtUnnamedPattern; + } } diff --git a/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java b/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java index 82a299e137d..c9ba441ff09 100644 --- a/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java +++ b/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java @@ -2429,4 +2429,12 @@ public void visitCtRecordPattern(spoon.reflect.code.CtRecordPattern recordPatter replaceElementIfExist(recordPattern.getType(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypedElementTypeReplaceListener(recordPattern)); replaceInListIfExist(recordPattern.getComments(), new spoon.support.visitor.replace.ReplacementVisitor.CtElementCommentsReplaceListener(recordPattern)); } + + // auto-generated, see spoon.generating.ReplacementVisitorGenerator + @java.lang.Override + public void visitCtUnnamedPattern(spoon.reflect.code.CtUnnamedPattern unnamedPattern) { + replaceInListIfExist(unnamedPattern.getAnnotations(), new spoon.support.visitor.replace.ReplacementVisitor.CtElementAnnotationsReplaceListener(unnamedPattern)); + replaceElementIfExist(unnamedPattern.getType(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypedElementTypeReplaceListener(unnamedPattern)); + replaceInListIfExist(unnamedPattern.getComments(), new spoon.support.visitor.replace.ReplacementVisitor.CtElementCommentsReplaceListener(unnamedPattern)); + } } diff --git a/src/test/java/spoon/test/api/Metamodel.java b/src/test/java/spoon/test/api/Metamodel.java index 57ab38f9670..8bfbcb496f6 100644 --- a/src/test/java/spoon/test/api/Metamodel.java +++ b/src/test/java/spoon/test/api/Metamodel.java @@ -1377,6 +1377,16 @@ private static void initTypes(List types) { )); + types.add(new Type("CtUnnamedPattern", spoon.reflect.code.CtUnnamedPattern.class, spoon.support.reflect.code.CtUnnamedPatternImpl.class, fm -> fm + .field(CtRole.IS_IMPLICIT, false, false) + .field(CtRole.POSITION, false, false) + .field(CtRole.CAST, true, true) + .field(CtRole.ANNOTATION, false, false) + .field(CtRole.TYPE, false, false) + .field(CtRole.COMMENT, false, false) + + )); + types.add(new Type("CtNewClass", spoon.reflect.code.CtNewClass.class, spoon.support.reflect.code.CtNewClassImpl.class, fm -> fm .field(CtRole.TYPE, true, false) .field(CtRole.IS_IMPLICIT, false, false) diff --git a/src/test/java/spoon/test/comment/CommentTest.java b/src/test/java/spoon/test/comment/CommentTest.java index 18c4d132cba..64bd5e37dd8 100644 --- a/src/test/java/spoon/test/comment/CommentTest.java +++ b/src/test/java/spoon/test/comment/CommentTest.java @@ -882,7 +882,7 @@ public void testDocumentationContract() throws Exception { launcher.getEnvironment().setNoClasspath(true); launcher.getEnvironment().setCommentEnabled(true); - launcher.getEnvironment().setComplianceLevel(21); + launcher.getEnvironment().setComplianceLevel(22); // launcher.getEnvironment().setPreviewFeaturesEnabled(true); // interfaces. diff --git a/src/test/java/spoon/test/parent/SetParentTest.java b/src/test/java/spoon/test/parent/SetParentTest.java index 79f4abeb1c3..592906c627a 100644 --- a/src/test/java/spoon/test/parent/SetParentTest.java +++ b/src/test/java/spoon/test/parent/SetParentTest.java @@ -22,6 +22,7 @@ import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; import spoon.reflect.code.CtTypePattern; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtType; @@ -104,9 +105,12 @@ private void testSetParentDoesNotAlterState(CtType toTest) throws Throwable { } private static CtElement createCompatibleParent(CtType e) { - return CtTypePattern.class.getSimpleName().equals(e.getSimpleName()) - ? createInstanceOfBinaryOperator() - : e.getFactory().createAssignment(); + if (CtUnnamedPattern.class.getSimpleName().equals(e.getSimpleName())) { + return factory.Core().createRecordPattern(); + } else if (CtTypePattern.class.getSimpleName().equals(e.getSimpleName())) { + return createInstanceOfBinaryOperator(); + } + return e.getFactory().createAssignment(); } private static CtBinaryOperator createInstanceOfBinaryOperator() { diff --git a/src/test/java/spoon/test/pattern/RecordPatternTest.java b/src/test/java/spoon/test/pattern/RecordPatternTest.java index 17f50116b17..9d47fc4cbec 100644 --- a/src/test/java/spoon/test/pattern/RecordPatternTest.java +++ b/src/test/java/spoon/test/pattern/RecordPatternTest.java @@ -14,8 +14,10 @@ import spoon.reflect.code.CtRecordPattern; import spoon.reflect.code.CtSwitch; import spoon.reflect.code.CtTypePattern; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.visitor.filter.TypeFilter; import spoon.support.compiler.VirtualFile; +import spoon.testing.assertions.SpoonAssertions; import java.util.List; @@ -25,12 +27,13 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static spoon.testing.assertions.SpoonAssertions.assertThat; public class RecordPatternTest { private static CtModel createModelFromString(String code) { Launcher launcher = new Launcher(); - launcher.getEnvironment().setComplianceLevel(21); + launcher.getEnvironment().setComplianceLevel(22); launcher.addInputResource(new VirtualFile(code)); return launcher.buildModel(); } @@ -166,6 +169,24 @@ void testTypePatternWithVar(String var) { assertEquals(pattern.contains("final"), variable.isFinal()); } + @Test + void testUnnamedPatternInRecordPattern() { + // contract: an unnamed pattern has the proper inferred type and is printed correctly + CtSwitch ctSwitch = createFromSwitch("var i = 1; record Int(int i) {}", "Int(_)"); + List recordPatterns = ctSwitch.getElements(new TypeFilter<>(CtRecordPattern.class)); + assertThat(recordPatterns).hasSize(1); + CtRecordPattern pattern = recordPatterns.get(0); + assertThat(pattern).getPatternList().hasSize(1); + CtPattern component = pattern.getPatternList().get(0); + assertThat(component).isInstanceOf(CtUnnamedPattern.class); + assertThat((CtUnnamedPattern) component) + .getType() + .getSimpleName() + .isNotNull() + .isEqualTo("int"); + assertThat(pattern).asString().contains("Int(_)"); + } + @Test void testRecordPatternInSwitch() { CtSwitch ctSwitch = createFromSwitch( diff --git a/src/test/java/spoon/test/pattern/SwitchPatternTest.java b/src/test/java/spoon/test/pattern/SwitchPatternTest.java index abce85da7d0..9600bd4b85a 100644 --- a/src/test/java/spoon/test/pattern/SwitchPatternTest.java +++ b/src/test/java/spoon/test/pattern/SwitchPatternTest.java @@ -26,7 +26,7 @@ class SwitchPatternTest { private static CtModel createModelFromString(String code) { Launcher launcher = new Launcher(); - launcher.getEnvironment().setComplianceLevel(21); + launcher.getEnvironment().setComplianceLevel(22); launcher.addInputResource(new VirtualFile(code)); return launcher.buildModel(); } diff --git a/src/test/java/spoon/test/variable/VariableTest.java b/src/test/java/spoon/test/variable/VariableTest.java index f937e4bb32e..5875c79de9e 100644 --- a/src/test/java/spoon/test/variable/VariableTest.java +++ b/src/test/java/spoon/test/variable/VariableTest.java @@ -25,21 +25,30 @@ import spoon.Launcher; import spoon.processing.AbstractProcessor; import spoon.reflect.CtModel; +import spoon.reflect.code.CtCatchVariable; import spoon.reflect.code.CtLambda; import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.cu.SourcePosition; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtParameter; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtVariable; +import spoon.reflect.factory.Factory; import spoon.reflect.factory.TypeFactory; import spoon.reflect.visitor.filter.TypeFilter; import spoon.support.sniper.SniperJavaPrettyPrinter; +import spoon.testing.utils.ModelTest; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.List; +import java.util.Objects; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static spoon.testing.assertions.SpoonAssertions.assertThat; public class VariableTest { @@ -154,7 +163,7 @@ public void testInferredVariableAreMarked() { @Test @DisabledForJreRange(max = JRE.JAVA_9) public void testInferredVariableArePrintedWithVar(@TempDir File outputDir) throws IOException { - // contract: if a variable is marked as inferred in the model, it must be pretty-printed with a 'var' keyword + // contract: if a variable is marked as inferred in the model, it must be pretty-printed with a 'var' keyword Launcher launcher = new Launcher(); launcher.getEnvironment().setComplianceLevel(10); launcher.addInputResource("./src/test/resources/spoon/test/var/Main.java"); @@ -191,4 +200,30 @@ public void testVarInLambda() { assertEquals("java.lang.Long", lambda.getParameters().get(1).getType().getQualifiedName()); assertEquals("(var x,var y) -> x + y", lambda.toString()); // we should print var, if it was in the original code } + + @ModelTest(value = "./src/test/resources/spoon/test/unnamed/UnnamedVar.java", complianceLevel = 22) + void testUnnamedVariable(Factory factory) throws IOException { + // contract: each appearance of an unnamed variable is recognized and printed correctly + // each method in the source class has one unnamed variable + // we compare the output from printing to the original source + CtType type = factory.Type().get("spoon.test.unnamed.UnnamedVar"); + assertThat(type.getMethods()).isNotEmpty(); + List lines = java.nio.file.Files.readAllLines(type.getPosition().getFile().toPath()); + for (CtMethod method : type.getMethods()) { + List> locals = method.getBody().getElements(new TypeFilter<>(CtVariable.class)); + assertThat(locals).describedAs(method.getSimpleName()).hasSize(1); + CtVariable variable = locals.get(0); + assertThat(variable).getSimpleName().isEqualTo("_"); + if (variable instanceof CtLocalVariable v) { + assertTrue(v.isUnnamed()); + } else if (variable instanceof CtParameter v) { + assertTrue(v.isUnnamed()); + } else if (variable instanceof CtCatchVariable v) { + assertTrue(v.isUnnamed()); + } + assertThat(variable).getPosition().isNotEqualTo(SourcePosition.NOPOSITION); + String line = lines.get(variable.getPosition().getLine() - 1); + assertThat(line).contains(variable.toString()); + } + } } diff --git a/src/test/java/spoon/testing/assertions/CtUnnamedPatternAssert.java b/src/test/java/spoon/testing/assertions/CtUnnamedPatternAssert.java new file mode 100644 index 00000000000..efae378b90d --- /dev/null +++ b/src/test/java/spoon/testing/assertions/CtUnnamedPatternAssert.java @@ -0,0 +1,23 @@ +package spoon.testing.assertions; +import org.assertj.core.api.AbstractObjectAssert; +import spoon.reflect.code.CtUnnamedPattern; +public class CtUnnamedPatternAssert extends AbstractObjectAssert implements CtUnnamedPatternAssertInterface { + CtUnnamedPatternAssert(CtUnnamedPattern actual) { + super(actual, CtUnnamedPatternAssert.class); + } + + @Override + public CtUnnamedPatternAssert self() { + return this; + } + + @Override + public CtUnnamedPattern actual() { + return this.actual; + } + + @Override + public void failWithMessage(String errorMessage, Object... arguments) { + super.failWithMessage(errorMessage, arguments); + } +} diff --git a/src/test/java/spoon/testing/assertions/CtUnnamedPatternAssertInterface.java b/src/test/java/spoon/testing/assertions/CtUnnamedPatternAssertInterface.java new file mode 100644 index 00000000000..3e1bdbed6c7 --- /dev/null +++ b/src/test/java/spoon/testing/assertions/CtUnnamedPatternAssertInterface.java @@ -0,0 +1,11 @@ +package spoon.testing.assertions; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; +import spoon.reflect.code.CtUnnamedPattern; +import spoon.reflect.reference.CtTypeReference; +public interface CtUnnamedPatternAssertInterface, W extends CtUnnamedPattern> extends SpoonAssert , CtExpressionAssertInterface , CtPatternAssertInterface { + default ListAssert> getTypeCasts() { + return Assertions.assertThat(actual().getTypeCasts()); + } +} diff --git a/src/test/java/spoon/testing/assertions/SpoonAssertions.java b/src/test/java/spoon/testing/assertions/SpoonAssertions.java index bfa69268093..b00e9da732c 100644 --- a/src/test/java/spoon/testing/assertions/SpoonAssertions.java +++ b/src/test/java/spoon/testing/assertions/SpoonAssertions.java @@ -63,6 +63,7 @@ import spoon.reflect.code.CtTypeAccess; import spoon.reflect.code.CtTypePattern; import spoon.reflect.code.CtUnaryOperator; +import spoon.reflect.code.CtUnnamedPattern; import spoon.reflect.code.CtVariableAccess; import spoon.reflect.code.CtVariableRead; import spoon.reflect.code.CtVariableWrite; @@ -606,6 +607,10 @@ public static CtTypeMemberWildcardImportReferenceAssert assertThat(CtTypeMemberW return new CtTypeMemberWildcardImportReferenceAssert(ctTypeMemberWildcardImportReference); } + public static CtUnnamedPatternAssert assertThat(CtUnnamedPattern ctUnnamedPattern) { + return new CtUnnamedPatternAssert(ctUnnamedPattern); + } + public static CtNewClassAssert assertThat(CtNewClass ctNewClass) { return new CtNewClassAssert(ctNewClass); } diff --git a/src/test/resources/spoon/test/unnamed/UnnamedVar.java b/src/test/resources/spoon/test/unnamed/UnnamedVar.java new file mode 100644 index 00000000000..058f43162d7 --- /dev/null +++ b/src/test/resources/spoon/test/unnamed/UnnamedVar.java @@ -0,0 +1,44 @@ +package spoon.test.unnamed; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +class UnnamedVar { + void localVariableDeclaration() { + int _ = 3; + } + + void forStatement() { + for (int _ = 0;;) {} + } + + void tryWithResources(Supplier resourceProvider) { + try (java.lang.AutoCloseable _ = resourceProvider.get()) { + + } + } + + record MyRecord(String s) {} + void pattern(Object o) { + switch (o) { + case MyRecord(java.lang.String _) -> {} + default -> {} + } + } + + void exception() { + try { + + } catch (java.lang.Exception _) { + + } + } + + void lambda() { + Stream.empty().map(_ -> null); + } + + void lambda2() { + Stream.empty().map((var _) -> null); + } +} \ No newline at end of file