diff --git a/java/java.source.base/src/org/netbeans/api/java/source/ElementHandle.java b/java/java.source.base/src/org/netbeans/api/java/source/ElementHandle.java index 076e41c446ae..5c1fad00a391 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/ElementHandle.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/ElementHandle.java @@ -107,7 +107,7 @@ private ElementHandle(final ElementKind kind, String... signatures) { * Resolves an {@link Element} from the {@link ElementHandle}. * @param compilationInfo representing the {@link javax.tools.JavaCompiler.CompilationTask} * in which the {@link Element} should be resolved. - * @return resolved subclass of {@link Element} or null if the elment does not exist on + * @return resolved subclass of {@link Element} or null if the element does not exist on * the classpath/sourcepath of {@link javax.tools.JavaCompiler.CompilationTask}. */ @SuppressWarnings ("unchecked") // NOI18N @@ -143,12 +143,6 @@ private T resolveImpl (final ModuleElement module, final JavacTaskImpl jt) { if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "Resolving element kind: {0}", this.kind); // NOI18N ElementKind simplifiedKind = this.kind; - if (simplifiedKind.name().equals("RECORD")) { - simplifiedKind = ElementKind.CLASS; //TODO: test - } - if (simplifiedKind.name().equals("RECORD_COMPONENT")) { - simplifiedKind = ElementKind.FIELD; //TODO: test - } switch (simplifiedKind) { case PACKAGE: assert signatures.length == 1; @@ -156,6 +150,7 @@ private T resolveImpl (final ModuleElement module, final JavacTaskImpl jt) { case CLASS: case INTERFACE: case ENUM: + case RECORD: case ANNOTATION_TYPE: { assert signatures.length == 1; final Element type = getTypeElementByBinaryName (module, signatures[0], jt); @@ -213,6 +208,7 @@ private T resolveImpl (final ModuleElement module, final JavacTaskImpl jt) { } case FIELD: case ENUM_CONSTANT: + case RECORD_COMPONENT: { assert signatures.length == 3; final Element type = getTypeElementByBinaryName (module, signatures[0], jt); diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java b/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java index 14b097f0afb8..af7ab34baa7b 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java @@ -322,6 +322,7 @@ private static boolean isSupported(Element el) { case ENUM_CONSTANT: case RECORD: //TODO: record component + case RECORD_COMPONENT: return true; case PARAMETER: //only method and constructor parameters supported (not lambda): @@ -869,4 +870,4 @@ public Void scan(Tree node, Void p) { return result; } -} \ No newline at end of file +} diff --git a/java/java.source.base/src/org/netbeans/api/java/source/WorkingCopy.java b/java/java.source.base/src/org/netbeans/api/java/source/WorkingCopy.java index ff93ca70b8b5..87a76549adcb 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/WorkingCopy.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/WorkingCopy.java @@ -1221,6 +1221,7 @@ String template(ElementKind kind) { case INTERFACE: return "Templates/Classes/Interface.java"; // NOI18N case ANNOTATION_TYPE: return "Templates/Classes/AnnotationType.java"; // NOI18N case ENUM: return "Templates/Classes/Enum.java"; // NOI18N + case RECORD: return "Templates/Classes/Record.java"; // NOI18N case PACKAGE: return "Templates/Classes/package-info.java"; // NOI18N default: Logger.getLogger(WorkingCopy.class.getName()).log(Level.SEVERE, "Cannot resolve template for {0}", kind); @@ -1248,6 +1249,9 @@ FileObject doCreateFromTemplate(CompilationUnitTree cut) throws IOException { case ENUM: kind = ElementKind.ENUM; break; + case RECORD: + kind = ElementKind.RECORD; + break; default: Logger.getLogger(WorkingCopy.class.getName()).log(Level.SEVERE, "Cannot resolve template for {0}", cut.getTypeDecls().get(0).getKind()); kind = null; diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java index 559d428ebb64..cfcb50435bba 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java @@ -104,6 +104,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; +import javax.lang.model.element.Modifier; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.lexer.JavaTokenId; @@ -231,7 +232,7 @@ public void reset(int margin, int col) { public int getIndent() { return out.leftMargin; } - + public void setIndent(int indent) { out.leftMargin = indent; } @@ -270,14 +271,14 @@ public final void print(String s) { if (s == null) return; out.append(s); - } + } public final void print(Name n) { if (n == null) return; out.append(n.toString()); } - + private void print(javax.lang.model.element.Name n) { if (n == null) return; @@ -291,11 +292,11 @@ public void print(JCTree t) { doAccept(t, true); blankLines(t, false); } - + public void print(DCTree t) { print(t, false); } - + public void print(DCTree t, boolean noMarginAfter) { if (t == null) return; blankLines(t, true, false); @@ -303,7 +304,7 @@ public void print(DCTree t, boolean noMarginAfter) { doAccept(t); blankLines(t, false, noMarginAfter); } - + private Map overrideStartPositions; private int getOldPos(JCTree oldT) { @@ -318,7 +319,7 @@ private int getOldPos(JCTree oldT) { public int endPos(JCTree t) { return TreeInfo.getEndPos(t, diffContext.origUnit.endPositions); } - + private java.util.List getStatements(Tree tree) { switch (tree.getKind()) { case BLOCK: return ((BlockTree) tree).getStatements(); @@ -326,31 +327,31 @@ private java.util.List getStatements(Tree tree) { default: return null; } } - + private java.util.List printOriginalPartOfFieldGroup(FieldGroupTree fgt) { java.util.List variables = fgt.getVariables(); TreePath tp = TreePath.getPath(diffContext.origUnit, variables.get(0)); TreePath parent = tp != null ? tp.getParentPath() : null; - + if (parent == null) return variables; - + java.util.List statements = getStatements(parent.getLeaf()); - + if (statements == null) return variables; JCVariableDecl firstDecl = fgt.getVariables().get(0); int startIndex = statements.indexOf(firstDecl); - + if (startIndex < 0) return variables; //XXX: should not happen - + int origCount = 0; int s = statements.size(); for (JCTree t : variables) { if (startIndex >= s || statements.get(startIndex++) != t) break; origCount++; } - + if (origCount < 2) return variables; - + int firstPos = getOldPos(firstDecl); int groupStart = startIndex; // find the start of the field group among the original statement. The fieldgroup is detected using @@ -381,7 +382,7 @@ private java.util.List printOriginalPartOfFieldGroup(FieldGroupT overrideStartPositions = null; return variables.subList(origCount, variables.size()); } - + public Set oldTrees = Collections.emptySet(); public SortedSet reindentRegions = new TreeSet<>(new Comparator() { @Override public int compare(int[] o1, int[] o2) { @@ -394,7 +395,7 @@ private java.util.List printOriginalPartOfFieldGroup(FieldGroupT private void doAccept(JCTree t, boolean printComments/*XXX: should ideally always print comments?*/) { if (!handlePossibleOldTrees(Collections.singletonList(t), printComments)) { if (printComments) printPrecedingComments(t, true); - + int start = out.length(); if (t instanceof FieldGroupTree) { @@ -404,7 +405,7 @@ private void doAccept(JCTree t, boolean printComments/*XXX: should ideally alway printEnumConstants(List.from(fgt.getVariables().toArray(new JCTree[0])), !fgt.isEnum() || fgt.moreElementsFollowEnum(), printComments); } else { java.util.List remainder = printOriginalPartOfFieldGroup(fgt); - + //XXX: this will unroll the field group (see FieldGroupTest.testMove187766) //XXX: copied from visitClassDef boolean firstMember = remainder.size() == fgt.getVariables().size(); @@ -430,14 +431,14 @@ private void doAccept(JCTree t, boolean printComments/*XXX: should ideally alway if (tag != null) { tag2Span.put(tag, new int[]{start + initialOffset, end + initialOffset}); } - + if (printComments) { printInnerCommentsAsTrailing(t, true); printTrailingComments(t, true); } } } - + private void doAccept(DCTree t) { // int start = toString().length(); @@ -465,36 +466,36 @@ public boolean handlePossibleOldTrees(java.util.List toPrint, CommentSet cs = commentHandler.getComments(t); if (cs.hasChanges()) return false; } - + if (toPrint.size() > 1) { //verify that all the toPrint trees belong to the same parent, and appear //in the same uninterrupted order under that parent: TreePath tp = TreePath.getPath(diffContext.mainUnit, toPrint.get(0)); TreePath parent = tp.getParentPath(); - + if (parent == null) return false; //XXX: should not happen, right? - + java.util.List statements = getStatements(parent.getLeaf()); - + if (statements == null) return false; - + int startIndex = statements.indexOf(toPrint.get(0)); - + if (startIndex < 0) return false; //XXX: should not happen - + for (JCTree t : toPrint) { if (statements.get(startIndex++) != t) return false; } } - + doPrintOriginalTree(toPrint, includeComments); - + return true; } - + private void doPrintOriginalTree(java.util.List toPrint, final boolean includeComments) { if (out.isWhitespaceLine()) toLeftMargin(); - + JCTree firstTree = toPrint.get(0); JCTree lastTree = toPrint.get(toPrint.size() - 1); @@ -507,7 +508,7 @@ private void doPrintOriginalTree(java.util.List toPrint, final } else { realStart = getOldPos(firstTree); } - + final int newStart = out.length() + initialOffset; final int[] realEnd = {endPos(lastTree)}; @@ -520,7 +521,7 @@ public Void scan(Tree node, Void p) { realEnd[0] = Math.max(realEnd[0], Math.max(CasualDiff.commentEnd(old, CommentSet.RelativePosition.INLINE), CasualDiff.commentEnd(old, CommentSet.RelativePosition.TRAILING))); trailingCommentsHandled.add(node); } - + Object tag = tree2Tag != null ? tree2Tag.get(node) : null; if (tag != null) { @@ -528,7 +529,7 @@ public Void scan(Tree node, Void p) { int e = endPos((JCTree) node); tag2Span.put(tag, new int[]{s - realStart + newStart, e - realStart + newStart}); } - + } return super.scan(node, p); } @@ -557,10 +558,10 @@ private void copyToIndented(int from, int to) { } String text = origText.substring(from, to); - + int newLine = text.indexOf("\n") + 1; boolean wasWhitespaceLine = out.isWhitespaceLine(); - + if (newLine == 0 && !wasWhitespaceLine) { print(text); } else { @@ -575,8 +576,8 @@ private void copyToIndented(int from, int to) { /** * Adjusts {@link #reindentRegions} if the char buffer conntents is * trimmed. - * - * @param limit + * + * @param limit */ @Override public void trimmed(int limit) { @@ -595,7 +596,7 @@ public void trimmed(int limit) { } } } - + /** Print a package declaration. */ @@ -640,9 +641,14 @@ public String getMethodHeader(MethodTree t, String s) { print(tree.name); s = replace(s, NAME); } - print('('); - wrapTrees(tree.params, WrapStyle.WRAP_NEVER, out.col); - print(')'); + boolean isRecord = enclClass.getKind()== Kind.RECORD; + // return type null makes this method a constructor + boolean isCandidateCompactCtor = isRecord && null == t.getReturnType(); + if (!isCandidateCompactCtor) { + print('('); + wrapTrees(tree.params, WrapStyle.WRAP_NEVER, out.col); + print(')'); + } s = replace(s, PARAMETERS); if (tree.thrown.nonEmpty()) { print(" throws "); @@ -844,7 +850,7 @@ public void visitPackageDef(JCPackageDecl tree) { printPackage(tree.pid); } } - + @Override public void visitImport(JCImport tree) { print("import "); @@ -977,6 +983,9 @@ private void printEnumConstants(java.util.List defs, boolean f @Override public void visitMethodDef(JCMethodDecl tree) { + boolean methodIsConstructor = null==tree.getReturnType(); + boolean enclosingIsRecord= enclClass.getKind() == Kind.RECORD; + boolean paramsIsComponents= paramsIsComponents(tree.getParameters()); if ((tree.mods.flags & Flags.SYNTHETIC)==0 && tree.name != names.init || enclClass != null) { @@ -995,18 +1004,9 @@ public void visitMethodDef(JCMethodDecl tree) { needSpace(); print(tree.name); } - print(cs.spaceBeforeMethodDeclParen() ? " (" : "("); - if (cs.spaceWithinMethodDeclParens() && tree.params.nonEmpty()) - print(' '); - boolean oldPrintingMethodParams = printingMethodParams; - printingMethodParams = true; - wrapTrees(tree.params, cs.wrapMethodParams(), cs.alignMultilineMethodParams() - ? out.col : out.leftMargin + cs.getContinuationIndentSize(), - true); - printingMethodParams = oldPrintingMethodParams; - if (cs.spaceWithinMethodDeclParens() && tree.params.nonEmpty()) - needSpace(); - print(')'); + if (!(methodIsConstructor && enclosingIsRecord && paramsIsComponents)){ + printMethodParams(tree); + } if (tree.thrown.nonEmpty()) { wrap("throws ", cs.wrapThrowsKeyword()); wrapTrees(tree.thrown, cs.wrapThrowsList(), cs.alignMultilineThrows() @@ -1026,6 +1026,21 @@ public void visitMethodDef(JCMethodDecl tree) { } } + void printMethodParams(JCMethodDecl tree) { + print(cs.spaceBeforeMethodDeclParen() ? " (" : "("); + if (cs.spaceWithinMethodDeclParens() && tree.params.nonEmpty()) + print(' '); + boolean oldPrintingMethodParams = printingMethodParams; + printingMethodParams = true; + wrapTrees(tree.params, cs.wrapMethodParams(), cs.alignMultilineMethodParams() + ? out.col : out.leftMargin + cs.getContinuationIndentSize(), + true); + printingMethodParams = oldPrintingMethodParams; + if (cs.spaceWithinMethodDeclParens() && tree.params.nonEmpty()) + needSpace(); + print(')'); + } + @Override public void visitVarDef(JCVariableDecl tree) { boolean notEnumConst = (tree.mods.flags & Flags.ENUM) == 0; @@ -1656,7 +1671,7 @@ else if (tree.clazz.type != null) printNewClassBody(tree); } } - + public void printNewClassBody(JCNewClass tree) { JCClassDecl enclClassPrev = enclClass; enclClass = tree.def; @@ -1828,8 +1843,8 @@ public void visitTypeCast(JCTypeCast tree) { @Override public void visitTypeUnion(JCTypeUnion that) { boolean sep = cs.spaceAroundBinaryOps(); - wrapTrees(that.getTypeAlternatives(), - cs.wrapDisjunctiveCatchTypes(), + wrapTrees(that.getTypeAlternatives(), + cs.wrapDisjunctiveCatchTypes(), cs.alignMultilineDisjunctiveCatchTypes() ? out.col : out.leftMargin + cs.getContinuationIndentSize(), false, sep, sep, "|"); // NOI18N } @@ -1959,15 +1974,15 @@ private static String quote(String val, char keep, boolean charContext) { } return sb.toString(); } - + private static final String[] typeTagNames = new String[TypeTag.values().length]; - + static { for (TypeTag tt : TypeTag.values()) { typeTagNames[tt.ordinal()] = tt.name().toLowerCase(Locale.ENGLISH); } } - + /** * Workaround for defect #239258. Converts typetag names into lowercase using ENGLISH locale. */ @@ -2000,7 +2015,7 @@ public void visitAnnotatedType(JCAnnotatedType tree) { print(' '); printExpr(tree.underlyingType); } - + @Override public void visitTypeParameter(JCTypeParameter tree) { print(tree.name); @@ -2195,7 +2210,7 @@ private void blankLines(JCTree tree, boolean before) { return; } } - + private boolean isFirst(JCTree tree, List list) { for (JCTree t : list) { if (!isSynthetic(t)) { @@ -2204,7 +2219,7 @@ private boolean isFirst(JCTree tree, List list) { } return false; } - + private boolean isLast(JCTree tree, List list) { boolean b = false; for (JCTree t : list) { @@ -2214,7 +2229,7 @@ private boolean isLast(JCTree tree, List list) { } return b; } - + /** * The following tags are block-tags *
    @@ -2729,6 +2744,42 @@ public void setDocCommentKind(JavaTokenId docCommentKind) { this.docCommentKind = docCommentKind; } + /** + * Check that the given parameters are equal to the classes (record) component. + * + * return true iff parameters and components are of same length, in same name and type order + * @param parameters of the method + * @return if the parameters equal the record components. + */ + private boolean paramsIsComponents(List parameters) { + record TypeInfo(String name, String type){} + TypeInfo[] params = parameters.stream() + .map(p -> new TypeInfo(p.getName().toString(), p.getType().toString())) + .toArray(TypeInfo[]::new); + TypeInfo[] components = enclClass.getMembers().stream() + .filter(VeryPretty::isRecordComponent) + .map(x ->(VariableTree) x) + .map(p -> new TypeInfo(p.getName().toString(), p.getType().toString())) + .toArray(TypeInfo[]::new); + // short circuit on different length. + if (params.length != components.length) return false; + return Arrays.deepEquals(params, components); + } + + /** + * A member is a normal field when not a class, (or enum ...) not a method + * and not static. For record that should be a record component then. + * @param t + * @return true if fields of the class/record are of same amount, order and types + */ + static boolean isRecordComponent(JCTree t){ + if (t.getKind()==Kind.CLASS) return false; + if (t.getKind()==Kind.METHOD) return false; + if (t.getKind()==Kind.VARIABLE && t instanceof VariableTree vt){ + return !vt.getModifiers().getFlags().contains(Modifier.STATIC); + } + return false; + } private final class Linearize extends ErrorAwareTreeScanner> { @Override public Boolean scan(Tree node, java.util.List p) { @@ -2766,12 +2817,12 @@ private void adjustSpans(Iterable original, String code) { if (tree2Tag == null) { return; //nothing to copy } - + java.util.List linearized = new LinkedList(); if (!new Linearize().scan(original, linearized) != Boolean.TRUE) { return; //nothing to copy } - + ClassPath empty = ClassPathSupport.createClassPath(new URL[0]); ClasspathInfo cpInfo = ClasspathInfo.create(JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries(), empty, empty); JavacTaskImpl javacTask = JavacParser.createJavacTask(cpInfo, null, null, null, null, null, null, null, Arrays.asList(FileObjects.memoryFileObject("", "Scratch.java", code))); @@ -2798,7 +2849,7 @@ private static String whitespace(int num) { private boolean printAnnotationsFormatted(List annotations) { if (reallyPrintAnnotations) return false; - + VeryPretty del = new VeryPretty(diffContext, cs, new HashMap(), tree2Doc, new HashMap(), origText, 0); del.reallyPrintAnnotations = true; del.printingMethodParams = printingMethodParams; @@ -2807,7 +2858,7 @@ private boolean printAnnotationsFormatted(List annotations) { String str = del.out.toString(); int col = printingMethodParams ? out.leftMargin + cs.getContinuationIndentSize() : out.col; - + str = Reformatter.reformat(str + " class A{}", cs, cs.getRightMargin() - col); str = str.trim().replace("\n", "\n" + whitespace(col)); @@ -2823,7 +2874,7 @@ private boolean printAnnotationsFormatted(List annotations) { return true; } - + private void printAnnotations(List annotations) { if (annotations.isEmpty()) return ; @@ -2831,7 +2882,7 @@ private void printAnnotations(List annotations) { toColExactly(out.leftMargin); return ; } - + while (annotations.nonEmpty()) { printNoParenExpr(annotations.head); if (annotations.tail != null && annotations.tail.nonEmpty()) { @@ -2889,19 +2940,19 @@ else if (addSpace) needSpace(); } } - + private static final String[] flagLowerCaseNames = new String[Flag.values().length]; - + static { for (Flag flag : Flag.values()) { flagLowerCaseNames[flag.ordinal()] = flag.name().toLowerCase(Locale.ENGLISH); } } - + /** - * Workaround for defect #239258. Prints flag names converted to lowercase in ENGLISH locale to + * Workaround for defect #239258. Prints flag names converted to lowercase in ENGLISH locale to * avoid weird Turkish I > i-without-dot-above conversion. - * + * * @param flags flags * @return flag names, space-separated. */ @@ -3009,7 +3060,7 @@ private void printExprs(List < T > trees, String sep) { private void printStat(JCTree tree) { printStat(tree, false, false); } - + private void printStat(JCTree tree, boolean member, boolean first) { printStat(tree, member, first, false, false, false); } @@ -3017,12 +3068,12 @@ private void printStat(JCTree tree, boolean member, boolean first) { /** * Prints blank lines before, positions to the exact column (optional), prints tree and * blank lines after. And optional additional newline. - * + * * @param tree * @param member * @param first * @param col - * @param nl + * @param nl */ private void printStat(JCTree tree, boolean member, boolean first, boolean col, boolean nl, boolean printComments) { if(tree==null) { @@ -3040,13 +3091,13 @@ private void printStat(JCTree tree, boolean member, boolean first, boolean col, if (col) { toColExactly(out.leftMargin); } - // because of comment duplication + // because of comment duplication if(printComments) printPrecedingComments(tree, !member); printInnerCommentsAsTrailing(tree, !member); printExpr(tree, TreeInfo.notExpression); int tag = tree.getTag().ordinal();//XXX: comparing ordinals!!! if(JCTree.Tag.APPLY.ordinal()<=tag && tag<=JCTree.Tag.MOD_ASG.ordinal()) print(';'); - + printTrailingComments(tree, !member); blankLines(tree, false); if (nl) { @@ -3122,7 +3173,7 @@ private void printBlock(JCTree tree, List stats, BracePlacemen } public int conditionStartHack = (-1); - + private void printBlock(JCTree tree, List stats, BracePlacement bracePlacement, boolean spaceBeforeLeftBrace, boolean members, boolean printComments) { if (printComments) printPrecedingComments(tree, true); int old = indent(); @@ -3446,14 +3497,14 @@ private void wrap(String s, WrapStyle wrapStyle) { private void wrapTrees(List trees, WrapStyle wrapStyle, int wrapIndent) { wrapTrees(trees, wrapStyle, wrapIndent, false); //TODO: false for "compatibility", with the previous release, but maybe should be true for everyone? } - + private void wrapTrees(List trees, WrapStyle wrapStyle, int wrapIndent, boolean wrapFirst) { wrapTrees(trees, wrapStyle, wrapIndent, wrapFirst, cs.spaceBeforeComma(), cs.spaceAfterComma(), ","); // NOI18N } private void wrapTrees(List trees, WrapStyle wrapStyle, int wrapIndent, boolean wrapFirst, boolean spaceBeforeSeparator, boolean spaceAfterSeparator, String separator) { - + boolean first = true; for (List < T > l = trees; l.nonEmpty(); l = l.tail) { if (!first) { @@ -3462,7 +3513,7 @@ private void wrapTrees(List trees, WrapStyle wrapStyle, in } print(separator); } - + if (!first || wrapFirst) { switch(first && wrapStyle != WrapStyle.WRAP_NEVER ? WrapStyle.WRAP_IF_LONG : wrapStyle) { case WRAP_IF_LONG: @@ -3487,7 +3538,7 @@ private void wrapTrees(List trees, WrapStyle wrapStyle, in first = false; } } - + private void wrapAssignOpTree(final String operator, int col, final Runnable print) { final boolean spaceAroundAssignOps = cs.spaceAroundAssignOps(); if (cs.wrapAfterAssignOps()) { @@ -3506,7 +3557,7 @@ private void wrapAssignOpTree(final String operator, int col, final Runnable pri } }); } - + private void wrapTree(WrapStyle wrapStyle, boolean needsSpaceBefore, int colAfterWrap, Runnable print) { switch(wrapStyle) { case WRAP_NEVER: diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RecordTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RecordTest.java index bc33aa888e5d..91279a55f6ab 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RecordTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RecordTest.java @@ -194,6 +194,59 @@ public void run(WorkingCopy workingCopy) throws IOException { assertEquals(golden, res); } + /** + * Is the compact constructor preserved? + * Added check for #7044 + * + */ + public void testPreserveCompact() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + public record R(String first, String component) { + public R { + assert null != first; + } + } + """); + String golden = + """ + package hierbas.del.litoral; + public record R(String first) { + public R { + assert null != first; + } + } + """; + + JavaSource src = getJavaSource(testFile); + Task task = new Task() { + + public void run(WorkingCopy workingCopy) throws IOException { + workingCopy.toPhase(Phase.RESOLVED); + CompilationUnitTree cut = workingCopy.getCompilationUnit(); + TreeMaker make = workingCopy.getTreeMaker(); + + Tree recordDecl = cut.getTypeDecls().get(0); + assertEquals(Kind.RECORD, recordDecl.getKind()); + ClassTree classTree = (ClassTree) recordDecl; + for (Tree m : classTree.getMembers()) { + if (m.getKind() == Kind.VARIABLE && + ((VariableTree) m).getName().contentEquals("component")) { + workingCopy.rewrite(classTree, make.removeClassMember(classTree, m)); + break; + } + } + } + + }; + src.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } + public void testRemoveComponent() throws Exception { testFile = new File(getWorkDir(), "Test.java"); TestUtilities.copyStringToFile(testFile, diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/InnerToOuterRefactoring.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/InnerToOuterRefactoring.java index 52d457c59b8e..ab9c6cfae744 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/InnerToOuterRefactoring.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/InnerToOuterRefactoring.java @@ -22,10 +22,10 @@ import org.netbeans.modules.refactoring.api.AbstractRefactoring; import org.openide.util.lookup.Lookups; -/** +/** * Convert Inner to Top-Level refactoring implementation class. This refactoring * is capable of converting an inner class into a top-level class. - * + * * @see org.netbeans.modules.refactoring.spi.RefactoringPlugin * @see org.netbeans.modules.refactoring.spi.RefactoringPluginFactory * @see org.netbeans.modules.refactoring.api.AbstractRefactoring @@ -39,18 +39,23 @@ public final class InnerToOuterRefactoring extends AbstractRefactoring { // parameters of the refactoring private String className; private String referenceName; - + + private boolean innerIsRecord; + /** * Creates a new instance of InnerToOuterRefactoring. - * - * @param sourceType An inner class that should be converted to a top-level class. + * + * @param sourceType An inner class that should be converted to a top-level + * class. */ public InnerToOuterRefactoring(TreePathHandle sourceType) { super(Lookups.singleton(sourceType)); } - - /** Returns the type the members of which should be pulled up - * by this refactoring. + + /** + * Returns the type the members of which should be pulled up by this + * refactoring. + * * @return Source of the members to be pulled up. */ public TreePathHandle getSourceType() { @@ -58,34 +63,79 @@ public TreePathHandle getSourceType() { } // --- PARAMETERS ---------------------------------------------------------- - - /** Returns the name for the top-level class to be created. + /** + * Returns the name for the top-level class to be created. + * * @return Class name. */ public String getClassName() { return className; } - /** Sets name for the top-level class to be created. + /** + * Sets name for the top-level class to be created. + * * @param className Class name. */ public void setClassName(String className) { this.className = className; } - /** Returns name of the field that should be generated as a reference to the original - * outer class. If null, no field will be generated. - * @return Name of the field to be generated or null if no field will be generated. + /** + * Returns name of the field that should be generated as a reference to the + * original outer class. If null, no field will be generated. + * + * @return Name of the field to be generated or null if no field will be + * generated. */ public String getReferenceName() { return referenceName; } - - /** Sets name of the field that should be generated as a reference to the original - * outer class. Can be set to null which indicates that no field should be generated. - * @param referenceName Name of the field or null if no field should be generated. - */ + + /** + * Sets name of the field that should be generated as a reference to the + * original outer class. Can be set to null which indicates that no field + * should be generated. + * + * @param referenceName Name of the field or null if no field should be + * generated. + */ public void setReferenceName(String referenceName) { this.referenceName = referenceName; } + + /** + * Inner records need special handling because of the RecordComponents which + * are declared before the first curly brace, which differs from class, + * interface and enum. + * + * Also, the compact constructor should be considered. + * + * A compact constructor consists of the name of the Record, no parameters + * (not even the parens) and a block that does NOT assign the fields. + * + * @return the current value for this refactoring + */ + public boolean isInnerIsRecord() { + + return innerIsRecord; + } + + /** + * Inner records need special handling because of the RecordComponents which + * are declared before the first curly brace, which differs from class, + * interface and enum. + * + * Also, the compact constructor should be considered. + * + * A compact constructor consists of the name of the Record, no parameters + * (not even the parens) and a block that does NOT assign the fields. + * + * @param innerIsRecord use when inner class needs the special handling of + * an inner record. + */ + public void setInnerIsRecord(boolean innerIsRecord) { + this.innerIsRecord = innerIsRecord; + } + } diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/InnerToOuterTransformer.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/InnerToOuterTransformer.java index 226e6ae0b404..bfcaab4e062a 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/InnerToOuterTransformer.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/InnerToOuterTransformer.java @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - package org.netbeans.modules.refactoring.java.plugins; import com.sun.source.tree.*; @@ -52,7 +51,7 @@ public class InnerToOuterTransformer extends RefactoringVisitor { private InnerToOuterRefactoring refactoring; private boolean isInInnerClass = false; private Set referencedPrivateElement; - + private Element getCurrentElement() { return workingCopy.getTrees().getElement(getCurrentPath()); } @@ -78,7 +77,7 @@ public Tree visitIdentifier(IdentifierTree node, Element p) { return null; } if (inner.equals(current)) { - Tree newTree = make.setLabel(node, refactoring.getClassName()); + Tree newTree = make.setLabel(node, refactoring.getClassName()); rewrite(node, newTree); } else if (isThisReferenceToOuter() && isThisInInner()) { if (current.getModifiers().contains(Modifier.PRIVATE)) { @@ -105,7 +104,7 @@ public Tree visitIdentifier(IdentifierTree node, Element p) { TreePath elementPath = workingCopy.getTrees().getPath(current); JavaFileObject sourceFile = elementPath != null ? elementPath.getCompilationUnit().getSourceFile() : null; if ( (parent != null && parent.getKind() == Tree.Kind.CASE && ((CaseTree) parent).getExpression() == node && current.getKind() == ElementKind.ENUM_CONSTANT) - || path.getCompilationUnit().getSourceFile() == sourceFile) { + || path.getCompilationUnit().getSourceFile() == sourceFile) { rewrite(node, make.Identifier(current.getSimpleName())); } else { rewrite(node, make.QualIdent(current)); @@ -141,7 +140,7 @@ public Tree visitNewClass(NewClassTree arg0, Element arg1) { else { thisString = "this"; // NOI18N } - + } if (thisString != null && currentElement instanceof ExecutableElement) { ExecutableElement constr = (ExecutableElement) currentElement; @@ -154,7 +153,7 @@ public Tree visitNewClass(NewClassTree arg0, Element arg1) { removeEnclosingExpression = true; } } - + int index = constr.getParameters().size(); if (constr.isVarArgs()) { index--; @@ -209,7 +208,7 @@ public Tree visitNewClass(NewClassTree arg0, Element arg1) { } return super.visitNewClass(arg0, arg1); } - + private TypeElement getOuter(TypeElement element) { while (element != null && !workingCopy.getTypes().isSubtype(element.asType(),outer.asType())) { element = workingCopy.getElementUtilities().enclosingTypeElement(element); @@ -231,7 +230,7 @@ public Tree visitMethod(MethodTree constructor, Element element) { rewrite(superCall, newSuperCall); } } - + } return super.visitMethod(constructor, element); } @@ -242,26 +241,26 @@ public Tree visitClass(ClassTree classTree, Element element) { if (currentElement == null) { return super.visitClass(classTree, element); } - GeneratorUtilities genUtils = GeneratorUtilities.get(workingCopy); // helper + GeneratorUtilities genUtils = GeneratorUtilities.get(workingCopy); // helper if (currentElement!=null && currentElement == outer) { Element outerouter = outer.getEnclosingElement(); Tree superVisit = super.visitClass(classTree, element); - TreePath tp = workingCopy.getTrees().getPath(inner); - if (tp==null) { + TreePath innerTp = workingCopy.getTrees().getPath(inner); + if (innerTp==null) { //#194346 return superVisit; } - ClassTree innerClass = (ClassTree) tp.getLeaf(); + ClassTree innerClass = (ClassTree) innerTp.getLeaf(); ClassTree newInnerClass = innerClass; newInnerClass = genUtils.importComments(newInnerClass, workingCopy.getCompilationUnit()); newInnerClass = make.setLabel(newInnerClass, refactoring.getClassName()); - + newInnerClass = refactorInnerClass(newInnerClass); - + TreePath outerPath = workingCopy.getTrees().getPath(outer); - + if (outerouter.getKind() == ElementKind.PACKAGE) { FileObject sourceRoot=ClassPath.getClassPath(workingCopy.getFileObject(), ClassPath.SOURCE).findOwnerRoot(workingCopy.getFileObject()); ClassTree outerTree = (ClassTree) workingCopy.getTrees().getTree(outer); @@ -270,7 +269,7 @@ public Tree visitClass(ClassTree classTree, Element element) { if(outerPath != null) { JavaRefactoringUtils.cacheTreePathInfo(outerPath, workingCopy); } - CompilationUnitTree compilationUnit = tp.getCompilationUnit(); + CompilationUnitTree compilationUnit = innerTp.getCompilationUnit(); String relativePath = RefactoringUtils.getPackageName(compilationUnit).replace('.', '/') + '/' + refactoring.getClassName() + ".java"; // NOI18N CompilationUnitTree newCompilation = JavaPluginUtils.createCompilationUnit(sourceRoot, relativePath, newInnerClass, workingCopy, make); rewrite(null, newCompilation); @@ -290,53 +289,53 @@ public Tree visitClass(ClassTree classTree, Element element) { rewrite(outerouterTree, newOuterOuter); return newOuterOuter; } - + } else if (refactoring.getReferenceName() != null && currentElement!=null && workingCopy.getTypes().isSubtype(currentElement.asType(), inner.asType()) && currentElement!=inner) { - VariableTree variable = make.Variable(make.Modifiers(Collections.emptySet()), refactoring.getReferenceName(), make.Type(outer.asType()), null); + VariableTree variable = make.Variable(make.Modifiers(Collections.emptySet()), refactoring.getReferenceName(), make.Type(outer.asType()), null); for (Tree member:classTree.getMembers()) { - if (member.getKind() == Tree.Kind.METHOD) { - MethodTree m = (MethodTree) member; + if (member.getKind() == Tree.Kind.METHOD) { + MethodTree m = (MethodTree) member; if (m.getReturnType()==null) { - + for( VariableTree var: m.getParameters() ) { if( var.getName().contentEquals(refactoring.getReferenceName()) ) { - problem = MoveTransformer.createProblem(problem, true, NbBundle.getMessage(InnerToOuterTransformer.class, "ERR_InnerToOuter_OuterNameClashSubtype", refactoring.getReferenceName(), refactoring.getClassName(), currentElement.getSimpleName())); - } + problem = MoveTransformer.createProblem(problem, true, NbBundle.getMessage(InnerToOuterTransformer.class, "ERR_InnerToOuter_OuterNameClashSubtype", refactoring.getReferenceName(), refactoring.getClassName(), currentElement.getSimpleName())); } + } - MethodInvocationTree superCall = (MethodInvocationTree) ((ExpressionStatementTree) m.getBody().getStatements().get(0)).getExpression(); - List newArgs = new ArrayList(superCall.getArguments()); - - MethodTree newConstructor = null; - ExpressionTree exprTree = (ExpressionTree)make.Identifier(variable.getName().toString()); - if (hasVarArgs(m)) { - int index = m.getParameters().size() - 1; - newArgs.add(index, exprTree); - newConstructor = make.insertMethodParameter(m, index, variable); - } else { - newArgs.add(exprTree); - newConstructor = make.addMethodParameter(m, variable); - } - MethodInvocationTree method = make.MethodInvocation( - Collections.emptyList(), - make.Identifier("super"), // NOI18N - newArgs); - - BlockTree block = make.insertBlockStatement(m.getBody(), 0, make.ExpressionStatement(method)); - block = make.removeBlockStatement(block, 1); - - newConstructor = make.Constructor( - make.Modifiers(newConstructor.getModifiers().getFlags(), newConstructor.getModifiers().getAnnotations()), - newConstructor.getTypeParameters(), - newConstructor.getParameters(), - newConstructor.getThrows(), - block); - - rewrite(m, newConstructor); + MethodInvocationTree superCall = (MethodInvocationTree) ((ExpressionStatementTree) m.getBody().getStatements().get(0)).getExpression(); + List newArgs = new ArrayList(superCall.getArguments()); + + MethodTree newConstructor = null; + ExpressionTree exprTree = (ExpressionTree) make.Identifier(variable.getName().toString()); + if (hasVarArgs(m)) { + int index = m.getParameters().size() - 1; + newArgs.add(index, exprTree); + newConstructor = make.insertMethodParameter(m, index, variable); + } else { + newArgs.add(exprTree); + newConstructor = make.addMethodParameter(m, variable); } + MethodInvocationTree method = make.MethodInvocation( + Collections.emptyList(), + make.Identifier("super"), // NOI18N + newArgs); + + BlockTree block = make.insertBlockStatement(m.getBody(), 0, make.ExpressionStatement(method)); + block = make.removeBlockStatement(block, 1); + + newConstructor = make.Constructor( + make.Modifiers(newConstructor.getModifiers().getFlags(), newConstructor.getModifiers().getAnnotations()), + newConstructor.getTypeParameters(), + newConstructor.getParameters(), + newConstructor.getThrows(), + block); + + rewrite(m, newConstructor); } - } + } } + } if (currentElement == inner) { try { @@ -346,7 +345,7 @@ public Tree visitClass(ClassTree classTree, Element element) { isInInnerClass = false; } } - + return super.visitClass(classTree, element); } @@ -359,7 +358,7 @@ public Tree visitCompilationUnit(CompilationUnitTree node, Element p) { for (Element privEl : this.referencedPrivateElement) { problem = MoveTransformer.createProblem(problem, false, NbBundle.getMessage(InnerToOuterRefactoringPlugin.class, "WRN_InnerToOuterRefToPrivate", privEl)); } - + Trees trees = workingCopy.getTrees(); CompilationUnitTree newNode = node; for (ImportTree imp : node.getImports()) { @@ -376,7 +375,7 @@ public Tree visitCompilationUnit(CompilationUnitTree node, Element p) { } return result; } - + private Problem problem; public Problem getProblem() { @@ -391,7 +390,7 @@ private boolean containsImport(String imp) { } return false; } - + @Override public Tree visitMemberSelect(MemberSelectTree memberSelect, Element element) { @@ -456,9 +455,9 @@ public Tree visitMemberSelect(MemberSelectTree memberSelect, Element element) { rewrite(variable.getModifiers(), make.removeModifiersModifier(variable.getModifiers(), Modifier.PRIVATE)); } } - + } - + return super.visitMemberSelect(memberSelect, element); } @@ -495,7 +494,7 @@ private boolean isThisReferenceToInner() { TypeElement encl = workingCopy.getElementUtilities().enclosingTypeElement(cur); return encl!=null && workingCopy.getTypes().isSubtype(encl.asType(), inner.asType()) ; } - + private boolean isThisReferenceToOuter() { Element cur = getCurrentElement(); if (cur==null || cur.getKind() == ElementKind.PACKAGE) { @@ -542,7 +541,7 @@ private boolean isIn(Element el) { } return false; } - + private boolean hasVarArgs(MethodTree mt) { List list = mt.getParameters(); if (list.isEmpty()) { @@ -551,8 +550,11 @@ private boolean hasVarArgs(MethodTree mt) { VariableTree vt = (VariableTree)list.get(list.size() - 1); return vt.toString().indexOf("...") != -1; // [NOI18N] [TODO] temporal hack, will be rewritten } - + private ClassTree refactorInnerClass(ClassTree innerClass) { + if (innerClass.getKind() == Tree.Kind.RECORD) { + return refactorInnerRecord(innerClass); + } ClassTree newInnerClass = innerClass; String referenceName = refactoring.getReferenceName(); GeneratorUtilities genUtils = GeneratorUtilities.get(workingCopy); @@ -568,11 +570,13 @@ private ClassTree refactorInnerClass(ClassTree innerClass) { } else { outerType = outer.asType(); } + // for none static inner classes, add a member to point to the 'old' outer. + // because there has to be an explicit member after refactoring. if (referenceName != null) { VariableTree variable = make.Variable(make.Modifiers(EnumSet.of(Modifier.PRIVATE, Modifier.FINAL)), refactoring.getReferenceName(), make.Type(outerType), null); newInnerClass = genUtils.insertClassMember(newInnerClass, variable); } - + ModifiersTree modifiersTree = newInnerClass.getModifiers(); ModifiersTree newModifiersTree = make.removeModifiersModifier(modifiersTree, Modifier.PRIVATE); newModifiersTree = make.removeModifiersModifier(newModifiersTree, Modifier.STATIC); @@ -582,16 +586,17 @@ private ClassTree refactorInnerClass(ClassTree innerClass) { } rewrite(modifiersTree, newModifiersTree); + // create constructor which refers with member to old outer. if (referenceName != null) { for (Tree member:newInnerClass.getMembers()) { if (member.getKind() == Tree.Kind.METHOD) { - MethodTree m = (MethodTree) member; - if (m.getName().contentEquals("") || m.getReturnType() == null) { - VariableTree parameter = make.Variable(make.Modifiers(EnumSet.of(Modifier.FINAL)), refactoring.getReferenceName(), make.Type(outerType), null); - MethodTree newConstructor = hasVarArgs(m) ? - make.insertMethodParameter(m, m.getParameters().size() - 1, parameter) : - make.addMethodParameter(m, parameter); - + MethodTree ctor = (MethodTree) member; + if (ctor.getName().contentEquals("") || ctor.getReturnType() == null) { + VariableTree parameter = make.Variable(make.Modifiers(EnumSet.of(Modifier.FINAL)), referenceName, make.Type(outerType), null); + MethodTree newConstructor = hasVarArgs(ctor) ? + make.insertMethodParameter(ctor, ctor.getParameters().size() - 1, parameter) : + make.addMethodParameter(ctor, parameter); + AssignmentTree assign = make.Assignment(make.Identifier("this."+referenceName), make.Identifier(referenceName)); // NOI18N BlockTree block = make.insertBlockStatement(newConstructor.getBody(), 1, make.ExpressionStatement(assign)); Set modifiers = EnumSet.noneOf(Modifier.class); @@ -599,20 +604,20 @@ private ClassTree refactorInnerClass(ClassTree innerClass) { modifiers.remove(Modifier.PRIVATE); newConstructor = make.Constructor( make.Modifiers(modifiers,newConstructor.getModifiers().getAnnotations()), - newConstructor.getTypeParameters(), + newConstructor.getTypeParameters(), newConstructor.getParameters(), newConstructor.getThrows(), block); - newInnerClass = make.removeClassMember(newInnerClass, m); - genUtils.copyComments(m, newConstructor, true); - genUtils.copyComments(m, newConstructor, false); + newInnerClass = make.removeClassMember(newInnerClass, ctor); + genUtils.copyComments(ctor, newConstructor, true); + genUtils.copyComments(ctor, newConstructor, false); newInnerClass = genUtils.insertClassMember(newInnerClass, newConstructor); } } } } - + if(innerClass != newInnerClass) { genUtils.copyComments(innerClass, newInnerClass, true); genUtils.copyComments(innerClass, newInnerClass, false); @@ -620,6 +625,38 @@ private ClassTree refactorInnerClass(ClassTree innerClass) { return newInnerClass; } + private ClassTree refactorInnerRecord(ClassTree innerRecord) { + refactoring.setInnerIsRecord(true); + ClassTree newInnerRecord = innerRecord; + List members = newInnerRecord.getMembers(); + int insertAt=0; + for (Tree member : members) { + Tree.Kind kind = member.getKind(); +// System.out.println("member = " + kind + "" + member); + + switch (kind) { + // The fields, except the static, should loose private final modifiers + // so that they appear without these modifiers in the record header + case VARIABLE: + VariableTree vt = (VariableTree) member; + ModifiersTree mt = vt.getModifiers(); + if (mt.getFlags().contains(Modifier.STATIC)) { + continue; + } + Tree mtype = vt.getType(); + Name mName = vt.getName(); + ModifiersTree mods = make.Modifiers(EnumSet.noneOf(Modifier.class)); + VariableTree newMember = make.RecordComponent(mods, mName, mtype); + newInnerRecord = make.removeClassMember(newInnerRecord, member); + newInnerRecord = make.insertClassMember(newInnerRecord, insertAt, newMember); + insertAt++; + break; + default: + } + } + return newInnerRecord; + } + private boolean isThisInInner() { TreePath t=getCurrentPath(); Tree innerTree = workingCopy.getTrees().getTree(inner); diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaPluginUtils.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaPluginUtils.java index 5377f15eb677..a0eea7f238b1 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaPluginUtils.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaPluginUtils.java @@ -523,8 +523,11 @@ private static String guessLiteralName(String str) { public static CompilationUnitTree createCompilationUnit(FileObject sourceRoot, String relativePath, Tree typeDecl, WorkingCopy workingCopy, TreeMaker make) { GeneratorUtilities genUtils = GeneratorUtilities.get(workingCopy); CompilationUnitTree newCompilation; + ElementKind clzOrRecord = ElementKind.CLASS; + if (typeDecl.getKind()== Kind.RECORD) + clzOrRecord=ElementKind.RECORD; try { - newCompilation = genUtils.createFromTemplate(sourceRoot, relativePath, ElementKind.CLASS); + newCompilation = genUtils.createFromTemplate(sourceRoot, relativePath, clzOrRecord);//ElementKind.CLASS); List typeDecls = newCompilation.getTypeDecls(); if (typeDecls.isEmpty()) { newCompilation = make.addCompUnitTypeDecl(newCompilation, typeDecl); diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/spi/RefactoringVisitor.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/spi/RefactoringVisitor.java index 357fb4b64047..844f77025c9e 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/spi/RefactoringVisitor.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/spi/RefactoringVisitor.java @@ -183,6 +183,7 @@ public Tree scan(Tree tree, Element p) { case CLASS: case ENUM: case INTERFACE: + case RECORD: case VARIABLE: TreePath path = new TreePath(currentPath, tree); scanJavadoc(path, p); @@ -456,6 +457,7 @@ public DocTree visitVersion(VersionTree node, Element p) { return docScanner.visitVersion(node, p, null); } + /** * @since 1.47 */ diff --git a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/InnerOuterRecordTest.java b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/InnerOuterRecordTest.java new file mode 100644 index 000000000000..1f50164ae12d --- /dev/null +++ b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/InnerOuterRecordTest.java @@ -0,0 +1,598 @@ +/* + * 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.modules.refactoring.java.test; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.TreePath; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Name; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; +import org.netbeans.api.java.source.CompilationController; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.Task; +import org.netbeans.api.java.source.TreePathHandle; +import org.netbeans.modules.refactoring.api.Problem; +import org.netbeans.modules.refactoring.api.RefactoringSession; +import org.netbeans.modules.refactoring.java.api.InnerToOuterRefactoring; +import static org.netbeans.modules.refactoring.java.test.RefactoringTestBase.addAllProblems; +import org.openide.util.Exceptions; + +/** + * Test inner to outer refactoring for test. + * + * In the input files, and the expected outcomes, the indentation does not + * really matter as far as the tests are concerned because the indentation is + * stripped away before the remaining source lines are compared to the expected + * lines. + * + * @author homberghp {@code } + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class InnerOuterRecordTest extends RefactoringTestBase { + + public InnerOuterRecordTest(String name) { + super(name, "16"); + //ensure we are running on at least 16. + try { + SourceVersion.valueOf("RELEASE_16"); //NOI18N + } catch (IllegalArgumentException ex) { + //OK, no RELEASE_16, skip test + throw new RuntimeException("need at least Java 16 for record"); + } + } + + + public void test9ApacheNetbeans7044() throws Exception { + // initial outer has record with meaningful canonical constructor. + // note that Inner class should be in last member for assumptions in the test. + String source = + """ + package t; + import java.time.LocalDate; + import java.util.Objects; + public class A { + void useStudent() { + F s = new F(42,"Jan Klaassen", LocalDate.now().minusDays(1)); + System.out.println("student = " + s); + } + record F(int id, String name, LocalDate dob) { + /** + * Validate stuff. + */ + public F { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(dob); + assert !name.isEmpty() && !name.isBlank(); + assert dob.isAfter(LocalDate.EPOCH); + } + } + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + import java.util.Objects; + public class A { + void useStudent() { + F s = new F(42,"Jan Klaassen", LocalDate.now().minusDays(1)); + System.out.println("student = " + s); + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + import java.time.LocalDate; + import java.util.Objects; + /** + * + * @author junit + */ + record F(int id, String name, LocalDate dob) { + /** + * Validate stuff. + */ + public F { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(dob); + assert !name.isEmpty() && !name.isBlank(); + assert dob.isAfter(LocalDate.EPOCH); + } + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + public void test1BasicClassInClass() throws Exception { + // initial outer has record with meaningful canonical constructor. + String source = + """ + package t; + import java.time.LocalDate; + import java.util.Objects; + public class A { + void useStudent() { + F s = new F(42, "Jan Klaassen", LocalDate.now().minusDays(1)); + System.out.println("student = " + s); + } + public static class F { + int id; + String name; + LocalDate dob + public Student(int id, String name, LocalDate dob) { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(dob); + assert !name.isEmpty() && !name.isBlank(); + assert dob.isAfter(LocalDate.EPOCH); + this.id=id; + this.name=name; + this.dob=dob; + } + } + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + import java.util.Objects; + public class A { + void useStudent() { + F s = new F(42, "Jan Klaassen", LocalDate.now().minusDays(1)); + System.out.println("student = " + s); + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + import java.time.LocalDate; + import java.util.Objects; + /** + * + * @author junit + */ + public class F { + int id; + String name; + LocalDate dob; + public F(int id, String name, LocalDate dob) { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(dob); + assert !name.isEmpty() && !name.isBlank(); + assert dob.isAfter(LocalDate.EPOCH); + this.id = id; + this.name = name; + this.dob = dob; + } + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + public void test2BasicRecordInRecord() throws Exception { + String source = + """ + package t; + import java.time.LocalDate; + record A(int id, String name, LocalDate dob) { + static F f; + record F(int x, int y){ + /** I should be back. */ + static String code = "nix"; + } + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + record A(int id, String name, LocalDate dob) { + static F f; + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + /** + * + * @author hom + */ + record F(int x, int y) { + /** I should be back. */ + static String code = "nix"; + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + /** + * Test to verify what happens to the compact constructor in the outer + * record. It appears to survive the refactoring. + * + * @throws Exception + */ + public void test3OuterWithCompact() throws Exception { + String source = + """ + package t; + import java.time.LocalDate; + /** Record with compact ctor. */ + record A(F f){ + public A{ + assert f!=null; + } + record F(int id, String name, LocalDate dob){} + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + /** Record with compact ctor. */ + record A(F f){ + public A{ + assert f!=null; + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + import java.time.LocalDate; + /** + * + * @author junit + */ + record F(int id, String name, LocalDate dob) { + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + public void test4InnerWithCompact() throws Exception { + String source = + """ + package t; + import java.time.LocalDate; + record A(F f) { + public A { + assert f != null; + } + record F(int id, String name, LocalDate dob) { + public F { + if (dob.isBefore(LocalDate.EPOCH)) { + throw new IllegalArgumentException("to old " + dob); + } + } + } + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + record A(F f) { + public A { + assert f != null; + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + import java.time.LocalDate; + /** + * + * @author junit + */ + record F(int id, String name, LocalDate dob) { + public F { + if (dob.isBefore(LocalDate.EPOCH)) { + throw new IllegalArgumentException("to old " + dob); + } + } + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + // outer may have effect + public void test5ClassWithInnerRecord() throws Exception { + String source = + """ + package t; + import java.time.LocalDate; + class A { + final F f; + public A(F f) { + assert f != null; + this.f=f; + } + record F(int id, String name, LocalDate dob) { + public F { + if (dob.isBefore(LocalDate.EPOCH)) { + throw new IllegalArgumentException("to old " + dob); + } + } + } + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + class A { + final F f; + public A(F f) { + assert f != null; + this.f=f; + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + import java.time.LocalDate; + /** + * + * @author junit + */ + record F(int id, String name, LocalDate dob) { + public F { + if (dob.isBefore(LocalDate.EPOCH)) { + throw new IllegalArgumentException("to old " + dob); + } + } + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + public void test6InnerWithCompactAndMethodAndExtraCtor() throws Exception { + String source = + """ + package t; + import java.time.LocalDate; + record A(F f) { + enum Suite { + SPADE, CLUB, DIAMOND, HEART; + } + public A { + assert f != null; + } + record F(int id, String name, LocalDate dob) { + public F { + if (dob.isBefore(LocalDate.EPOCH)) { + throw new IllegalArgumentException("to old " + dob); + } + } + public F(int id, String name){ + this(id,name,LocalDate.now()); + } + boolean bornBefore(LocalDate someDate) { + return dob.isBefore(someDate); + } + } + } + """; + String newOuter = + """ + package t; + import java.time.LocalDate; + record A(F f) { + enum Suite { + SPADE, CLUB, DIAMOND, HEART; + } + public A { + assert f != null; + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + import java.time.LocalDate; + /** + * + * @author junit + */ + record F(int id, String name, LocalDate dob) { + public F { + if (dob.isBefore(LocalDate.EPOCH)) { + throw new IllegalArgumentException("to old " + dob); + } + } + public F(int id, String name) { + this(id, name, LocalDate.now()); + } + boolean bornBefore(LocalDate someDate) { + return dob.isBefore(someDate); + } + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + public void test7Generic() throws Exception { + String source = + """ + package t; + record A(F f) { + public A { + assert f != null; + } + record F(P first, Q second) { + public F { + assert null != first; + assert null != second; + } + } + } + """; + String newOuter = + """ + package t; + record A(F f) { + public A { + assert f != null; + } + } + """; + String newInner = + """ + /* + * Refactoring License + */ + package t; + /** + * + * @author junit + */ + record F(P first, Q second) { + public F { + assert null != first; + assert null != second; + } + } + """; + innerOuterSetupAndTest(source, newOuter, newInner); + } + + void innerOuterSetupAndTest(String source, String newOuterName, String newInnerName) throws Exception { + writeFilesNoIndexing(src, new File("t/A.java", source)); + performInnerToOuterTest2(null); + verifyContent(src, new File("t/A.java", newOuterName), new File("t/F.java", newInnerName)); + } + boolean debug = false; + + // variant for record inner to outer test + private void performInnerToOuterTest2(String newOuterName, Problem... expectedProblems) throws Exception { + final InnerToOuterRefactoring[] r = new InnerToOuterRefactoring[1]; + JavaSource.forFileObject(src.getFileObject("t/A.java")).runUserActionTask(new Task() { + @Override + public void run(CompilationController parameter) { + try { + parameter.toPhase(JavaSource.Phase.RESOLVED); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + CompilationUnitTree cut = parameter.getCompilationUnit(); + if (debug) { + System.err.println("cut is of type " + cut.getClass().getCanonicalName()); + } + ClassTree outer = (ClassTree) cut.getTypeDecls().get(0); + if (debug) { + printNumbered(System.err, "start source " + outer.getKind().toString(), outer.toString()); + } + List members = outer.getMembers(); + int m = 0; + if (debug) { + printMembers(members, m); + } + // selecting the last element assumes that the inner class is the last member in the outer class. + Tree lastInnerClass = + outer.getMembers().get(outer.getMembers().size() - 1); + if (debug && lastInnerClass instanceof ClassTree lct) { +// String n = "lastInnerClass " + lastInnerClass.getKind().toString(); +// printNumbered(System.err, n, lastInnerClass.toString()); + printClassTree(lct); + } + TreePath tp = TreePath.getPath(cut, lastInnerClass); + try { + r[0] = + new InnerToOuterRefactoring(TreePathHandle.create(tp, parameter)); + } catch (Throwable t) { + System.err.println("InnerOuter refatoring failed with exception " + t); + t.printStackTrace(System.out); + throw t; + } + } + }, true); + r[0].setClassName("F"); + if (debug) { + printNumbered(System.err, "result ", r[0].toString()); + } + r[0].setReferenceName(newOuterName); + RefactoringSession rs = RefactoringSession.create("Session"); + List problems = new LinkedList(); + addAllProblems(problems, r[0].preCheck()); + addAllProblems(problems, r[0].prepare(rs)); + addAllProblems(problems, rs.doRefactoring(true)); + assertProblems(Arrays.asList(expectedProblems), problems); + } + + // test helper + static void printMembers(List members, int m) { + printMembers(members, m, ""); + } + + // test helper + static void printMembers(List members, int m, String indent) { + for (Tree member : members) { + printNumbered(System.err, indent + "member %d %15s".formatted(m, member.getKind()), member.toString()); + String toString = member.toString(); + if (member instanceof ClassTree ct) { + int n = 0; + Name simpleName = ct.getSimpleName(); + List members1 = ct.getMembers(); + printMembers(members1, n, indent + " " + m + " "); + } + m++; + } + } + + // test helper + static void printClassTree(ClassTree ct) { + printMembers(ct.getMembers(), 0, "class " + ct.getSimpleName() + " type " + ct.getKind() + " "); + } +} diff --git a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RefactoringTestBase.java b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RefactoringTestBase.java index ebc261b08126..abe06b46cd6e 100644 --- a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RefactoringTestBase.java +++ b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RefactoringTestBase.java @@ -21,6 +21,7 @@ import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; +import java.io.PrintStream; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.event.ChangeListener; @@ -84,7 +86,38 @@ public RefactoringTestBase(String name, String sourcelevel) { this.sourcelevel = sourcelevel; } + static boolean debug = false; +// static boolean skipIndexing = false; + + /** + * Write given files to sourceRoot and fully re index. + * + * First the (file) children of sourceRoot are deleted. This method can take + * a substantial time of your patience, so use wisely. See the doc in + * {@link IndexManager#refreshIndexAndWait} + * + * @param sourceRoot sic + * @param files to save + * @throws Exception whenever + */ protected static void writeFilesAndWaitForScan(FileObject sourceRoot, File... files) throws Exception { + writeFilesAndWaitForScan(true, sourceRoot, files); + } + + /** + * Write given files to sourceRoot and possibly reindex. + * + * First the (file) children of sourceRoot are deleted. This method can take + * a substantial time of your patience, so use wisely. See the doc in + * {@link IndexManager#refreshIndexAndWait} + * + * @param fulleIndex fully reindex the type repo + * @param sourceRoot sic + * @param files to save + * @throws Exception whenever + */ + protected static void writeFilesAndWaitForScan(boolean fullIndex, FileObject sourceRoot, File... files) throws Exception { + long currentTimeMillis = System.currentTimeMillis(); for (FileObject c : sourceRoot.getChildren()) { c.delete(); } @@ -93,10 +126,39 @@ protected static void writeFilesAndWaitForScan(FileObject sourceRoot, File... fi FileObject fo = FileUtil.createData(sourceRoot, f.filename); TestUtilities.copyStringToFile(fo, f.content); } + + if (fullIndex) { + IndexingManager.getDefault().refreshIndexAndWait(sourceRoot.toURL(), null, true); + long currentTimeMillis1 = System.currentTimeMillis(); + if (debug) { + System.err.println("writeFilesAndWaitForScan took " + (currentTimeMillis1 - currentTimeMillis) + " millis"); + } + } + } - IndexingManager.getDefault().refreshIndexAndWait(sourceRoot.toURL(), null, true); + /** + * Save file but do not reindex. + * + * Deletes the existing files under sourceRoot, then saves the given files. + * + * Makes tests run faster. In particular single tests. + * + * @param sourceRoot sic + * @param files to save + * @throws Exception whenever + */ + protected static void writeFilesNoIndexing(FileObject sourceRoot, File... files) throws Exception { + writeFilesAndWaitForScan(false, sourceRoot, files); } + /** + * Verify that the given file(names) are present in the sourceRoot and that + * the files in said sourceRoot are equal to the given files. + * + * @param sourceRoot to contain generated (refactored) files + * @param files expected files + * @throws Exception well why not? + */ protected void verifyContent(FileObject sourceRoot, File... files) throws Exception { List todo = new LinkedList(); @@ -109,33 +171,30 @@ protected void verifyContent(FileObject sourceRoot, File... files) throws Except while (!todo.isEmpty()) { FileObject file = todo.remove(0); - if (file.isData()) { + if (file.isData()) { // normal file content.put(FileUtil.getRelativePath(sourceRoot, file), copyFileToString(FileUtil.toFile(file))); - } else { + } else { // it is a folder todo.addAll(Arrays.asList(file.getChildren())); } } - + // only do full line compare for InnerOuterRecorTest to not break exsiting tests, which make different assumptions. + boolean fullLineCompare = this.getClass() == InnerOuterRecordTest.class; + Throwable exception = null; for (File f : files) { + // take the element from the map filled by sourceRootTraversal. String fileContent = content.remove(f.filename); - assertNotNull(f); assertNotNull(f.content); - assertNotNull("Cannot find " + f.filename + " in map " + content, fileContent); - try { - assertEquals(getName() ,f.content.replaceAll("[ \t\r\n\n]+", " "), fileContent.replaceAll("[ \t\r\n\n]+", " ")); - } catch (Throwable t) { - System.err.println("expected:"); - System.err.println(f.content); - System.err.println("actual:"); - System.err.println(fileContent); - throw t; + assertNotNull("Cannot find expected " + f.filename + " in map filled by sourceRoot " + content, fileContent); + if (fullLineCompare) { + assertLinesEqual2(f.filename, f.content, fileContent); + } else { // original tests. + assertLinesEqual1(f.content, fileContent); } } - - assertTrue(content.toString(), content.isEmpty()); + assertTrue("not all files processeed", content.isEmpty()); } - + /** * Returns a string which contains the contents of a file. * @@ -366,10 +425,10 @@ private void prepareTest() throws Exception { src = FileUtil.createFolder(projectFolder, "src"); test = FileUtil.createFolder(projectFolder, "test"); - FileObject cache = FileUtil.createFolder(workdir, "cache"); + FileObject cache = FileUtil.createFolder(workdir, "cache"); - CacheFolder.setCacheFolder(cache); - } + CacheFolder.setCacheFolder(cache); + } @ServiceProvider(service=MimeDataProvider.class) public static final class MimeDataProviderImpl implements MimeDataProvider { @@ -384,7 +443,7 @@ public Lookup getLookup(MimePath mimePath) { return null; } - + } protected static boolean problemIsFatal(List problems) { @@ -411,10 +470,90 @@ protected void runTest() throws Throwable { super.runTest(); return; } catch (Throwable t) { - if (exc == null) exc = t; + if (exc == null) { + exc = t; + } } } throw exc; } -} \ No newline at end of file + /** + * Prints a source by splitting on the line breaks and prefixing with name + * and line number. + * + * @param out the stream to print to + * @param name the name as prefix to each line + * @param source the source code to print to the out stream. + */ + public static void printNumbered(final PrintStream out, final String name, String source) { + AtomicInteger c = new AtomicInteger(1); + source.trim().lines().forEach(l -> out.println("%s [%4d] %s".formatted(name, c.getAndIncrement(), l))); + } + + /** + * Compare strings by replacing all multiples of whitrespace([ \t\n\r]) with + * a space. + * + * The test programmer chooses this to make it easier to write the input and + * the expected strings. + * + * @param expected to compare + * @param actual to compare + */ + public void assertLinesEqual1(String expected, String actual) { + try { + assertEquals(getName(), expected.replaceAll("[ \t\r\n\n]+", " "), actual.replaceAll("[ \t\r\n\n]+", " ")); + } catch (Throwable t) { + System.err.println("expected:"); + System.err.println(expected); + System.err.println("actual:"); + System.err.println(actual); + throw t; + } + } + + /** + * Compare strings by splitting them into lines, remove empty lines, and trim white + * space. + * + * Only when any of the lines differ, all lines are printed with the unequal + * lines flagged. + * + * Before the lines are compared, they are trimmed and the white space is + * normalized by collapsing multiple white space characters into one. This + * should make the tests less brittle. + * + * If any of the compared lines are unequal, this test fails and the + * comparison result is shown on stderr in a simplified diff format. + * + * @param expected to compare + * @param actual to compare + */ + public void assertLinesEqual2(String name, String expected, String actual) { + expected = expected.trim().replaceAll("([ \t\r\n])\\1+", "$1"); + actual = actual.trim().replaceAll("([ \t\r\n])\\1+", "$1"); + String[] linesExpected = expected.lines().filter(l-> !l.isEmpty()).toArray(String[]::new); + String[] linesActual = actual.lines().filter(l-> !l.isEmpty()).toArray(String[]::new); + int limit = Math.max(linesExpected.length, linesActual.length); + StringBuilder sb = new StringBuilder(); + boolean equals = true; + for (int i = 0; i < limit; i++) { + String e = (i < linesExpected.length ? linesExpected[i] : "").trim(); + String a = (i < linesActual.length ? linesActual[i] : "").trim(); + // somehow my user is inserted, so avoid to test those lines. + if (e.contains("@author") && a.contains("@author")){ + e=a="* @author goes here"; + } + boolean same = e.equals(a); + String sep = same ? " " : " | "; + equals &= same; + sb.append(String.format(name + " [%3d] %-80s%s%-80s%n", i, e, sep, a)); + } + if (!equals) { + System.err.println("test " + getName() + " failed"); + System.err.println(sb.toString()); + fail("lines differ, see stderr for more details."); + } + } +} diff --git a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RenameRecordTest.java b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RenameRecordTest.java index 43fbba7faa72..da6e5ae7f020 100644 --- a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RenameRecordTest.java +++ b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/RenameRecordTest.java @@ -344,7 +344,7 @@ public void testRenameRecord() throws Exception { package test; public record Te|st(int component) { public Test { - component = ""; + component = 42; } } """; @@ -367,7 +367,7 @@ private Test test() { package test; public record NewName(int component) { public NewName { - component = ""; + component = 42; } } """), @@ -381,6 +381,38 @@ private NewName test() { } """)); + } + + public void testRenameInnerRecord() throws Exception { + String testCode = """ + package test; + public record Test(Component component) { + public Test { + assert component !=null; + } + record Comp|onent(int c){ + assert c >= 0; + } + } + """; + TestInput splitCode = TestUtilities.splitCodeAndPos(testCode); + writeFilesAndWaitForScan(src, + new File("Test.java", splitCode.code())); + JavaRenameProperties props = new JavaRenameProperties(); + performRename(src.getFileObject("Test.java"), splitCode.pos(), "Part", props, true); + verifyContent(src, new File("Test.java", + """ + package test; + public record Test(Part component) { + public Test { + assert component !=null; + } + record Part(int c){ + assert c >= 0; + } + } + """)); + } private void performRename(FileObject source, final int absPos, final String newname, final JavaRenameProperties props, final boolean searchInComments, Problem... expectedProblems) throws Exception { final RenameRefactoring[] r = new RenameRefactoring[1];