Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package org.openrewrite.style;

public enum LineWrapSetting {
DoNotWrap, WrapAlways;
// Eventually we would add values like WrapIfTooLong or ChopIfTooLong

DoNotWrap, WrapAlways, ChopIfTooLong;
// Eventually we would add values like WrapIfTooLong
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class WrappingAndBracesTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(toRecipe(() -> new WrappingAndBracesVisitor<>(new WrappingAndBracesStyle(
120,
new WrappingAndBracesStyle.IfStatement(false),
new WrappingAndBracesStyle.ChainedMethodCalls(WrapAlways, Arrays.asList("builder", "newBuilder")),
new WrappingAndBracesStyle.Annotations(WrapAlways),
Expand Down Expand Up @@ -925,6 +926,7 @@ final String method4(){
void annotationWrappingWithNulls() {
rewriteRun(spec ->
spec.recipe(toRecipe(() -> new WrappingAndBracesVisitor<>(new WrappingAndBracesStyle(
120,
new WrappingAndBracesStyle.IfStatement(false),
new WrappingAndBracesStyle.ChainedMethodCalls(DoNotWrap, emptyList()),
null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.java.format;

import org.openrewrite.Cursor;
import org.openrewrite.PrintOutputCapture;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Space;
import org.openrewrite.java.tree.Statement;

import static java.util.Collections.emptyList;

class LengthCalculator {

public static int computeTreeLineLength(J tree, Cursor cursor) {
Object cursorValue = cursor.getValue();
if (cursorValue instanceof J) {
J j = (J) cursorValue;
boolean hasNewLine = j.getPrefix().getWhitespace().contains("\n") || j.getComments().stream().anyMatch(c -> c.getSuffix().contains("\n"));
Cursor parent = cursor.getParentTreeCursor();
boolean isCompilationUnit = parent.getValue() instanceof J.CompilationUnit;
if (!hasNewLine && !isCompilationUnit) {
return computeTreeLineLength(parent.getValue(), parent);
}
} else {
throw new RuntimeException("Unable to calculate length due to unexpected cursor value: " + cursorValue.getClass());
}
Comment on lines +27 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a service mechanism for supplying language-specific implementations of concepts that appear in multiple languages. This strikes me as a good candidate for that pattern. See JavaSourceFile.service().
This is how a JavaVisitor can visit a kotlin source file and have maybeAddImport() use the kotlin version of AddImport to do it.

I see this as the default SourcePositionService. While you don't need to do it right now, it could be a reasonable place to put a getLineNumber() or getStartColumn()/getEndColumn() methods which are sometimes requested. Methods like these could be relevant in any language or data format.

There could be Groovy/Kotlin variations which, for example, return false more often in needsSemicolon as those languages only require them to separate multiple statements on the same line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took this approach from ColumnPositionCalculator already present. Is that then also a good candidate?

This method though does not getLineNumber but gets the length of the current element (including its newlines) starting from the first element that starts on it's line.

Counting the prefix (indentation only) of that element + the length. I struggled to find an explanatory name for that. Any suggestions?


TreeVisitor<?, PrintOutputCapture<TreeVisitor<?, ?>>> printer = tree.printer(cursor);
PrintOutputCapture<TreeVisitor<?, ?>> capture = new PrintOutputCapture<>(printer, PrintOutputCapture.MarkerPrinter.SANITIZED);

printer.visit(trimPrefix(tree), capture, cursor.getParentOrThrow());

return capture.getOut().length() + getSuffixLength(tree);
}

private static int getSuffixLength(J tree) {
if (tree instanceof Statement && needsSemicolon((Statement) tree)) {
return 1;
}
return 0;
}

private static boolean needsSemicolon(Statement statement) {
return statement instanceof J.MethodInvocation || statement instanceof J.VariableDeclarations || statement instanceof J.Assignment || statement instanceof J.Package;
}

private static J trimPrefix(J tree) {
Space prefix = tree.getPrefix();
String whitespace = prefix.getLastWhitespace().replaceFirst("^.*\\n*", "");
prefix = prefix.withComments(emptyList()).withWhitespace(whitespace);
return tree.withPrefix(prefix);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P
return c;
}

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, P p) {
if (method.getName().getComments().isEmpty() && method.getName().getPrefix().getWhitespace().contains("\n")) {
method = method.withName(method.getName().withPrefix(Space.EMPTY));
}
return super.visitMethodInvocation(method, p);
}

@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P p) {
J.MethodDeclaration m = super.visitMethodDeclaration(method, p);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,6 @@ private int computeFirstParameterColumn(J.MethodDeclaration method) {
if (firstArg.getPrefix().getLastWhitespace().contains("\n")) {
int declPrefixLength = getLengthOfWhitespace(method.getPrefix().getLastWhitespace());
int argPrefixLength = getLengthOfWhitespace(firstArg.getPrefix().getLastWhitespace());
//noinspection ConstantConditions to be backwards compatible with older style versions
if (declPrefixLength >= argPrefixLength) {
return declPrefixLength + style.getContinuationIndent();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, P ctx

try {
// styles are parent loaded, so the getters may or may not be present and they may or may not return null
if (style != null && style.getChainedMethodCalls() != null && style.getChainedMethodCalls().getWrap() == LineWrapSetting.WrapAlways) {
if (style != null && style.getChainedMethodCalls() != null &&
(style.getChainedMethodCalls().getWrap() == LineWrapSetting.WrapAlways || style.getChainedMethodCalls().getWrap() == LineWrapSetting.ChopIfTooLong)) {
List<MethodMatcher> matchers = style.getChainedMethodCalls().getBuilderMethods().stream()
.map(name -> String.format("*..* %s(..)", name))
.map(MethodMatcher::new)
Expand All @@ -60,6 +61,11 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, P ctx
return m;
}

// Not long enough to wrap
if (style.getChainedMethodCalls().getWrap() == LineWrapSetting.ChopIfTooLong && LengthCalculator.computeTreeLineLength(method, getCursor()) <= style.getHardWrapAt()) {
return m;
}

//Only update the whitespace, preserving comments
if (after.getComments().isEmpty()) {
after = after.withWhitespace("\n");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public static SpacesStyle spaces() {

public static WrappingAndBracesStyle wrappingAndBraces() {
return new WrappingAndBracesStyle(
120,
new WrappingAndBracesStyle.IfStatement(false),
new WrappingAndBracesStyle.ChainedMethodCalls(DoNotWrap, emptyList()),
new WrappingAndBracesStyle.Annotations(WrapAlways),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
@With
public class WrappingAndBracesStyle implements JavaStyle {

int hardWrapAt;
IfStatement ifStatement;
ChainedMethodCalls chainedMethodCalls;
@Nullable Annotations classAnnotations;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static class Builder {
new NamedStyles(UUID.randomUUID(), "junit", "Unit Test style", "Only used in unit tests", emptySet(),
List.of(
new WrappingAndBracesStyle(
120,
new WrappingAndBracesStyle.IfStatement(false),
new WrappingAndBracesStyle.ChainedMethodCalls(WrapAlways, asList("builder", "newBuilder", "stream")),
new WrappingAndBracesStyle.Annotations(WrapAlways),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.java.format;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.ExecutionContext;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.test.RewriteTest;

import static org.assertj.core.api.Assertions.assertThat;
import static org.openrewrite.java.Assertions.java;

class LengthCalculatorTest implements RewriteTest {

@DocumentExample
@Test
void correctlyCalculatesLineLength() {
rewriteRun(
spec -> spec.recipe(RewriteTest.toRecipe(() -> new JavaIsoVisitor<>() {

@Override
public J.Package visitPackage(J.Package pkg, ExecutionContext ctx) {
assertThat(LengthCalculator.computeTreeLineLength(pkg, getCursor())).isEqualTo(20);
return super.visitPackage(pkg, ctx);
}

@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
if ("Test".equals(classDecl.getSimpleName())) {
assertThat(LengthCalculator.computeTreeLineLength(classDecl, getCursor())).isEqualTo(381);
}
if ("Inner".equals(classDecl.getSimpleName())) {
assertThat(LengthCalculator.computeTreeLineLength(classDecl, getCursor())).isEqualTo(72);
}
return super.visitClassDeclaration(classDecl, ctx);
}

@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
assertThat(LengthCalculator.computeTreeLineLength(method, getCursor())).isEqualTo(285);
return super.visitMethodDeclaration(method, ctx);
}

@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
assertThat(LengthCalculator.computeTreeLineLength(multiVariable, getCursor())).isEqualTo(80);
return super.visitVariableDeclarations(multiVariable, ctx);
}

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
assertThat(LengthCalculator.computeTreeLineLength(method, getCursor())).isEqualTo(80);
return super.visitMethodInvocation(method, ctx);
}
})),
java(
"""
package com.example;

// Comments should not affect line length calculation
public class Test {
public void /* multiline comments can impact though */ example() {
String text = "This is a sample string to test line length calculation";
// Another comment that is not counted
String invocation = String.valueOf("Both lines share the same length.");
}

class Inner {
// Inner class to test nested structures
}
}
"""
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.java.format;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;
import static org.openrewrite.test.RewriteTest.toRecipe;

class MinimumViableSpacingVisitorTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec
.parser(JavaParser.fromJavaVersion().dependsOn("""
package com.example;

public class MyObject {
public static Builder builder() { return new Builder(); }
public static Builder newBuilder() { return new Builder(); }
public static class Builder {
Builder name(String n) { return this; }
Builder age(int a) { return this; }
Builder items(java.util.List<String> items) { return this; }
Builder nested(MyObject nested) { return this; }
MyObject build() { return new MyObject(); }
}
}
"""))
.recipe(toRecipe(() -> new MinimumViableSpacingVisitor<>(null)));
}

@DocumentExample
@Test
void reformatChainedMethodInvocationToSingleLine() {
rewriteRun(
java(
"""
package com.example;
class Test {
void test() {
MyObject myObject = MyObject.builder()
.

name("John");
}
}
""",
"""
package com.example;
class Test {
void test() {
MyObject myObject = MyObject.builder()
.name("John");
}
}
"""
)
);
}

@Test
void doNotReformatChainedMethodInvocationToSingleLineWhenCommentInPrefixOfName() {
rewriteRun(
java(
"""
package com.example;
class Test {
void test() {
MyObject myObject = MyObject.builder()
. //Some comment
name("John");
}
}
"""
)
);
}
}
Loading