Skip to content

Commit 51141c0

Browse files
author
Vincent Potucek
committed
[fix] removeSemicolons() should not be applied to multiline strings in groovy #2780
Signed-off-by: Vincent Potucek <vpotucek@me.com>
1 parent 5743609 commit 51141c0

File tree

7 files changed

+173
-27
lines changed

7 files changed

+173
-27
lines changed

lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@
1515
*/
1616
package com.diffplug.spotless.groovy;
1717

18-
import java.io.BufferedReader;
1918
import java.io.Serial;
2019
import java.io.Serializable;
21-
import java.io.StringReader;
2220

2321
import com.diffplug.spotless.FormatterFunc;
2422
import com.diffplug.spotless.FormatterStep;
2523

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

5048
FormatterFunc toFormatter() {
5149
return raw -> {
52-
try (BufferedReader reader = new BufferedReader(new StringReader(raw))) {
53-
StringBuilder result = new StringBuilder();
54-
String line;
55-
while ((line = reader.readLine()) != null) {
56-
result.append(removeSemicolon(line));
57-
result.append(System.lineSeparator());
50+
StringBuilder result = new StringBuilder(raw.length());
51+
52+
// State tracking
53+
boolean inSingleQuoteString = false;
54+
boolean inDoubleQuoteString = false;
55+
boolean inTripleSingleQuoteString = false;
56+
boolean inTripleDoubleQuoteString = false;
57+
boolean inSingleLineComment = false;
58+
boolean inMultiLineComment = false;
59+
boolean escaped = false;
60+
61+
for (int i = 0; i < raw.length(); i++) {
62+
char c = raw.charAt(i);
63+
64+
// Check for triple quotes first (needs lookahead)
65+
if (!inSingleLineComment && !inMultiLineComment && i + 2 < raw.length()) {
66+
String triple = raw.substring(i, i + 3);
67+
if (triple.equals("'''") && !inDoubleQuoteString && !inTripleDoubleQuoteString) {
68+
inTripleSingleQuoteString = !inTripleSingleQuoteString;
69+
result.append(triple);
70+
i += 2;
71+
continue;
72+
} else if (triple.equals("\"\"\"") && !inSingleQuoteString && !inTripleSingleQuoteString) {
73+
inTripleDoubleQuoteString = !inTripleDoubleQuoteString;
74+
result.append(triple);
75+
i += 2;
76+
continue;
77+
}
78+
}
79+
80+
// Handle escaping
81+
if (c == '\\' && (inSingleQuoteString || inDoubleQuoteString ||
82+
inTripleSingleQuoteString || inTripleDoubleQuoteString)) {
83+
escaped = !escaped;
84+
result.append(c);
85+
continue;
86+
}
87+
88+
// Check for comments (only if not in string)
89+
if (!inSingleQuoteString && !inDoubleQuoteString &&
90+
!inTripleSingleQuoteString && !inTripleDoubleQuoteString && !escaped) {
91+
92+
// Single line comment
93+
if (c == '/' && i + 1 < raw.length() && raw.charAt(i + 1) == '/' && !inMultiLineComment) {
94+
inSingleLineComment = true;
95+
}
96+
// Multi-line comment start
97+
else if (c == '/' && i + 1 < raw.length() && raw.charAt(i + 1) == '*' && !inSingleLineComment) {
98+
inMultiLineComment = true;
99+
}
100+
// Multi-line comment end
101+
else if (c == '*' && i + 1 < raw.length() && raw.charAt(i + 1) == '/' && inMultiLineComment) {
102+
inMultiLineComment = false;
103+
result.append(c);
104+
if (i + 1 < raw.length()) {
105+
result.append(raw.charAt(i + 1));
106+
i++;
107+
}
108+
continue;
109+
}
110+
}
111+
112+
// Check for string quotes (only if not in comment and not already in triple quotes)
113+
if (!inSingleLineComment && !inMultiLineComment && !escaped) {
114+
if (c == '\'' && !inDoubleQuoteString && !inTripleSingleQuoteString && !inTripleDoubleQuoteString) {
115+
inSingleQuoteString = !inSingleQuoteString;
116+
} else if (c == '"' && !inSingleQuoteString && !inTripleSingleQuoteString && !inTripleDoubleQuoteString) {
117+
inDoubleQuoteString = !inDoubleQuoteString;
118+
}
58119
}
59-
return result.toString();
120+
121+
// End single line comment on newline
122+
if ((c == '\n' || c == '\r') && inSingleLineComment) {
123+
inSingleLineComment = false;
124+
}
125+
126+
// Check if we should remove this semicolon
127+
if (c == ';' && !inSingleQuoteString && !inDoubleQuoteString &&
128+
!inTripleSingleQuoteString && !inTripleDoubleQuoteString &&
129+
!inSingleLineComment && !inMultiLineComment) {
130+
131+
// Look ahead to see if this semicolon is at the end of the line
132+
boolean isEndOfLine = true;
133+
for (int j = i + 1; j < raw.length(); j++) {
134+
char next = raw.charAt(j);
135+
if (next == '\n' || next == '\r') {
136+
break; // End of line
137+
} else if (!Character.isWhitespace(next)) {
138+
isEndOfLine = false;
139+
break;
140+
}
141+
// If it's whitespace but not newline, continue checking
142+
}
143+
144+
// Only remove if it's at the end of the line
145+
if (isEndOfLine) {
146+
continue; // Skip this semicolon
147+
}
148+
}
149+
150+
result.append(c);
151+
escaped = false;
60152
}
61-
};
62-
}
63153

64-
/**
65-
* Removes the last semicolon in a line if it exists.
66-
*
67-
* @param line the line to remove the semicolon from
68-
* @return the line without the last semicolon
69-
*/
70-
private String removeSemicolon(String line) {
71-
// Find the last semicolon in a string and remove it.
72-
int lastSemicolon = line.lastIndexOf(";");
73-
if (lastSemicolon != -1 && lastSemicolon == line.length() - 1) {
74-
return line.substring(0, lastSemicolon);
75-
} else {
76-
return line;
77-
}
154+
return result.toString();
155+
};
78156
}
79157
}
80158
}

