diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaParserVisitor.java b/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaParserVisitor.java index 4111b5861d0..99310b248ff 100644 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaParserVisitor.java +++ b/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaParserVisitor.java @@ -73,16 +73,16 @@ public S.CompilationUnit visitCompilationUnit(ScalaParseResult parseResult) { CompilationUnitResult result = converter.convertToCompilationUnit(parseResult, source, typeFactory); J.Package packageDecl = result.getPackageDecl(); - List statements = result.getStatements(); + List> statements = result.getStatements(); // Filter out any Unknown statements that contain the entire source with package if (packageDecl != null) { final String packageName = packageDecl.getPackageName(); statements = statements.stream() - .filter(stmt -> { - if (stmt instanceof J.Unknown) { - String text = ((J.Unknown) stmt).getSource().getText().trim(); + .filter(rp -> { + if (rp.getElement() instanceof J.Unknown) { + String text = ((J.Unknown) rp.getElement()).getSource().getText().trim(); // Skip if this Unknown contains the same package declaration boolean shouldFilter = text.startsWith("package " + packageName); return !shouldFilter; @@ -117,12 +117,14 @@ public S.CompilationUnit visitCompilationUnit(ScalaParseResult parseResult) { unknownSource ); - statements.add(unknown); + statements.add(JRightPadded.build(unknown)); } // Get remaining source for EOF String remainingSource = converter.getRemainingSource(parseResult, source, result.getLastCursorPosition()); - Space eof = remainingSource.isEmpty() ? EMPTY : Space.build(remainingSource, Collections.emptyList()); + // Trailing source can hold comments, which belong in Space.comments rather than + // its whitespace + Space eof = remainingSource.isEmpty() ? EMPTY : ScalaSpace.format(remainingSource); // Build S.CompilationUnit return new S.CompilationUnit( @@ -135,7 +137,7 @@ public S.CompilationUnit visitCompilationUnit(ScalaParseResult parseResult) { charsetBomMarked, // boolean charsetBomMarked null, // Checksum checksum packageDecl == null ? null : JRightPadded.build(packageDecl), - JRightPadded.withElements(Collections.emptyList(), statements), + statements, eof // Space eof ); } diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaPrinter.java b/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaPrinter.java index 74ea6d02660..6d1f6696962 100644 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaPrinter.java +++ b/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaPrinter.java @@ -21,6 +21,7 @@ import org.openrewrite.Tree; import org.openrewrite.java.JavaPrinter; import org.openrewrite.java.marker.ImplicitReturn; +import org.openrewrite.marker.Markers; import org.openrewrite.java.marker.OmitParentheses; import org.openrewrite.java.marker.Quoted; import org.openrewrite.java.tree.Expression; @@ -33,7 +34,6 @@ import org.openrewrite.java.tree.TypeTree; import org.openrewrite.marker.Marker; import org.openrewrite.scala.marker.AmpersandIntersection; -import org.openrewrite.scala.marker.AsInstanceOfPrefix; import org.openrewrite.scala.marker.BlockArgument; import org.openrewrite.scala.marker.DottedMatch; import org.openrewrite.scala.marker.Implicit; @@ -42,9 +42,11 @@ import org.openrewrite.scala.marker.SObject; import org.openrewrite.scala.marker.Semicolon; import org.openrewrite.scala.marker.TrailingComma; +import org.openrewrite.scala.marker.CasePattern; +import org.openrewrite.scala.marker.DoKeyword; +import org.openrewrite.scala.marker.ThenKeyword; import org.openrewrite.scala.marker.TypeProjection; import org.openrewrite.scala.marker.ScalaForLoop; -import org.openrewrite.scala.marker.TypeAscription; import org.openrewrite.scala.marker.PartialFunctionLiteral; import org.openrewrite.scala.marker.ContextFunctionArrow; import org.openrewrite.scala.marker.UnderscorePlaceholderLambda; @@ -141,7 +143,14 @@ public J visitTypeParameter(J.TypeParameter typeParam, PrintOutputCapture

p) // Print type parameter, but bounds use Scala syntax beforeSyntax(typeParam, Space.Location.TYPE_PARAMETERS_PREFIX, p); visit(typeParam.getAnnotations(), p); + // Variance binds directly to the name + for (J.Modifier m : typeParam.getModifiers()) { + visitSpace(m.getPrefix(), Space.Location.MODIFIER_PREFIX, p); + p.append(m.getKeyword()); + } visit(typeParam.getName(), p); + typeParam.getMarkers().findFirst(org.openrewrite.scala.marker.TypeParameterBounds.class) + .ifPresent(m -> p.append(m.text())); // Print bounds if present using Scala syntax. // Each bound element may be a J.TypeBound (with explicit Kind) or a plain @@ -253,7 +262,17 @@ public J visitAssignmentOperation(J.AssignmentOperation assignOp, PrintOutputCap @Override public J visitWhileLoop(J.WhileLoop whileLoop, PrintOutputCapture

