Skip to content

Commit 2dada82

Browse files
committed
Require Line Terminator After Opening Text Block Delimiter
Salesforce requires the opening ''' of a triple-quoted text block to be immediately followed by a line terminator; no other character, not even whitespace, may appear before it. The single-line form '''hello''' is rejected by the Apex compiler with "was expecting '\n'", so apexfmt must not accept it either. Update the TextBlockLiteral lexer rule to require an optional carriage return and a mandatory line feed after the opening delimiter, and regenerate the lexer. Add formatter tests covering rejection of the single-line form, rejection of whitespace before the opening newline, and round-tripping of a CRLF terminator.
1 parent 908d183 commit 2dada82

4 files changed

Lines changed: 832 additions & 781 deletions

File tree

formatter/text_block_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,54 @@ func TestFormatter_MultipleTextBlockSwitchCases(t *testing.T) {
377377
}
378378
}
379379

380+
// TestFormatter_SingleLineTextBlockRejected asserts apexfmt rejects a
381+
// triple-quoted literal whose opening ”' is not immediately followed by a
382+
// line terminator. Salesforce requires the newline (reporting "was
383+
// expecting '\n'"), so the single-line form is not valid Apex and the
384+
// formatter must surface a parse error rather than silently accept it.
385+
func TestFormatter_SingleLineTextBlockRejected(t *testing.T) {
386+
input := "public class Foo {\n" +
387+
"\tpublic static String s = '''hello''';\n" +
388+
"}\n"
389+
f := NewFormatter("", strings.NewReader(input))
390+
if err := f.Format(); err == nil {
391+
t.Fatalf("expected a parse error for a single-line text block, got none")
392+
}
393+
}
394+
395+
// TestFormatter_WhitespaceBeforeOpeningNewlineRejected asserts that even
396+
// whitespace between the opening ”' and the line terminator is rejected,
397+
// matching Salesforce, which allows no character — not even a space — before
398+
// the required newline.
399+
func TestFormatter_WhitespaceBeforeOpeningNewlineRejected(t *testing.T) {
400+
input := "public class Foo {\n" +
401+
"\tpublic static String s = ''' \n" +
402+
"hello''';\n" +
403+
"}\n"
404+
f := NewFormatter("", strings.NewReader(input))
405+
if err := f.Format(); err == nil {
406+
t.Fatalf("expected a parse error for whitespace before the opening newline, got none")
407+
}
408+
}
409+
410+
// TestFormatter_CRLFAfterOpeningDelimiterRoundTrips asserts that a CRLF line
411+
// terminator immediately after the opening ”' satisfies the requirement and
412+
// the block is preserved verbatim.
413+
func TestFormatter_CRLFAfterOpeningDelimiterRoundTrips(t *testing.T) {
414+
input := "public class Foo {\n" +
415+
"\tpublic static String s = '''\r\n" +
416+
"hello''';\n" +
417+
"}\n"
418+
f := NewFormatter("", strings.NewReader(input))
419+
if err := f.Format(); err != nil {
420+
t.Fatalf("Format() failed for CRLF after opening delimiter: %v", err)
421+
}
422+
got := string(f.formatted)
423+
if got != input {
424+
t.Fatalf("CRLF text block was altered\nwant:\n%q\n got:\n%q", input, got)
425+
}
426+
}
427+
380428
// TestFormatter_TextBlockInsideIndentedMethodBody is the end-to-end
381429
// regression: a text block nested inside two levels of block indentation
382430
// must come out with the surrounding code re-indented normally but its

grammar/ApexLexer.g4

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,14 @@ BooleanLiteral
330330

331331
// Triple-quoted (multi-line) string literal. Must be matched before
332332
// StringLiteral so the longest-match rule picks it up on inputs that
333-
// start with three single quotes. The content allows backslash-escaped
334-
// characters (including an escaped ''' sequence via \''' and an explicit
335-
// line continuation \<line-terminator>); non-greedy '.*?' stops at the
336-
// first unescaped ''' triple.
333+
// start with three single quotes. Salesforce requires the opening '''
334+
// to be immediately followed by a line terminator (LF or CRLF); no other
335+
// character — not even whitespace — may appear before it. The content
336+
// allows backslash-escaped characters (including an escaped ''' sequence
337+
// via \''' and an explicit line continuation \<line-terminator>);
338+
// non-greedy '.*?' stops at the first unescaped ''' triple.
337339
TextBlockLiteral
338-
: '\'\'\'' ( '\\' . | ~[\\] )*? '\'\'\''
340+
: '\'\'\'' '\r'? '\n' ( '\\' . | ~[\\] )*? '\'\'\''
339341
;
340342

341343
StringLiteral

parser/ApexLexer.interp

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)