plugin-maven/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
77
- Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757))
88
### Fixed
99
- [fix] `NPE` due to workingTreeIterator being null for git ignored files. #911 ([#2771](https://github.com/diffplug/spotless/issues/2771))
10+
- [fix] `removeSemicolons()` should not be applied to multiline strings in groovy #2780 ([#2792](https://github.com/diffplug/spotless/issues/2792))
1011
### Changes
1112
* Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
1213
* Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))

plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/RemoveSemicolonsTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 DiffPlug
2+
* Copyright 2023-2025 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
1515
*/
1616
package com.diffplug.spotless.maven.groovy;
1717

18+
import org.junit.jupiter.api.Nested;
1819
import org.junit.jupiter.api.Test;
1920

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

47+
/**
48+
* <a href="https://github.com/diffplug/spotless/issues/2780">`removeSemicolons()` should not be applied to multiline strings in groovy</a>
49+
*/
50+
@Nested
51+
class Issue2780 {
52+
@Test
53+
void testMultilineStrings() throws Exception {
54+
writePomWithGroovySteps("<removeSemicolons/>");
55+
56+
String path = "src/main/groovy/test.groovy";
57+
setFile(path).toResource("groovy/removeSemicolons/Issue2780/MultilineString.test");
58+
mavenRunner().withArguments("spotless:apply").runNoError();
59+
assertFile(path).sameAsResource("groovy/removeSemicolons/Issue2780/MultilineStringFormatted.test");
60+
}
61+
62+
@Test
63+
void testComments() throws Exception {
64+
writePomWithGroovySteps("<removeSemicolons/>");
65+
66+
String path = "src/main/groovy/test.groovy";
67+
setFile(path).toResource("groovy/removeSemicolons/Issue2780/Comments.test");
68+
mavenRunner().withArguments("spotless:apply").runNoError();
69+
assertFile(path).sameAsResource("groovy/removeSemicolons/Issue2780/CommentsFormatted.test");
70+
}
71+
}
72+
4673
private void runTest(String sourceContent, String targetContent) throws Exception {
4774
String path = "src/main/groovy/test.groovy";
4875
setFile(path).toContent(sourceContent);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
println("Hello");
2+
def x = 5;
3+
return x;
4+
// This comment has a semicolon;
5+
/* Multi-line comment;
6+
with semicolons; */
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
println("Hello")
2+
def x = 5
3+
return x
4+
// This comment has a semicolon;
5+
/* Multi-line comment;
6+
with semicolons; */
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
println("Hello");
2+
def x = 5;
3+
return x;
4+
def multilineString = '''
5+
function (doc, meta) {
6+
if (doc._class == "springdata.Doc") {
7+
emit(meta.id, null);
8+
}
9+
}
10+
'''.stripIndent()
11+
def another = """
12+
SELECT * FROM users;
13+
WHERE active = true;
14+
"""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
println("Hello")
2+
def x = 5
3+
return x
4+
def multilineString = '''
5+
function (doc, meta) {
6+
if (doc._class == "springdata.Doc") {
7+
emit(meta.id, null);
8+
}
9+
}
10+
'''.stripIndent()
11+
def another = """
12+
SELECT * FROM users;
13+
WHERE active = true;
14+
"""

0 commit comments

Comments
 (0)