diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/errors/Utilities.java b/java/java.hints/src/org/netbeans/modules/java/hints/errors/Utilities.java index 36740a5ee2c0..048f00ea428c 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/errors/Utilities.java +++ b/java/java.hints/src/org/netbeans/modules/java/hints/errors/Utilities.java @@ -125,6 +125,7 @@ import org.openide.text.NbDocument; import org.openide.util.Exceptions; import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.YieldTree; import com.sun.source.util.TreePathScanner; import com.sun.source.util.Trees; import com.sun.tools.javac.api.JavacScope; @@ -1447,6 +1448,11 @@ public Boolean visitReturn(ReturnTree node, Void p) { return true; } + @Override + public Boolean visitYield(YieldTree node, Void p) { + return true; + } + @Override public Boolean visitBreak(BreakTree node, Void p) { Tree target = info.getTreeUtilities().getBreakContinueTarget(getCurrentPath()); diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/introduce/IntroduceMethodFix.java b/java/java.hints/src/org/netbeans/modules/java/hints/introduce/IntroduceMethodFix.java index 90fd6910ef18..976aa8cd464c 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/introduce/IntroduceMethodFix.java +++ b/java/java.hints/src/org/netbeans/modules/java/hints/introduce/IntroduceMethodFix.java @@ -33,6 +33,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.TypeParameterTree; import com.sun.source.tree.VariableTree; +import com.sun.source.tree.YieldTree; import com.sun.source.util.SourcePositions; import com.sun.source.util.TreePath; import java.awt.GraphicsEnvironment; @@ -262,6 +263,25 @@ static Fix computeIntroduceMethod(CompilationInfo info, int start, int end, Map< returnType = methodReturnType; returnAssignTo = null; declareVariableForReturnValue = false; + } else if (exitsFromAllBranches && scanner.hasYields) { + returnType = info.getTypes().getNullType(); + for (TreePath exit : scanner.selectionExits) { + if (exit.getLeaf().getKind() == Tree.Kind.YIELD) { + Tree target = info.getTreeUtilities().getBreakContinueTargetTree(exit); + TreePath switchExpr = exit; + + while (switchExpr != null && switchExpr.getLeaf() != target) { + switchExpr = switchExpr.getParentPath(); + } + + if (switchExpr != null) { + returnType = info.getTrees().getTypeMirror(switchExpr); + break; + } + } + } + returnAssignTo = null; + declareVariableForReturnValue = false; } else { returnType = info.getTypes().getNoType(TypeKind.VOID); returnAssignTo = null; @@ -628,7 +648,11 @@ private void generateMethodInvocation(List nueStatements, List nueStatements, List methodStatements) { if (returnSingleValue) { + for (TreePath resolved : resolvedExits) { + if (resolved.getLeaf().getKind() == Tree.Kind.YIELD) { + YieldTree yield = (YieldTree) resolved.getLeaf(); + + copy.rewrite(resolved.getLeaf(), make.Return(yield.getValue())); + } + } return; } if (resolvedExits != null) { @@ -728,7 +762,7 @@ private boolean resolveAndInitialize() { resolvedExits.add(resolved); } - returnSingleValue = exitsFromAllBranches && branchExit.getLeaf().getKind() == Tree.Kind.RETURN && outcomeVariable == null && returnType.getKind() != TypeKind.VOID; + returnSingleValue = exitsFromAllBranches && (branchExit.getLeaf().getKind() == Tree.Kind.RETURN || branchExit.getLeaf().getKind() == Tree.Kind.YIELD) && outcomeVariable == null && returnType.getKind() != TypeKind.VOID; } // initialization make = copy.getTreeMaker(); diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/introduce/ScanStatement.java b/java/java.hints/src/org/netbeans/modules/java/hints/introduce/ScanStatement.java index dcca325af815..8d6199d6cfb4 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/introduce/ScanStatement.java +++ b/java/java.hints/src/org/netbeans/modules/java/hints/introduce/ScanStatement.java @@ -31,6 +31,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.tree.WhileLoopTree; +import com.sun.source.tree.YieldTree; import com.sun.source.util.TreePath; import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner; import java.util.ArrayList; @@ -71,6 +72,7 @@ final class ScanStatement extends ErrorAwareTreePathScanner { private final Map> assignmentsForUse; final Set usedTypeVariables = new HashSet(); boolean hasReturns = false; + boolean hasYields = false; private boolean hasBreaks = false; private boolean hasContinues = false; private boolean secondPass = false; @@ -236,6 +238,15 @@ public Void visitReturn(ReturnTree node, Void p) { return super.visitReturn(node, p); } + @Override + public Void visitYield(YieldTree node, Void p) { + if (isMethodCode() && phase == PHASE_INSIDE_SELECTION) { + selectionExits.add(getCurrentPath()); + hasYields = true; + } + return super.visitYield(node, p); + } + @Override public Void visitBreak(BreakTree node, Void p) { if (isMethodCode() && phase == PHASE_INSIDE_SELECTION && !treesSeensInSelection.contains(info.getTreeUtilities().getBreakContinueTargetTree(getCurrentPath()))) { @@ -313,6 +324,7 @@ String verifyExits(boolean exitsFromAllBranches) { i += hasReturns ? 1 : 0; i += hasBreaks ? 1 : 0; i += hasContinues ? 1 : 0; + i += hasYields ? 1 : 0; if (i > 1) { return "ERR_Too_Many_Different_Exits"; // NOI18N } @@ -325,6 +337,7 @@ String verifyExits(boolean exitsFromAllBranches) { for (TreePath tp : selectionExits) { if (tp.getLeaf().getKind() == Tree.Kind.RETURN) { if (!exitsFromAllBranches) { + //TODO: the same for yield ReturnTree rt = (ReturnTree) tp.getLeaf(); TreePath currentReturnValue = rt.getExpression() != null ? new TreePath(tp, rt.getExpression()) : null; if (!returnValueComputed) { diff --git a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/errors/UtilitiesTest.java b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/errors/UtilitiesTest.java index fba07ad5ed5b..dae9982fe2f2 100644 --- a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/errors/UtilitiesTest.java +++ b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/errors/UtilitiesTest.java @@ -448,6 +448,27 @@ public void testExitsAllBranchesSwitchDefaultMissing() throws Exception { false); } + public void testExitsAllBranchesYield() throws Exception { + performExitsTest( + """ + package test; + public class Test { + public int test(int param) { + return switch (param) { + default -> { + i|f (param == 0) { + yield 0; + } else { + yield 1; + } + } + }; + } + } + """, + true); + } + private void performExitsTest(String code, boolean expected) throws Exception { int caretPos = code.indexOf('|'); code = code.replace("|", ""); diff --git a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/introduce/IntroduceHintTest.java b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/introduce/IntroduceHintTest.java index 62daba64f035..6a4e8a1e832f 100644 --- a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/introduce/IntroduceHintTest.java +++ b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/introduce/IntroduceHintTest.java @@ -2618,6 +2618,151 @@ public void testIntroduceMethodReturn233433() throws Exception { 1, 0); } + public void testYield1() throws Exception { + sourceLevel = "17"; + performFixTest(""" + package test; + public class Test { + private int convert(int a) { + return switch (a) { + case 0 -> { + int i = 0; + |System.err.println(i); + int j = 0; + yield i + j;| + } + default -> -1; + }; + } + } + """, + """ + package test; + public class Test { + private int convert(int a) { + return switch (a) { + case 0 -> { + int i = 0; + yield name(i); + } + default -> -1; + }; + } + private int name(int i) { + System.err.println(i); + int j = 0; + return i + j; + } + } + """, + new DialogDisplayerImpl3("name", null, true), + 1, 0); + } + + public void testYield2() throws Exception { + sourceLevel = "17"; + performFixTest(""" + package test; + public class Test { + private Object convert(int a) { + return switch (a) { + case 0 -> { + int i = 0; + |System.err.println(i); + if (i == 0) { + yield ""; + } else { + yield 1; + }| + } + default -> -1; + }; + } + } + """, + """ + package test; + public class Test { + private Object convert(int a) { + return switch (a) { + case 0 -> { + int i = 0; + yield name(i); + } + default -> -1; + }; + } + private Object name(int i) { + System.err.println(i); + if (i == 0) { + return ""; + } else { + return 1; + } + } + } + """, + new DialogDisplayerImpl3("name", null, true), + 1, 0); + } + + public void testYield3() throws Exception { + sourceLevel = "17"; + performFixTest(""" + package test; + public class Test { + private Object convert(int a) { + return switch (a) { + case 0 -> { + int i = 0; + |System.err.println(i); + if (i <= 0) { + if (i == 0) { + yield ""; + } else { + System.err.println("wrong"); + yield ""; + } + }| + yield 1; + } + default -> -1; + }; + } + } + """, + """ + package test; + public class Test { + private Object convert(int a) { + return switch (a) { + case 0 -> { + int i = 0; + if (name(i)) + yield ""; + yield 1; + } + default -> -1; + }; + } + private boolean name(int i) { + System.err.println(i); + if (i <= 0) { + if (i == 0) { + return true; + } else { + System.err.println("wrong"); + return true; + } + } + return false; + } + } + """, + new DialogDisplayerImpl3("name", null, true), + 1, 0); + } + protected void prepareTest(String code) throws Exception { clearWorkDir(); diff --git a/java/java.source.base/apichanges.xml b/java/java.source.base/apichanges.xml index 27b3dac9e0e1..769c89bc4614 100644 --- a/java/java.source.base/apichanges.xml +++ b/java/java.source.base/apichanges.xml @@ -25,6 +25,18 @@ Java Source API + + + Added TreeMaker.Yield + + + + + + Adding TreeMaker.Yield. + + + API Support for module imports diff --git a/java/java.source.base/nbproject/project.properties b/java/java.source.base/nbproject/project.properties index 55e004633df1..2995d90f4932 100644 --- a/java/java.source.base/nbproject/project.properties +++ b/java/java.source.base/nbproject/project.properties @@ -23,7 +23,7 @@ javadoc.name=Java Source Base javadoc.title=Java Source Base javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=2.80.0 +spec.version.base=2.81.0 test.qa-functional.cp.extra=${refactoring.java.dir}/modules/ext/nb-javac-api.jar test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:\ ${o.n.core.dir}/lib/boot.jar:\ diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java b/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java index 78a5ebbab65d..a4fbc34b7e42 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java @@ -1168,6 +1168,17 @@ public ReturnTree Return(ExpressionTree expression) { return delegate.Return(expression); } + /** + * Creates a new YieldTree. + * + * @param expression the expression to be yielded. + * @see com.sun.source.tree.YieldTree + * @since 2.81 + */ + public YieldTree Yield(ExpressionTree expression) { + return delegate.Yield(expression); + } + /** * Creates a new SwitchTree. * diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java index 0cbfc878045e..40e48deec707 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java @@ -2192,7 +2192,7 @@ protected int diffCase(JCCase oldT, JCCase newT, int[] bounds) { localPointer = diffInnerComments(oldT, newT, localPointer); JCClassDecl oldEnclosing = printer.enclClass; printer.enclClass = null; - localPointer = diffList(filterHidden(oldT.stats), filterHidden(newT.stats), localPointer, est, Measure.MEMBER, printer); + localPointer = diffList(filterCaseStatements(oldT), filterCaseStatements(newT), localPointer, est, Measure.MEMBER, printer); printer.enclClass = oldEnclosing; if (localPointer < endPos(oldT)) { copyTo(localPointer, localPointer = endPos(oldT)); @@ -2208,6 +2208,17 @@ protected int diffCase(JCCase oldT, JCCase newT, int[] bounds) { return localPointer; } + private List filterCaseStatements(JCCase c) { + List filtered = filterHidden(c.stats); + + if (c.getCaseKind() == CaseTree.CaseKind.RULE && + filtered.size() == 1 && filtered.get(0) instanceof JCYield yield) { + filtered = List.of(make.Exec(yield.value)); + } + + return filtered; + } + protected int diffSynchronized(JCSynchronized oldT, JCSynchronized newT, int[] bounds) { int localPointer = bounds[0]; // lock diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/SwitchExpressionTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/SwitchExpressionTest.java index 553de43193af..b7c4ac2c5fb1 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/SwitchExpressionTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/SwitchExpressionTest.java @@ -246,4 +246,117 @@ public Void visitYield(YieldTree node, Void p) { } + public void testAnythingToBlock() throws Exception { + String code = """ + package test; + public class Test { + private void test(int p) { + var v = switch (p) { + case 1 -> p = 0; + default -> throw IllegalStateException(); + } + } + } + """; + String golden = """ + package test; + public class Test { + private void test(int p) { + var v = switch (p) { + case 1 -> { + } + default -> { + } + } + } + } + """; + + prepareTest("Test", code); + + + JavaSource js = getJavaSource(); + assertNotNull(js); + + Task task = new Task() { + + public void run(WorkingCopy workingCopy) throws IOException { + workingCopy.toPhase(JavaSource.Phase.RESOLVED); + + TreeMaker make = workingCopy.getTreeMaker(); + + new TreePathScanner<>() { + @Override + public Object visitCase(CaseTree node, Object p) { + workingCopy.rewrite(node, make.CasePatterns(node.getLabels(), make.Block(List.of(), false))); + return super.visitCase(node, p); + } + }.scan(workingCopy.getCompilationUnit(), null); + } + + }; + + js.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(getTestFile()); + //System.err.println(res); + assertEquals(golden, res); + + } + + public void testBlockToAnything() throws Exception { + String code = """ + package test; + public class Test { + private void test(int p) { + var v = switch (p) { + case 1 -> {} + default -> { throw IllegalStateException(); } + } + } + } + """; + String golden = """ + package test; + public class Test { + private void test(int p) { + var v = switch (p) { + case 1 -> 0; + default -> throw IllegalStateException(); + } + } + } + """; + + prepareTest("Test", code); + + + JavaSource js = getJavaSource(); + assertNotNull(js); + + Task task = new Task() { + + public void run(WorkingCopy workingCopy) throws IOException { + workingCopy.toPhase(JavaSource.Phase.RESOLVED); + + TreeMaker make = workingCopy.getTreeMaker(); + + new TreePathScanner<>() { + @Override + public Object visitCase(CaseTree node, Object p) { + BlockTree bt = (BlockTree) node.getBody(); + Tree newBody = bt.getStatements().isEmpty() ? make.Literal(0) + : bt.getStatements().get(0); + workingCopy.rewrite(node, make.CasePatterns(node.getLabels(), newBody)); + return super.visitCase(node, p); + } + }.scan(workingCopy.getCompilationUnit(), null); + } + + }; + + js.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(getTestFile()); + //System.err.println(res); + assertEquals(golden, res); + } } diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/YieldTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/YieldTest.java new file mode 100644 index 000000000000..396ff637769d --- /dev/null +++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/YieldTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.api.java.source.gen; + +import com.sun.source.tree.*; +import com.sun.source.util.TreePathScanner; +import java.io.File; +import org.netbeans.api.java.source.Task; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.JavaSource.*; +import org.netbeans.api.java.source.TestUtilities; +import org.netbeans.api.java.source.TreeMaker; +import org.netbeans.api.java.source.WorkingCopy; +import org.netbeans.junit.NbTestSuite; +import org.openide.filesystems.FileUtil; + +/** + * Test modifications in the yield statement. + * + * @author Pavel Flaska + */ +public class YieldTest extends GeneratorTestMDRCompat { + + /** Creates a new instance of TryTest */ + public YieldTest(String name) { + super(name); + } + + public static NbTestSuite suite() { + NbTestSuite suite = new NbTestSuite(); + suite.addTestSuite(YieldTest.class); + return suite; + } + + /** + * Change yield expression. + */ + public void testChangeYieldExpression() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + + public class Test { + public int taragui(int param) { + return switch (param) { + default -> { + yield 0; + } + }; + } + } + """); + String golden = + """ + package hierbas.del.litoral; + + public class Test { + public int taragui(int param) { + return switch (param) { + default -> { + yield param++; + } + }; + } + } + """; + JavaSource testSource = JavaSource.forFileObject(FileUtil.toFileObject(testFile)); + Task task = new Task() { + + public void run(WorkingCopy workingCopy) throws java.io.IOException { + workingCopy.toPhase(Phase.RESOLVED); + TreeMaker make = workingCopy.getTreeMaker(); + + new TreePathScanner<>() { + @Override + public Object visitYield(YieldTree node, Object p) { + workingCopy.rewrite(node, make.Yield(make.Unary(Tree.Kind.POSTFIX_INCREMENT, make.Identifier("param")))); + return super.visitYield(node, p); + } + }.scan(workingCopy.getCompilationUnit(), null); + } + + }; + testSource.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } + + public void testAddYield() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + + public class Test { + public int taragui(int param) { + return switch (param) { + default -> { + } + }; + } + } + """); + String golden = + """ + package hierbas.del.litoral; + + public class Test { + public int taragui(int param) { + return switch (param) { + default -> { + yield 0; + } + }; + } + } + """; + JavaSource testSource = JavaSource.forFileObject(FileUtil.toFileObject(testFile)); + Task task = new Task() { + + public void run(WorkingCopy workingCopy) throws java.io.IOException { + workingCopy.toPhase(Phase.RESOLVED); + TreeMaker make = workingCopy.getTreeMaker(); + + new TreePathScanner<>() { + @Override + public Object visitCase(CaseTree node, Object p) { + BlockTree body = (BlockTree) node.getBody(); + workingCopy.rewrite(body, make.addBlockStatement(body, make.Yield(make.Literal(0)))); + return super.visitCase(node, p); + } + }.scan(workingCopy.getCompilationUnit(), null); + } + + }; + testSource.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } + + String getGoldenPckg() { + return ""; + } + + String getSourcePckg() { + return ""; + } + +}