Skip to content

Commit a9b01a9

Browse files
committed
Add support for multiline strings in compiler directives
1 parent 4345db7 commit a9b01a9

File tree

13 files changed

+185
-23
lines changed

13 files changed

+185
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Support for multiline string literals within compiler directives.
1213
- Support for the `TEXTBLOCK` directive.
1314
- **API:** `CompilerDirectiveParser` can now return a new `TextBlockDirective` type.
1415
- **API:** `CheckVerifier::withCompilerVersion` method.

delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,9 @@ private List<Issue> execute() {
280280
var sensorContext = SensorContextTester.create(FileUtils.getTempDirectory());
281281
sensorContext.settings().setProperty(DelphiProperties.TEST_TYPE_KEY, "Test.TTestSuite");
282282

283-
var compilerDirectiveParser = new CompilerDirectiveParserImpl(Platform.WINDOWS);
283+
var compilerDirectiveParser =
284+
new CompilerDirectiveParserImpl(
285+
Platform.WINDOWS, file.getTextBlockLineEndingModeRegistry());
284286

285287
var checkRegistrar = mock(MasterCheckRegistrar.class);
286288
when(checkRegistrar.getRuleKey(check))

delphi-frontend/src/main/java/au/com/integradev/delphi/executor/DelphiChecksExecutor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ public DelphiChecksExecutor(
5757
@Override
5858
public void execute(Context context, DelphiInputFile delphiFile) {
5959
Platform platform = delphiProjectHelper.getToolchain().platform;
60-
CompilerDirectiveParser compilerDirectiveParser = new CompilerDirectiveParserImpl(platform);
60+
CompilerDirectiveParser compilerDirectiveParser =
61+
new CompilerDirectiveParserImpl(platform, delphiFile.getTextBlockLineEndingModeRegistry());
6162
Function<DelphiCheck, DelphiCheckContext> createCheckContext =
6263
check ->
6364
new DelphiCheckContextImpl(

delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
import org.slf4j.Logger;
5252
import org.slf4j.LoggerFactory;
5353
import org.sonar.plugins.communitydelphi.api.directive.CompilerDirective;
54-
import org.sonar.plugins.communitydelphi.api.directive.CompilerDirectiveParser;
5554
import org.sonar.plugins.communitydelphi.api.directive.ConditionalDirective;
5655
import org.sonar.plugins.communitydelphi.api.directive.SwitchDirective.SwitchKind;
5756
import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective.LineEndingKind;
@@ -152,7 +151,7 @@ private void processToken(Token token) {
152151
tokenIndex++;
153152

154153
if (token.getType() == DelphiLexer.TkCompilerDirective) {
155-
CompilerDirectiveParser parser = new CompilerDirectiveParserImpl(platform);
154+
var parser = new CompilerDirectiveParserImpl(platform, getTextBlockLineEndingModeRegistry());
156155
DelphiToken directiveToken = new DelphiTokenImpl(token);
157156
parser.parse(directiveToken).ifPresent(this::processDirective);
158157
} else if (!parentDirective.isEmpty()) {

delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static au.com.integradev.delphi.preprocessor.directive.CompilerDirectiveParserImpl.DirectiveBracketType.PAREN;
2323

2424
import au.com.integradev.delphi.compiler.Platform;
25+
import au.com.integradev.delphi.preprocessor.TextBlockLineEndingModeRegistry;
2526
import au.com.integradev.delphi.preprocessor.directive.expression.Expression;
2627
import au.com.integradev.delphi.preprocessor.directive.expression.ExpressionLexer;
2728
import au.com.integradev.delphi.preprocessor.directive.expression.ExpressionLexer.ExpressionLexerError;
@@ -46,7 +47,6 @@
4647

4748
public class CompilerDirectiveParserImpl implements CompilerDirectiveParser {
4849
private static final ExpressionLexer EXPRESSION_LEXER = new ExpressionLexer();
49-
private static final ExpressionParser EXPRESSION_PARSER = new ExpressionParser();
5050

5151
private static final char END_OF_INPUT = '\0';
5252

@@ -56,6 +56,7 @@ enum DirectiveBracketType {
5656
}
5757

5858
private final Platform platform;
59+
private final TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry;
5960

6061
// Parser state
6162
private String data;
@@ -65,8 +66,10 @@ enum DirectiveBracketType {
6566
private DelphiToken token;
6667
private DirectiveBracketType directiveBracketType;
6768

68-
public CompilerDirectiveParserImpl(Platform platform) {
69+
public CompilerDirectiveParserImpl(
70+
Platform platform, TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry) {
6971
this.platform = platform;
72+
this.textBlockLineEndingModeRegistry = textBlockLineEndingModeRegistry;
7073
}
7174

7275
@Override
@@ -300,12 +303,17 @@ private Expression readExpression() {
300303

301304
try {
302305
var tokens = EXPRESSION_LEXER.lex(input.toString());
303-
return EXPRESSION_PARSER.parse(tokens);
306+
return expressionParser().parse(tokens);
304307
} catch (ExpressionLexerError | ExpressionParserError e) {
305308
throw new CompilerDirectiveParserError(e, token);
306309
}
307310
}
308311

312+
private ExpressionParser expressionParser() {
313+
int index = token.getIndex();
314+
return new ExpressionParser(textBlockLineEndingModeRegistry.getLineEndingMode(index));
315+
}
316+
309317
private boolean isEndOfDirective(char character) {
310318
boolean result = false;
311319

delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexer.java

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,12 @@ public List<Token> lex(String data) {
8787
}
8888

8989
private char peekChar() {
90-
if (position < data.length()) {
91-
return data.charAt(position);
90+
return peekChar(0);
91+
}
92+
93+
private char peekChar(int offset) {
94+
if (position + offset < data.length()) {
95+
return data.charAt(position + offset);
9296
}
9397
return END_OF_INPUT;
9498
}
@@ -123,7 +127,7 @@ private Token readToken() {
123127
} else if (SYNTAX_CHARACTERS.containsKey(character)) {
124128
return readSyntaxToken();
125129
} else if (character == '\'') {
126-
return readSingleQuoteString();
130+
return readString();
127131
} else {
128132
throw new ExpressionLexerError("Unexpected character: '" + character + "'");
129133
}
@@ -182,25 +186,83 @@ private Token readIdentifier() {
182186
return new Token(type, text);
183187
}
184188

185-
private Token readSingleQuoteString() {
186-
getChar();
189+
private Token readString() {
190+
Token result = readMultilineString();
191+
if (result == null) {
192+
result = readSingleLineString();
193+
}
194+
return result;
195+
}
196+
197+
private Token readSingleLineString() {
187198
StringBuilder value = new StringBuilder();
199+
value.append(getChar());
200+
188201
char character;
189202

190203
while ((character = getChar()) != END_OF_INPUT) {
204+
value.append(character);
191205
if (character == '\'') {
192-
if (peekChar() != '\'') {
206+
if (peekChar() == '\'') {
207+
value.append(getChar());
208+
} else {
193209
break;
194210
}
195-
getChar();
196-
} else {
197-
value.append(character);
198211
}
199212
}
200213

201214
return new Token(TokenType.STRING, value.toString());
202215
}
203216

217+
private Token readMultilineString() {
218+
int lookahead = lookaheadMultilineString();
219+
if (lookahead == 0) {
220+
return null;
221+
}
222+
223+
String value = data.substring(position, position + lookahead);
224+
position += lookahead;
225+
226+
return new Token(TokenType.MULTILINE_STRING, value);
227+
}
228+
229+
private int lookaheadMultilineString() {
230+
int startQuotes = lookaheadSingleQuotes(0);
231+
if (startQuotes >= 3 && (startQuotes % 2 == 1) && isNewLine(peekChar(startQuotes))) {
232+
int i = startQuotes;
233+
while (true) {
234+
switch (peekChar(++i)) {
235+
case '\'':
236+
int quotes = Math.min(startQuotes, lookaheadSingleQuotes(i));
237+
i += quotes;
238+
if (quotes == startQuotes) {
239+
return i;
240+
}
241+
break;
242+
243+
case END_OF_INPUT:
244+
return 0;
245+
246+
default:
247+
// do nothing
248+
}
249+
}
250+
}
251+
return 0;
252+
}
253+
254+
private int lookaheadSingleQuotes(int i) {
255+
int result = 0;
256+
while (peekChar(i++) == '\'') {
257+
++result;
258+
}
259+
return result;
260+
}
261+
262+
private boolean isNewLine(int c) {
263+
return c == '\r' || c == '\n';
264+
}
265+
204266
private static boolean isHexDigit(char character) {
205267
character = Character.toLowerCase(character);
206268
return Character.isDigit(character) || (character >= 'a' && character <= 'f');

delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParser.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,19 @@
3939
import static au.com.integradev.delphi.preprocessor.directive.expression.Token.TokenType.XOR;
4040
import static java.util.Objects.requireNonNullElse;
4141

42+
import au.com.integradev.delphi.preprocessor.TextBlockLineEndingMode;
4243
import au.com.integradev.delphi.preprocessor.directive.expression.Token.TokenType;
4344
import com.google.common.collect.ImmutableSet;
4445
import com.google.common.collect.Sets;
46+
import java.util.ArrayDeque;
4547
import java.util.ArrayList;
48+
import java.util.Deque;
4649
import java.util.HashSet;
4750
import java.util.List;
4851
import java.util.Set;
52+
import java.util.stream.Collectors;
4953
import javax.annotation.Nullable;
54+
import org.apache.commons.lang3.StringUtils;
5055

5156
public class ExpressionParser {
5257
public static class ExpressionParserError extends RuntimeException {
@@ -70,10 +75,16 @@ public static class ExpressionParserError extends RuntimeException {
7075
private static final ImmutableSet<TokenType> UNARY_OPERATORS =
7176
Sets.immutableEnumSet(PLUS, MINUS, NOT);
7277

78+
private final TextBlockLineEndingMode textBlockLineEndingMode;
79+
7380
// Parser state
7481
private List<Token> tokens;
7582
private int position;
7683

84+
public ExpressionParser(TextBlockLineEndingMode textBlockLineEndingMode) {
85+
this.textBlockLineEndingMode = textBlockLineEndingMode;
86+
}
87+
7788
public Expression parse(List<Token> tokens) {
7889
this.tokens = tokens;
7990
this.position = 0;
@@ -169,6 +180,7 @@ private Expression parsePrimary() {
169180
Token token = peekToken();
170181
switch (token.getType()) {
171182
case STRING:
183+
case MULTILINE_STRING:
172184
case INTEGER:
173185
case REAL:
174186
return parseLiteral();
@@ -191,7 +203,64 @@ private Expression parsePrimary() {
191203

192204
private Expression parseLiteral() {
193205
Token token = getToken();
194-
return Expressions.literal(token.getType(), token.getText());
206+
207+
String text;
208+
switch (token.getType()) {
209+
case STRING:
210+
text = evaluateString(token.getText());
211+
break;
212+
case MULTILINE_STRING:
213+
text = evaluateMultilineString(token.getText(), textBlockLineEndingMode);
214+
break;
215+
default:
216+
text = token.getText();
217+
}
218+
219+
return Expressions.literal(token.getType(), text);
220+
}
221+
222+
private static String evaluateString(String text) {
223+
text = text.substring(1, text.length() - 1);
224+
text = text.replace("''", "'");
225+
return text;
226+
}
227+
228+
private String evaluateMultilineString(String text, TextBlockLineEndingMode lineEndingMode) {
229+
Deque<String> lines = text.lines().collect(Collectors.toCollection(ArrayDeque<String>::new));
230+
231+
lines.removeFirst();
232+
233+
String last = lines.removeLast();
234+
String indentation = readLeadingWhitespace(last);
235+
236+
String lineEnding;
237+
switch (lineEndingMode) {
238+
case CR:
239+
lineEnding = "\r";
240+
break;
241+
case LF:
242+
lineEnding = "\n";
243+
break;
244+
default:
245+
lineEnding = "\r\n";
246+
}
247+
248+
return lines.stream()
249+
.map(line -> StringUtils.removeStart(line, indentation))
250+
.collect(Collectors.joining(lineEnding));
251+
}
252+
253+
private static String readLeadingWhitespace(String input) {
254+
StringBuilder result = new StringBuilder();
255+
for (int i = 0; i < input.length(); ++i) {
256+
char c = input.charAt(i);
257+
if (c <= 0x20 || c == 0x3000) {
258+
result.append(c);
259+
} else {
260+
break;
261+
}
262+
}
263+
return result.toString();
195264
}
196265

197266
private Expression parseIdentifier() {

delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Expressions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ private static ExpressionValue createValue(TokenType type, String text) {
186186
case REAL:
187187
return createReal(doubleFromTextWithDigitSeparators(text));
188188
case STRING:
189+
case MULTILINE_STRING:
189190
return createString(text);
190191
default:
191192
throw new AssertionError("Unhandled literal expression type: " + type.name());

delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Token.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ enum TokenType {
2525
REAL,
2626
IDENTIFIER,
2727
STRING,
28+
MULTILINE_STRING,
2829
EQUALS,
2930
NOT_EQUALS,
3031
LESS_THAN,

delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import static org.assertj.core.api.Assertions.assertThat;
2222
import static org.assertj.core.api.Assertions.assertThatThrownBy;
23+
import static org.mockito.Mockito.mock;
2324

2425
import au.com.integradev.delphi.antlr.DelphiLexer;
2526
import au.com.integradev.delphi.antlr.ast.token.DelphiTokenImpl;
@@ -53,7 +54,7 @@ class CompilerDirectiveParserTest {
5354

5455
@BeforeEach
5556
void setup() {
56-
parser = new CompilerDirectiveParserImpl(Platform.WINDOWS);
57+
parser = new CompilerDirectiveParserImpl(Platform.WINDOWS, mock());
5758
}
5859

5960
@Test

0 commit comments

Comments
 (0)