Skip to content

Commit 365840d

Browse files
Fix Space parsing of type casts in Scala (#7306)
1 parent b6fdc34 commit 365840d

5 files changed

Lines changed: 106 additions & 13 deletions

File tree

rewrite-scala/src/main/java/org/openrewrite/scala/ScalaPrinter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.openrewrite.scala.marker.SObject;
3535
import org.openrewrite.scala.marker.TypeProjection;
3636
import org.openrewrite.scala.marker.ScalaForLoop;
37+
import org.openrewrite.scala.marker.AsInstanceOfPrefix;
3738
import org.openrewrite.scala.marker.TypeAscription;
3839
import org.openrewrite.scala.marker.UnderscorePlaceholderLambda;
3940
import org.openrewrite.scala.tree.S;
@@ -882,6 +883,8 @@ public J visitTypeCast(J.TypeCast typeCast, PrintOutputCapture<P> p) {
882883
// Existing asInstanceOf handling
883884
beforeSyntax(typeCast, Space.Location.TYPE_CAST_PREFIX, p);
884885
visit(typeCast.getExpression(), p);
886+
typeCast.getMarkers().findFirst(AsInstanceOfPrefix.class)
887+
.ifPresent(sp -> visitSpace(sp.getPrefix(), Space.Location.LANGUAGE_EXTENSION, p));
885888
p.append(".asInstanceOf");
886889
if (typeCast.getClazz() instanceof J.ControlParentheses) {
887890
J.ControlParentheses<?> controlParens = (J.ControlParentheses<?>) typeCast.getClazz();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.scala.marker;
17+
18+
import lombok.Value;
19+
import lombok.With;
20+
import org.openrewrite.Tree;
21+
import org.openrewrite.java.tree.Space;
22+
import org.openrewrite.marker.Marker;
23+
24+
import java.util.UUID;
25+
26+
/**
27+
* Stores the whitespace that appears before {@code .asInstanceOf} in Scala
28+
* constructs like {@code expr.asInstanceOf[Type]} where the dot may be on a new line.
29+
*/
30+
@Value
31+
@With
32+
public class AsInstanceOfPrefix implements Marker {
33+
UUID id;
34+
Space prefix;
35+
36+
public static AsInstanceOfPrefix create(Space prefix) {
37+
return new AsInstanceOfPrefix(Tree.randomId(), prefix);
38+
}
39+
}

rewrite-scala/src/main/scala/org/openrewrite/scala/internal/ScalaTreeVisitor.scala

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import org.openrewrite.scala.marker.BlockArgument
3535
import org.openrewrite.scala.marker.TypeAscription
3636
import org.openrewrite.scala.marker.UnderscorePlaceholderLambda
3737
import org.openrewrite.scala.marker.Curried
38+
import org.openrewrite.scala.marker.AsInstanceOfPrefix
3839
import org.openrewrite.scala.tree.S
3940

4041
import java.util
@@ -4285,11 +4286,17 @@ class ScalaTreeVisitor(
42854286
case e: Expression => e
42864287
case _ => return visitUnknown(ta)
42874288
}
4288-
4289-
// Update cursor past ".asInstanceOf"
4289+
4290+
// Capture whitespace before ".asInstanceOf" (e.g., newline + indentation)
42904291
val asInstanceOfEnd = sel.span.end
4291-
if (asInstanceOfEnd > cursor) {
4292+
val selectPrefix = if (asInstanceOfEnd > cursor) {
4293+
val between = source.substring(cursor, asInstanceOfEnd)
4294+
val dotPos = between.indexOf('.')
4295+
val sp = if (dotPos > 0) Space.format(between.substring(0, dotPos)) else Space.EMPTY
42924296
cursor = asInstanceOfEnd
4297+
sp
4298+
} else {
4299+
Space.EMPTY
42934300
}
42944301

42954302
// Now handle the type argument in brackets
@@ -4320,10 +4327,16 @@ class ScalaTreeVisitor(
43204327
cursor = ta.span.end
43214328
}
43224329

4330+
val markers = if (selectPrefix != Space.EMPTY) {
4331+
Markers.EMPTY.addIfAbsent(AsInstanceOfPrefix.create(selectPrefix))
4332+
} else {
4333+
Markers.EMPTY
4334+
}
4335+
43234336
return new J.TypeCast(
43244337
Tree.randomId(),
4325-
Space.EMPTY, // TypeCast itself has no prefix - the space is handled by the variable initializer
4326-
Markers.EMPTY,
4338+
Space.EMPTY,
4339+
markers,
43274340
new J.ControlParentheses[TypeTree](
43284341
Tree.randomId(),
43294342
spaceBeforeBracket,
@@ -4337,41 +4350,51 @@ class ScalaTreeVisitor(
43374350
// Check if this is isInstanceOf
43384351
if (sel.name.toString == "isInstanceOf" && ta.args.size == 1) {
43394352
// This is a type check operation: obj.isInstanceOf[Type]
4340-
4353+
43414354
// Extract prefix
43424355
val startPos = Math.max(0, ta.span.start - offsetAdjustment)
43434356
val prefix = if (startPos > cursor && startPos <= source.length) {
43444357
Space.format(source.substring(cursor, startPos))
43454358
} else {
43464359
Space.EMPTY
43474360
}
4348-
4361+
43494362
// Update cursor to start of the expression (sel.qualifier)
43504363
cursor = Math.max(0, sel.qualifier.span.start - offsetAdjustment)
4351-
4364+
43524365
// Visit the expression being checked
43534366
val expr = visitTree(sel.qualifier) match {
43544367
case e: Expression => e
43554368
case _ => return visitUnknown(ta)
43564369
}
4357-
4370+
4371+
// Capture whitespace before ".isInstanceOf" (e.g., newline + indentation)
4372+
val isInstanceOfEnd = sel.span.end - offsetAdjustment
4373+
val exprAsInstanceOfPrefix = if (isInstanceOfEnd > cursor) {
4374+
val between = source.substring(cursor, isInstanceOfEnd)
4375+
val dotPos = between.indexOf('.')
4376+
if (dotPos > 0) Space.format(between.substring(0, dotPos)) else Space.EMPTY
4377+
} else {
4378+
Space.EMPTY
4379+
}
4380+
43584381
// Update cursor to start of type argument
43594382
cursor = Math.max(0, ta.args.head.span.start - offsetAdjustment)
4360-
4383+
43614384
// Visit the target type
43624385
val clazz = visitTree(ta.args.head) match {
43634386
case tt: TypeTree => tt
43644387
case _ => return visitUnknown(ta)
43654388
}
4366-
4389+
43674390
// Update cursor to the end of the TypeApply
43684391
updateCursor(ta.span.end)
4369-
4392+
43704393
return new J.InstanceOf(
43714394
Tree.randomId(),
43724395
prefix,
43734396
Markers.EMPTY,
4374-
JRightPadded.build(expr),
4397+
new JRightPadded(expr, exprAsInstanceOfPrefix, Markers.EMPTY),
43754398
clazz,
43764399
null, // pattern (not used in Scala)
43774400
null // type

rewrite-scala/src/test/java/org/openrewrite/scala/tree/InstanceOfTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ void instanceOfWithParentheses() {
122122
);
123123
}
124124

125+
@Test
126+
void instanceOfOnNewLine() {
127+
rewriteRun(
128+
scala(
129+
"""
130+
object Test {
131+
val x = Seq.empty
132+
.isInstanceOf[Seq[String]]
133+
}
134+
"""
135+
)
136+
);
137+
}
138+
125139
@Test
126140
void multipleInstanceOfChecks() {
127141
rewriteRun(

rewrite-scala/src/test/java/org/openrewrite/scala/tree/TypeCastTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ void castWithParentheses() {
122122
);
123123
}
124124

125+
@Test
126+
void castOnNewLine() {
127+
rewriteRun(
128+
scala(
129+
"""
130+
object Test {
131+
val x = Seq.empty
132+
.asInstanceOf[Seq[String]]
133+
}
134+
"""
135+
)
136+
);
137+
}
138+
125139
@Test
126140
void castChain() {
127141
rewriteRun(

0 commit comments

Comments
 (0)