1515 */
1616package com .diffplug .spotless .groovy ;
1717
18- import java .io .BufferedReader ;
1918import java .io .Serial ;
2019import java .io .Serializable ;
21- import java .io .StringReader ;
2220
2321import com .diffplug .spotless .FormatterFunc ;
2422import 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}
0 commit comments