p) { if (!whileLoop.getMarkers().findFirst(IndentedSyntax.class).isPresent()) { - return super.visitWhileLoop(whileLoop, p); + Optional doKeyword = whileLoop.getMarkers().findFirst(DoKeyword.class); + if (!doKeyword.isPresent()) { + return super.visitWhileLoop(whileLoop, p); + } + beforeSyntax(whileLoop, Space.Location.WHILE_PREFIX, p); + p.append("while"); + visit(whileLoop.getCondition(), p); + p.append(doKeyword.get().text()); + visit(whileLoop.getBody(), p); + afterSyntax(whileLoop, p); + return whileLoop; } // Scala 3 paren-less form: `while cond do body` beforeSyntax(whileLoop, Space.Location.WHILE_PREFIX, p); @@ -272,7 +291,20 @@ public J visitWhileLoop(J.WhileLoop whileLoop, PrintOutputCapture

p) { @Override public J visitIf(J.If iff, PrintOutputCapture

p) { if (!iff.getMarkers().findFirst(IndentedSyntax.class).isPresent()) { - return super.visitIf(iff, p); + Optional then = iff.getMarkers().findFirst(ThenKeyword.class); + if (!then.isPresent()) { + return super.visitIf(iff, p); + } + beforeSyntax(iff, Space.Location.IF_PREFIX, p); + p.append("if"); + visit(iff.getIfCondition(), p); + p.append(then.get().text()); + visit(iff.getThenPart(), p); + if (iff.getElsePart() != null) { + visit(iff.getElsePart(), p); + } + afterSyntax(iff, p); + return iff; } // Scala 3 paren-less form: `if cond then thenp [else elsep]` beforeSyntax(iff, Space.Location.IF_PREFIX, p); @@ -483,10 +515,19 @@ public J visitMethodDeclaration(J.MethodDeclaration method, PrintOutputCapture

p) { return visitFunctionCall((S.FunctionCall) tree, p); } else if (tree instanceof S.ConstructorInvocation) { return visitConstructorInvocation((S.ConstructorInvocation) tree, p); + } else if (tree instanceof S.LiteralType) { + return visitLiteralType((S.LiteralType) tree, p); } else if (tree instanceof S.SingletonType) { return visitSingletonType((S.SingletonType) tree, p); } else if (tree instanceof S.RepeatedType) { @@ -762,9 +815,12 @@ public J visitScalaCompilationUnit(S.CompilationUnit scu, PrintOutputCapture

visit(scu.getPackageDeclaration(), p); } - for (int i = 0; i < scu.getStatements().size(); i++) { - Statement statement = scu.getStatements().get(i); - visit(statement, p); + for (JRightPadded rp : scu.getPadding().getStatements()) { + visit(rp.getElement(), p); + visitSpace(rp.getAfter(), Space.Location.LANGUAGE_EXTENSION, p); + if (rp.getMarkers().findFirst(Semicolon.class).isPresent()) { + p.append(';'); + } } visitSpace(scu.getEof(), Space.Location.COMPILATION_UNIT_EOF, p); @@ -961,6 +1017,12 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, PrintOutputCapture< // J.VariableDeclarations modeled like a Scala parameter (no implicit val/var, // type comes after the name with `:`). We can't fall through to visitVariableDeclarations // because that one is for field/local declarations and always emits a val/var keyword. + if (classDecl.getPadding().getPrimaryConstructor() != null) { + // The modifier can stand alone, without a parameter list + classDecl.getPadding().getPrimaryConstructor().getMarkers() + .findFirst(org.openrewrite.scala.marker.ConstructorModifier.class) + .ifPresent(m -> p.append(m.text())); + } if (classDecl.getPadding().getPrimaryConstructor() != null && !classDecl.getPadding().getPrimaryConstructor().getMarkers().findFirst(OmitParentheses.class).isPresent()) { JContainer primaryConstructor = classDecl.getPadding().getPrimaryConstructor(); @@ -973,11 +1035,18 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, PrintOutputCapture< if (element instanceof J.VariableDeclarations) { J.VariableDeclarations varDecl = (J.VariableDeclarations) element; visitSpace(varDecl.getPrefix(), Space.Location.VARIABLE_DECLARATIONS_PREFIX, p); + for (J.Modifier m : varDecl.getModifiers()) { + if (isClauseKeyword(m)) { + visit(m, p); + } + } visit(varDecl.getLeadingAnnotations(), p); // Print modifiers as-is: includes `val`/`var`/`private`/etc. when present // on a class constructor param; absent for plain `(name: T)` form. for (J.Modifier m : varDecl.getModifiers()) { - visit(m, p); + if (!isClauseKeyword(m)) { + visit(m, p); + } } boolean omitParamName = !varDecl.getVariables().isEmpty() && varDecl.getVariables().get(0).getMarkers().findFirst( @@ -1020,36 +1089,21 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, PrintOutputCapture< } if (classDecl.getPadding().getImplements() != null) { - // In Scala, implements are printed with "with" keyword - // The container already has the proper space before the first keyword - - String firstKeyword = ""; - String separator = ""; - - if (classDecl.getPadding().getExtends() != null) { - // If we have extends, traits use "with" - firstKeyword = "with"; - separator = "with"; - } else { - // If no extends, first trait uses "extends" - firstKeyword = "extends"; - separator = "with"; - } - // Custom handling for Scala traits JContainer implContainer = classDecl.getPadding().getImplements(); - visitSpace(implContainer.getBefore(), Space.Location.IMPLEMENTS, p); - p.append(firstKeyword); - List> elements = implContainer.getPadding().getElements(); + + visitSpace(implContainer.getBefore(), Space.Location.IMPLEMENTS, p); + // Without an `extends` clause the first parent carries the `extends` keyword + p.append(classDecl.getPadding().getExtends() == null ? "extends" : parentSeparator(elements.get(0))); + for (int i = 0; i < elements.size(); i++) { JRightPadded elem = elements.get(i); visit(elem.getElement(), p); - + if (i < elements.size() - 1) { - // Print space after element and the separator visitSpace(elem.getAfter(), Space.Location.IMPLEMENTS_SUFFIX, p); - p.append(separator); + p.append(parentSeparator(elements.get(i + 1))); } } } @@ -1058,6 +1112,8 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, PrintOutputCapture< visitContainer(" permits", classDecl.getPadding().getPermits(), JContainer.Location.PERMITS, ",", "", p); } + classDecl.getMarkers().findFirst(org.openrewrite.scala.marker.DerivesClause.class) + .ifPresent(m -> p.append(m.text())); visit(classDecl.getBody(), p); afterSyntax(classDecl, p); return classDecl; @@ -1068,6 +1124,39 @@ public J visitClassDeclaration(J.ClassDeclaration classDecl, PrintOutputCapture< } } + @Override + protected void beforeSyntax(Space prefix, Markers markers, Space.@Nullable Location loc, PrintOutputCapture

p) { + // `f(using ctx)`: the keyword opens the argument list, ahead of the first argument + markers.findFirst(org.openrewrite.scala.marker.UsingArguments.class) + .ifPresent(m -> p.append(m.text())); + super.beforeSyntax(prefix, markers, loc, p); + // Scala 3 `inline if`: the keyword precedes the expression it modifies + markers.findFirst(org.openrewrite.scala.marker.InlineKeyword.class) + .ifPresent(m -> p.append(m.text())); + } + + @Override + protected void afterSyntax(J j, PrintOutputCapture

p) { + // A Scala 3 end marker closes the element it is attached to, after its body + j.getMarkers().findFirst(org.openrewrite.scala.marker.EndMarker.class) + .ifPresent(m -> p.append(m.text())); + j.getMarkers().findFirst(org.openrewrite.scala.marker.CaptureSet.class) + .ifPresent(m -> p.append(m.text())); + super.afterSyntax(j, p); + } + + /** `using`/`implicit` open a whole parameter clause, so they print ahead of any + * parameter annotations rather than with the other modifiers. */ + private static boolean isClauseKeyword(J.Modifier modifier) { + return "using".equals(modifier.getKeyword()) || "implicit".equals(modifier.getKeyword()); + } + + private static String parentSeparator(JRightPadded parent) { + return parent.getMarkers().findFirst(org.openrewrite.scala.marker.ParentSeparator.class) + .map(org.openrewrite.scala.marker.ParentSeparator::text) + .orElse("with"); + } + private void visitTypeParameters(@Nullable JContainer typeParams, PrintOutputCapture

p) { if (typeParams != null && !typeParams.getElements().isEmpty()) { // In Scala, type parameters use square brackets, not angle brackets @@ -1084,10 +1173,30 @@ private void visitTypeParameters(@Nullable JContainer typeParam @Override public J visitBlock(J.Block block, PrintOutputCapture

p) { + String selfType = block.getMarkers() + .findFirst(org.openrewrite.scala.marker.SelfType.class) + .map(org.openrewrite.scala.marker.SelfType::text) + .orElse(null); + if (selfType != null && + !block.getMarkers().findFirst(org.openrewrite.scala.marker.OmitBraces.class).isPresent() && + !block.getMarkers().findFirst(IndentedSyntax.class).isPresent()) { + // Braced body: the clause follows the `{` + beforeSyntax(block, Space.Location.BLOCK_PREFIX, p); + p.append('{'); + p.append(selfType); + visitStatements(block.getPadding().getStatements(), JRightPadded.Location.BLOCK_STATEMENT, p); + visitSpace(block.getEnd(), Space.Location.BLOCK_END, p); + p.append('}'); + afterSyntax(block, p); + return block; + } // OmitBraces blocks print statements without { } — used for braceless bodies, // synthetic lambda body blocks, and expression-position blocks if (block.getMarkers().findFirst(org.openrewrite.scala.marker.OmitBraces.class).isPresent()) { beforeSyntax(block, Space.Location.BLOCK_PREFIX, p); + if (selfType != null) { + p.append(selfType); + } visitStatements(block.getPadding().getStatements(), JRightPadded.Location.BLOCK_STATEMENT, p); visitSpace(block.getEnd(), Space.Location.BLOCK_END, p); afterSyntax(block, p); @@ -1097,6 +1206,9 @@ public J visitBlock(J.Block block, PrintOutputCapture

p) { if (block.getMarkers().findFirst(IndentedSyntax.class).isPresent()) { beforeSyntax(block, Space.Location.BLOCK_PREFIX, p); p.append(':'); + if (selfType != null) { + p.append(selfType); + } visitStatements(block.getPadding().getStatements(), JRightPadded.Location.BLOCK_STATEMENT, p); visitSpace(block.getEnd(), Space.Location.BLOCK_END, p); afterSyntax(block, p); @@ -1151,6 +1263,7 @@ public J visitForEachLoop(J.ForEachLoop forEachLoop, PrintOutputCapture

p) { visit(iterable.getElement(), p); visitSpace(iterable.getAfter(), JRightPadded.Location.FOREACH_ITERABLE.getAfterLocation(), p); p.append(')'); + forEachLoop.getMarkers().findFirst(DoKeyword.class).ifPresent(m -> p.append(m.text())); // Print the body visitStatement(forEachLoop.getPadding().getBody(), JRightPadded.Location.FOR_BODY, p); @@ -1204,13 +1317,10 @@ public J visitConstructorInvocation(S.ConstructorInvocation ci, PrintOutputCaptu @Override public J visitTypeCast(J.TypeCast typeCast, PrintOutputCapture

p) { - // asInstanceOf handling + // The parser builds a J.MethodInvocation for `asInstanceOf`; a J.TypeCast reaches + // here only from a recipe, and Scala spells a cast this way. beforeSyntax(typeCast, Space.Location.TYPE_CAST_PREFIX, p); visit(typeCast.getExpression(), p); - Optional asInstanceOfPrefix = typeCast.getMarkers().findFirst(AsInstanceOfPrefix.class); - if (asInstanceOfPrefix.isPresent()) { - visitSpace(asInstanceOfPrefix.get().getPrefix(), Space.Location.LANGUAGE_EXTENSION, p); - } p.append(".asInstanceOf"); if (typeCast.getClazz() instanceof J.ControlParentheses) { J.ControlParentheses controlParens = (J.ControlParentheses) typeCast.getClazz(); @@ -1265,6 +1375,11 @@ public J visitVariableDeclarations(J.VariableDeclarations multiVariable, PrintOu } } + // An explicit `final` does not decide the keyword: `final var` is legal + if (multiVariable.getMarkers().findFirst(org.openrewrite.scala.marker.VarKeyword.class).isPresent()) { + valVarKeyword = "var"; + } + // Print val/var/given (unless it's a lambda parameter) if (!isLambdaParam) { if (valVarPrefix != null) { @@ -1736,6 +1851,8 @@ public J visitTuplePattern(S.TuplePattern tuplePattern, PrintOutputCapture

p) public J visitWildcard(S.Wildcard wildcard, PrintOutputCapture

p) { beforeSyntax(wildcard, Space.Location.LANGUAGE_EXTENSION, p); + wildcard.getMarkers().findFirst(org.openrewrite.scala.marker.KindParameterVariance.class) + .ifPresent(m -> p.append(m.text())); p.append('_'); afterSyntax(wildcard, p); return wildcard; @@ -1848,6 +1965,13 @@ public J visitAnonymousGiven(S.AnonymousGiven g, PrintOutputCapture

p) { return g; } + public J visitLiteralType(S.LiteralType literalType, PrintOutputCapture

p) { + beforeSyntax(literalType, Space.Location.LANGUAGE_EXTENSION, p); + visit(literalType.getLiteral(), p); + afterSyntax(literalType, p); + return literalType; + } + public J visitSingletonType(S.SingletonType singletonType, PrintOutputCapture

p) { beforeSyntax(singletonType, Space.Location.LANGUAGE_EXTENSION, p); visit(singletonType.getQualifier(), p); @@ -1993,6 +2117,9 @@ public J visitFunctionType(S.FunctionType functionType, PrintOutputCapture

p) visitSpace(rt.getBefore(), Space.Location.LANGUAGE_EXTENSION, p); if (functionType.getMarkers().findFirst(ContextFunctionArrow.class).isPresent()) { p.append("?=>"); + } else if (functionType.getMarkers() + .findFirst(org.openrewrite.scala.marker.PureFunctionArrow.class).isPresent()) { + p.append("->"); } else { p.append("=>"); } @@ -2072,8 +2199,17 @@ public J visitExtensionMethods(S.ExtensionMethods ext, PrintOutputCapture

p) if (element instanceof J.VariableDeclarations) { J.VariableDeclarations varDecl = (J.VariableDeclarations) element; visitSpace(varDecl.getPrefix(), Space.Location.VARIABLE_DECLARATIONS_PREFIX, p); + for (J.Modifier m : varDecl.getModifiers()) { + if (isClauseKeyword(m)) { + visit(m, p); + } + } visit(varDecl.getLeadingAnnotations(), p); - visit(varDecl.getModifiers(), p); + for (J.Modifier m : varDecl.getModifiers()) { + if (!isClauseKeyword(m)) { + visit(m, p); + } + } if (!varDecl.getVariables().isEmpty()) { visit(varDecl.getVariables().get(0).getName(), p); } @@ -2127,10 +2263,13 @@ public J visitFor(S.For forLoop, PrintOutputCapture

p) { p.append(close); } visitSpace(forLoop.getBeforeBody(), Space.Location.LANGUAGE_EXTENSION, p); + Optional doKeyword = forLoop.getMarkers().findFirst(DoKeyword.class); if (forLoop.isYielding()) { p.append("yield"); } else if (parenless) { p.append("do"); + } else if (doKeyword.isPresent()) { + p.append(doKeyword.get().text()); } visit(forLoop.getBody(), p); afterSyntax(forLoop, p); @@ -2139,6 +2278,9 @@ public J visitFor(S.For forLoop, PrintOutputCapture

p) { public J visitForEnumerator(S.For.Enumerator enumerator, PrintOutputCapture

p) { beforeSyntax(enumerator.getPrefix(), enumerator.getMarkers(), Space.Location.LANGUAGE_EXTENSION, p); + if (enumerator.getMarkers().findFirst(CasePattern.class).isPresent()) { + p.append("case"); + } switch (enumerator.getKind()) { case Generator: if (enumerator.getLhs() != null) visit(enumerator.getLhs(), p); diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaVisitor.java b/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaVisitor.java index bb44545d812..231941140b2 100644 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaVisitor.java +++ b/rewrite-scala/src/main/java/org/openrewrite/scala/ScalaVisitor.java @@ -246,6 +246,14 @@ public J visitConstructorInvocation(S.ConstructorInvocation constructorInvocatio return c; } + public J visitLiteralType(S.LiteralType literalType, P p) { + S.LiteralType l = literalType; + l = l.withPrefix(visitSpace(l.getPrefix(), Space.Location.LANGUAGE_EXTENSION, p)); + l = l.withMarkers(visitMarkers(l.getMarkers(), p)); + l = l.withLiteral(visitAndCast(l.getLiteral(), p)); + return l; + } + public J visitSingletonType(S.SingletonType singletonType, P p) { S.SingletonType s = singletonType; s = s.withPrefix(visitSpace(s.getPrefix(), Space.Location.LANGUAGE_EXTENSION, p)); diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/internal/ScalaSpace.java b/rewrite-scala/src/main/java/org/openrewrite/scala/internal/ScalaSpace.java index cd657c85d45..44a930e5574 100644 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/internal/ScalaSpace.java +++ b/rewrite-scala/src/main/java/org/openrewrite/scala/internal/ScalaSpace.java @@ -109,8 +109,11 @@ public static Space format(String formatting, int beginIndex, int toIndex) { } else if (last == '/' && blockDepth > 0) { blockDepth++; comment.append(c); // the '/' is already in the comment buffer - } else { + } else if (blockDepth > 0) { comment.append(c); + } else { + // A bare `*` outside a comment, as in a `import a.*` wildcard + prefix.append(c); } break; default: diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/marker/AsInstanceOfPrefix.java b/rewrite-scala/src/main/java/org/openrewrite/scala/marker/AsInstanceOfPrefix.java deleted file mode 100644 index a9129c45650..00000000000 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/marker/AsInstanceOfPrefix.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2026 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * 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.openrewrite.scala.marker; - -import lombok.Value; -import lombok.With; -import org.openrewrite.Tree; -import org.openrewrite.java.tree.Space; -import org.openrewrite.marker.Marker; - -import java.util.UUID; - -/** - * Stores the whitespace that appears before {@code .asInstanceOf} in Scala - * constructs like {@code expr.asInstanceOf[Type]} where the dot may be on a new line. - */ -@Value -@With -public class AsInstanceOfPrefix implements Marker { - UUID id; - Space prefix; - - public static AsInstanceOfPrefix create(Space prefix) { - return new AsInstanceOfPrefix(Tree.randomId(), prefix); - } -} diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/marker/TypeAscription.java b/rewrite-scala/src/main/java/org/openrewrite/scala/marker/TypeAscription.java deleted file mode 100644 index bbb5d9e62cc..00000000000 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/marker/TypeAscription.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - *

- * Licensed under the Moderne Source Available License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://docs.moderne.io/licensing/moderne-source-available-license - *

- * 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.openrewrite.scala.marker; - -import org.openrewrite.Tree; -import org.openrewrite.marker.Marker; - -import java.util.UUID; - -/** - * Marks a {@link org.openrewrite.java.tree.J.TypeCast} that represents a Scala type - * ascription ({@code expr: Type}) rather than a Java-style cast ({@code (Type) expr}). - *

- * When this marker is present, the printer emits {@code expr: Type} instead of - * {@code (Type) expr}. - */ -public class TypeAscription implements Marker { - private final UUID id; - - public TypeAscription(UUID id) { - this.id = id; - } - - @Override - public UUID getId() { - return id; - } - - @Override - public TypeAscription withId(UUID id) { - return new TypeAscription(id); - } - - public static TypeAscription create() { - return new TypeAscription(Tree.randomId()); - } -} diff --git a/rewrite-scala/src/main/java/org/openrewrite/scala/tree/S.java b/rewrite-scala/src/main/java/org/openrewrite/scala/tree/S.java index eca663cd4a0..cc0335ebb4a 100644 --- a/rewrite-scala/src/main/java/org/openrewrite/scala/tree/S.java +++ b/rewrite-scala/src/main/java/org/openrewrite/scala/tree/S.java @@ -1742,6 +1742,51 @@ public S.ConstructorInvocation withArguments(JContainer arguments) { } } + /** + * A Scala literal type: the {@code true} of {@code def f(): true}, or {@code "a"}, {@code -1}. + * The literal itself is the type; there is no {@code .type} suffix as in {@link SingletonType}. + */ + @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + final class LiteralType implements S, TypeTree, Expression { + + @With @Getter @EqualsAndHashCode.Include + UUID id; + + @With @Getter + Space prefix; + + @With @Getter + Markers markers; + + // Not `J.Literal` so that negated literals like `-1` are captured too + @With @Getter + Expression literal; + + @With @Getter + @Nullable + JavaType type; + + public LiteralType(UUID id, Space prefix, Markers markers, Expression literal, + @Nullable JavaType type) { + this.id = id; + this.prefix = prefix; + this.markers = markers; + this.literal = literal; + this.type = type; + } + + @Override + public

J acceptScala(ScalaVisitor

v, P p) { + return v.visitLiteralType(this, p); + } + + @Override + public CoordinateBuilder.Expression getCoordinates() { + return new CoordinateBuilder.Expression(this); + } + } + /** * Represents a Scala singleton type: {@code foo.type}. * The qualifier is any expression, typically an object/module reference. diff --git a/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaASTConverter.scala b/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaASTConverter.scala index ebc77449703..dfcd4136a39 100644 --- a/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaASTConverter.scala +++ b/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaASTConverter.scala @@ -21,7 +21,7 @@ import org.openrewrite.Tree import org.openrewrite.java.internal.JavaTypeFactory import org.openrewrite.java.tree.* import org.openrewrite.marker.Markers -import org.openrewrite.scala.marker.{IndentedSyntax, PackageSemicolon} +import org.openrewrite.scala.marker.{IndentedSyntax, OmitBraces, PackageSemicolon} import org.openrewrite.scala.tree.S import java.util @@ -35,12 +35,12 @@ import java.util.{Collections, List as JList} */ class CompilationUnitResult( val packageDecl: J.Package, - val statements: JList[Statement], + val statements: JList[JRightPadded[Statement]], val lastCursorPosition: Int ) { def getPackageDecl: J.Package = packageDecl - def getStatements: JList[Statement] = statements + def getStatements: JList[JRightPadded[Statement]] = statements def getLastCursorPosition: Int = lastCursorPosition } @@ -54,7 +54,7 @@ class ScalaASTConverter { * Converts a Scala parse result to compilation unit components. */ def convertToCompilationUnit(parseResult: ScalaParseResult, source: String, typeFactory: JavaTypeFactory = null): CompilationUnitResult = { - val statements = new util.ArrayList[Statement]() + val statements = new util.ArrayList[JRightPadded[Statement]]() var packageDecl: J.Package = null // Use the context from the parse result (carries type info from batch compilation) @@ -89,7 +89,7 @@ class ScalaASTConverter { case pkgDef: Trees.PackageDef[?] if isBracedPackage(pkgDef, visitor) => // A single top-level braced package is a scope that owns its body, so it // becomes a statement rather than the compilation unit's package header. - statements.add(buildBracedPackage(pkgDef, visitor)) + statements.add(JRightPadded.build(buildBracedPackage(pkgDef, visitor))) case pkgDef: Trees.PackageDef[?] => // Extract package declaration and create J.Package using the visitor // This ensures the cursor is properly updated @@ -120,18 +120,17 @@ class ScalaASTConverter { // Top-level import (no enclosing package). val converted = visitor.visitTree(imp) converted match { - case stmt: Statement => statements.add(stmt) + case stmt: Statement => statements.add(JRightPadded.build(stmt)) case null => // Skip null returns case _ => // Skip non-statements } case _ => // Single statement - System.out.println(s"Processing single statement: ${tree.getClass.getSimpleName}") val converted = visitor.visitTree(tree) converted match { case null => // Skip null returns case _: J.Empty => // Skip empty nodes - case stmt: Statement => statements.add(stmt) + case stmt: Statement => statements.add(JRightPadded.build(stmt)) case _ => // Skip non-statements } } @@ -145,28 +144,33 @@ class ScalaASTConverter { * Braced packages nested among the statements become [[S.PackageDeclaration]] so * they keep their own scope; non-braced nested packages are not yet modeled. */ - private def convertBody(rawStats: Seq[Trees.Tree[?]], visitor: ScalaTreeVisitor): util.ArrayList[Statement] = { - val out = new util.ArrayList[Statement]() + private def convertBody(rawStats: Seq[Trees.Tree[?]], visitor: ScalaTreeVisitor): util.ArrayList[JRightPadded[Statement]] = { + val out = new util.ArrayList[JRightPadded[Statement]]() // Sort by source position to ensure source order is preserved — // the Dotty parser may reorder brace imports internally. val sortedStats = rawStats.sortBy(s => if (s.span.exists) s.span.start else Int.MaxValue) - sortedStats.foreach { - case pkg: Trees.PackageDef[?] if isBracedPackage(pkg, visitor) => - out.add(buildBracedPackage(pkg, visitor)) - case _: Trees.PackageDef[?] => - // Non-braced nested package — not yet modeled, skip. - case imp: Trees.Import[?] => - visitor.visitTree(imp) match { - case stmt: Statement => out.add(stmt) - case _ => - } - case stat => - visitor.visitTree(stat) match { - case null => - case _: J.Empty => - case stmt: Statement => out.add(stmt) - case _ => - } + sortedStats.zipWithIndex.foreach { case (stat, idx) => + val converted: Statement = stat match { + case pkg: Trees.PackageDef[?] if isBracedPackage(pkg, visitor) => + buildBracedPackage(pkg, visitor) + case pkg: Trees.PackageDef[?] => + buildChainedPackage(pkg, visitor) + case other => + visitor.visitTree(other) match { + case _: J.Empty => null + case stmt: Statement => stmt + case _ => null + } + } + if (converted != null) { + // An explicit `;` separating top-level statements rides on the padding + val statEnd = if (stat.span.exists) Math.max(0, stat.span.end - visitor.getOffsetAdjustment) else visitor.getCursor + val nextStart = sortedStats.drop(idx + 1).collectFirst { + case n if n.span.exists => Math.max(0, n.span.start - visitor.getOffsetAdjustment) + }.getOrElse(visitor.getSourceLength) + val (trailingSpace, markers) = visitor.consumeTrailingSemicolon(statEnd, nextStart) + out.add(new JRightPadded[Statement](converted, trailingSpace, markers)) + } } out } @@ -195,6 +199,35 @@ class ScalaASTConverter { * (so nested/sibling braced packages are handled uniformly). The `{`/`}` are owned * by the body [[J.Block]]; the [[J.Package]] head carries only `package `. */ + /** + * A chained package clause (`package a` followed by `package b`) scopes everything after + * it, exactly as a braced one scopes its body, so it becomes an S.PackageDeclaration whose + * block omits the braces. + */ + private def buildChainedPackage(pkgDef: Trees.PackageDef[?], visitor: ScalaTreeVisitor): S.PackageDeclaration = { + val prefix = visitor.extractPrefix(pkgDef.span) + + val packageExpr: Expression = TypeTree.build(packageNameFromSource(pkgDef, visitor), '`') + val namePkg = new J.Package( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + packageExpr.withPrefix(spaceBeforePackageName(pkgDef, visitor)), + Collections.emptyList() + ) + visitor.updateCursor(pkgDef.pid.span.end) + + val body = new J.Block( + Tree.randomId(), + Space.EMPTY, + Markers.build(Collections.singletonList(new OmitBraces(Tree.randomId()))), + JRightPadded.build(false), + convertBody(pkgDef.stats, visitor), + Space.EMPTY + ) + new S.PackageDeclaration(Tree.randomId(), prefix, Markers.EMPTY, namePkg, body) + } + private def buildBracedPackage(pkgDef: Trees.PackageDef[?], visitor: ScalaTreeVisitor): S.PackageDeclaration = { val prefix = visitor.extractPrefix(pkgDef.span) @@ -203,7 +236,7 @@ class ScalaASTConverter { Tree.randomId(), Space.EMPTY, Markers.EMPTY, - packageExpr.withPrefix(Space.build(" ", Collections.emptyList())), + packageExpr.withPrefix(spaceBeforePackageName(pkgDef, visitor)), Collections.emptyList() ) @@ -221,7 +254,7 @@ class ScalaASTConverter { val bodyStmts = convertBody(pkgDef.stats, visitor) val rpStmts = new util.ArrayList[JRightPadded[Statement]]() - bodyStmts.forEach(s => rpStmts.add(JRightPadded.build(s))) + rpStmts.addAll(bodyStmts) // Consume the space before `}` (becomes the block end) and the `}` itself. val afterStart = visitor.getCursor - srcOffset @@ -293,7 +326,7 @@ class ScalaASTConverter { Tree.randomId(), prefix, markers, - packageExpr.withPrefix(Space.build(" ", Collections.emptyList())), + packageExpr.withPrefix(spaceBeforePackageName(pkgDef, visitor)), Collections.emptyList() ) } @@ -317,6 +350,19 @@ class ScalaASTConverter { * marker, which the printer renders back with backticks. Falls back to * [[extractPackageName]] when the span is unusable. */ + /** The run between the `package` keyword and the package name, which need not be one space. */ + private def spaceBeforePackageName(pkgDef: Trees.PackageDef[?], visitor: ScalaTreeVisitor): Space = { + if (pkgDef.pid.span.exists) { + val srcText = visitor.getSourceText + val nameStart = pkgDef.pid.span.start - visitor.getOffsetAdjustment + val keyword = if (nameStart <= srcText.length) srcText.lastIndexOf("package", nameStart) else -1 + if (keyword >= 0 && keyword + "package".length <= nameStart) { + return ScalaSpace.format(srcText.substring(keyword + "package".length, nameStart)) + } + } + Space.build(" ", Collections.emptyList()) + } + private def packageNameFromSource(pkgDef: Trees.PackageDef[?], visitor: ScalaTreeVisitor): String = { if (pkgDef.pid.span.exists) { val srcText = visitor.getSourceText @@ -345,7 +391,9 @@ class ScalaASTConverter { * Converts a Scala parse result to a list of statements (backward compatibility). */ def convertToStatements(parseResult: ScalaParseResult, source: String): JList[Statement] = { - convertToCompilationUnit(parseResult, source, null).statements + val out = new util.ArrayList[Statement]() + convertToCompilationUnit(parseResult, source, null).statements.forEach(rp => out.add(rp.getElement)) + out } /** diff --git a/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaTreeVisitor.scala b/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaTreeVisitor.scala index a7cd2bf3d17..df435398495 100644 --- a/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaTreeVisitor.scala +++ b/rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaTreeVisitor.scala @@ -33,22 +33,33 @@ import org.openrewrite.scala.marker.Implicit import org.openrewrite.scala.marker.InfixTypeNotation import org.openrewrite.scala.marker.LambdaParameter import org.openrewrite.scala.marker.IndentedSyntax +import org.openrewrite.scala.marker.InlineKeyword import org.openrewrite.scala.marker.OmitBraces import org.openrewrite.scala.marker.OmitImportBraces import org.openrewrite.scala.marker.PackageObject +import org.openrewrite.scala.marker.PureFunctionArrow +import org.openrewrite.scala.marker.DerivesClause +import org.openrewrite.scala.marker.EndMarker +import org.openrewrite.scala.marker.KindParameterVariance +import org.openrewrite.scala.marker.ParentSeparator +import org.openrewrite.scala.marker.CasePattern +import org.openrewrite.scala.marker.DoKeyword import org.openrewrite.scala.marker.SObject +import org.openrewrite.scala.marker.ThenKeyword +import org.openrewrite.scala.marker.SelfType import org.openrewrite.scala.marker.Semicolon import org.openrewrite.scala.marker.TrailingComma +import org.openrewrite.scala.marker.UsingArguments import org.openrewrite.scala.marker.TypeProjection import org.openrewrite.scala.marker.ScalaForLoop import org.openrewrite.scala.marker.BlockArgument import org.openrewrite.scala.marker.CommaContinuation import org.openrewrite.scala.marker.FunctionApplication -import org.openrewrite.scala.marker.AsInstanceOfPrefix -import org.openrewrite.scala.marker.TypeAscription import org.openrewrite.scala.marker.UnderscorePlaceholderLambda import org.openrewrite.scala.marker.PartialFunctionLiteral import org.openrewrite.scala.marker.ContextFunctionArrow +import org.openrewrite.scala.marker.CaptureSet +import org.openrewrite.scala.marker.TypeParameterBounds import org.openrewrite.scala.marker.Curried import org.openrewrite.scala.marker.InfixNotation import org.openrewrite.scala.marker.RightAssociative @@ -204,6 +215,8 @@ class ScalaTreeVisitor( def getOffsetAdjustment: Int = offsetAdjustment + def getSourceLength: Int = source.length + def updateCursor(position: Int): Unit = { val adjustedPosition = Math.max(0, position - offsetAdjustment) if (adjustedPosition > cursor && adjustedPosition <= source.length) { @@ -268,6 +281,8 @@ class ScalaTreeVisitor( case stt: Trees.SingletonTypeTree[?] => visitSingletonTypeTree(stt) case rtt: Trees.RefinedTypeTree[?] => visitRefinedTypeTree(rtt) case ann: Trees.Annotated[?] => visitAnnotated(ann) + // Capture checking wraps a result type; the wrapper itself has no source of its own + case car: untpd.CapturesAndResult => visitTree(car.parent) case mac: untpd.MacroTree => visitMacroTree(mac) case ext: untpd.ExtMethods => visitExtMethods(ext) case forYield: untpd.ForYield => visitForYield(forYield) @@ -489,23 +504,30 @@ class ScalaTreeVisitor( val openIdx = positionOfNext("(", cursor) argContainerPrefix = if (openIdx > cursor) ScalaSpace.format(source, cursor, openIdx) else Space.EMPTY if (openIdx >= 0) cursor = openIdx + 1 + val usingText1 = consumeUsingKeyword() for (i <- app.args.indices) { val arg = app.args(i) val argExpr = asExpression(visitTree(arg)) val argEnd = Math.max(0, arg.span.end - offsetAdjustment) + var lastMarkers = Markers.EMPTY val afterSpace = if (i == app.args.size - 1) { val closePos = positionOfNext(")", Math.max(cursor, argEnd)) - if (closePos > argEnd) ScalaSpace.format(source, argEnd, closePos) else Space.EMPTY + if (closePos > argEnd) { + val (sp, mk) = trailingCommaBefore(argEnd, closePos) + lastMarkers = mk + sp + } else Space.EMPTY } else { val commaPos = positionOfNext(",", Math.max(cursor, argEnd)) val space = if (commaPos > argEnd) ScalaSpace.format(source, argEnd, commaPos) else Space.EMPTY if (commaPos >= cursor) cursor = commaPos + 1 space } - args.add(new JRightPadded(argExpr, afterSpace, Markers.EMPTY)) + args.add(new JRightPadded(argExpr, afterSpace, lastMarkers)) } val closeParen = positionOfNext(")", cursor) if (closeParen >= 0) cursor = closeParen + 1 + withUsing(args, usingText1) } updateCursor(app.span.end) @@ -611,7 +633,13 @@ class ScalaTreeVisitor( // Create the annotation type: an Ident for a simple name (@deprecated) or a // Select chain for a qualified name (@scala.annotation.implicitNotFound). val annotTypeTree: NameTree = tpt match { - case id: Trees.Ident[?] => ident(id.name.toString) + case id: Trees.Ident[?] => + // `T @ unchecked`: the space between `@` and the name belongs to the name + val nameStart = Math.max(0, id.span.start - offsetAdjustment) + val atIdx = if (nameStart <= source.length) source.lastIndexOf('@', nameStart) else -1 + val afterAt = if (atIdx >= 0 && atIdx + 1 < nameStart) Space.format(source.substring(atIdx + 1, nameStart)) + else Space.EMPTY + ident(id.name.toString, afterAt, quoted = isBacktickQuoted(id.span)) case sel: Trees.Select[?] => // Qualified name: skip the leading '@', then map the Select chain to a J.FieldAccess. val selStart = Math.max(0, sel.span.start - offsetAdjustment) @@ -841,6 +869,7 @@ class ScalaTreeVisitor( val args = new util.ArrayList[JRightPadded[Expression]]() var argContainerPrefix = Space.EMPTY + var usingText2: String = null val markers = new util.ArrayList[org.openrewrite.marker.Marker]() import org.openrewrite.scala.marker.FunctionApplication markers.add(FunctionApplication.create()) @@ -882,12 +911,24 @@ class ScalaTreeVisitor( } else { // Normal parenthesized arguments: Seq(1, 2) val parenPos = positionOfNext("(") + val parenIsNext = parenPos >= 0 && source.substring(cursor, parenPos).forall(_.isWhitespace) if (parenPos >= 0) { if (parenPos > cursor) { argContainerPrefix = ScalaSpace.format(source, cursor, parenPos) } cursor = parenPos + 1 } + usingText2 = consumeUsingKeyword() + + // The parentheses of an empty list can still hold whitespace or a comment, as in + // `f(\n)`, which a J.Empty carries so the container prints it back. + if (app.args.isEmpty && parenIsNext) { + val closePos = positionOfNext(")", cursor) + if (closePos > cursor) { + args.add(JRightPadded.build(new J.Empty(Tree.randomId(), + ScalaSpace.format(source, cursor, closePos), Markers.EMPTY).asInstanceOf[Expression])) + } + } for ((arg, i) <- app.args.zipWithIndex) { val visited = visitTree(arg) @@ -909,10 +950,10 @@ class ScalaTreeVisitor( } else { val argEnd = Math.max(0, arg.span.end - offsetAdjustment) val closePos = positionOfNext(")", Math.max(cursor, argEnd)) - val beforeClose = if (closePos > argEnd) { - ScalaSpace.format(source, argEnd, closePos) - } else Space.EMPTY - args.add(new JRightPadded(expr, beforeClose, Markers.EMPTY)) + val (beforeClose, lastMarkers) = if (closePos > argEnd) { + trailingCommaBefore(argEnd, closePos) + } else (Space.EMPTY, Markers.EMPTY) + args.add(new JRightPadded(expr, beforeClose, lastMarkers)) } } } @@ -925,6 +966,7 @@ class ScalaTreeVisitor( } val methodName = ident("apply") + withUsing(args, usingText2) new J.MethodInvocation( Tree.randomId(), @@ -938,8 +980,41 @@ class ScalaTreeVisitor( ) } + /** Prefix for an application whose callee is a `new` expression. Dotty's Apply span for a + * curried constructor call starts at the type, leaving `new` in the gap ahead of it, where + * it would be absorbed as whitespace and then printed a second time by the NewClass. + */ + private def extractPrefixKeepingNew(span: Spans.Span): Space = { + val start = Math.max(0, span.start - offsetAdjustment) + if (start <= cursor || start > source.length) extractPrefix(span) + else { + val gap = source.substring(cursor, start) + val idx = positionOfNextIn(gap, "new", 0) + if (idx < 0) extractPrefix(span) + else { + val sp = ScalaSpace.format(source, cursor, cursor + idx) + cursor = cursor + idx + sp + } + } + } + + /** Splits the run before a closing `)` into a trailing-comma marker and the space that + * follows it, so a source trailing comma round-trips instead of sitting in whitespace. + */ + private def trailingCommaBefore(from: Int, to: Int): (Space, Markers) = { + if (from >= to || to > source.length) (Space.EMPTY, Markers.EMPTY) + else { + val between = source.substring(from, to) + val commaIdx = positionOfNextIn(between, ",", 0) + if (commaIdx < 0) (ScalaSpace.format(source, from, to), Markers.EMPTY) + else (ScalaSpace.format(source, from + commaIdx + 1, to), + Markers.EMPTY.add(TrailingComma.create(ScalaSpace.format(source, from, from + commaIdx)))) + } + } + private def visitMethodInvocation(app: Trees.Apply[?]): J = { - val prefix = extractPrefix(app.span) + val prefix = extractPrefixKeepingNew(app.span) // Note: We deliberately don't create J.ArrayAccess for explicit .apply() calls. // In Scala, arr.apply(0) is an explicit method call and should be represented as such. @@ -1080,26 +1155,33 @@ class ScalaTreeVisitor( val functionAfterSpace = if (firstNonWs > cursor) ScalaSpace.format(source, cursor, firstNonWs) else Space.EMPTY if (firstNonWs < source.length) cursor = firstNonWs + 1 // past `(` + val fnCallUsing = consumeUsingKeyword() for (i <- app.args.indices) { val arg = app.args(i) val argExpr = asExpression(visitTree(arg)) val argEnd = Math.max(0, arg.span.end - offsetAdjustment) + var lastMarkers = Markers.EMPTY val afterSpace = if (i == app.args.size - 1) { val closePos = positionOfNext(")", Math.max(cursor, argEnd)) - if (closePos > argEnd) ScalaSpace.format(source, argEnd, closePos) else Space.EMPTY + if (closePos > argEnd) { + val (sp, mk) = trailingCommaBefore(argEnd, closePos) + lastMarkers = mk + sp + } else Space.EMPTY } else { val commaPos = positionOfNext(",", Math.max(cursor, argEnd)) val space = if (commaPos > argEnd) ScalaSpace.format(source, argEnd, commaPos) else Space.EMPTY if (commaPos >= cursor) cursor = commaPos + 1 space } - outerArgs.add(new JRightPadded(argExpr, afterSpace, Markers.EMPTY)) + outerArgs.add(new JRightPadded(argExpr, afterSpace, lastMarkers)) } val closeParen = positionOfNext(")", cursor) if (closeParen >= 0) cursor = closeParen + 1 finishAtAppEnd() + withUsing(outerArgs, fnCallUsing) return S.FunctionCall.build(Tree.randomId(), prefix, Markers.EMPTY, new JRightPadded(fn, functionAfterSpace, Markers.EMPTY), JContainer.build(Space.EMPTY, outerArgs, Markers.EMPTY), methodType) @@ -1115,21 +1197,27 @@ class ScalaTreeVisitor( val select = asExpression(visitTree(app.fun)) val openIdx = positionOfNext("(", cursor) if (openIdx >= 0) cursor = openIdx + 1 + val curriedUsing = consumeUsingKeyword() val outerArgs = new util.ArrayList[JRightPadded[Expression]]() for (i <- app.args.indices) { val arg = app.args(i) val argExpr = asExpression(visitTree(arg)) val argEnd = Math.max(0, arg.span.end - offsetAdjustment) + var lastMarkers = Markers.EMPTY val afterSpace = if (i == app.args.size - 1) { val closePos = positionOfNext(")", Math.max(cursor, argEnd)) - if (closePos > argEnd) ScalaSpace.format(source, argEnd, closePos) else Space.EMPTY + if (closePos > argEnd) { + val (sp, mk) = trailingCommaBefore(argEnd, closePos) + lastMarkers = mk + sp + } else Space.EMPTY } else { val commaPos = positionOfNext(",", Math.max(cursor, argEnd)) val space = if (commaPos > argEnd) ScalaSpace.format(source, argEnd, commaPos) else Space.EMPTY if (commaPos >= cursor) cursor = commaPos + 1 space } - outerArgs.add(new JRightPadded(argExpr, afterSpace, Markers.EMPTY)) + outerArgs.add(new JRightPadded(argExpr, afterSpace, lastMarkers)) } val closeParen = positionOfNext(")", cursor) if (closeParen >= 0) cursor = closeParen + 1 @@ -1137,6 +1225,7 @@ class ScalaTreeVisitor( val end = Math.max(0, app.span.end - offsetAdjustment) if (end > cursor && end <= source.length) cursor = end } + withUsing(outerArgs, curriedUsing) val mt = typeFor(app.span) match { case m: JavaType.Method => m; case _ => null } val nameId = new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, Collections.emptyList(), "apply", null, null) @@ -1156,6 +1245,7 @@ class ScalaTreeVisitor( val isColonArg = !isBlockArg && firstArgNonWs >= 0 && firstArgNonWs < source.length && source.charAt(firstArgNonWs) == ':' + var usingText3: String = null var argContainerPrefix = Space.EMPTY val args = new util.ArrayList[JRightPadded[Expression]]() @@ -1203,6 +1293,23 @@ class ScalaTreeVisitor( } cursor = parenPos + 1 } + usingText3 = consumeUsingKeyword() + } else { + // See the empty-list handling above; a call with no parentheses at all leaves the + // cursor where it is. + val open = indexOfNextNonWhitespace(cursor) + val appEnd = Math.min(source.length, Math.max(0, app.span.end - offsetAdjustment)) + if (open < appEnd && source.charAt(open) == '(') { + val closePos = positionOfNext(")", open + 1) + if (closePos > open && closePos <= appEnd) { + if (open > cursor) argContainerPrefix = ScalaSpace.format(source, cursor, open) + if (closePos > open + 1) { + args.add(JRightPadded.build(new J.Empty(Tree.randomId(), + ScalaSpace.format(source, open + 1, closePos), Markers.EMPTY).asInstanceOf[Expression])) + } + cursor = closePos + 1 + } + } } for (i <- app.args.indices) { @@ -1262,6 +1369,7 @@ class ScalaTreeVisitor( val name = ident(methodName, nameSpace, quoted = methodNameQuoted) + withUsing(args, usingText3) val argContainer = JContainer.build( argContainerPrefix, args, @@ -1360,7 +1468,6 @@ class ScalaTreeVisitor( elements.asScala.toSeq, identity, ")", initPrefix, Markers.EMPTY) } - // Update cursor to end of expression updateCursor(app.span.end) new J.NewArray( @@ -1941,15 +2048,15 @@ class ScalaTreeVisitor( // Apply the prefix space to the expression val exprWithPrefix: Expression = expr.withPrefix(argPrefix) - // For the last arg, capture trailing whitespace before ')'. - val afterSpace = if (i == app.args.size - 1) { + // For the last arg, capture the run before ')', which can hold a trailing comma. + val (afterSpace, lastMarkers) = if (i == app.args.size - 1) { val argEnd = Math.max(0, arg.span.end - offsetAdjustment) val closePos = positionOfNext(")", Math.max(cursor, argEnd)) - if (closePos > argEnd) ScalaSpace.format(source, argEnd, closePos) - else Space.EMPTY - } else Space.EMPTY + if (closePos > argEnd) trailingCommaBefore(argEnd, closePos) + else (Space.EMPTY, Markers.EMPTY) + } else (Space.EMPTY, Markers.EMPTY) - args.add(new JRightPadded[Expression](exprWithPrefix, afterSpace, Markers.EMPTY)) + args.add(new JRightPadded[Expression](exprWithPrefix, afterSpace, lastMarkers)) case j: J => // Scala statement-as-expression (e.g. if/else, match, block, try) wrapped so it // can sit in an argument list. Apply argPrefix and trailing space the same way @@ -2160,7 +2267,29 @@ class ScalaTreeVisitor( JContainer.build(beforeParenSpace, args, Markers.EMPTY) } else { - null + // Dotty models `new Foo { ... }` and `new Foo() { ... }` alike, so the + // empty list is read from the source: a container prints `()`, null + // prints nothing. + val typeEnd = Math.max(0, newInner.tpt.span.end - offsetAdjustment) + val searchStart = Math.max(cursor, typeEnd) + // The list opens on the token right after the type, so a `(` further + // along — in the body, say — is not one. + val open = indexOfNextNonWhitespace(searchStart) + val close = if (open < source.length && source.charAt(open) == '(') + positionOfNextIn(source, ")", open + 1) else -1 + if (close > open) { + val elements = new util.ArrayList[JRightPadded[Expression]]() + if (close > open + 1) { + val interior = new J.Empty(Tree.randomId(), + ScalaSpace.format(source.substring(open + 1, close)), Markers.EMPTY) + elements.add(JRightPadded.build(interior.asInstanceOf[Expression])) + } + val beforeParenSpace = ScalaSpace.format(source.substring(searchStart, open)) + updateCursor(close + 1) + JContainer.build(beforeParenSpace, elements, Markers.EMPTY) + } else { + null + } } // Capture any remaining curried parameter lists verbatim. @@ -2178,7 +2307,8 @@ class ScalaTreeVisitor( } } else { // Simple interface/trait: new Runnable { ... } - val typeTree = visitTree(firstParent).asInstanceOf[TypeTree] + val typeTree = visitParentType(firstParent) + if (typeTree == null) throw unmappedException(firstParent) (typeTree, null) } } @@ -2194,16 +2324,12 @@ class ScalaTreeVisitor( sourceBefore("with"), Markers.EMPTY)) for (i <- 1 until parents.size - 1) { - val tt = visitTree(parents(i)) match { - case t: TypeTree => t - case _ => throw unmappedException(parents(i)) - } + val tt = visitParentType(parents(i)) + if (tt == null) throw unmappedException(parents(i)) mixinElements.add(new JRightPadded[TypeTree](tt, sourceBefore("with"), Markers.EMPTY)) } - val lastTt = visitTree(parents.last) match { - case t: TypeTree => t - case _ => throw unmappedException(parents.last) - } + val lastTt = visitParentType(parents.last) + if (lastTt == null) throw unmappedException(parents.last) mixinElements.add(JRightPadded.build(lastTt)) new J.IntersectionType( Tree.randomId(), @@ -2246,10 +2372,10 @@ class ScalaTreeVisitor( } else if (colonIndex >= 0) { (':', colonIndex) } else { - ('', -1) + (NoBodyDelimiter, -1) } } else { - ('', -1) + (NoBodyDelimiter, -1) } val body = if (bodyDelimiterIndex >= 0) { @@ -2658,7 +2784,16 @@ class ScalaTreeVisitor( if (afterChar == ',') { val sepSpace = ScalaSpace.format(source.substring(afterSel, q)) cursor = q + 1 - selectorElems.add(new JRightPadded(selectorNode.withPrefix(selPrefix), sepSpace, Markers.EMPTY)) + if (idx == selectors.size - 1) { + // Scala 3 allows a trailing comma before the closing brace + val afterComma = indexOfNextNonWhitespace(cursor) + val tailSpace = ScalaSpace.format(source.substring(cursor, afterComma)) + cursor = afterComma + selectorElems.add(new JRightPadded(selectorNode.withPrefix(selPrefix), tailSpace, + Markers.EMPTY.add(TrailingComma.create(sepSpace)))) + } else { + selectorElems.add(new JRightPadded(selectorNode.withPrefix(selPrefix), sepSpace, Markers.EMPTY)) + } } else if (afterChar == '}') { val tailSpace = ScalaSpace.format(source.substring(afterSel, q)) cursor = q @@ -3098,43 +3233,8 @@ class ScalaTreeVisitor( val leadingWs = sourceSnippet.length - trimmedSnippet.length modifierEndPos = leadingWs - // Check for access modifiers (including scoped: private[scope], protected[this]) - if (trimmedSnippet.startsWith("private")) { - var keyword = "private" - var keyLen = keyword.length - val afterKw = trimmedSnippet.substring(keyLen) - if (afterKw.startsWith("[")) { - val cb = positionOfNextIn(afterKw, "]", 0) - if (cb >= 0) { keyword = "private" + afterKw.substring(0, cb + 1); keyLen = keyword.length } - } - val modPrefix = if (leadingWs > 0) ScalaSpace.format(sourceSnippet.substring(0, leadingWs)) else Space.EMPTY - modifiers.add(new J.Modifier( - Tree.randomId(), modPrefix, Markers.EMPTY, - keyword, J.Modifier.Type.Private, Collections.emptyList() - )) - modifierEndPos = leadingWs + keyLen - lastModifierKeywordEnd = modifierEndPos - if (modifierEndPos < sourceSnippet.length && sourceSnippet.charAt(modifierEndPos) == ' ') modifierEndPos += 1 - } else if (trimmedSnippet.startsWith("protected")) { - var keyword = "protected" - var keyLen = keyword.length - val afterKw = trimmedSnippet.substring(keyLen) - if (afterKw.startsWith("[")) { - val cb = positionOfNextIn(afterKw, "]", 0) - if (cb >= 0) { keyword = "protected" + afterKw.substring(0, cb + 1); keyLen = keyword.length } - } - val modPrefix = if (leadingWs > 0) ScalaSpace.format(sourceSnippet.substring(0, leadingWs)) else Space.EMPTY - modifiers.add(new J.Modifier( - Tree.randomId(), modPrefix, Markers.EMPTY, - keyword, J.Modifier.Type.Protected, Collections.emptyList() - )) - modifierEndPos = leadingWs + keyLen - lastModifierKeywordEnd = modifierEndPos - if (modifierEndPos < sourceSnippet.length && sourceSnippet.charAt(modifierEndPos) == ' ') modifierEndPos += 1 - } - - // Check for remaining modifiers (in any order) before val/var/def - // These include: implicit, override, abstract, final, lazy, sealed + // Modifiers in any order before val/var/given: the access modifiers (which may carry + // a scope, `private[scope]`), plus implicit, override, abstract, final, lazy, sealed var scanning = true while (scanning && modifierEndPos < sourceSnippet.length) { val remaining = sourceSnippet.substring(modifierEndPos) @@ -3146,7 +3246,29 @@ class ScalaTreeVisitor( else if (leadingWs > 0) ScalaSpace.format(sourceSnippet.substring(0, leadingWs)) else Space.EMPTY - if (remaining.startsWith("final ")) { + val accessKeyword = + if (remaining.startsWith("private")) "private" + else if (remaining.startsWith("protected")) "protected" + else null + val scopedAccessKeyword = if (accessKeyword == null) null else { + val afterKw = remaining.substring(accessKeyword.length) + val closeBracket = if (afterKw.startsWith("[")) positionOfNextIn(afterKw, "]", 0) else -1 + if (closeBracket >= 0) accessKeyword + afterKw.substring(0, closeBracket + 1) else accessKeyword + } + // An identifier can open with the same letters, as in `privateName` + val isAccessModifier = scopedAccessKeyword != null && + remaining.length > scopedAccessKeyword.length && + !Character.isLetterOrDigit(remaining.charAt(scopedAccessKeyword.length)) + + if (isAccessModifier) { + modifiers.add(new J.Modifier(Tree.randomId(), modSpace, Markers.EMPTY, + scopedAccessKeyword, + if (accessKeyword == "private") J.Modifier.Type.Private else J.Modifier.Type.Protected, + Collections.emptyList())) + lastModifierKeywordEnd = modifierEndPos + scopedAccessKeyword.length + modifierEndPos = lastModifierKeywordEnd + if (modifierEndPos < sourceSnippet.length && sourceSnippet.charAt(modifierEndPos) == ' ') modifierEndPos += 1 + } else if (remaining.startsWith("final ")) { hasExplicitFinal = true modifiers.add(new J.Modifier(Tree.randomId(), modSpace, Markers.EMPTY, "final", J.Modifier.Type.Final, Collections.emptyList())) @@ -3178,6 +3300,21 @@ class ScalaTreeVisitor( "sealed", J.Modifier.Type.Sealed, Collections.emptyList())) lastModifierKeywordEnd = modifierEndPos + "sealed".length modifierEndPos += "sealed ".length + } else if (remaining.startsWith("transparent ")) { + modifiers.add(new J.Modifier(Tree.randomId(), modSpace, Markers.EMPTY, + "transparent", J.Modifier.Type.LanguageExtension, Collections.emptyList())) + lastModifierKeywordEnd = modifierEndPos + "transparent".length + modifierEndPos += "transparent ".length + } else if (remaining.startsWith("inline ")) { + modifiers.add(new J.Modifier(Tree.randomId(), modSpace, Markers.EMPTY, + "inline", J.Modifier.Type.LanguageExtension, Collections.emptyList())) + lastModifierKeywordEnd = modifierEndPos + "inline".length + modifierEndPos += "inline ".length + } else if (remaining.startsWith("opaque ")) { + modifiers.add(new J.Modifier(Tree.randomId(), modSpace, Markers.EMPTY, + "opaque", J.Modifier.Type.LanguageExtension, Collections.emptyList())) + lastModifierKeywordEnd = modifierEndPos + "opaque".length + modifierEndPos += "opaque ".length } else { scanning = false } @@ -3428,9 +3565,10 @@ class ScalaTreeVisitor( } } - // Update cursor to end of ValDef + val endMarkerMarkers = withEndMarker(Markers.EMPTY, vd.span, vd.name.toString) + updateCursor(vd.span.end) - + // Create variable declarator val namedVariable = new J.VariableDeclarations.NamedVariable( Tree.randomId(), @@ -3456,8 +3594,13 @@ class ScalaTreeVisitor( if (isGiven) { markerList.add(org.openrewrite.scala.marker.Given(Tree.randomId())) } - val variableMarkers = - if (markerList.isEmpty) Markers.EMPTY else Markers.build(markerList) + if (valVarKeyword == "var") { + markerList.add(org.openrewrite.scala.marker.VarKeyword(Tree.randomId())) + } + val variableMarkers = { + val base = if (markerList.isEmpty) Markers.EMPTY else Markers.build(markerList) + endMarkerMarkers.findFirst(classOf[EndMarker]).map[Markers](m => base.add(m)).orElse(base) + } new J.VariableDeclarations( Tree.randomId(), @@ -3583,6 +3726,8 @@ class ScalaTreeVisitor( } private def visitModuleDef(md: untpd.ModuleDef): J.ClassDeclaration = { + // Set while extracting a braceless body, read when the declaration's markers are built + var moduleEndMarker: String = null val hasAnnotations = md.mods != null && md.mods.annotations.nonEmpty val leadingAnnotations = new util.ArrayList[J.Annotation]() @@ -3606,8 +3751,9 @@ class ScalaTreeVisitor( var isEnumCase = false // When annotations were consumed, cursor sits after them; modifierText must // be derived from the post-annotation position, not the ModuleDef span start. - val modifierScanStart = if (hasAnnotations) cursor else adjustedStart - if (modifierScanStart >= cursor && adjustedEnd <= source.length) { + // A companion object's span starts back at its class, so scan from the cursor, which is + // already past everything consumed so far. + if (cursor < adjustedEnd && adjustedEnd <= source.length) { val sourceSnippet = source.substring(cursor, adjustedEnd) objectIndex = findKeyword(sourceSnippet, "object") val caseIndex = findKeyword(sourceSnippet, "case") @@ -3787,20 +3933,26 @@ class ScalaTreeVisitor( if (tmpl.parents.size > 1) { val implementsList = new util.ArrayList[JRightPadded[TypeTree]]() - // Find space before first "with" + val separators = new util.ArrayList[String]() + + // Find space before the separator introducing the first additional parent var containerSpace = Space.format(" ") if (cursor < source.length && tmpl.parents(1).span.exists) { val firstWithParentStart = Math.max(0, tmpl.parents(1).span.start - offsetAdjustment) if (cursor < firstWithParentStart) { val beforeFirstWith = source.substring(cursor, firstWithParentStart) - val withIdx = positionOfNextIn(beforeFirstWith, "with", 0) - if (withIdx >= 0) { - containerSpace = Space.format(beforeFirstWith.substring(0, withIdx)) - cursor = cursor + withIdx + "with".length + val (sepIdx, sep) = parentSeparatorIn(beforeFirstWith) + if (sepIdx >= 0) { + containerSpace = Space.format(beforeFirstWith.substring(0, sepIdx)) + cursor = cursor + sepIdx + sep.length + separators.add(sep) } } } - + if (separators.isEmpty) { + separators.add("with") + } + for (i <- 1 until tmpl.parents.size) { val parent = tmpl.parents(i) val visitedParent = visitTree(parent) @@ -3818,18 +3970,24 @@ class ScalaTreeVisitor( val nextStart = Math.max(0, tmpl.parents(i + 1).span.start - offsetAdjustment) if (thisEnd < nextStart && nextStart <= source.length) { val between = source.substring(thisEnd, nextStart) - val withIdx = positionOfNextIn(between, "with", 0) - if (withIdx >= 0) { - trailingSpace = Space.format(between.substring(0, withIdx)) - // Update cursor past "with" - cursor = thisEnd + withIdx + "with".length + val (sepIdx, sep) = parentSeparatorIn(between) + if (sepIdx >= 0) { + trailingSpace = Space.format(between.substring(0, sepIdx)) + cursor = thisEnd + sepIdx + sep.length + separators.add(sep) } else { trailingSpace = Space.format(between) + separators.add("with") } } } - - implementsList.add(new JRightPadded(implType, trailingSpace, Markers.EMPTY)) + + val sepMarkers = if (i - 1 < separators.size) { + Markers.EMPTY.add(ParentSeparator(Tree.randomId(), separators.get(i - 1))) + } else { + Markers.EMPTY + } + implementsList.add(new JRightPadded(implType, trailingSpace, sepMarkers)) } implementings = JContainer.build(containerSpace, implementsList, Markers.EMPTY) } @@ -3864,6 +4022,8 @@ class ScalaTreeVisitor( val visitedSpans = new java.util.HashSet[Int]() // Sort by source position to preserve source order (Dotty may reorder imports) val sortedBody = tmpl.body.sortBy(s => if (s.span.exists) s.span.start else Int.MaxValue) + val moduleEnd = if (md.span.exists) Math.max(0, md.span.end - offsetAdjustment) else source.length + val selfTypeText = consumeSelfType(sortedBody, moduleEnd) sortedBody.foreach { stat => val isSynth = stat.span.isSynthetic || { def containsTripleQuestion(t: Trees.Tree[?]): Boolean = t match { @@ -3876,11 +4036,27 @@ class ScalaTreeVisitor( containsTripleQuestion(stat) } if (stat.span.exists && !isSynth && visitedSpans.add(stat.span.start)) { - visitTree(stat) match { - case stmt: Statement => statements.add(JRightPadded.build(stmt)) - case expr: Expression => - statements.add(JRightPadded.build(new S.ExpressionStatement(Tree.randomId(), expr))) - case _ => + val statEnd = Math.max(0, stat.span.end - offsetAdjustment) + val visited: Statement = visitTree(stat) match { + case stmt: Statement => stmt + case expr: Expression => new S.ExpressionStatement(Tree.randomId(), expr) + case _ => null + } + if (visited != null) { + val nextStart = { + var k = sortedBody.indexOf(stat) + 1 + var ns = if (md.span.exists) Math.max(0, md.span.end - offsetAdjustment) else source.length + while (k < sortedBody.size) { + val nxt = sortedBody(k) + if (nxt.span.exists && !nxt.span.isSynthetic) { + ns = Math.max(0, nxt.span.start - offsetAdjustment) + k = sortedBody.size + } else k += 1 + } + ns + } + val (trailingSpace, rpMarkers) = consumeTrailingSemicolon(statEnd, nextStart) + statements.add(new JRightPadded[Statement](visited, trailingSpace, rpMarkers)) } } } @@ -3890,9 +4066,16 @@ class ScalaTreeVisitor( if (cursor < source.length && md.span.exists) { val endPos = Math.max(0, md.span.end - offsetAdjustment) if (isBraceless) { - // No closing brace for braceless syntax + // No closing brace for braceless syntax. if (cursor < endPos) { - endSpace = ScalaSpace.format(source, cursor, Math.min(endPos, source.length)) + val bodyEnd = Math.min(endPos, source.length) + endMarkerAt(cursor, bodyEnd) match { + case Some((start, text)) => + moduleEndMarker = source.substring(cursor, start + text.length) + endSpace = Space.EMPTY + case None => + endSpace = ScalaSpace.format(source, cursor, bodyEnd) + } } cursor = endPos } else { @@ -3900,14 +4083,18 @@ class ScalaTreeVisitor( val closeBraceIdx = remaining.lastIndexOf('}') if (closeBraceIdx >= 0) { endSpace = Space.format(remaining.substring(0, closeBraceIdx)) - cursor = endPos + // The body ends at the `}`; a marker beyond it is the caller's to claim. + cursor = cursor + closeBraceIdx + 1 } } } - val blockMarkers = if (isBraceless) { + var blockMarkers = if (isBraceless) { Markers.build(Collections.singletonList(new IndentedSyntax(Tree.randomId()))) } else Markers.EMPTY + if (selfTypeText != null) { + blockMarkers = blockMarkers.add(SelfType(Tree.randomId(), selfTypeText)) + } new J.Block( Tree.randomId(), @@ -3930,17 +4117,20 @@ class ScalaTreeVisitor( ).withMarkers(Markers.build(Collections.singletonList(new OmitBraces(Tree.randomId())))) } - // Update cursor to end of module def - if (md.span.exists) { - cursor = Math.max(cursor, md.span.end - offsetAdjustment) - } - // Create the class declaration with SObject marker (and PackageObject for `package object`) - val objectMarkers = if (isPackageObject) { + val objectBaseMarkers = if (isPackageObject) { Markers.build(Arrays.asList(SObject.create(), PackageObject(Tree.randomId()))) } else { Markers.build(Collections.singletonList(SObject.create())) } + val objectMarkers = if (moduleEndMarker != null) + objectBaseMarkers.add(EndMarker(Tree.randomId(), moduleEndMarker)) + else withEndMarker(objectBaseMarkers, md.span, md.name.toString) + + if (md.span.exists) { + cursor = Math.max(cursor, md.span.end - offsetAdjustment) + } + new J.ClassDeclaration( Tree.randomId(), prefix, @@ -4069,6 +4259,18 @@ class ScalaTreeVisitor( private def visitIf(ifTree: Trees.If[?]): J = { val prefix = extractPrefix(ifTree.span) + // Scala 3 `inline if`: the keyword opens the tree's span, ahead of the `if`. + var inlineKeywordText: String = null + val beforeIfLimit = Math.min(source.length, Math.max(cursor, Math.max(0, ifTree.cond.span.start - offsetAdjustment))) + if (cursor < beforeIfLimit) { + val head = source.substring(cursor, beforeIfLimit) + val ifIdx = positionOfNextIn(head, "if", 0) + if (ifIdx > 0 && positionOfNextIn(head.substring(0, ifIdx), "inline", 0) >= 0) { + inlineKeywordText = head.substring(0, ifIdx) + cursor = cursor + ifIdx + } + } + // Find where the condition parentheses start val adjustedStart = Math.max(0, ifTree.span.start - offsetAdjustment) @@ -4167,15 +4369,17 @@ class ScalaTreeVisitor( // For Scala 3 `if cond then ...`, advance the cursor past the `then` keyword // (which sits between the condition and the then-branch). The space before `then` // is absorbed into afterCondSpace; space after is the thenp's prefix. - if (ifIsParenless) { - val thenpStart = Math.max(0, ifTree.thenp.span.start - offsetAdjustment) - if (cursor < thenpStart && thenpStart <= source.length) { - val between = source.substring(cursor, thenpStart) - val thenIdx = positionOfNextIn(between, "then", 0) - if (thenIdx >= 0) { - afterCondSpace = Space.format(between.substring(0, thenIdx)) - cursor = cursor + thenIdx + 4 - } + var thenKeywordText: String = null + val thenpStart = Math.max(0, ifTree.thenp.span.start - offsetAdjustment) + if (cursor < thenpStart && thenpStart <= source.length) { + val between = source.substring(cursor, thenpStart) + val thenIdx = positionOfNextIn(between, "then", 0) + if (thenIdx >= 0) { + // A parenthesized condition may be followed by `then` as well, and there the + // keyword sits outside the control parentheses. + if (ifIsParenless) afterCondSpace = Space.format(between.substring(0, thenIdx)) + else thenKeywordText = between.substring(0, thenIdx + 4) + cursor = cursor + thenIdx + 4 } } @@ -4226,12 +4430,17 @@ class ScalaTreeVisitor( } } - // Update cursor to end of the if expression - updateCursor(ifTree.span.end) - - val ifMarkers = if (ifIsParenless) + val ifBaseMarkers = if (ifIsParenless) Markers.build(Collections.singletonList(new IndentedSyntax(Tree.randomId()))) + else if (thenKeywordText != null) + Markers.build(Collections.singletonList(ThenKeyword(Tree.randomId(), thenKeywordText))) else Markers.EMPTY + val ifInlineMarkers = if (inlineKeywordText != null) + ifBaseMarkers.add(InlineKeyword(Tree.randomId(), inlineKeywordText)) + else ifBaseMarkers + val ifMarkers = withEndMarker(ifInlineMarkers, ifTree.span) + + updateCursor(ifTree.span.end) new J.If( Tree.randomId(), @@ -4352,15 +4561,15 @@ class ScalaTreeVisitor( } // Scala 3 `while cond do ...` — advance the cursor past the `do` keyword. - if (whileIsParenless) { - val bodyStart = Math.max(0, whileTree.body.span.start - offsetAdjustment) - if (cursor < bodyStart && bodyStart <= source.length) { - val between = source.substring(cursor, bodyStart) - val doIdx = positionOfNextIn(between, "do", 0) - if (doIdx >= 0) { - afterCondSpace = Space.format(between.substring(0, doIdx)) - cursor = cursor + doIdx + 2 - } + var doKeywordText: String = null + val whileBodyStart = Math.max(0, whileTree.body.span.start - offsetAdjustment) + if (cursor < whileBodyStart && whileBodyStart <= source.length) { + val between = source.substring(cursor, whileBodyStart) + val doIdx = positionOfNextIn(between, "do", 0) + if (doIdx >= 0) { + if (whileIsParenless) afterCondSpace = Space.format(between.substring(0, doIdx)) + else doKeywordText = between.substring(0, doIdx + 2) + cursor = cursor + doIdx + 2 } } @@ -4371,12 +4580,14 @@ class ScalaTreeVisitor( case null => throw unmappedException(whileTree) } - // Update cursor to end of the while loop - updateCursor(whileTree.span.end) - - val whileMarkers = if (whileIsParenless) + val whileBaseMarkers = if (whileIsParenless) Markers.build(Collections.singletonList(new IndentedSyntax(Tree.randomId()))) + else if (doKeywordText != null) + Markers.build(Collections.singletonList(DoKeyword(Tree.randomId(), doKeywordText))) else Markers.EMPTY + val whileMarkers = withEndMarker(whileBaseMarkers, whileTree.span) + + updateCursor(whileTree.span.end) new J.WhileLoop( Tree.randomId(), @@ -4618,6 +4829,18 @@ class ScalaTreeVisitor( JRightPadded.build(iterable).withAfter(iterableAfter) ) + // Scala 3 allows `do` after the parenthesized generator. + var doKeywordText: String = null + val forBodyStart = Math.max(0, forTree.body.span.start - offsetAdjustment) + if (cursor < forBodyStart && forBodyStart <= source.length) { + val between = source.substring(cursor, forBodyStart) + val doIdx = positionOfNextIn(between, "do", 0) + if (doIdx >= 0) { + doKeywordText = between.substring(0, doIdx + 2) + cursor = cursor + doIdx + 2 + } + } + // Visit the body — wrap non-Statement expressions so `for (x <- xs) x + 1` parses val bodyJ = visitTree(forTree.body) val body: Statement = bodyJ match { @@ -4626,12 +4849,16 @@ class ScalaTreeVisitor( case null => throw unmappedException(forTree.body) } + val forBaseMarkers = if (doKeywordText != null) + Markers.EMPTY.addIfAbsent(ScalaForLoop.create()).add(DoKeyword(Tree.randomId(), doKeywordText)) + else Markers.EMPTY.addIfAbsent(ScalaForLoop.create()) + val forMarkers = withEndMarker(forBaseMarkers, forTree.span) updateCursor(forTree.span.end) val forEachLoop = new J.ForEachLoop( Tree.randomId(), prefix, - Markers.EMPTY.addIfAbsent(ScalaForLoop.create()), + forMarkers, control, JRightPadded.build(body) ) @@ -4740,7 +4967,14 @@ class ScalaTreeVisitor( } idx } - if (semiIdx >= 0) { + // The continuation of a comma-separated import group (`import a.*, b.*`) owns + // the comma ahead of it, which it reads back to for its own prefix. + val nextIsImportContinuation = i < block.stats.length - 1 && + block.stats(i + 1).isInstanceOf[Trees.ImportOrExport[?]] && + positionOfNextIn(between, ",", 0) >= 0 + if (nextIsImportContinuation) { + cursor = trailStart + } else if (semiIdx >= 0) { trailingSpace = if (semiIdx > 0) Space.format(between.substring(0, semiIdx)) else Space.EMPTY rpMarkers = Markers.EMPTY.add(new Semicolon(Tree.randomId())) cursor = trailStart + semiIdx + 1 @@ -4844,7 +5078,6 @@ class ScalaTreeVisitor( } } - // Update cursor to end of the block updateCursor(block.span.end) val blockMarkers = if (!hasBraces) { @@ -4863,6 +5096,9 @@ class ScalaTreeVisitor( private def visitClassDef(td: Trees.TypeDef[?]): J.ClassDeclaration = { val hasAnnotations = td.mods.annotations.nonEmpty + // Set while extracting the body, read when the declaration's markers are built + var endMarkerText: String = null + var derivesText: String = null // Handle annotations first val leadingAnnotations = new util.ArrayList[J.Annotation]() @@ -5095,16 +5331,48 @@ class ScalaTreeVisitor( } } else None + // A primary constructor may carry an access modifier: `class X private (i: Int)`, + // `class X private[pkg] (i: Int)`. Returns the source offset just past it. + val afterCtorModifier: Int = { + // Bounded by the class's own span: past it the keyword belongs to the next declaration. + val classEnd = Math.min(source.length, Math.max(0, td.span.end - offsetAdjustment)) + var i = cursor + while (i < classEnd && source.charAt(i).isWhitespace) i += 1 + val keyword = if (i >= classEnd) "" + else if (source.startsWith("private", i)) "private" + else if (source.startsWith("protected", i)) "protected" + else "" + if (keyword.isEmpty) { + cursor + } else { + val afterKeyword = i + keyword.length + val isWholeWord = afterKeyword >= source.length || + !(Character.isLetterOrDigit(source.charAt(afterKeyword)) || source.charAt(afterKeyword) == '_') + if (!isWholeWord) { + cursor + } else { + // optional qualifier, as in `private[pkg]` + var q = afterKeyword + while (q < source.length && source.charAt(q).isWhitespace) q += 1 + if (q < source.length && source.charAt(q) == '[') { + val close = positionOfNextIn(source, "]", q) + if (close >= 0) close + 1 else afterKeyword + } else afterKeyword + } + } + } + // Source has a primary-constructor parameter list iff the next non-whitespace char is `(`. val ctorParenPos: Int = { - var i = cursor + var i = afterCtorModifier while (i < source.length && source.charAt(i).isWhitespace) i += 1 if (i < source.length && source.charAt(i) == '(') i else -1 } val primaryConstructor: JContainer[Statement] = if (ctorParenPos >= 0) { val params = stripTrailingCommaArtifact(firstValueParamList.getOrElse(Nil)) - val parenSpace = ScalaSpace.format(source, cursor, ctorParenPos) + val ctorModifierText = source.substring(cursor, afterCtorModifier) + val parenSpace = ScalaSpace.format(source, afterCtorModifier, ctorParenPos) cursor = ctorParenPos + 1 val jParams = new util.ArrayList[JRightPadded[Statement]]() @@ -5181,19 +5449,30 @@ class ScalaTreeVisitor( keepScanning = false } } - val containerMarkers: Markers = if (extraListsBuf.nonEmpty) { + var containerMarkers: Markers = if (extraListsBuf.nonEmpty) { cursor = scanCursor Markers.build(Collections.singletonList( org.openrewrite.scala.marker.ExtraConstructorParamLists( Tree.randomId(), extraListsBuf.toString))) } else Markers.EMPTY + if (ctorModifierText.nonEmpty) { + containerMarkers = containerMarkers.add( + org.openrewrite.scala.marker.ConstructorModifier(Tree.randomId(), ctorModifierText)) + } JContainer.build(parenSpace, jParams, containerMarkers) } else { // No `(` in source — non-constructor class definition. Emit an empty container // marked with OmitParentheses so the printer skips emitting `(...)`. - JContainer.build(Space.EMPTY, new util.ArrayList[JRightPadded[Statement]](), - Markers.build(Collections.singletonList(new OmitParentheses(Tree.randomId())))) + // The modifier can still stand alone, as in `class Byte private extends AnyVal`. + val ctorMarkers = new util.ArrayList[org.openrewrite.marker.Marker]() + ctorMarkers.add(new OmitParentheses(Tree.randomId())) + if (afterCtorModifier > cursor) { + ctorMarkers.add(org.openrewrite.scala.marker.ConstructorModifier( + Tree.randomId(), source.substring(cursor, afterCtorModifier))) + cursor = afterCtorModifier + } + JContainer.build(Space.EMPTY, new util.ArrayList[JRightPadded[Statement]](), Markers.build(ctorMarkers)) } // Extract extends/implements from Template @@ -5254,7 +5533,10 @@ class ScalaTreeVisitor( if (sourceParents.size > 1) { val implementsList = new util.ArrayList[JRightPadded[TypeTree]]() - // Extract space before the first "with" or "extends" (if no extends clause) + // Separator introducing each parent after the first, in source order + val separators = new util.ArrayList[String]() + + // Extract space before the first separator, or "extends" if there is no extends clause var containerSpace = Space.EMPTY if (extendings == null && sourceParents.nonEmpty) { // No extends clause, so first trait uses "extends" @@ -5262,30 +5544,41 @@ class ScalaTreeVisitor( if (firstParent.span.exists) { containerSpace = sourceBefore("extends") } + separators.add("extends") } else if (extendings != null && sourceParents.size > 1) { - // We have extends, so look for first "with" - containerSpace = sourceBefore("with") + val (space, sep) = sourceBeforeParentSeparator() + containerSpace = space + separators.add(sep) } - + for (i <- 1 until sourceParents.size) { val parent = sourceParents(i) val savedCursorWith = cursor - val implType: TypeTree = parentTypeTree(visitTree(parent)) match { - case tt: TypeTree => tt - case null => - cursor = savedCursorWith - throw unmappedException(parent) + val implType: TypeTree = parent match { + case _: untpd.Parens => visitParentType(parent) + case _ => parentTypeTree(visitTree(parent)) + } + if (implType == null) { + cursor = savedCursorWith + throw unmappedException(parent) } + val sepMarkers = if (i - 1 < separators.size) { + Markers.EMPTY.add(ParentSeparator(Tree.randomId(), separators.get(i - 1))) + } else { + Markers.EMPTY + } + // Build the right-padded element val rightPadded = if (i < sourceParents.size - 1) { - // Not the last element, look for space before next "with" - val afterSpace = sourceBefore("with") - new JRightPadded(implType, afterSpace, Markers.EMPTY) + // Not the last element, consume the separator introducing the next one + val (afterSpace, sep) = sourceBeforeParentSeparator() + separators.add(sep) + new JRightPadded(implType, afterSpace, sepMarkers) } else { // Last element, no trailing space needed - JRightPadded.build(implType) + new JRightPadded(implType, Space.EMPTY, sepMarkers) } implementsList.add(rightPadded) @@ -5363,12 +5656,12 @@ class ScalaTreeVisitor( result } if (braceIndex >= 0 && (colonIndex < 0 || braceIndex < colonIndex)) { - val prefix = Space.format(afterCursor.substring(0, braceIndex)) + val prefix = splitDerives(afterCursor.substring(0, braceIndex), t => derivesText = t) cursor = cursor + braceIndex + 1 prefix } else if (colonIndex >= 0) { isClassBraceless = true - val prefix = Space.format(afterCursor.substring(0, colonIndex)) + val prefix = splitDerives(afterCursor.substring(0, colonIndex), t => derivesText = t) cursor = cursor + colonIndex + 1 prefix } else { @@ -5391,6 +5684,9 @@ class ScalaTreeVisitor( // Sort by source position to preserve source order val sortedBody = template.body.sortBy(s => if (s.span.exists) s.span.start else Int.MaxValue) val classEndForBody = if (td.span.exists) Math.max(0, td.span.end - offsetAdjustment) else source.length + // A self-type clause (`self =>`, `this: T =>`) sits between the body delimiter and the + // first statement. Dotty keeps it out of the body, so nothing else claims the source. + val selfTypeText = consumeSelfType(sortedBody, classEndForBody) for (idx <- sortedBody.indices) { val stat = sortedBody(idx) val isSyntheticStat = stat.span.isSynthetic || { @@ -5439,7 +5735,13 @@ class ScalaTreeVisitor( val classEnd = Math.max(0, td.span.end - offsetAdjustment) if (isClassBraceless) { if (cursor < classEnd) { - val es = ScalaSpace.format(source, cursor, Math.min(classEnd, source.length)) + val bodyEnd = Math.min(classEnd, source.length) + val es = endMarkerAt(cursor, bodyEnd) match { + case Some((start, text)) => + endMarkerText = source.substring(cursor, start + text.length) + Space.EMPTY + case None => ScalaSpace.format(source, cursor, bodyEnd) + } cursor = classEnd es } else Space.EMPTY @@ -5447,15 +5749,23 @@ class ScalaTreeVisitor( val remaining = source.substring(cursor, classEnd) val closeBraceIndex = remaining.lastIndexOf("}") if (closeBraceIndex >= 0) { + // An end marker follows the closing brace, outside the body + val afterBrace = cursor + closeBraceIndex + 1 + endMarkerAt(afterBrace, classEnd).foreach { case (start, text) => + endMarkerText = source.substring(afterBrace, start + text.length) + } cursor = classEnd Space.format(remaining.substring(0, closeBraceIndex)) } else Space.EMPTY } else Space.EMPTY } else Space.EMPTY - val classBlockMarkers = if (isClassBraceless) { + var classBlockMarkers = if (isClassBraceless) { Markers.build(Collections.singletonList(new IndentedSyntax(Tree.randomId()))) } else Markers.EMPTY + if (selfTypeText != null) { + classBlockMarkers = classBlockMarkers.add(SelfType(Tree.randomId(), selfTypeText)) + } new J.Block( Tree.randomId(), @@ -5478,9 +5788,18 @@ class ScalaTreeVisitor( updateCursor(td.span.end) } - val classDeclMarkers = if (isEnumCaseClass) { + var classDeclMarkers = if (isEnumCaseClass) { Markers.build(Collections.singletonList(SObject.create())) } else Markers.EMPTY + if (endMarkerText != null) { + classDeclMarkers = classDeclMarkers.add(EndMarker(Tree.randomId(), endMarkerText)) + } + if (derivesText != null) { + classDeclMarkers = classDeclMarkers.add(DerivesClause(Tree.randomId(), derivesText)) + } + if (endMarkerText == null) { + classDeclMarkers = withEndMarker(classDeclMarkers, td.span, td.name.toString) + } new J.ClassDeclaration( Tree.randomId(), @@ -5567,76 +5886,52 @@ class ScalaTreeVisitor( case sel: Trees.Select[?] => // Check if this is asInstanceOf if (sel.name.toString == "asInstanceOf" && ta.args.size == 1) { - // This is a type cast operation: obj.asInstanceOf[Type] - - // Visit the expression being cast (with its own prefix) - // The expression (sel.qualifier) is the object before .asInstanceOf - val expr = visitTree(sel.qualifier) match { + // `asInstanceOf` is a method on `Any`, and Dotty gives it the same tree as any + // other generic call, so it is modelled as one: the name's prefix holds the run + // after the `.`, and the type argument list is the method's type parameters. + val select = visitTree(sel.qualifier) match { case e: Expression => e case j: J => new S.StatementExpression(Tree.randomId(), j) case null => throw unmappedException(ta) } - - // Capture whitespace between the qualifier and ".asInstanceOf" - // (e.g. when ".asInstanceOf" sits on its own line as part of a chain). - val asInstanceOfEnd = Math.max(0, sel.span.end - offsetAdjustment) - val asInstanceOfNameLen = "asInstanceOf".length - val dotPos = asInstanceOfEnd - asInstanceOfNameLen - 1 - val asInstanceOfPrefix: Space = - if (cursor >= 0 && cursor <= dotPos && dotPos <= source.length) { - ScalaSpace.format(source.substring(cursor, dotPos)) - } else { - Space.EMPTY - } - updateCursor(sel.span.end) - - // Now handle the type argument in brackets - // Extract any space before the opening bracket - val typeArgStart = ta.args.head.span.start - offsetAdjustment - val spaceBeforeBracket = if (cursor < typeArgStart && typeArgStart <= source.length) { - val between = source.substring(cursor, typeArgStart) - // Find the bracket position - val bracketPos = positionOfNextIn(between, "[", 0) - if (bracketPos >= 0) { - cursor = cursor + bracketPos + 1 // Move past the bracket - Space.format(between.substring(0, bracketPos)) - } else { - Space.EMPTY - } - } else { - Space.EMPTY - } - - // Visit the target type. Use the type-position helper so that function types - // (`A => B`), tuple types, union/intersection types, etc. are mapped to a - // `TypeTree` rather than being misread as expressions. + val dotIdx = positionOfNext(".", cursor) + val selectAfter = if (dotIdx > cursor) ScalaSpace.format(source, cursor, dotIdx) else Space.EMPTY + if (dotIdx >= cursor) cursor = dotIdx + 1 + val nameStart = indexOfNextNonWhitespace(cursor) + val namePrefix = if (nameStart > cursor) ScalaSpace.format(source, cursor, nameStart) else Space.EMPTY + cursor = nameStart + "asInstanceOf".length + + val openBracket = positionOfNext("[", cursor) + val beforeBracket = if (openBracket > cursor) ScalaSpace.format(source, cursor, openBracket) else Space.EMPTY + if (openBracket >= cursor) cursor = openBracket + 1 + + // The target type goes through the type-position helper so function types + // (`A => B`), tuple, union and intersection types map to a `TypeTree`. The type + // argument container holds expressions, which every type legal here also is. val targetType = visitTypeTree(ta.args.head) match { - case tt: TypeTree => tt - case null => throw unmappedException(ta) + case tt: TypeTree with Expression => tt + case _ => throw unmappedException(ta) } - - // Update cursor past the closing bracket + val closeBracket = positionOfNext("]", cursor) + val beforeClose = if (closeBracket > cursor) ScalaSpace.format(source, cursor, closeBracket) else Space.EMPTY + if (closeBracket >= cursor) cursor = closeBracket + 1 updateCursor(ta.span.end) - val typeCastMarkers = - if (asInstanceOfPrefix.getWhitespace.nonEmpty || !asInstanceOfPrefix.getComments.isEmpty) { - Markers.EMPTY.addIfAbsent(AsInstanceOfPrefix.create(asInstanceOfPrefix)) - } else { - Markers.EMPTY - } + val typeArgs = new util.ArrayList[JRightPadded[Expression]]() + typeArgs.add(new JRightPadded[Expression](targetType, beforeClose, Markers.EMPTY)) + val noArgs = JContainer.build(Space.EMPTY, Collections.emptyList[JRightPadded[Expression]](), + Markers.build(Collections.singletonList(new OmitParentheses(Tree.randomId())))) - return new J.TypeCast( + return new J.MethodInvocation( Tree.randomId(), - Space.EMPTY, // TypeCast itself has no prefix - the space is handled by the variable initializer - typeCastMarkers, - new J.ControlParentheses[TypeTree]( - Tree.randomId(), - spaceBeforeBracket, - Markers.EMPTY, - JRightPadded.build(targetType) - ), - expr + Space.EMPTY, + Markers.EMPTY, + new JRightPadded(select, selectAfter, Markers.EMPTY), + JContainer.build(beforeBracket, typeArgs, Markers.EMPTY), + ident("asInstanceOf", namePrefix), + noArgs, + methodTypeOfTree(ta) ) } @@ -6511,14 +6806,6 @@ class ScalaTreeVisitor( if (closeParen >= 0) cursor = closeParen + 1 } - // Build additional param lists as J.Lambda.Parameters for later wrapping - for (extraList <- valueParamLists.tail) { - val extraParams = extraList.collect { case vd: Trees.ValDef[?] => vd } - if (extraParams.nonEmpty) { - curriedParamLists.add(visitParamListAsLambdaParams(extraParams)) - } - } - JContainer.build(parenSpace, jParams, Markers.EMPTY) } else if (hasParensInSource) { buildEmptyParamList() @@ -6535,6 +6822,17 @@ class ScalaTreeVisitor( Markers.build(Collections.singletonList(new org.openrewrite.scala.marker.OmitBraces(Tree.randomId())))) } + // Visited here so the cursor stays in token order: the curried lists sit between the + // first list and the return type, whose `:` the scan below looks for. + if (valueParamLists.nonEmpty) { + for (extraList <- valueParamLists.tail) { + val extraParams = extraList.collect { case vd: Trees.ValDef[?] => vd } + if (extraParams.nonEmpty) { + curriedParamLists.add(visitParamListAsLambdaParams(extraParams)) + } + } + } + // Handle return type `: ReturnType` — only if explicitly written in source val returnTypeExpression: TypeTree = dd.tpt match { case tpt if tpt != untpd.EmptyTree && tpt.span.exists => @@ -6575,6 +6873,7 @@ class ScalaTreeVisitor( // Handle method body var beforeEqualsSpace: Space = Space.EMPTY + var sawEquals = false val body: J.Block = dd.rhs match { case rhs if isProcedureSyntax && rhs.span.isSynthetic => // Procedure syntax: Scala 3 parser replaces body with `_root_.scala.Predef.???`. @@ -6589,6 +6888,7 @@ class ScalaTreeVisitor( if (equalsIdx >= 0) { beforeEquals = Space.format(beforeBody.substring(0, equalsIdx)) cursor = cursor + equalsIdx + 1 + sawEquals = true } } beforeEqualsSpace = beforeEquals @@ -6620,7 +6920,12 @@ class ScalaTreeVisitor( // For procedure syntax, cursor is already correctly set by reparseProcedureBody. // Don't use dd.span.end because it extends past the actual method body due to synthetic ??? span. + var endMarkerText: String = null if (!isProcedureSyntax) { + val defEnd = Math.min(Math.max(0, dd.span.end - offsetAdjustment), source.length) + endMarkerAt(cursor, defEnd).foreach { case (start, text) => + endMarkerText = source.substring(cursor, start + text.length) + } updateCursor(dd.span.end) } @@ -6663,9 +6968,14 @@ class ScalaTreeVisitor( if (isCurried) { markerList.add(new Curried(Tree.randomId())) } - if (beforeEqualsSpace != Space.EMPTY) { + // Record the run before `=` even when empty, so the printer does not fall back to the + // body's prefix and print it on both sides of the `=`. + if (sawEquals) { markerList.add(org.openrewrite.scala.marker.MethodBodyEqualsPrefix.create(beforeEqualsSpace)) } + if (endMarkerText != null) { + markerList.add(EndMarker(Tree.randomId(), endMarkerText)) + } val methodMarkers = if (!markerList.isEmpty) Markers.build(markerList) else Markers.EMPTY new J.MethodDeclaration( @@ -6866,6 +7176,22 @@ class ScalaTreeVisitor( val paramModifiers = new util.ArrayList[J.Modifier]() val isUsing = vd.mods != null && vd.mods.is(Flags.Given) val isScala2Implicit = vd.mods != null && vd.mods.is(Flags.Implicit) && !isUsing + // The untyped tree carries no flag for an `inline` parameter and its span opens on the + // keyword, so read it from the source at the cursor. It may follow `using`, as in + // `def f(using inline x: T)`. + def consumeInlineKeyword(): Boolean = { + var i = cursor + while (i < source.length && Character.isWhitespace(source.charAt(i))) i += 1 + val end = i + "inline".length + if (source.startsWith("inline", i) && + (end >= source.length || !(Character.isLetterOrDigit(source.charAt(end)) || source.charAt(end) == '_'))) { + paramModifiers.add(new J.Modifier(Tree.randomId(), + if (i > cursor) Space.format(source.substring(cursor, i)) else Space.EMPTY, + Markers.EMPTY, "inline", J.Modifier.Type.LanguageExtension, Collections.emptyList())) + cursor = i + "inline".length + true + } else false + } val prefix: Space = if (isScala2Implicit || isUsing) { val keyword = if (isUsing) "using" else "implicit" val spanStart = Math.max(0, vd.span.start - offsetAdjustment) @@ -6877,9 +7203,12 @@ class ScalaTreeVisitor( paramModifiers.add(new J.Modifier(Tree.randomId(), modPrefix, Markers.EMPTY, keyword, J.Modifier.Type.LanguageExtension, Collections.emptyList())) cursor += kwIdx + keyword.length + consumeInlineKeyword() Space.EMPTY } else extractPrefix(vd.span) } else extractPrefix(vd.span) + } else if (consumeInlineKeyword()) { + Space.EMPTY } else extractPrefix(vd.span) val paramStart = Math.max(0, vd.span.start - offsetAdjustment) val paramEnd = Math.max(0, vd.span.end - offsetAdjustment) @@ -7040,17 +7369,19 @@ class ScalaTreeVisitor( // The handler is a synthetic Match whose case clauses are the catch patterns. val catches: JLeftPadded[J.Block] = if (!parsedTry.handler.isEmpty && parsedTry.handler.span.exists) { - val cases: List[Trees.CaseDef[?]] = parsedTry.handler match { - case matchTree: Trees.Match[?] => matchTree.cases - case _ => Nil + parsedTry.handler match { + case matchTree: Trees.Match[?] => buildCatchBlock(matchTree.cases) + case handler => buildCatchExpression(handler) } - buildCatchBlock(cases) } else null val finallyBlock = buildTryFinalizer(parsedTry.finalizer) + val parsedTryMarkers = withEndMarker(Markers.EMPTY, parsedTry.span) updateCursor(parsedTry.span.end) - buildTryNode(prefix, body, catches, finallyBlock) + val parsedTryNode = buildTryNode(prefix, body, catches, finallyBlock) + if (parsedTryMarkers.getMarkers.isEmpty) parsedTryNode + else parsedTryNode.withMarkers[J](parsedTryNode.getMarkers.add(parsedTryMarkers.getMarkers.get(0))) } private def visitTryImpl(tryTree: Trees.Try[?]): J = { @@ -7065,8 +7396,11 @@ class ScalaTreeVisitor( val finallyBlock = buildTryFinalizer(tryTree.finalizer) + val tryMarkers = withEndMarker(Markers.EMPTY, tryTree.span) updateCursor(tryTree.span.end) - buildTryNode(prefix, body, catches, finallyBlock) + val tryNode = buildTryNode(prefix, body, catches, finallyBlock) + if (tryMarkers.getMarkers.isEmpty) tryNode + else tryNode.withMarkers[J](tryNode.getMarkers.add(tryMarkers.getMarkers.get(0))) } /** Visit the `try` body, wrapping a bare statement/expression in an OmitBraces block. */ @@ -7123,6 +7457,26 @@ class ScalaTreeVisitor( JLeftPadded.build(casesBlock).withBefore(beforeCatch) } + /** Build the handler of `try expr catch handler`, where the handler is a partial-function + * expression rather than a list of `case` clauses. */ + private def buildCatchExpression(handler: Trees.Tree[?]): JLeftPadded[J.Block] = { + val catchAbs = positionOfNext("catch") + val beforeCatch = if (catchAbs > cursor) ScalaSpace.format(source, cursor, catchAbs) else Space.EMPTY + if (catchAbs >= cursor) cursor = catchAbs + "catch".length + val stmt: Statement = visitTree(handler) match { + case s: Statement => s + case e: Expression => new S.ExpressionStatement(Tree.randomId(), e) + case j: J => new S.ExpressionStatement(Tree.randomId(), new S.StatementExpression(Tree.randomId(), j)) + case null => throw unmappedException(handler) + } + val stmts = new util.ArrayList[JRightPadded[Statement]]() + stmts.add(JRightPadded.build(stmt)) + val block = new J.Block(Tree.randomId(), Space.EMPTY, + Markers.build(Collections.singletonList(new OmitBraces(Tree.randomId()))), + JRightPadded.build(false), stmts, Space.EMPTY) + JLeftPadded.build(block).withBefore(beforeCatch) + } + /** Visit the `finally` block, if present, capturing the space before the `finally` keyword. */ private def buildTryFinalizer(finalizer: Trees.Tree[?]): JLeftPadded[J.Block] = { if (finalizer.isEmpty || !finalizer.span.exists) return null @@ -7203,11 +7557,11 @@ class ScalaTreeVisitor( // Scala 3 selector-style match `selector.match { ... }`: the `match` keyword is preceded // by a `.`. Record it via a marker so the dot isn't stored as (non-whitespace) Space. val beforeMatch = if (mi > 0) ms.substring(0, mi) else "" - val dotIdx = beforeMatch.indexOf('.') + val dotIdx = positionOfNextIn(beforeMatch, ".", 0) val isDottedMatch = dotIdx >= 0 val matchKeywordSpace = - if (isDottedMatch) Space.format(beforeMatch.substring(0, dotIdx)) - else if (mi > 0) Space.format(beforeMatch) + if (isDottedMatch) ScalaSpace.format(beforeMatch.substring(0, dotIdx)) + else if (mi > 0) ScalaSpace.format(beforeMatch) else Space.EMPTY if (mi >= 0) cursor = cursor + mi + 5 // Scala 3 `x match\n case ...` has no `{` before the cases — detect that form. @@ -7219,12 +7573,13 @@ class ScalaTreeVisitor( if (isBraceForm) cursor = cursor + braceIdx + 1 val casesBlock = buildCasesBlock(matchTree, isBraceForm).withPrefix(matchBraceSpace) - updateCursor(matchTree.span.end) - val selectorParens = new J.ControlParentheses[Expression](Tree.randomId(), Space.EMPTY, Markers.EMPTY, JRightPadded.build(selector).withAfter(matchKeywordSpace)) val markersList = new util.ArrayList[org.openrewrite.marker.Marker]() if (!isBraceForm) markersList.add(new IndentedSyntax(Tree.randomId())) if (isDottedMatch) markersList.add(DottedMatch.create()) - new J.Switch(Tree.randomId(), prefix, Markers.build(markersList), selectorParens, casesBlock) + val matchMarkers = withEndMarker(Markers.build(markersList), matchTree.span) + updateCursor(matchTree.span.end) + val selectorParens = new J.ControlParentheses[Expression](Tree.randomId(), Space.EMPTY, Markers.EMPTY, JRightPadded.build(selector).withAfter(matchKeywordSpace)) + new J.Switch(Tree.randomId(), prefix, matchMarkers, selectorParens, casesBlock) } /** @@ -7657,7 +8012,7 @@ class ScalaTreeVisitor( JContainer.build(Space.EMPTY, rpPatterns, Markers.EMPTY)) } - private def visitSingletonTypeTree(stt: Trees.SingletonTypeTree[?]): S.SingletonType = { + private def visitSingletonTypeTree(stt: Trees.SingletonTypeTree[?]): TypeTree = { // Singleton type reference: `None.type`, `obj.type`, `foo.bar.type` val prefix = extractPrefix(stt.span) val qualifier = visitTree(stt.ref) match { @@ -7666,14 +8021,17 @@ class ScalaTreeVisitor( case null => throw new UnsupportedOperationException( s"SingletonTypeTree.ref did not produce an Expression: ${stt.ref.getClass.getSimpleName}") } - // After visiting qualifier, cursor is at the end of qualifier. - // The remaining source should be whitespace followed by ".type". - val endPos = Math.max(0, stt.span.end - offsetAdjustment) - val between = if (cursor < endPos && endPos <= source.length) source.substring(cursor, endPos) else "" - val dotIdx = positionOfNextIn(between, ".", 0) - val beforeType = if (dotIdx > 0) Space.format(between.substring(0, dotIdx)) else Space.EMPTY - cursor = endPos - new S.SingletonType(Tree.randomId(), prefix, Markers.EMPTY, qualifier, beforeType, typeFor(stt.span)) + if (stt.ref.isInstanceOf[Trees.Literal[?]]) { + // Dotty wraps a literal type in a SingletonTypeTree + new S.LiteralType(Tree.randomId(), prefix, Markers.EMPTY, qualifier, typeFor(stt.span)) + } else { + val endPos = Math.max(0, stt.span.end - offsetAdjustment) + val between = if (cursor < endPos && endPos <= source.length) source.substring(cursor, endPos) else "" + val dotIdx = positionOfNextIn(between, ".", 0) + val beforeType = if (dotIdx > 0) Space.format(between.substring(0, dotIdx)) else Space.EMPTY + cursor = endPos + new S.SingletonType(Tree.randomId(), prefix, Markers.EMPTY, qualifier, beforeType, typeFor(stt.span)) + } } private def visitRefinedTypeTree(rtt: Trees.RefinedTypeTree[?]): S.RefinedType = { @@ -7734,13 +8092,37 @@ class ScalaTreeVisitor( new S.RefinedType(Tree.randomId(), prefix, Markers.EMPTY, parent, refinements, typeFor(rtt.span)) } + /** Stands in for the delimiter of an anonymous class that has no body. */ + private val NoBodyDelimiter: Char = '\u0000' + private def visitAnnotated(ann: Trees.Annotated[?]): J = { // Dotty's `Annotated` covers both annotated expressions (`e: @ann`, with colon) and // annotated types (`T @ann`, without colon). Branch on the source to produce the // right LST node: `S.AnnotatedExpression` for the former, `S.AnnotatedType` for // the latter. val prefix = extractPrefix(ann.span) - val arg: J = visitTree(ann.arg) + // A parenthesized annotated arg is a type, `(Context ?=> Symbol) @unchecked`, and + // J.Parentheses is not a TypeTree. The parenthesized type form is also an Expression, + // so it still serves an annotated expression. + val arg: J = ann.arg match { + case p: untpd.Parens => Option(visitParentType(p)).map(_.asInstanceOf[J]).getOrElse(visitTree(ann.arg)) + case t: untpd.Tuple => Option(visitTypeTree(t)).map(_.asInstanceOf[J]).getOrElse(visitTree(ann.arg)) + case _ => visitTree(ann.arg) + } + // Capture-checking syntax (`T^`, `T^{it}`) is a suffix on the type it follows. + val captureText = consumeCaptureSet() + if (captureText != null) { + updateCursor(ann.span.end) + return arg match { + case tt: TypeTree => + // The wrapped type carries the space ahead of it, which an enclosing type such as + // the `|` of `T^{this} | Null` has already claimed when the prefix comes back empty. + val prefixed: TypeTree = tt.withPrefix[TypeTree](prefix) + prefixed.withMarkers[TypeTree]( + prefixed.getMarkers.add(CaptureSet(Tree.randomId(), captureText))).asInstanceOf[J] + case other => other + } + } val annotStart = Math.max(0, ann.annot.span.start - offsetAdjustment) val between = if (cursor < annotStart && annotStart <= source.length) source.substring(cursor, annotStart) else "" val colonIdx = positionOfNextIn(between, ":", 0) @@ -7757,6 +8139,10 @@ class ScalaTreeVisitor( if (isAnnotatedType) { val typeExpr: TypeTree = arg match { case tt: TypeTree => tt + case par: J.Parentheses[?] if par.getTree.isInstanceOf[TypeTree] => + val inner = par.withPrefix[J.Parentheses[TypeTree]](Space.EMPTY) + new J.ParenthesizedTypeTree(Tree.randomId(), par.getPrefix, Markers.EMPTY, + Collections.emptyList(), inner) case _ => throw new UnsupportedOperationException( s"Annotated.arg in type position did not produce a TypeTree: ${ann.arg.getClass.getSimpleName}") } @@ -7862,7 +8248,12 @@ class ScalaTreeVisitor( var i = 0 while (i < firstClause.size) { val pTree = firstClause.get(i) - val pJ = visitTree(pTree) + // Parameter position: a ValDef here can carry `inline`, whose keyword the general + // definition path would mistake for part of the name. + val pJ = pTree match { + case vd: Trees.ValDef[?] => visitMethodParameter(vd) + case other => visitTree(other) + } val pStmt: Statement = pJ match { case s: Statement => s case _ => throw new UnsupportedOperationException( @@ -7928,8 +8319,14 @@ class ScalaTreeVisitor( val endPos = Math.max(0, ext.span.end - offsetAdjustment) val remaining = if (cursor < endPos && endPos <= source.length) source.substring(cursor, endPos) else "" + var extEndMarker: String = null val endSpace = if (isExtBraceless) { - Space.format(remaining) + endMarkerAt(cursor, endPos) match { + case Some((start, text)) => + extEndMarker = source.substring(cursor, start + text.length) + Space.EMPTY + case None => Space.format(remaining) + } } else { val closeBrace = remaining.lastIndexOf('}') if (closeBrace > 0) Space.format(remaining.substring(0, closeBrace)) else Space.EMPTY @@ -7940,7 +8337,10 @@ class ScalaTreeVisitor( } else Markers.EMPTY val body = new J.Block(Tree.randomId(), blockPrefix, blockMarkers, JRightPadded.build(false), methodStmts, endSpace) - S.ExtensionMethods.build(Tree.randomId(), prefix, Markers.EMPTY, typeParameters, parameters, body) + val extMarkers = + if (extEndMarker == null) Markers.EMPTY + else Markers.EMPTY.add(EndMarker(Tree.randomId(), extEndMarker)) + S.ExtensionMethods.build(Tree.randomId(), prefix, extMarkers, typeParameters, parameters, body) } /** Build the type-parameter clause `[A, B]` of an extension, advancing the cursor past `]`. */ @@ -8004,6 +8404,7 @@ class ScalaTreeVisitor( case _ => (S.For.Enumerator.Kind.Guard, null, enumTree, "if") } + var casePattern = false val enumPrefix: Space = if (kind == S.For.Enumerator.Kind.Guard) { // Locate the `if` keyword. Everything before `if` is this enumerator's prefix. val rhsStart = Math.max(0, rhsTree.span.start - offsetAdjustment) @@ -8015,7 +8416,19 @@ class ScalaTreeVisitor( s } else Space.EMPTY } else { - extractPrefix(enumTree.span) + // `for case (k, v) <- pairs` filters by pattern. Dotty leaves the keyword out of both + // the enumerator and the pattern, so consume it here; the pattern keeps the space + // that follows. + val lhsStart = Math.max(0, lhsTree.span.start - offsetAdjustment) + val lead = if (cursor < lhsStart && lhsStart <= source.length) source.substring(cursor, lhsStart) else "" + val caseIdx = positionOfNextIn(lead, "case", 0) + if (caseIdx >= 0) { + casePattern = true + cursor = cursor + caseIdx + 4 + Space.format(lead.substring(0, caseIdx)) + } else { + extractPrefix(enumTree.span) + } } var lhs: J = null @@ -8082,7 +8495,10 @@ class ScalaTreeVisitor( } else Space.EMPTY } - val enumerator = new S.For.Enumerator(Tree.randomId(), enumPrefix, Markers.EMPTY, + val enumMarkers = if (casePattern) + Markers.build(Collections.singletonList(CasePattern(Tree.randomId()))) + else Markers.EMPTY + val enumerator = new S.For.Enumerator(Tree.randomId(), enumPrefix, enumMarkers, kind, lhs, beforeOp, rhs) new JRightPadded(enumerator, after, rpMarkers) } @@ -8257,6 +8673,7 @@ class ScalaTreeVisitor( } // Capture space before body / `yield` / `do` + var doKeywordText: String = null val bodyStart = Math.max(0, body.span.start - offsetAdjustment) val rawBetween = if (cursor < bodyStart && bodyStart <= source.length) source.substring(cursor, bodyStart) else "" val beforeBody: Space = if (yielding) { @@ -8266,24 +8683,26 @@ class ScalaTreeVisitor( cursor = cursor + yieldIdx + "yield".length s } else Space.EMPTY - } else if (isParenless) { - // Paren-less `do` form: capture space before `do`, advance past `do`. - // Whitespace after `do` becomes the body's prefix. + } else { + // Capture the space before `do` and advance past it; whitespace after `do` + // becomes the body's prefix. val doIdx = positionOfNextIn(rawBetween, "do", 0) if (doIdx >= 0) { - val s = Space.format(rawBetween.substring(0, doIdx)) + if (!isParenless) doKeywordText = rawBetween.substring(0, doIdx + "do".length) cursor = cursor + doIdx + "do".length - s - } else Space.EMPTY - } else { - Space.format(rawBetween) + if (isParenless) Space.format(rawBetween.substring(0, doIdx)) else Space.EMPTY + } else if (isParenless) Space.EMPTY + else Space.format(rawBetween) } - if (!yielding && !isParenless) cursor = bodyStart + if (!yielding && !isParenless && doKeywordText == null) cursor = bodyStart val bodyJ = visitTree(body) updateCursor(spanEnd) - S.For.build(Tree.randomId(), prefix, Markers.EMPTY, enumerators, openBracket, + val forMarkers = if (doKeywordText != null) + Markers.build(Collections.singletonList(DoKeyword(Tree.randomId(), doKeywordText))) + else Markers.EMPTY + S.For.build(Tree.randomId(), prefix, forMarkers, enumerators, openBracket, yielding, beforeBody, bodyJ, null) } @@ -8599,6 +9018,26 @@ class ScalaTreeVisitor( * argument is itself a synthetic `ValDef` whose `tpt` carries the actual parameter * type and source span. */ + /** Index just past a leading parenthesized group in {@code text}, or 0 when it has none. */ + private def closeOfLeadingParen(text: String): Int = { + val open = text.indexWhere(!Character.isWhitespace(_)) + if (open < 0 || text.charAt(open) != '(') 0 + else { + var depth = 0 + var i = open + var close = -1 + while (i < text.length && close < 0) { + text.charAt(i) match { + case '(' => depth += 1 + case ')' => depth -= 1; if (depth == 0) close = i + case _ => + } + i += 1 + } + if (close >= 0) close + 1 else 0 + } + } + private def visitFunctionType(func: untpd.Function): S.FunctionType = { val funcStart = Math.max(0, func.span.start - offsetAdjustment) val funcEnd = Math.max(0, func.span.end - offsetAdjustment) @@ -8611,8 +9050,14 @@ class ScalaTreeVisitor( source.substring(funcStart, funcEnd) } else "" - // Find the arrow within the function-type source. - val relArrowIdx = positionOfNextIn(funcSource, "=>", 0) + // Find the arrow within the function-type source. A parenthesized parameter list can + // hold an arrow of its own (`(A => Unit) => B`), so the type's arrow follows the list. + val arrowSearchFrom = closeOfLeadingParen(funcSource) + val fatArrowIdx = positionOfNextIn(funcSource, "=>", arrowSearchFrom) + // capture checking writes a pure function as `A -> B` + val pureArrowIdx = positionOfNextIn(funcSource, "->", arrowSearchFrom) + val isPureArrow = pureArrowIdx >= 0 && (fatArrowIdx < 0 || pureArrowIdx < fatArrowIdx) + val relArrowIdx = if (isPureArrow) pureArrowIdx else fatArrowIdx val arrowAbs = if (relArrowIdx >= 0) funcStart + relArrowIdx else funcEnd // Detect whether the parameter list is parenthesized. A single unnamed param like @@ -8699,7 +9144,9 @@ class ScalaTreeVisitor( if (funcEnd > cursor) cursor = funcEnd - val funcMarkers = if (isContextArrow) + val funcMarkers = if (isPureArrow) + Markers.build(Collections.singletonList(PureFunctionArrow(Tree.randomId()))) + else if (isContextArrow) Markers.build(Collections.singletonList(new ContextFunctionArrow(java.util.UUID.randomUUID()))) else Markers.EMPTY @@ -8822,6 +9269,73 @@ class ScalaTreeVisitor( * arguments stay first-class LST nodes instead of being crammed into a * `J.Identifier` name. Returns null when no rich mapping fits, so callers can fall back. */ + /** Consumes a capture-set suffix (`^`, `^{it}`) at the cursor, returning its source. */ + private def consumeCaptureSet(): String = { + var i = cursor + while (i < source.length && (source.charAt(i) == ' ' || source.charAt(i) == '\t')) i += 1 + if (i >= source.length || source.charAt(i) != '^') { + null + } else { + var end = i + 1 + if (end < source.length && source.charAt(end) == '{') { + val close = positionOfMatchingClose('{', '}', end + 1) + if (close >= 0) end = close + 1 + } + val text = source.substring(cursor, end) + cursor = end + text + } + } + + + /** Consumes a call-site `using` keyword at the cursor, returning the source through it. */ + private def consumeUsingKeyword(): String = { + var i = cursor + while (i < source.length && source.charAt(i).isWhitespace) i += 1 + val end = i + "using".length + if (end <= source.length && source.startsWith("using", i) && + (end >= source.length || !(Character.isLetterOrDigit(source.charAt(end)) || source.charAt(end) == '_'))) { + val text = source.substring(cursor, end) + cursor = end + text + } else null + } + + /** Attaches a consumed `using` keyword to the argument list. */ + private def withUsing(args: util.ArrayList[JRightPadded[Expression]], text: String): Unit = { + if (text != null && !args.isEmpty) { + val first = args.get(0) + val elem = first.getElement + val marked = elem.withMarkers[Expression](elem.getMarkers.add(UsingArguments(Tree.randomId(), text))) + args.set(0, first.withElement(marked)) + } + } + + /** Visits a template parent. A parenthesized parent, as in `new X with (A => B)`, is a + * type rather than an expression, so it is visited in type position and stays + * parenthesized. Returns null when the parent maps to no type. + */ + private def visitParentType(tree: Trees.Tree[?]): TypeTree = tree match { + case p: untpd.Parens => + val prefix = extractPrefix(p.span) + val openIdx = positionOfNext("(", cursor) + if (openIdx >= 0) cursor = openIdx + 1 + val inner = visitTypeTree(p.t) + if (inner == null) null + else { + val beforeClose = sourceBefore(")") + new J.ParenthesizedTypeTree(Tree.randomId(), prefix, Markers.EMPTY, + Collections.emptyList(), + new J.Parentheses[TypeTree](Tree.randomId(), Space.EMPTY, Markers.EMPTY, + new JRightPadded[TypeTree](inner, beforeClose, Markers.EMPTY))) + } + case other => + visitTree(other) match { + case t: TypeTree => t + case _ => null + } + } + private def parentTypeTree(visited: J): TypeTree = visited match { case tt: TypeTree => tt case nc: J.NewClass if nc.getClazz != null && nc.getPadding.getArguments != null => @@ -8855,10 +9369,19 @@ class ScalaTreeVisitor( var i = 0 while (i < items.size) { val elem = convert(items(i)) - val after = - if (i < last) sourceBefore(",") - else sourceBefore(endDelim) - padded.add(JRightPadded.build(elem).withAfter(after)) + if (i < last) { + padded.add(JRightPadded.build(elem).withAfter(sourceBefore(","))) + } else { + // the run before the closing delimiter can hold a trailing comma + val delimIdx = positionOfNext(endDelim) + if (delimIdx < 0) { + padded.add(JRightPadded.build(elem).withAfter(sourceBefore(endDelim))) + } else { + val (after, elemMarkers) = trailingCommaBefore(cursor, delimIdx) + cursor = delimIdx + endDelim.length + padded.add(new JRightPadded(elem, after, elemMarkers)) + } + } i += 1 } JContainer.build(prefix, padded, markers) @@ -8915,6 +9438,16 @@ class ScalaTreeVisitor( * skipping over line/block comments. Similar to sourceBefore in * ReloadableJava17Parser. */ + /** Consumes the parent-list separator at the cursor, returning the space ahead of it + * and the separator itself. + */ + private def sourceBeforeParentSeparator(): (Space, String) = { + val withIdx = positionOfNext("with") + val commaIdx = positionOfNext(",") + val sep = if (withIdx >= 0 && (commaIdx < 0 || withIdx < commaIdx)) "with" else "," + (sourceBefore(sep), sep) + } + private def sourceBefore(untilDelim: String): Space = { val delimIndex = positionOfNext(untilDelim) if (delimIndex < 0) { @@ -8987,6 +9520,93 @@ class ScalaTreeVisitor( -1 } + /** Adds an {@link EndMarker} for an end marker sitting between the cursor and the end of + * {@code span}, if any. + */ + private def withEndMarker(markers: Markers, span: Spans.Span, name: String = null): Markers = { + // A `val`'s or `given`'s end marker sits beyond dotty's span for the definition, so when + // the name is known, bound the search by the source and claim the marker only if it names + // this definition — that is also what keeps a nested definition from taking its parent's. + val limit = + if (name != null) source.length + else if (span.exists) Math.min(Math.max(0, span.end - offsetAdjustment), source.length) + else return markers + val from = cursor + endMarkerAt(from, limit) match { + case Some((start, text)) if name == null || text.substring("end".length).trim == name => + cursor = start + text.length + markers.add(EndMarker(Tree.randomId(), source.substring(from, cursor))) + case _ => markers + } + } + + /** Splits a `derives` clause out of the text ahead of a body delimiter, handing it to + * {@code claim}. What remains is whitespace and belongs in the body's prefix. + */ + private def splitDerives(text: String, claim: String => Unit): Space = { + val idx = positionOfNextIn(text, "derives", 0) + if (idx < 0) Space.format(text) + else { + // the clause keeps the space ahead of it; only the run before the delimiter is prefix + val clauseEnd = text.stripTrailing().length + claim(text.substring(0, clauseEnd)) + Space.format(text.substring(clauseEnd)) + } + } + + /** Consumes a template's self-type clause, returning its verbatim source or null. + * The `=>` can only belong to a self type here: any body statement starts later. + */ + private def consumeSelfType(sortedBody: Seq[Trees.Tree[?]], bodyEnd: Int): String = { + val firstStatStart = sortedBody.collectFirst { + case st if st.span.exists && !st.span.isSynthetic => Math.max(0, st.span.start - offsetAdjustment) + }.getOrElse(bodyEnd) + if (cursor >= firstStatStart || firstStatStart > source.length) { + null + } else { + val between = source.substring(cursor, firstStatStart) + val arrow = positionOfNextIn(between, "=>", 0) + if (arrow < 0) null + else { + val text = between.substring(0, arrow + 2) + cursor = cursor + arrow + 2 + text + } + } + } + + /** Locates a Scala 3 end marker (`end foo`, `end if`) in {@code source} between the + * cursor and {@code limit}, as (offset of `end`, marker text). None when absent. + * Dotty's spans cover a trailing end marker, so capture it before advancing the cursor. + */ + private def endMarkerAt(from: Int, limit: Int): Option[(Int, String)] = { + def isWordChar(c: Char): Boolean = Character.isLetterOrDigit(c) || c == '_' || c == '$' + var i = from + while (i < limit && i < source.length && source.charAt(i).isWhitespace) i += 1 + if (i + 3 > source.length || !source.startsWith("end", i)) { + None + } else if (i + 3 < source.length && isWordChar(source.charAt(i + 3))) { + None + } else { + var j = i + 3 + while (j < source.length && (source.charAt(j) == ' ' || source.charAt(j) == '\t')) j += 1 + var k = j + while (k < source.length && isWordChar(source.charAt(k))) k += 1 + if (k > j && k <= limit) Some((i, source.substring(i, k))) else None + } + } + + /** The parent-list separator nearest the start of {@code text}, as (index, keyword). + * Index is -1 when neither appears. + */ + private def parentSeparatorIn(text: String): (Int, String) = { + val withIdx = positionOfNextIn(text, "with", 0) + val commaIdx = positionOfNextIn(text, ",", 0) + if (withIdx >= 0 && (commaIdx < 0 || withIdx < commaIdx)) (withIdx, "with") + else if (commaIdx >= 0) (commaIdx, ",") + else (-1, "with") + } + /** Returns the index of the bracket that matches the open bracket assumed to be at * {@code afterOpen - 1}, searching forward from {@code afterOpen} in {@code source}. * Skips nested bracket pairs, {@code //} and {@code /* */} comments, and @@ -9043,6 +9663,10 @@ class ScalaTreeVisitor( else if (c == '(' || c == '[') { val close = positionOfMatchingClose(c, if (c == '(') ')' else ']', i + 1) i = if (close >= 0) close + 1 else end + } else if (c == '^' && i + 1 < end && source.charAt(i + 1) == '{') { + // a capture set's brace belongs to the return type, not the body + val close = positionOfMatchingClose('{', '}', i + 2) + i = if (close >= 0) close + 1 else end } else if (c == '{') return true else if (c == '=') return false else i += 1 @@ -9060,7 +9684,7 @@ class ScalaTreeVisitor( * separator on the same line and consume it. Returns the JRightPadded * trailing space and markers; advances `cursor`. */ - private def consumeTrailingSemicolon(statEnd: Int, nextStart: Int): (Space, Markers) = { + def consumeTrailingSemicolon(statEnd: Int, nextStart: Int): (Space, Markers) = { val trailStart = Math.max(statEnd, cursor) // When the statement's rhs sits on its own line, Dotty extends the statement // span to include the trailing `;`, so the cursor already moved past it. The @@ -9115,19 +9739,8 @@ class ScalaTreeVisitor( i } - /** Find a keyword in text, ensuring it's a whole word (not part of an identifier). */ - private def findKeyword(text: String, keyword: String): Int = { - var pos = 0 - while (pos < text.length) { - val idx = text.indexOf(keyword, pos) - if (idx < 0) return -1 - val before = idx == 0 || !Character.isLetterOrDigit(text.charAt(idx - 1)) - val after = idx + keyword.length >= text.length || !Character.isLetterOrDigit(text.charAt(idx + keyword.length)) - if (before && after) return idx - pos = idx + 1 - } - -1 - } + /** Find a keyword in text, as a whole word and outside of comments. */ + private def findKeyword(text: String, keyword: String): Int = positionOfNextIn(text, keyword, 0) /** * Parse modifier keywords out of a raw text window — the source text between the @@ -9183,11 +9796,16 @@ class ScalaTreeVisitor( ("override", J.Modifier.Type.LanguageExtension), ("implicit", J.Modifier.Type.LanguageExtension), ("sealed", J.Modifier.Type.Sealed), - ("lazy", J.Modifier.Type.LanguageExtension) + ("lazy", J.Modifier.Type.LanguageExtension), + ("transparent", J.Modifier.Type.LanguageExtension), + ("inline", J.Modifier.Type.LanguageExtension), + ("opaque", J.Modifier.Type.LanguageExtension), + ("infix", J.Modifier.Type.LanguageExtension) ) /** Modifiers legal on a class/trait primary-constructor parameter. */ private val constructorParamModifierKeywords: List[(String, J.Modifier.Type)] = List( + ("inline", J.Modifier.Type.LanguageExtension), ("override", J.Modifier.Type.LanguageExtension), ("private", J.Modifier.Type.Private), ("protected", J.Modifier.Type.Protected), @@ -9217,6 +9835,36 @@ class ScalaTreeVisitor( case _ => null } + /** Consumes bound clauses sitting at the cursor inside a type-parameter list — `: Bound`, + * `<: Bound` or `>: Bound` — returning their verbatim source. Stops at the `,` or `]` that + * closes the parameter. + */ + private def consumeStrayBounds(): String = { + var i = cursor + while (i < source.length && (source.charAt(i) == ' ' || source.charAt(i) == '\t')) i += 1 + val opensBound = i < source.length && (source.charAt(i) == ':' || + ((source.charAt(i) == '<' || source.charAt(i) == '>') && + i + 1 < source.length && source.charAt(i + 1) == ':')) + if (!opensBound) null + else { + var depth = 0 + var end = i + var done = false + while (!done && end < source.length) { + source.charAt(end) match { + case '[' | '(' => depth += 1; end += 1 + case ']' | ')' if depth > 0 => depth -= 1; end += 1 + case ']' | ',' if depth == 0 => done = true + case '\n' => done = true + case _ => end += 1 + } + } + val text = source.substring(cursor, end) + cursor = end + text + } + } + private def visitTypeParameter(tparam: Trees.TypeDef[?]): J.TypeParameter = { val prefix = extractPrefix(tparam.span) @@ -9236,6 +9884,8 @@ class ScalaTreeVisitor( val adjustedEnd = Math.max(0, tparam.span.end - offsetAdjustment) var namePrefix = Space.EMPTY var nameStr = tparam.name.toString + // `+`/`-` is a modifier on the type parameter, not part of its name + var varianceKeyword = "" if (adjustedStart < adjustedEnd && adjustedStart >= cursor && adjustedEnd <= source.length) { val paramSource = source.substring(adjustedStart, adjustedEnd) @@ -9246,8 +9896,7 @@ class ScalaTreeVisitor( } // Check if it starts with + or - (after stripping whitespace) if (stripped.startsWith("+") || stripped.startsWith("-")) { - val variance = stripped.charAt(0) - nameStr = variance.toString + tparam.name.toString + varianceKeyword = stripped.charAt(0).toString cursor = adjustedStart + (paramSource.length - stripped.length) + 1 } // Check for higher-kinded type params like F[_] or F[_, _] @@ -9265,7 +9914,7 @@ class ScalaTreeVisitor( i += 1 } if (depth == 0) { - nameStr = stripped.substring(0, i) + nameStr = stripped.substring(varianceKeyword.length, i) // Trim any trailing bound syntax (e.g., F[_] <: Bound) val trimmed = nameStr.trim if (trimmed.nonEmpty) nameStr = trimmed @@ -9277,7 +9926,7 @@ class ScalaTreeVisitor( // Use source scanning to find the actual end position. if (nameStr.contains("[")) { // Higher-kinded: find the matching ] after the name - val nameStart = adjustedStart + (if (nameStr.startsWith("+") || nameStr.startsWith("-")) 1 else 0) + val nameStart = adjustedStart + varianceKeyword.length val bracketStart = positionOfNext("[", nameStart) if (bracketStart >= 0) { var depth = 1 @@ -9292,7 +9941,7 @@ class ScalaTreeVisitor( } else { // Simple name: advance past leading whitespace + name val wsLen = if (adjustedStart < adjustedEnd) (source.substring(adjustedStart, adjustedEnd).length - source.substring(adjustedStart, adjustedEnd).stripLeading().length) else 0 - val nameEnd = adjustedStart + wsLen + nameStr.length + val nameEnd = adjustedStart + wsLen + varianceKeyword.length + nameStr.length if (nameEnd > cursor && nameEnd <= source.length) cursor = nameEnd } @@ -9303,10 +9952,18 @@ class ScalaTreeVisitor( source.substring(adjustedStart, adjustedEnd).length - source.substring(adjustedStart, adjustedEnd).stripLeading().length else 0 val tokenStart = adjustedStart + leadingWsLen + // The variance modifier carries the leading whitespace, so the name follows it directly + val modifiers = new util.ArrayList[J.Modifier]() + if (varianceKeyword.nonEmpty) { + modifiers.add(new J.Modifier(Tree.randomId(), namePrefix, Markers.EMPTY, + varianceKeyword, J.Modifier.Type.LanguageExtension, Collections.emptyList())) + } + val bareNamePrefix = if (varianceKeyword.isEmpty) namePrefix else Space.EMPTY + val name: Expression = tparam.rhs match { case lt: untpd.LambdaTypeTree if nameStr.contains("[") => - buildHigherKindedName(tparam, lt, tokenStart, namePrefix) - case _ => ident(nameStr, namePrefix) + buildHigherKindedName(tparam, lt, tokenStart + varianceKeyword.length, bareNamePrefix) + case _ => ident(nameStr, bareNamePrefix) } def contextBoundName(cxBound: Trees.Tree[?]): String = { @@ -9360,6 +10017,13 @@ class ScalaTreeVisitor( // - TypeBoundsTree: upper/lower bounds like `<: Comparable` or `>: Null` // - untpd.ContextBounds: context bounds like `: ClassTag` (wraps TypeBoundsTree + cxBounds list) // Each bound is wrapped in J.TypeBound to carry its Kind (Upper/Lower). + // Dotty records bounds on a higher-kinded parameter's rhs as the kind itself, so the bounds + // written in source, `F[_]: Monad` or `It[a] <: Iterable[a]`, are left unclaimed. + val strayBounds: String = tparam.rhs match { + case _: untpd.LambdaTypeTree => consumeStrayBounds() + case _ => null + } + val bounds: JContainer[TypeTree] = tparam.rhs match { case cb: untpd.ContextBounds => // Context bounds: [T: ClassTag] or [T: Ordering : Show] @@ -9454,10 +10118,12 @@ class ScalaTreeVisitor( prefix, Markers.EMPTY, leadingAnnotations, - Collections.emptyList(), // modifiers + modifiers, name, bounds - ) + ).withMarkers( + if (strayBounds == null) Markers.EMPTY + else Markers.EMPTY.add(TypeParameterBounds(Tree.randomId(), strayBounds))) } /** @@ -9475,7 +10141,10 @@ class ScalaTreeVisitor( baseNameStart: Int, prefix: Space): J.ParameterizedType = { val varianceLen = if (baseNameStart < source.length && (source.charAt(baseNameStart) == '+' || source.charAt(baseNameStart) == '-')) 1 else 0 - val nameLen = td.name.toString.length + varianceLen + // a wildcard kind parameter is one character in source, whatever dotty named it + val rawName = td.name.toString + val sourceNameLen = if (rawName == "_" || rawName.startsWith("_$")) 1 else rawName.length + val nameLen = sourceNameLen + varianceLen val bracketPos = baseNameStart + nameLen val clazzName = source.substring(baseNameStart, Math.min(source.length, bracketPos)) val clazz: NameTree = ident(clazzName, Space.EMPTY) @@ -9504,7 +10173,14 @@ class ScalaTreeVisitor( buildHigherKindedName(inner, innerLt, innerStart, elemPrefix) case _ => val nm = inner.name.toString - if (nm == "_" || nm.startsWith("_$")) new S.Wildcard(Tree.randomId(), elemPrefix, Markers.EMPTY, null) + if (nm == "_" || nm.startsWith("_$")) { + // dotty's span starts at the variance marker, which the wildcard cannot carry + val variance = if (innerStart < source.length && + (source.charAt(innerStart) == '+' || source.charAt(innerStart) == '-')) { + Markers.EMPTY.add(KindParameterVariance(Tree.randomId(), source.charAt(innerStart).toString)) + } else Markers.EMPTY + new S.Wildcard(Tree.randomId(), elemPrefix, variance, null) + } else ident(source.substring(innerStart, innerEnd), elemPrefix) } val delimPos = if (idx < n - 1) { @@ -9800,7 +10476,23 @@ class ScalaTreeVisitor( val funcSource = extractSource(func.span) cursor = savedCursorBeforeFunc hasParentheses = funcSource.trim.startsWith("(") - val arrowIndex = positionOfNextIn(funcSource, "=>", 0) + // A parameter's own type can contain an arrow (`(cb: Int => Unit) => 1`), so the + // lambda's arrow is the first one after the parameter list. + val afterParamList = if (hasParentheses) { + var depth = 0 + var i = funcSource.indexOf('(') + var close = -1 + while (i >= 0 && i < funcSource.length && close < 0) { + funcSource.charAt(i) match { + case '(' => depth += 1 + case ')' => depth -= 1; if (depth == 0) close = i + case _ => + } + i += 1 + } + if (close >= 0) close + 1 else 0 + } else 0 + val arrowIndex = positionOfNextIn(funcSource, "=>", afterParamList) // When parenthesized, advance cursor past `(` so the first parameter's // extractPrefix captures the whitespace between `(` and the first arg. diff --git a/rewrite-scala/src/main/scala/org/openrewrite/scala/marker/ScalaMarkers.scala b/rewrite-scala/src/main/scala/org/openrewrite/scala/marker/ScalaMarkers.scala index b6e3032f5eb..fa8f5cad768 100644 --- a/rewrite-scala/src/main/scala/org/openrewrite/scala/marker/ScalaMarkers.scala +++ b/rewrite-scala/src/main/scala/org/openrewrite/scala/marker/ScalaMarkers.scala @@ -88,6 +88,15 @@ case class Curried(id: UUID) extends Marker { * Carries the source text between the last annotation/modifier and the * `val`/`var`/`given` keyword for Scala variable declarations. */ +/** + * Marks a `J.VariableDeclarations` written with `var`. A `val` is implicitly final, but an + * explicit `final` says nothing about which keyword the source used, and `final var` is legal. + */ +case class VarKeyword(id: UUID) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + case class ValVarKeyword(id: UUID, beforeKeyword: String) extends Marker { override def getId(): UUID = id override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] @@ -137,3 +146,142 @@ case class ExtraConstructorParamLists(id: UUID, text: String) extends Marker { override def getId(): UUID = id override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] } + +/** + * A Scala 3 `derives` clause, as in `case class C(i: Int) derives CanEqual`. Holds the + * verbatim source from `derives` up to the body delimiter, which the printer emits + * between the parent list and the body. + */ +case class DerivesClause(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * A Scala 3 capture set written as a suffix on a type: the `^` of `IterableOnce[A]^` or + * the `^{it}` of `C^{it}`. Dotty desugars it to a synthetic `retains` annotation that has + * no `@` in source, so the suffix is kept verbatim and printed after the type. + */ +case class CaptureSet(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The `using` keyword opening a call-site argument list, as in `f(using ctx)`. Carried by + * the first argument, which every printer path emits, and holds the source from the `(` + * through the keyword. + */ +case class UsingArguments(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * A function type written with the capture-checking pure arrow, `A -> B`, rather than `=>`. + */ +case class PureFunctionArrow(id: UUID) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * Bounds written on a higher-kinded type parameter: the `: Monad` of `[F[_]: Monad]` or the + * `<: Iterable[a]` of `[It[a] <: Iterable[a]]`. Dotty records bounds on the parameter's rhs + * only for a plain parameter, so for a higher-kinded one the rhs is the kind itself and the + * source is kept verbatim, to be printed after the name. + */ +case class TypeParameterBounds(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * A self-type clause opening a template body, as in `trait T { self => ... }` or + * `trait T:\n this: U =>`. Holds the verbatim source from the body delimiter through + * the `=>`. Dotty keeps the clause out of the body, so there is no statement to map it to. + */ +case class SelfType(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * A Scala 3 end marker closing a definition, e.g. the `end X` of `class X: ... end X`. + * Holds the verbatim source from the end of the element's own content through the + * marker, so it covers the newline and indent ahead of it — a method body is an + * expression with no trailing space of its own. Dotty checks end markers and then + * discards them, so there is no tree to map them to. + */ +case class EndMarker(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The `+` or `-` variance marker on a kind parameter of a higher-kinded type + * parameter, as in the `F[-_, +_]` of `def f[F[-_, +_]]`. The variance is source + * syntax the wildcard itself does not carry. + */ +case class KindParameterVariance(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The access modifier on a class's primary constructor, e.g. the `private` in + * `class X private (i: Int)`. Holds the verbatim source text including the space + * ahead of it, which the printer emits between the class name and the `(`. + */ +case class ConstructorModifier(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The `case` of a for-comprehension generator that filters by pattern, + * `for case (k, v) <- pairs do ...`. + */ +case class CasePattern(id: UUID) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The `then` of a Scala 3 `if (cond) then ...`, which is optional after a parenthesized + * condition. Holds the verbatim source from the closing `)` through the keyword. + */ +case class ThenKeyword(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The `inline` of a Scala 3 `inline if`, which modifies an expression rather than a + * declaration and so has no modifier list to sit in. Holds the verbatim source from the + * keyword up to the expression's own keyword. + */ +case class InlineKeyword(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The `do` of a Scala 3 `while (cond) do ...` or `for (...) do ...`, which is optional + * after a parenthesized head. Holds the verbatim source from the closing `)` through + * the keyword. + */ +case class DoKeyword(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} + +/** + * The keyword introducing one parent in a class declaration's parent list. Scala 3 + * accepts either `with` or `,` and the two may be mixed (`extends A, B with C`), so + * each parent after the first carries the separator that introduces it. + */ +case class ParentSeparator(id: UUID, text: String) extends Marker { + override def getId(): UUID = id + override def withId[M <: Marker](newId: UUID): M = copy(id = newId).asInstanceOf[M] +} diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/MethodDeclarationTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/MethodDeclarationTest.java index 2ac3f21f044..84951763a16 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/MethodDeclarationTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/MethodDeclarationTest.java @@ -372,6 +372,22 @@ def add(x:Int)(y:Int) = x + y ); } + @Test + void curriedMethodWithEmptyFirstParameterList() { + rewriteRun( + scala( + """ + object Test { + def f()(c: Int): Int = c + def g()(using c: Int): Unit = { + () + } + } + """ + ) + ); + } + @Test void multilineParameterListWithClosingParenOnOwnLine() { rewriteRun( @@ -809,6 +825,19 @@ def this() = this(0) ); } + @Test + void curriedAuxiliaryConstructor() { + rewriteRun( + scala( + """ + class A(a: Int) { + def this()(implicit o: Ordering[Int]) = this(0) + } + """ + ) + ); + } + @Test void auxiliaryConstructorWithBlockBody() { rewriteRun( @@ -1103,4 +1132,162 @@ void trailingCommaInTypeParametersSingleLine() { ) ); } + + @Test + void varianceOnHigherKindedTypeParameter() { + rewriteRun( + scala( + """ + trait X[To, From] { + def substituteBoth[F[-_, +_]](ftf: F[To, From]): F[From, To] + } + """ + ) + ); + } + + @Test + void endMarkerOnMethod() { + rewriteRun( + scala( + """ + object Test: + def foo(): Int = + 1 + end foo + """ + ) + ); + } + @Test + void implicitParameterWithAnnotation() { + rewriteRun( + scala( + """ + object Test { + def foo[B](implicit @implicitNotFound("m") ev: Ordering[B]): Unit = () + } + """ + ) + ); + } + + @Test + void endMarkerOnExtension() { + rewriteRun( + scala( + """ + extension (x: Int) + def double: Int = x * 2 + end extension + """ + ) + ); + } + + @Test + void endMarkerOnExtensionWithSeveralMethods() { + rewriteRun( + scala( + """ + extension (x: Int) + def a: Int = x + def b: Int = x + end extension + """ + ) + ); + } + + @Test + void curriedImplicitParameterWithAnnotation() { + rewriteRun( + scala( + """ + object O { + def map[B](f: Int => B)(implicit @implicitNotFound("m") ev: Ordering[B]): Int = 1 + } + """ + ) + ); + } + + @Test + void contextBoundOnHigherKindedTypeParameter() { + rewriteRun( + scala( + """ + trait Monad[F[_]] + trait Par[F[_]] + object Test { + def f[F[_]: Monad, A](x: A): A = x + def g[F[_]: Monad: Par](x: Int): Int = x + } + """ + ) + ); + } + + @Test + void upperBoundOnHigherKindedTypeParameter() { + rewriteRun( + scala( + """ + object Test { + def f[It[a] <: Iterable[a], A](x: A): A = x + def g[It1[a] <: Iterable[a], El2, It2[a] <: Iterable[a]](x: El2): El2 = x + } + """ + ) + ); + } + + @Test + void inlineParameter() { + rewriteRun( + scala( + """ + object Test { + inline def f(inline op: Boolean): Boolean = op + inline def g(a: Int, inline op: Boolean, b: Int): Boolean = op + inline def h(inline op: => Int): Int = op + inline def i[A](using inline z: List[A]): List[A] = z + inline def j( + inline op: Boolean + ): Boolean = op + } + """ + ) + ); + } + + @Test + void noSpaceBeforeBodyEquals() { + rewriteRun( + scala( + """ + object Test { + def desc(sym: Int)= { + 1 + } + } + """ + ) + ); + } + + @Test + void inlineExtensionParameter() { + rewriteRun( + scala( + """ + object Test { + extension (inline x: String) + inline def foo: Int = 1 + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotatedExprTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotatedExprTest.java index 8b99b4fad8f..b54cd1dec45 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotatedExprTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotatedExprTest.java @@ -54,4 +54,108 @@ def f(x: Any): String = (x: @unchecked) match { ) ); } + @Test + void captureSetSuffixOnType() { + rewriteRun( + scala( + """ + import language.experimental.captureChecking + trait T { + def f(xs: IterableOnce[Int]^): Int = 1 + } + """ + ) + ); + } + + @Test + void captureSetSuffixInReturnType() { + rewriteRun( + scala( + """ + import language.experimental.captureChecking + trait T { + def f(): Iterator[Int]^ = Iterator.empty + } + """ + ) + ); + } + + @Test + void captureSetWithExplicitSet() { + rewriteRun( + scala( + """ + import language.experimental.captureChecking + trait T { + def f(): List[Int]^{this} = Nil + } + """ + ) + ); + } + + @Test + void parenthesizedAnnotatedType() { + rewriteRun( + scala( + """ + object O { + def f(g: (Int => Long) @unchecked): Int = 1 + def h(g: (Int ?=> Long) @unchecked): Int = 1 + } + """ + ) + ); + } + + @Test + void tupleAnnotatedTypeArgument() { + rewriteRun( + scala( + """ + object O { + def f(x: Any): Int = x match { + case it: Iterable[(Int, Long) @unchecked] => 1 + case _ => 0 + } + } + """ + ) + ); + } + + @Test + void captureSetSuffixInsideEnclosingType() { + rewriteRun( + scala( + """ + import language.experimental.captureChecking + class C { + var it: Iterator[Int]^{this} | Null = null + def go(f: Iterable[Int]^{this} => Int): Int = 1 + def trySplit(): Iterator[Int]^{this} | Null = null + } + """ + ) + ); + } + + @Test + void spaceBetweenAtAndAnnotationName() { + rewriteRun( + scala( + """ + object O { + def f(x: Any): Int = x match { + case tree: String @ unchecked => 1 + case _ => 0 + } + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotationTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotationTest.java index 6c1d05b4230..2dbdd8cd9a1 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotationTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AnnotationTest.java @@ -365,4 +365,17 @@ class Test { ) ); } + @Test + void backtickedAnnotationName() { + rewriteRun( + scala( + """ + object O { + @`inline` def f(): Int = 1 + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/TypeCastTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AsInstanceOfTest.java similarity index 74% rename from rewrite-scala/src/test/java/org/openrewrite/scala/tree/TypeCastTest.java rename to rewrite-scala/src/test/java/org/openrewrite/scala/tree/AsInstanceOfTest.java index 0810ada10e8..5881e9165a0 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/TypeCastTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/AsInstanceOfTest.java @@ -16,11 +16,13 @@ package org.openrewrite.scala.tree; import org.junit.jupiter.api.Test; +import org.openrewrite.java.tree.J; import org.openrewrite.test.RewriteTest; +import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.scala.Assertions.scala; -class TypeCastTest implements RewriteTest { +class AsInstanceOfTest implements RewriteTest { @Test void simpleCast() { @@ -173,4 +175,43 @@ void castToFunctionType() { ) ); } + + @Test + void newlineBetweenDotAndKeyword() { + rewriteRun( + scala( + """ + object Test { + val obj: Any = 1 + val num = obj. + asInstanceOf[Int] + } + """ + ) + ); + } + + @Test + void isAMethodInvocation() { + rewriteRun( + scala( + """ + object Test { + val obj: Any = 1 + val num = obj.asInstanceOf[Int] + } + """, + spec -> spec.afterRecipe(cu -> { + J.ClassDeclaration test = (J.ClassDeclaration) cu.getStatements().get(0); + J.VariableDeclarations num = (J.VariableDeclarations) test.getBody().getStatements().get(1); + J.MethodInvocation cast = (J.MethodInvocation) num.getVariables().get(0).getInitializer(); + assertThat(cast.getSimpleName()).isEqualTo("asInstanceOf"); + assertThat(cast.getArguments()).isEmpty(); + assertThat(cast.getTypeParameters()).singleElement() + .isInstanceOfSatisfying(J.Identifier.class, t -> assertThat(t.getSimpleName()).isEqualTo("Int")); + }) + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ClassDeclarationTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ClassDeclarationTest.java index 84b24699b08..c23007fc7fc 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ClassDeclarationTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ClassDeclarationTest.java @@ -455,6 +455,14 @@ class C[A /* <: */ <: AnyRef] """ ) ); + // parseModifierKeywords — modifier keyword in block comment between modifiers + rewriteRun( + scala( + """ + private /*final*/ case class SomePrintedTree(phase: String) + """ + ) + ); } @Test @@ -618,4 +626,273 @@ case class Matchup(users: Users): // score is x10 ) ); } + + @Test + void commaSeparatedParents() { + rewriteRun( + scala( + """ + trait A + trait B + class C extends A, B + """ + ) + ); + } + + @Test + void privateConstructor() { + rewriteRun( + scala( + """ + final class X private () + """ + ) + ); + } + + @Test + void qualifiedPrivateConstructor() { + rewriteRun( + scala( + """ + class X private[scala] (val x: Int) + """ + ) + ); + } + + @Test + void usingWithAnnotatedParameter() { + rewriteRun( + scala( + """ + class X(using @deprecated ctx: String) + """ + ) + ); + } + + @Test + void endMarker() { + rewriteRun( + scala( + """ + class X: + def f(): Int = 1 + end X + """ + ) + ); + } + + @Test + void endMarkerAfterBracedBody() { + rewriteRun( + scala( + """ + class X { + def f(): Int = 1 + } + end X + """ + ) + ); + } + + @Test + void endMarkerOnTrait() { + rewriteRun( + scala( + """ + trait T: + def f(): Int + end T + """ + ) + ); + } + + @Test + void nestedEndMarkers() { + rewriteRun( + scala( + """ + class X: + def f(): Int = + 1 + end f + end X + """ + ) + ); + } + + @Test + void selfTypeAlias() { + rewriteRun( + scala( + """ + trait T { self => + def f(): Int = 1 + } + """ + ) + ); + } + + @Test + void selfTypeAnnotation() { + rewriteRun( + scala( + """ + trait U + trait T { this: U => + def f(): Int = 1 + } + """ + ) + ); + } + + @Test + void selfTypeInIndentedBody() { + rewriteRun( + scala( + """ + trait U + trait T: + this: U => + def f(): Int = 1 + """ + ) + ); + } + + @Test + void selfTypeOnObject() { + rewriteRun( + scala( + """ + object O { self => + val x = 1 + } + """ + ) + ); + } + + @Test + void derivesClause() { + rewriteRun( + scala( + """ + case class C(i: Int) derives CanEqual { + def f(): Int = i + } + """ + ) + ); + } + + @Test + void derivesMultipleWithIndentedBody() { + rewriteRun( + scala( + """ + case class C(i: Int) derives CanEqual, Show: + def f(): Int = i + """ + ) + ); + } + + @Test + void parentIsParenthesizedFunctionType() { + rewriteRun( + scala( + """ + trait T + class C extends T with (Int => Unit) { + def apply(i: Int): Unit = () + } + """ + ) + ); + } + + @Test + void parentIsParenthesizedByNameFunctionType() { + rewriteRun( + scala( + """ + trait T + class C extends T with (() => Unit) { + def apply(): Unit = () + } + """ + ) + ); + } + + @Test + void constructorModifierWithoutParameterList() { + rewriteRun( + scala( + """ + final abstract class Byte private extends AnyVal { + def toByte: Byte + } + """ + ) + ); + } + + @Test + void scala3Modifiers() { + rewriteRun( + scala( + """ + object O { + transparent inline def f(): Int = 1 + private inline def g(): Int = 2 + infix def and(o: Int): Int = o + } + """ + ) + ); + } + + @Test + void nestedHigherKindedTypeParameter() { + rewriteRun( + scala( + """ + trait Q[F[_[_], _]] { + def f(): Int = 1 + } + """ + ) + ); + } + + @Test + void accessModifierOfFollowingClassIsNotClaimed() { + rewriteRun( + scala( + """ + class Foo + + private class Bar + """, + spec -> spec.afterRecipe(cu -> { + J.ClassDeclaration bar = (J.ClassDeclaration) cu.getStatements().get(1); + assertThat(bar.getSimpleName()).isEqualTo("Bar"); + assertThat(bar.getModifiers()).singleElement() + .extracting(J.Modifier::getType).isEqualTo(J.Modifier.Type.Private); + }) + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/CompilationUnitTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/CompilationUnitTest.java index dd5f151a2b8..c0bfa390943 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/CompilationUnitTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/CompilationUnitTest.java @@ -103,6 +103,19 @@ void withNestedPackage() { ); } + @Test + void packageKeywordFollowedByMoreThanOneSpace() { + rewriteRun( + scala( + """ + package com.example + + val x = 42 + """ + ) + ); + } + @Test void packageWithBacktickedSegment() { rewriteRun( @@ -593,4 +606,170 @@ void docCommentOnAnnotatedClassBelongsToTheClass() { ); } + @Test + void trailingSemicolonAfterTopLevelClass() { + rewriteRun( + scala( + """ + class X; + """ + ) + ); + } + + @Test + void semicolonBetweenTopLevelClasses() { + rewriteRun( + scala( + """ + class A; + class B + """ + ) + ); + } + + @Test + void trailingSemicolonAfterTopLevelImport() { + rewriteRun( + scala( + """ + import scala.collection.mutable; + class X + """ + ) + ); + } + + @Test + void semicolonAfterTopLevelVals() { + rewriteRun( + scala( + """ + val a = 1; + val b = 2; + """ + ) + ); + } + + @Test + void semicolonInObjectBody() { + rewriteRun( + scala( + """ + object O { + val a = 1; + def f(): Int = 1; + } + """ + ) + ); + } + + @Test + void commentAfterLastStatementInIndentedBody() { + rewriteRun( + scala( + """ + class X: + val a = 1 + // trailing + """ + ) + ); + } + + @Test + void commentAfterIndentedMethodBody() { + rewriteRun( + scala( + """ + object O: + def f(): Int = + 1 + // trailing + """ + ) + ); + } + + @Test + void commentAfterIndentedVal() { + rewriteRun( + scala( + """ + object O: + val x = + 1 + // note + """ + ) + ); + } + + @Test + void commentAfterIfExpression() { + rewriteRun( + scala( + """ + object O: + def f(b: Boolean): Int = + if b then + 1 + else + 2 + // done + """ + ) + ); + } + + @Test + void commentAfterMatch() { + rewriteRun( + scala( + """ + object O: + def f(i: Int): Int = + i match + case _ => 1 + // done + """ + ) + ); + } + + @Test + void chainedPackageClauses() { + rewriteRun( + scala( + """ + package a + package b + + class X + """ + ) + ); + } + + @Test + void chainedPackageClausesWithImportAndMembers() { + rewriteRun( + scala( + """ + package dotty.tools + package dotc + package cc + + import core.* + + class X + object O + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ControlFlowTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ControlFlowTest.java index f9c4bb22d0f..480e515b780 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ControlFlowTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ControlFlowTest.java @@ -253,6 +253,58 @@ void ifElseIfElse() { ); } + @Test + void inlineIf() { + rewriteRun( + scala( + """ + object Test { + inline def f(inline b: Boolean): Int = + inline if b then 1 else 2 + inline def g(inline b: Boolean): Int = inline if (b) 1 else 2 + } + """ + ) + ); + } + + @Test + void doAfterParenthesizedHead() { + rewriteRun( + scala( + """ + object Test { + def f(it: Iterator[Int]): Unit = + while (it.hasNext) do + println(it.next()) + def g(xs: List[Int]): Unit = for (x <- xs) do println(x) + def h(xs: List[Int], ys: List[Int]): Unit = + for (x <- xs; y <- ys) do + println(x + y) + } + """ + ) + ); + } + + @Test + void thenAfterParenthesizedCondition() { + rewriteRun( + scala( + """ + object Test { + def f(x: Boolean): Int = if (x) then 1 else 2 + def g(x: Boolean): Int = + if (x) then + 1 + else + 2 + } + """ + ) + ); + } + @Test void forLoop() { rewriteRun( @@ -408,4 +460,53 @@ void compoundAssignment() { ) ); } + + @Test + void endIf() { + rewriteRun( + scala( + """ + object O: + def f(b: Boolean): Int = + if b then + 1 + else + 2 + end if + """ + ) + ); + } + + @Test + void endWhile() { + rewriteRun( + scala( + """ + object O: + def f(): Unit = + var i = 0 + while i < 3 do + i = i + 1 + end while + """ + ) + ); + } + + @Test + void endMatch() { + rewriteRun( + scala( + """ + object O: + def f(i: Int): Int = + i match + case _ => 1 + end match + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ForYieldTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ForYieldTest.java index 0acf89c81fb..0c9727ee7f5 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ForYieldTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ForYieldTest.java @@ -151,6 +151,27 @@ void anonymousGivenBinding() { ); } + @Test + void casePatternGenerator() { + rewriteRun( + scala( + """ + object Test { + def f(xs: List[(Int, Int)]): List[Int] = + for case (a, b) <- xs yield a + def g(xs: List[Any]): Unit = + for case s: String <- xs do println(s) + def h(xs: List[Any]): Unit = { + for { + case s: String <- xs + } println(s) + } + } + """ + ) + ); + } + @Test void namedGivenBinding() { rewriteRun( diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/FunctionTypeTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/FunctionTypeTest.java index 817f6d1c749..21c7c2d2bda 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/FunctionTypeTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/FunctionTypeTest.java @@ -158,4 +158,48 @@ public J visitFunctionType(S.FunctionType functionType, Integer p) { assertThat(ref.get()).as("should find an S.FunctionType").isNotNull(); return ref.get(); } + @Test + void parameterListHoldsAnArrow() { + rewriteRun( + scala( + """ + object O { + def f(k: (Int => Unit) => Unit): Int = 1 + def g(k: (Int => Unit, Long) => Unit): Int = 1 + def h(): (Int => Unit) => Unit = ??? + } + """ + ) + ); + } + + @Test + void pureFunctionArrow() { + rewriteRun( + scala( + """ + import language.experimental.captureChecking + object O { + def f(g: Int -> Long): Int = 1 + } + """ + ) + ); + } + + @Test + void byNameParameterUnderCaptureChecking() { + rewriteRun( + scala( + """ + import language.experimental.captureChecking + object O { + def fill[T](n: Int)(elem: => T): Int = n + def f(g: => Int^): Int = 1 + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ImportTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ImportTest.java index 98827b47766..1c81a8ac73a 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ImportTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ImportTest.java @@ -374,6 +374,23 @@ def foo(): Int = ); } + @Test + void commaContinuationInsideBlock() { + rewriteRun( + scala( + """ + object O { + def f: Int = { + import scala.util.*, scala.math.* + import java.util.List, java.util.Map, java.util.Set + 1 + } + } + """ + ) + ); + } + @Test void commaContinuationBrace() { rewriteRun( @@ -420,4 +437,36 @@ class B { val y = 2 } ) ); } + @Test + void wildcardImportAfterTrailingComment() { + // The `*` reaches the compilation unit's EOF space, which parses comments; + // a bare `*` there is not a block-comment delimiter. + rewriteRun( + scala( + """ + package p + + import core.* + // trailing + """ + ) + ); + } + + @Test + void trailingCommaInSelectors() { + rewriteRun( + scala( + """ + import java.math.{ + BigDecimal => BigDec, + MathContext, + RoundingMode => JRM, + } + class X + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/LambdaTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/LambdaTest.java index 12b81af9905..1f457b4fead 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/LambdaTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/LambdaTest.java @@ -388,4 +388,18 @@ void trailingCommaInLambdaParams() { ) ); } + @Test + void parameterTypeContainsAnArrow() { + rewriteRun( + scala( + """ + object O { + val f = (cb: Int => Unit) => 1 + val g = (acc: Boolean, cb: Int => Unit) => acc + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MatchTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MatchTest.java index c65a77ca723..4053b246fa3 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MatchTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MatchTest.java @@ -605,4 +605,17 @@ void significantCharactersInComments() { ) ); } + + @Test + void blockCommentBeforeMatchKeyword() { + rewriteRun( + scala( + """ + def tupleArity(tp: Int): Int = tp/*.dealias*/ match { + case _ => 1 + } + """ + ) + ); + } } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MethodInvocationTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MethodInvocationTest.java index e46c8252f36..5ec13bf78e4 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MethodInvocationTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/MethodInvocationTest.java @@ -514,4 +514,113 @@ def g(): Seq[Int] = Seq(1, 2) ); } } + @Test + void usingArgument() { + rewriteRun( + scala( + """ + object O { + def f(using s: String): Int = 1 + val r = f(using "a") + } + """ + ) + ); + } + + @Test + void usingArgumentInCurriedCall() { + rewriteRun( + scala( + """ + object O { + def f(x: Int)(using s: String): Int = x + val r = f(1)(using "a") + } + """ + ) + ); + } + + @Test + void usingArgumentOnSelect() { + rewriteRun( + scala( + """ + class C { def g(using s: String): Int = 1 } + object O { + val c = new C + val r = c.g(using "a") + } + """ + ) + ); + } + + @Test + void usingArgumentsMultiple() { + rewriteRun( + scala( + """ + object O { + def f(using a: Int, b: Int): Int = a + val r = f(using 1, 2) + } + """ + ) + ); + } + + @Test + void trailingCommaInArguments() { + rewriteRun( + scala( + """ + object O { + def f(a: Int, b: Int): Int = a + val r = f( + 1, + 2, + ) + } + """ + ) + ); + } + + @Test + void emptyArgumentListWithInteriorSpace() { + rewriteRun( + scala( + """ + object O { + def f(): Int = 1 + val a = f( + ) + val b = f(/* none */) + val c = "x".trim( + ) + } + """ + ) + ); + } + + @Test + void nestedBlockCommentBeforeTrailingComma() { + rewriteRun( + scala( + """ + object O { + def f(a: Int, b: Int): Int = a + val x = f( + 1, + 2 /* x /* y */ z */, + ) + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/NewClassTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/NewClassTest.java index ea8de4a1812..459bbecfb2b 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/NewClassTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/NewClassTest.java @@ -364,4 +364,89 @@ class Foo(a: Int)(b: Int) ) ); } + @Test + void soleParentIsParenthesizedFunctionType() { + rewriteRun( + scala( + """ + object O { + val f = new (Int => Unit) { + def apply(i: Int): Unit = () + } + } + """ + ) + ); + } + + @Test + void mixinIsParenthesizedFunctionType() { + rewriteRun( + scala( + """ + class B + object O { + val h = new B with (Int => Unit) { + def apply(i: Int): Unit = () + } + } + """ + ) + ); + } + + @Test + void constructorWithThreeArgumentLists() { + rewriteRun( + scala( + """ + class M[A](s: String)(f: Int => Int)(g: Int => Int) + object O { + val m = new M[Int]("x")(identity)(identity) + } + """ + ) + ); + } + + @Test + void anonymousClassWithEmptyArgumentList() { + rewriteRun( + scala( + """ + object O { + val a = new Object() { + override def toString: String = "a" + } + val b = new Object { + override def toString: String = "b" + } + val c = new Object(): + override def toString: String = "c" + val d = new Object { + def f(i: Int): Int = i + } + } + """ + ) + ); + } + + @Test + void trailingCommaInConstructorArguments() { + rewriteRun( + scala( + """ + class C(a: Int, b: Int) + object O { + val c = new C( + 1, + 2, + ) + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ObjectDeclarationTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ObjectDeclarationTest.java index 920a9b42fc1..afb2d78b7e8 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ObjectDeclarationTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/ObjectDeclarationTest.java @@ -148,4 +148,63 @@ void significantCharactersInComments() { ) ); } + @Test + void privateCompanionObject() { + rewriteRun( + scala( + """ + package p + + private[p] final class B + + private object B { + val x = 1 + } + """ + ) + ); + } + + @Test + void endMarkerOnIndentedObject() { + rewriteRun( + scala( + """ + object Conversion: + extension [T](x: T) + def underlying: T = x + end Conversion + """ + ) + ); + } + + @Test + void endMarkerOnNestedObject() { + rewriteRun( + scala( + """ + object Outer: + object experimental: + val x = 1 + end experimental + """ + ) + ); + } + + @Test + void endMarkerAfterBracedBody() { + rewriteRun( + scala( + """ + object O { + def f: Int = 1 + } + end O + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/SingletonTypeTreeTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/SingletonTypeTreeTest.java index 3a7594d4ec4..f146809ef1f 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/SingletonTypeTreeTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/SingletonTypeTreeTest.java @@ -51,4 +51,17 @@ void qualifiedSingletonType() { ) ); } + + @Test + void literalTypeInReturnPosition() { + rewriteRun( + scala( + """ + object Test { + def unapply(s: String): true = true + } + """ + ) + ); + } } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/TryTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/TryTest.java index 3413110a034..f7c88422823 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/TryTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/TryTest.java @@ -535,4 +535,35 @@ void significantCharactersInComments() { """ )); } + @Test + void endMarkerOnTry() { + rewriteRun( + scala( + """ + object O: + def f(): Int = + try + 1 + catch + case _: Exception => 0 + end try + """ + ) + ); + } + + @Test + void catchWithPartialFunctionExpression() { + rewriteRun( + scala( + """ + object O { + def f(part0: String, badPart: PartialFunction[Throwable, String]): String = + try part0 catch badPart + } + """ + ) + ); + } + } diff --git a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/VariableDeclarationsTest.java b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/VariableDeclarationsTest.java index 2610f11c3f5..2f4fa5592a6 100644 --- a/rewrite-scala/src/test/java/org/openrewrite/scala/tree/VariableDeclarationsTest.java +++ b/rewrite-scala/src/test/java/org/openrewrite/scala/tree/VariableDeclarationsTest.java @@ -227,4 +227,88 @@ void significantCharactersInComments() { ) ); } + + @Test + void endMarkerOnVal() { + rewriteRun( + scala( + """ + object O: + val x = + 1 + end x + """ + ) + ); + } + + @Test + void endMarkerOnGiven() { + rewriteRun( + scala( + """ + object O: + given x: Int = 1 + end x + """ + ) + ); + } + + @Test + void inlineVal() { + rewriteRun( + scala( + """ + object O { + inline val X = 8 + private inline val Y = 9 + } + """ + ) + ); + } + + @Test + void inlineGiven() { + rewriteRun( + scala( + """ + object O { + inline given x: Int = 1 + } + """ + ) + ); + } + + @Test + void accessModifierAfterAnotherModifier() { + rewriteRun( + scala( + """ + class C { + override implicit protected val a: String = "a" + implicit protected val b: String = "b" + lazy private val c: String = "c" + override private[this] val d: String = "d" + } + """ + ) + ); + } + + @Test + void finalVar() { + rewriteRun( + scala( + """ + class C { + private final var isBlocked: Boolean = false + } + """ + ) + ); + } + }