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
1 change: 1 addition & 0 deletions antlr/BaseApexLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ BooleanLiteral
// Multi-line strings (Salesforce Summer '26).
// Body must start on a new line after the opening '''. The first newline is
// part of the token but conventionally stripped at runtime by the platform.
// CRLF acceptance via [\r\n] is deliberate and covered by tests; bare CR is also accepted.
// Declared before StringLiteral so longest-match prefers '''...''' over the
// degenerate ''+'...'+'' fallback when a newline follows the opening.
// Backslashes must form a valid EscapeSequence (matching StringLiteral
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import java.util.Map;
import org.antlr.v4.runtime.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class ApexLexerTest {

Expand Down Expand Up @@ -113,6 +115,45 @@ void testMultilineStringBasic() {
assertEquals(ApexLexer.MultilineStringLiteral, tokens.get(0).getType());
}

@ParameterizedTest
@ValueSource(strings = { "\n", "\r\n", "\r" })
void testMultilineStringLineEndingsPreserveRawText(String newline) {
String body = newline + "Hello" + newline + " World\\n" + newline;
String source = "'''" + body + "'''";
Map.Entry<ApexLexer, SyntaxErrorCounter> lc = createLexer(source);
CommonTokenStream tokens = new CommonTokenStream(lc.getKey());
tokens.fill();
assertEquals(0, lc.getValue().getNumErrors());
assertEquals(2, tokens.size()); // MultilineStringLiteral + EOF
Token literal = tokens.get(0);
assertEquals(ApexLexer.MultilineStringLiteral, literal.getType());
assertEquals(source, literal.getText());
// Remove only the delimiters: retain the opening newline and raw escapes.
assertEquals(body, literal.getText().substring(3, source.length() - 3));
}

@Test
void testMultilineStringCRLFLineAndColumnTracking() {
Map.Entry<ApexLexer, SyntaxErrorCounter> lc = createLexer(
"\r\n '''\r\nHello\r\nWorld\r\n''' x\r\n y"
);
CommonTokenStream tokens = new CommonTokenStream(lc.getKey());
tokens.fill();
assertEquals(0, lc.getValue().getNumErrors());
Token literal = tokens.get(1); // leading whitespace is on a hidden channel
assertEquals(ApexLexer.MultilineStringLiteral, literal.getType());
assertEquals(2, literal.getLine());
assertEquals(2, literal.getCharPositionInLine());
Token sameLine = tokens.get(3);
assertEquals("x", sameLine.getText());
assertEquals(5, sameLine.getLine());
assertEquals(4, sameLine.getCharPositionInLine());
Token nextLine = tokens.get(5);
assertEquals("y", nextLine.getText());
assertEquals(6, nextLine.getLine());
assertEquals(2, nextLine.getCharPositionInLine());
}

@Test
void testMultilineStringClosingOnOwnLine() {
Map.Entry<ApexLexer, SyntaxErrorCounter> lc = createLexer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class ApexParserTest {

Expand Down Expand Up @@ -323,6 +325,22 @@ void testMultilineStringLiteral() {
assertEquals(0, parserAndCounter.getValue().getNumErrors());
}

@ParameterizedTest
@ValueSource(strings = { "\n", "\r\n", "\r" })
void testMultilineStringLineEndingsPreserveRawText(String newline) {
String body = newline + "Hello" + newline + " World\\n" + newline;
String source = "'''" + body + "'''";
Map.Entry<ApexParser, SyntaxErrorCounter> parserAndCounter = createParser(
source
);
ApexParser.LiteralContext context = parserAndCounter.getKey().literal();
assertNotNull(context.MultilineStringLiteral());
String text = context.MultilineStringLiteral().getText();
assertEquals(source, text);
assertEquals(body, text.substring(3, text.length() - 3));
assertEquals(0, parserAndCounter.getValue().getNumErrors());
}

@Test
void testMultilineStringInClassBody() {
Map.Entry<ApexParser, SyntaxErrorCounter> parserAndCounter = createParser(
Expand Down
34 changes: 34 additions & 0 deletions npm/test/ApexLexerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,40 @@ test("Multi-line string: basic body, closing on same line", () => {
expect(tokens.tokens[0].type).toBe(ApexLexer.MultilineStringLiteral);
});

test.each([
["LF", "\n"],
["CRLF", "\r\n"],
["bare CR", "\r"],
])("Multi-line string: %s preserves raw token and body", (_name, newline) => {
const body = newline + "Hello" + newline + " World\\n" + newline;
const source = "'''" + body + "'''";
const { tokens, errors } = lex(source);
expect(errors).toEqual(0);
expect(tokens.tokens.length).toBe(2); // MultilineStringLiteral + EOF
const literal = tokens.tokens[0];
expect(literal.type).toBe(ApexLexer.MultilineStringLiteral);
expect(literal.text).toBe(source);
// Remove only the delimiters: retain the opening newline and raw escapes.
expect(literal.text.slice(3, -3)).toBe(body);
});

test("Multi-line string: CRLF line/column tracking", () => {
const { tokens, errors } = lex("\r\n '''\r\nHello\r\nWorld\r\n''' x\r\n y");
expect(errors).toEqual(0);
const literal = tokens.tokens[1]; // leading whitespace is on a hidden channel
expect(literal.type).toBe(ApexLexer.MultilineStringLiteral);
expect(literal.line).toBe(2);
expect(literal.column).toBe(2);
const sameLine = tokens.tokens[3];
expect(sameLine.text).toBe("x");
expect(sameLine.line).toBe(5);
expect(sameLine.column).toBe(4);
const nextLine = tokens.tokens[5];
expect(nextLine.text).toBe("y");
expect(nextLine.line).toBe(6);
expect(nextLine.column).toBe(2);
});

test("Multi-line string: closing on its own line", () => {
const { tokens, errors } = lex("'''\nhello\n'''");
expect(errors).toEqual(0);
Expand Down
16 changes: 16 additions & 0 deletions npm/test/ApexParserTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,22 @@ test("Multiline String Literal", () => {
);
});

test.each([
["LF", "\n"],
["CRLF", "\r\n"],
["bare CR", "\r"],
])("Multiline String Literal: %s preserves raw text", (_name, newline) => {
const body = newline + "Hello" + newline + " World\\n" + newline;
const source = "'''" + body + "'''";
const [parser, errorCounter] = createParser(source);
const context = parser.literal();
expect(errorCounter.getNumErrors()).toEqual(0);
expect(context.MultilineStringLiteral()).toBeTruthy();
const text = context.MultilineStringLiteral().getText();
expect(text).toBe(source);
expect(text.slice(3, -3)).toBe(body);
});

test("Multiline String Literal in class body", () => {
const [parser, errorCounter] = createParser(
"public class Hello { String s = '''\n{\n \"name\": \"John\"\n}'''; }"
Expand Down
Loading