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
130 changes: 104 additions & 26 deletions lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@
*/
package com.diffplug.spotless.groovy;

import java.io.BufferedReader;
import java.io.Serial;
import java.io.Serializable;
import java.io.StringReader;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.FormatterStep;

/**
* Removes all semicolons from the end of lines.
* Removes unnecessary semicolons from Groovy code. Preserves semicolons inside strings and comments.
*
* @author Jose Luis Badano
*/
Expand All @@ -49,32 +47,112 @@ private static final class State implements Serializable {

FormatterFunc toFormatter() {
return raw -> {
try (BufferedReader reader = new BufferedReader(new StringReader(raw))) {
StringBuilder result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(removeSemicolon(line));
result.append(System.lineSeparator());
StringBuilder result = new StringBuilder(raw.length());

// State tracking
boolean inSingleQuoteString = false;
boolean inDoubleQuoteString = false;
boolean inTripleSingleQuoteString = false;
boolean inTripleDoubleQuoteString = false;
boolean inSingleLineComment = false;
boolean inMultiLineComment = false;
boolean escaped = false;

for (int i = 0; i < raw.length(); i++) {
char c = raw.charAt(i);

// Check for triple quotes first (needs lookahead)
if (!inSingleLineComment && !inMultiLineComment && i + 2 < raw.length()) {
String triple = raw.substring(i, i + 3);
if ("'''".equals(triple) && !inDoubleQuoteString && !inTripleDoubleQuoteString) {
inTripleSingleQuoteString = !inTripleSingleQuoteString;
result.append(triple);
i += 2;
continue;
} else if ("\"\"\"".equals(triple) && !inSingleQuoteString && !inTripleSingleQuoteString) {
inTripleDoubleQuoteString = !inTripleDoubleQuoteString;
result.append(triple);
i += 2;
continue;
}
}

// Handle escaping
if (c == '\\' && (inSingleQuoteString || inDoubleQuoteString ||
inTripleSingleQuoteString || inTripleDoubleQuoteString)) {
escaped = !escaped;
result.append(c);
continue;
}

// Check for comments (only if not in string)
if (!inSingleQuoteString && !inDoubleQuoteString &&
!inTripleSingleQuoteString && !inTripleDoubleQuoteString && !escaped) {

// Single line comment
if (c == '/' && i + 1 < raw.length() && raw.charAt(i + 1) == '/' && !inMultiLineComment) {
inSingleLineComment = true;
}
// Multi-line comment start
else if (c == '/' && i + 1 < raw.length() && raw.charAt(i + 1) == '*' && !inSingleLineComment) {
inMultiLineComment = true;
}
// Multi-line comment end
else if (c == '*' && i + 1 < raw.length() && raw.charAt(i + 1) == '/' && inMultiLineComment) {
inMultiLineComment = false;
result.append(c);
if (i + 1 < raw.length()) {
result.append(raw.charAt(i + 1));
i++;
}
continue;
}
}

// Check for string quotes (only if not in comment and not already in triple quotes)
if (!inSingleLineComment && !inMultiLineComment && !escaped) {
if (c == '\'' && !inDoubleQuoteString && !inTripleSingleQuoteString && !inTripleDoubleQuoteString) {
inSingleQuoteString = !inSingleQuoteString;
} else if (c == '"' && !inSingleQuoteString && !inTripleSingleQuoteString && !inTripleDoubleQuoteString) {
inDoubleQuoteString = !inDoubleQuoteString;
}
}
return result.toString();

// End single line comment on newline
if ((c == '\n' || c == '\r') && inSingleLineComment) {
inSingleLineComment = false;
}

// Check if we should remove this semicolon
if (c == ';' && !inSingleQuoteString && !inDoubleQuoteString &&
!inTripleSingleQuoteString && !inTripleDoubleQuoteString &&
!inSingleLineComment && !inMultiLineComment) {

// Look ahead to see if this semicolon is at the end of the line
boolean isEndOfLine = true;
for (int j = i + 1; j < raw.length(); j++) {
char next = raw.charAt(j);
if (next == '\n' || next == '\r') {
break; // End of line
} else if (!Character.isWhitespace(next)) {
isEndOfLine = false;
break;
}
// If it's whitespace but not newline, continue checking
}

// Only remove if it's at the end of the line
if (isEndOfLine) {
continue; // Skip this semicolon
}
}

result.append(c);
escaped = false;
}
};
}

/**
* Removes the last semicolon in a line if it exists.
*
* @param line the line to remove the semicolon from
* @return the line without the last semicolon
*/
private String removeSemicolon(String line) {
// Find the last semicolon in a string and remove it.
int lastSemicolon = line.lastIndexOf(";");
if (lastSemicolon != -1 && lastSemicolon == line.length() - 1) {
return line.substring(0, lastSemicolon);
} else {
return line;
}
return result.toString();
};
}
}
}
1 change: 1 addition & 0 deletions plugin-maven/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
- Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757))
### Fixed
- [fix] `NPE` due to workingTreeIterator being null for git ignored files. #911 ([#2771](https://github.com/diffplug/spotless/issues/2771))
- [fix] `removeSemicolons()` should not be applied to multiline strings in groovy #2780 ([#2792](https://github.com/diffplug/spotless/issues/2792))
### Changes
* Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
* Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 DiffPlug
* Copyright 2023-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,7 @@
*/
package com.diffplug.spotless.maven.groovy;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import com.diffplug.spotless.maven.MavenIntegrationHarness;
Expand Down Expand Up @@ -43,6 +44,32 @@ void testRemoveSemicolons() throws Exception {
assertFile(path).sameAsResource("groovy/removeSemicolons/GroovyCodeWithSemicolonsFormatted.test");
}

/**
* <a href="https://github.com/diffplug/spotless/issues/2780">`removeSemicolons()` should not be applied to multiline strings in groovy</a>
*/
@Nested
class Issue2780 {
@Test
void testMultilineStrings() throws Exception {
writePomWithGroovySteps("<removeSemicolons/>");

String path = "src/main/groovy/test.groovy";
setFile(path).toResource("groovy/removeSemicolons/Issue2780/MultilineString.test");
mavenRunner().withArguments("spotless:apply").runNoError();
assertFile(path).sameAsResource("groovy/removeSemicolons/Issue2780/MultilineStringFormatted.test");
}

@Test
void testComments() throws Exception {
writePomWithGroovySteps("<removeSemicolons/>");

String path = "src/main/groovy/test.groovy";
setFile(path).toResource("groovy/removeSemicolons/Issue2780/Comments.test");
mavenRunner().withArguments("spotless:apply").runNoError();
assertFile(path).sameAsResource("groovy/removeSemicolons/Issue2780/CommentsFormatted.test");
}
}

private void runTest(String sourceContent, String targetContent) throws Exception {
String path = "src/main/groovy/test.groovy";
setFile(path).toContent(sourceContent);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
println("Hello");
def x = 5;
return x;
// This comment has a semicolon;
/* Multi-line comment;
with semicolons; */
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
println("Hello")
def x = 5
return x
// This comment has a semicolon;
/* Multi-line comment;
with semicolons; */
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
println("Hello");
def x = 5;
return x;
def multilineString = '''
function (doc, meta) {
if (doc._class == "springdata.Doc") {
emit(meta.id, null);
}
}
'''.stripIndent()
def another = """
SELECT * FROM users;
WHERE active = true;
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
println("Hello")
def x = 5
return x
def multilineString = '''
function (doc, meta) {
if (doc._class == "springdata.Doc") {
emit(meta.id, null);
}
}
'''.stripIndent()
def another = """
SELECT * FROM users;
WHERE active = true;
"""
Loading