Skip to content
Merged
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 @@ -20,6 +20,77 @@

class JfmtStyleTest {

@Test
void keepsAdjacentBlockCommentsSeparatedAndStable() {
String input =
"""
/*
* License.
*/

/** Type documentation. */
class Example {}
""";

String formatted = JfmtTestSupport.format(input);

assertEquals(
"""
/*
* License.
*/
/** Type documentation. */
class Example {}
""",
formatted);
assertEquals(formatted, JfmtTestSupport.format(formatted));
}

@Test
void leavesAdjacentCommentMarkersInsideStringsUntouched() {
String input = "class Example {\n"
+ "String value = \"*//**\";\n"
+ "String text = \"\"\"\n"
+ "*//**\n"
+ "\"\"\";\n"
+ "}\n";

String formatted = JfmtTestSupport.format(input);

assertEquals(2, formatted.split("\\*//\\*\\*", -1).length - 1);
assertEquals(formatted, JfmtTestSupport.format(formatted));
}

@Test
void keepsLongSwitchArmsStable() {
String input =
"""
final class MavenCommand {
private final ToolRuntime tools;
private final List<ToolDefinition> definitions;
private final ResolutionOptions javadocOptions;

int run(JaInvocation commandLine, ModuleSourcePath moduleSourcePath, InputStream in,
PrintStream out, PrintStream err)
throws IOException {
if (commandLine.toolArguments().isEmpty()) {
throw new IllegalArgumentException("maven requires an operation: export, install, deploy, or deploy-central");
}
return switch (commandLine.toolArguments().getFirst()) {
case "export" -> new MavenExporter(tools).run(commandLine, moduleSourcePath, in, out, err);
case "install", "deploy", "deploy-central" -> new MavenDeploymentCommand(tools, definitions,
javadocOptions).run(commandLine, moduleSourcePath, in, out, err);
default -> throw new IllegalArgumentException("Unknown maven operation: " + commandLine.toolArguments().getFirst());
};
}
}
""";

String formatted = JfmtTestSupport.format(input);

assertEquals(formatted, JfmtTestSupport.format(formatted));
}

@Test
void placesStaticImportsAfterOrdinaryImports() {
String input = "import static java.util.Collections.emptyList;import sun.misc.Unsafe;import java.util.List;import jdk.internal.misc.VM;class Example{List<?> values=emptyList();Unsafe unsafe;Class<?> vm=VM.class;}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,36 @@ void expandsArgumentFiles() throws Exception {
assertEquals("class Example {\n int value;\n}\n", Files.readString(source));
}

@Test
void formatsMultipleFilesIdempotently() throws Exception {
var files = new String[64];
for (int i = 0; i < files.length; i++) {
Path source = write(
temporaryDirectory.resolve("Example" + i + ".java"),
"""
class Example%d {
int run(String operation) {
return switch (operation) {
case "install", "deploy", "deploy-central" -> new MavenDeploymentCommand(tools, definitions, javadocOptions)
.run(commandLine, moduleSourcePath, in, out, err);
default -> 0;
};
}
}
""".formatted(i));
files[i] = source.toString();
}

Invocation formatted = run("", files);
var checkArguments = new String[files.length + 1];
checkArguments[0] = "--check";
System.arraycopy(files, 0, checkArguments, 1, files.length);
Invocation checked = run("", checkArguments);

assertEquals(0, formatted.exitCode(), formatted.error());
assertEquals(0, checked.exitCode(), checked.error());
}

@Test
void acceptsOptionsAfterFileOperands() throws Exception {
Path source = write(temporaryDirectory.resolve("Example.java"), "class Example{int value;}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,83 @@ public JfmtFormatter() {
* Format a Java source file, reorder imports, and remove unused imports.
*/
public String formatSource(String input) throws FormatterException {
return formatter.formatSourceAndFixImports(input);
String formatted = separateAdjacentBlockComments(formatter.formatSourceAndFixImports(input));
for (int pass = 0; pass < 8; pass++) {
String next = separateAdjacentBlockComments(formatter.formatSource(formatted));
if (next.equals(formatted)) {
return formatted;
}
formatted = next;
}
throw new FormatterException("Formatting did not converge");
}

private static String separateAdjacentBlockComments(String input) {
var output = new StringBuilder(input.length());
String lineSeparator = input.contains("\r\n") ? "\r\n" : "\n";
var state = LexicalState.NORMAL;
for (int i = 0; i < input.length();) {
if (state == LexicalState.NORMAL && input.startsWith("\"\"\"", i)) {
output.append("\"\"\"");
i += 3;
state = LexicalState.TEXT_BLOCK;
} else if (state == LexicalState.NORMAL && input.startsWith("//", i)) {
output.append("//");
i += 2;
state = LexicalState.LINE_COMMENT;
} else if (state == LexicalState.NORMAL && input.startsWith("/*", i)) {
output.append("/*");
i += 2;
state = LexicalState.BLOCK_COMMENT;
} else if (state == LexicalState.NORMAL && input.charAt(i) == '"') {
output.append('"');
i++;
state = LexicalState.STRING;
} else if (state == LexicalState.NORMAL && input.charAt(i) == '\'') {
output.append('\'');
i++;
state = LexicalState.CHARACTER;
} else if ((state == LexicalState.STRING || state == LexicalState.CHARACTER || state == LexicalState.TEXT_BLOCK)
&& input.charAt(i) == '\\'
&& i + 1 < input.length()) {
output.append(input, i, i + 2);
i += 2;
} else if (state == LexicalState.STRING && input.charAt(i) == '"') {
output.append('"');
i++;
state = LexicalState.NORMAL;
} else if (state == LexicalState.CHARACTER && input.charAt(i) == '\'') {
output.append('\'');
i++;
state = LexicalState.NORMAL;
} else if (state == LexicalState.TEXT_BLOCK && input.startsWith("\"\"\"", i)) {
output.append("\"\"\"");
i += 3;
state = LexicalState.NORMAL;
} else if (state == LexicalState.LINE_COMMENT && (input.charAt(i) == '\n' || input.charAt(i) == '\r')) {
output.append(input.charAt(i++));
state = LexicalState.NORMAL;
} else if (state == LexicalState.BLOCK_COMMENT && input.startsWith("*/", i)) {
output.append("*/");
i += 2;
state = LexicalState.NORMAL;
if (input.startsWith("/*", i)) {
output.append(lineSeparator);
}
} else {
output.append(input.charAt(i++));
}
}
return output.toString();
}

private enum LexicalState {
NORMAL,
STRING,
CHARACTER,
TEXT_BLOCK,
LINE_COMMENT,
BLOCK_COMMENT
}

/** Format specified character ranges of a Java source file. */
Expand Down
Loading