diff --git a/formatter/format_test.go b/formatter/format_test.go index c6cf2e9..39a0894 100644 --- a/formatter/format_test.go +++ b/formatter/format_test.go @@ -23,7 +23,7 @@ func (e *testErrorListener) SyntaxError(_ antlr.Recognizer, _ interface{}, line, func TestStatement(t *testing.T) { if testing.Verbose() { - log.SetLevel(log.DebugLevel) + log.SetLevel(log.TraceLevel) } tests := []struct { @@ -319,26 +319,63 @@ OneDayDischargeFollowUp.twoHoursDelay, when Schema.Account a { system.debug('doot'); } +}`, + }, + { + `go(a, // line comment in args +b);`, + `go(a, // line comment in args +b);`, + }, + { + `Integer i = new doot(go(a, b)).then();`, + `Integer i = new doot(go(a, b)).then();`, + }, + { + `System.runAs(user) { + Fixtures.MegaOrder mo = new Fixtures.MegaOrderFactory() + .setOrderFac(Fixtures.order() + .put(Order__c.Shipping_Destination__c, Fixtures.shippingDestination() + .save().Id) + .put(Order__c.Subtotal__c, 100) // We need any value here + ).usingShipCompliant(true) + .save(); +}`, + `System.runAs(user) { + Fixtures.MegaOrder mo = new Fixtures.MegaOrderFactory() + .setOrderFac(Fixtures.order() + .put(Order__c.Shipping_Destination__c, Fixtures.shippingDestination() + .save().Id) + .put(Order__c.Subtotal__c, 100) // We need any value here + ) + .usingShipCompliant(true) + .save(); }`, }, } - for _, tt := range tests { - input := antlr.NewInputStream(tt.input) - lexer := parser.NewApexLexer(input) - stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + dmp := diffmatchpatch.New() + for i, tt := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - p := parser.NewApexParser(stream) - p.RemoveErrorListeners() - p.AddErrorListener(&testErrorListener{t: t}) + input := antlr.NewInputStream(tt.input) + lexer := parser.NewApexLexer(input) + stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) - v := NewFormatVisitor(stream) - out, ok := v.visitRule(p.Statement()).(string) - if !ok { - t.Errorf("Unexpected result parsing apex") - } - if out != tt.output { - t.Errorf("unexpected format. expected:\n%q\ngot:\n%q\n", tt.output, out) - } + p := parser.NewApexParser(stream) + p.RemoveErrorListeners() + p.AddErrorListener(&testErrorListener{t: t}) + + v := NewFormatVisitor(stream) + out, ok := v.visitRule(p.Statement()).(string) + if !ok { + t.Errorf("Unexpected result parsing apex") + } + out = removeExtraCommentIndentation(out) + if out != tt.output { + diffs := dmp.DiffMain(tt.output, out, false) + t.Errorf("unexpected format. expected:\n%q\ngot:\n%q\ndiff:\n%s\n", tt.output, out, dmp.DiffPrettyText(diffs)) + } + }) } } @@ -617,6 +654,88 @@ public class A {}`, public String getSid() { return this.getProperty(SID_PROPERTY); } +}`, + }, + { + `class TestClass { + List vals = new List{ + /* MULTI + */ + + // test comment 2 + 'val3' + }; +}`, + `class TestClass { + List vals = new List{ + /* MULTI + */ + + // test comment 2 + 'val3' }; +}`, + }, + { + `class TestClass { + List vals = new List{ + /* MULTI + */ + // test comment 1 + + // test comment 2 + 'val3' + }; +}`, + `class TestClass { + List vals = new List{ + /* MULTI + */ + // test comment 1 + + // test comment 2 + 'val3' }; +}`, + }, + { + `class TestClass { + public void go() { + // Line Comment + + /* MULTI + * + */ + } +}`, + `class TestClass { + public void go() { + // Line Comment + + /* MULTI + * + */ + } +}`, + }, + { + `public class TestClass { + public void go() { + // Line Comment + + /* MULTI + * + */ + return; + } +}`, + `public class TestClass { + public void go() { + // Line Comment + + /* MULTI + * + */ + return; + } }`, }, } diff --git a/formatter/formatter.go b/formatter/formatter.go index 581e73d..2400d7d 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -180,6 +180,10 @@ func removeExtraCommentIndentation(input string) string { input = strings.ReplaceAll(input, "\n\uFFFB\n", "\n\uFFFB") log.Trace(fmt.Sprintf("ADJUSTED(4): %q", input)) + doubleCapturedNewlines := regexp.MustCompile("\n(\ufffb\t*\ufffa\n)") + input = doubleCapturedNewlines.ReplaceAllString(input, "$1") + log.Trace(fmt.Sprintf("ADJUSTED(5): %q", input)) + newlinePrefixedInlineComment := regexp.MustCompile("\n\t*\uFFF9\n") input = newlinePrefixedInlineComment.ReplaceAllString(input, "\uFFF9\n") diff --git a/formatter/indent_test.go b/formatter/indent_test.go index 4bc0c70..8bcc22e 100644 --- a/formatter/indent_test.go +++ b/formatter/indent_test.go @@ -46,7 +46,7 @@ func TestIndent(t *testing.T) { }, { "\ufffa\n// First Comment\n\n\ufffb\ufffa// Second Comment\n\ufffbgo();", - "\t\ufffa\n\t// First Comment\n\ufffb\n\t\ufffa// Second Comment\n\ufffb\n\tgo();", + "\t\ufffa\n\t// First Comment\n\n\ufffb\t\ufffa// Second Comment\n\ufffb\n\tgo();", }, { "\ufffa\n/*\n\t * Property getters\n\t **/\n\ufffb", @@ -180,6 +180,7 @@ func TestSplitLeadingFFFAOrFFFBOrNewline(t *testing.T) { "/**", "\t\t */", "", + "", "\ufffb", "}", }, @@ -315,6 +316,16 @@ func TestSplitLeadingFFFAOrFFFBOrNewline(t *testing.T) { "\ufffb", }, }, + { + name: "Preserve all newlines in comments", + input: "\t*/\n\n\ufffb", + expected: []string{ + "\t*/", + "", + "", + "\ufffb", + }, + }, } for _, tc := range testCases { diff --git a/formatter/visitor.go b/formatter/visitor.go index e033407..34fe6c2 100644 --- a/formatter/visitor.go +++ b/formatter/visitor.go @@ -41,8 +41,6 @@ const ( PositionAfter ) -var manyNewlines = regexp.MustCompile(`\n{3,}`) - func NewFormatVisitor(tokens *antlr.CommonTokenStream) *FormatVisitor { return &FormatVisitor{ tokens: tokens, @@ -78,6 +76,13 @@ func (v *FormatVisitor) visitRule(node antlr.RuleNode) interface{} { _ = afterHiddenTokens result = appendHiddenTokens(v, result, afterHiddenTokens, PositionAfter) + if result.(string) == "{}" { + inbetweenTokens := interleaveHiddenTokens( + getHiddenTokensBetween(v.tokens, start, stop), + ) + result = fmt.Sprintf("%s}", appendHiddenTokens(v, "{", inbetweenTokens, PositionAfter)) + } + return result } @@ -274,15 +279,20 @@ func SplitLeadingFFFAOrFFFBOrNewline(data []byte, atEOF bool) (advance int, toke delimiterLen := len(fffb) // Split AFTER the delimiter log.Trace(fmt.Sprintf("HAS \\uFFFB IN LINE: %q", string(line[:fffbIdx+delimiterLen]))) - // Advance past the newline after \uFFFB - return fffbIdx + delimiterLen + 1, line[:fffbIdx+delimiterLen], nil + advance := 0 + if bytes.IndexByte(line[:fffbIdx+delimiterLen], '\n') == 0 { + // Advance past the newline after \uFFFB + advance = 1 + } + return fffbIdx + delimiterLen + advance, line[:fffbIdx+delimiterLen], nil } // ---------------------------------------------------------------- // 2c. No Delimiters => Return Entire Line // ---------------------------------------------------------------- log.Trace(fmt.Sprintf("NO DELIMITER: %q", string(line))) - if len(line) > 0 && bytes.Index(data, fffb) == newlineIdx+1 { + var fffbFollowsNewlines = regexp.MustCompile(`(s?)^` + "\n+\uFFFB") + if len(line) > 0 && fffbFollowsNewlines.Match(data[newlineIdx:]) { // \uFFFB follows newline. We want to keep the newline by returning an // extra empty line so we don't advance over the newline. return newlineIdx, line, nil @@ -308,22 +318,21 @@ func indentTo(text string, indents int) string { log.Debug(fmt.Sprintf("INDENTING: %q\n", text)) for scanner.Scan() { - log.Trace(fmt.Sprintf("INDENTING LINE: %q\n", scanner.Text())) - if scanner.Text() == "\uFFFB" { - // indentedText.WriteString("\n") - indentedText.WriteString(scanner.Text()) + t := scanner.Text() + log.Trace(fmt.Sprintf("INDENTING LINE: %q\n", t)) + if t == "\uFFFB" { + indentedText.WriteString(t) continue } if isFirstLine { isFirstLine = false - } else { + } else if !strings.HasPrefix(t, "\uFFFA") && !strings.HasPrefix(t, "\uFFF9") { indentedText.WriteString("\n") } if scanner.Text() == "" { indentedText.WriteString(scanner.Text()) continue } - t := scanner.Text() t = strings.Repeat("\t", indents) + t indentedText.WriteString(t) } @@ -375,8 +384,10 @@ func appendHiddenTokens(v *FormatVisitor, result interface{}, tokens []antlr.Tok trailingWhitespace := getTrailingWhitespace(text) leading := "" trailing := "" - if n := countNewlines(leadingWhitespace); n > 0 { - leading = strings.Repeat("\n", n) + if n := countNewlines(leadingWhitespace); n > 1 { + leading = strings.Repeat("\n", 2) + } else if countNewlines(leadingWhitespace) == 1 { + leading = "\n" } else if len(leadingWhitespace) > 0 && position == PositionAfter { leading = " " } @@ -396,7 +407,7 @@ func appendHiddenTokens(v *FormatVisitor, result interface{}, tokens []antlr.Tok if containsNewline { text = "\uFFFA" + text + "\uFFFB" + "\n" } else if lineComment { - text = "\uFFF9" + text + "\uFFFB" + "\n" + text = "\uFFF9" + text + "\n\uFFFB" } else { text = "\uFFF9" + text + "\uFFFB" } @@ -434,6 +445,38 @@ func getStartStop(node antlr.RuleNode) (start, stop antlr.Token) { return ctx.GetStart(), ctx.GetStop() } +func getHiddenTokensBetween(tokens *antlr.CommonTokenStream, start, stop antlr.Token) ([]antlr.Token, []antlr.Token) { + if start == nil || stop == nil || len(tokens.GetAllTokens()) == 0 { + return nil, nil + } + after := tokens.GetHiddenTokensToRight(start.GetTokenIndex(), WHITESPACE_CHANNEL) + before := tokens.GetHiddenTokensToLeft(stop.GetTokenIndex(), WHITESPACE_CHANNEL) + inAfter := make(map[int]struct{}) + for _, t := range after { + inAfter[t.GetTokenIndex()] = struct{}{} + } + whitespaceTokens := []antlr.Token{} + for _, t := range before { + if _, exists := inAfter[t.GetTokenIndex()]; exists { + whitespaceTokens = append(whitespaceTokens, t) + } + } + + after = tokens.GetHiddenTokensToRight(start.GetTokenIndex(), COMMENTS_CHANNEL) + before = tokens.GetHiddenTokensToLeft(stop.GetTokenIndex(), COMMENTS_CHANNEL) + inAfter = make(map[int]struct{}) + for _, t := range after { + inAfter[t.GetTokenIndex()] = struct{}{} + } + commentTokens := []antlr.Token{} + for _, t := range before { + if _, exists := inAfter[t.GetTokenIndex()]; exists { + commentTokens = append(commentTokens, t) + } + } + return whitespaceTokens, commentTokens +} + func getHiddenTokens(tokens *antlr.CommonTokenStream, token antlr.Token, direction HiddenTokenDirection) ([]antlr.Token, []antlr.Token) { if token == nil || len(tokens.GetAllTokens()) == 0 { return nil, nil