From 2c33a1e3f236637b11d2f0804914d62739b3dd48 Mon Sep 17 00:00:00 2001 From: Eduardo Asafe <e@asafe.dev> Date: Mon, 11 Mar 2024 08:47:31 -0300 Subject: [PATCH] Fix Deletion of Trailing Comments Fix formatter bug that dropped trailing comments. Co-authored-by: Christian G. Warden <cwarden@xerus.org> --- formatter/comments_test.go | 70 ++++++++++++++++++++++++++++++++++ formatter/formatter.go | 2 +- formatter/visitor.go | 78 +++++++++++++++++++++++++++++++++++--- 3 files changed, 144 insertions(+), 6 deletions(-) diff --git a/formatter/comments_test.go b/formatter/comments_test.go index 37ded9c..16482a9 100644 --- a/formatter/comments_test.go +++ b/formatter/comments_test.go @@ -97,3 +97,73 @@ System.debug('I am on a separate line!');`, } } + +func TestTrailingComments(t *testing.T) { + if testing.Verbose() { + log.SetLevel(log.DebugLevel) + + } + tests := + []struct { + input string + output string + }{ + { + `private class T1Exception extends Exception {} //test`, + `private class T1Exception extends Exception {} //test`, + }, + { + `public class MyClass { public static void noop() {} + // Comment Inside Compilation Unit + // Line 2 +}`, + `public class MyClass { + public static void noop() {} + // Comment Inside Compilation Unit + // Line 2 +}`}, + { + `public class MyClass { public static void noop() {}} +// Comment Outside Compilation Unit Moved Inside +// Line 2`, + `public class MyClass { + public static void noop() {} + // Comment Outside Compilation Unit Moved Inside + // Line 2 +}`}, + { + ` +/* comment with whitespace before */ +private class T1Exception {}`, + `/* comment with whitespace before */ +private class T1Exception {}`, + }, + { + `/* comment with whitespace after */ + +private class T1Exception {}`, + `/* comment with whitespace after */ + +private class T1Exception {}`, + }, + } + for _, tt := range tests { + input := antlr.NewInputStream(tt.input) + lexer := parser.NewApexLexer(input) + stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + + p := parser.NewApexParser(stream) + p.RemoveErrorListeners() + p.AddErrorListener(&testErrorListener{t: t}) + + v := NewFormatVisitor(stream) + out, ok := v.visitRule(p.CompilationUnit()).(string) + if !ok { + t.Errorf("Unexpected result parsing apex") + } + out = removeExtraCommentIndentation(out) + if out != tt.output { + t.Errorf("unexpected format. expected:\n%q\ngot:\n%q\n", tt.output, out) + } + } +} diff --git a/formatter/formatter.go b/formatter/formatter.go index 344b994..9afa33c 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -76,7 +76,7 @@ func (f *Formatter) Format() error { if f.source == nil { src, err := readFile(f.filename, f.reader) if err != nil { - return fmt.Errorf("Failed to read file %s: %w", f.SourceName(), err) + return fmt.Errorf("failed to read file %s: %w", f.SourceName(), err) } f.source = src } diff --git a/formatter/visitor.go b/formatter/visitor.go index 88ce56c..81ba464 100644 --- a/formatter/visitor.go +++ b/formatter/visitor.go @@ -46,6 +46,7 @@ func (v *FormatVisitor) visitRule(node antlr.RuleNode) interface{} { panic(fmt.Sprintf("MISSING VISIT FUNCTION FOR %T", node)) } commentsWithNewlines := commentsWithTrailingNewlines(beforeComments, beforeWhitespace) + hasComments := beforeComments != nil && len(beforeComments) > 0 if beforeComments != nil { comments := []string{} for _, c := range beforeComments { @@ -54,8 +55,8 @@ func (v *FormatVisitor) visitRule(node antlr.RuleNode) interface{} { // Mark the start and end of comments so we can remove indentation // added to multi-line comments, preserving the whitespace within // them. See removeIndentationFromComment. - if _, exists := commentsWithNewlines[index]; exists { - comments = append(comments, "\uFFFA"+c.GetText()+"\uFFFB\n") + if n, exists := commentsWithNewlines[index]; exists { + comments = append(comments, "\uFFFA"+c.GetText()+"\uFFFB"+strings.Repeat("\n", n)) } else { comments = append(comments, "\uFFFA"+c.GetText()+"\uFFFB") } @@ -75,7 +76,7 @@ func (v *FormatVisitor) visitRule(node antlr.RuleNode) interface{} { if beforeWhitespace != nil { injectNewline := false for _, c := range beforeWhitespace { - if len(strings.Split(c.GetText(), "\n")) > 2 { + if !hasComments && len(strings.Split(c.GetText(), "\n")) > 2 { if _, seen := v.newlinesOutput[c.GetTokenIndex()]; !seen { v.newlinesOutput[c.GetTokenIndex()] = struct{}{} injectNewline = true @@ -86,6 +87,46 @@ func (v *FormatVisitor) visitRule(node antlr.RuleNode) interface{} { result = fmt.Sprintf("\n%s", result) } } + stop := node.(antlr.ParserRuleContext).GetStop() + var afterWhitespace, afterComments []antlr.Token + if stop == nil { + return result + } + if len(v.tokens.GetAllTokens()) > 0 { + afterWhitespace = v.tokens.GetHiddenTokensToRight(stop.GetTokenIndex(), WHITESPACE_CHANNEL) + afterComments = v.tokens.GetHiddenTokensToRight(stop.GetTokenIndex(), COMMENTS_CHANNEL) + } + + if afterComments == nil { + return result + } + afterCommentsWithLeadingNewlines := commentsWithLeadingNewlines(afterComments, afterWhitespace) + comments := []string{} + + for _, c := range afterComments { + index := c.GetTokenIndex() + if _, seen := v.commentsOutput[index]; !seen { + // Mark the start and end of comments so we can remove indentation + // added to multi-line comments, preserving the whitespace within + // them. See removeIndentationFromComment. + leading := "" + if _, exists := afterCommentsWithLeadingNewlines[index]; exists { + leading = "\n" + } + comments = append(comments, leading+"\uFFFA"+c.GetText()+"\uFFFB") + v.commentsOutput[index] = struct{}{} + } + } + + if len(comments) > 0 { + allComments := strings.Join(comments, "") + containsNewline := strings.Contains(allComments, "\n") + if !containsNewline { + result = fmt.Sprintf("%s %s", result, strings.TrimSuffix(strings.TrimPrefix(allComments, "\uFFFA"), "\uFFFB")) + } else { + result = fmt.Sprintf("%s%s", result, allComments) + } + } return result } @@ -154,8 +195,8 @@ func unwrap(v *FormatVisitor) (*FormatVisitor, bool) { } // Find comments that have trailing newlines -func commentsWithTrailingNewlines(comments []antlr.Token, whitespace []antlr.Token) map[int]struct{} { - result := make(map[int]struct{}) +func commentsWithTrailingNewlines(comments []antlr.Token, whitespace []antlr.Token) map[int]int { + result := make(map[int]int) whitespaceMap := make(map[int]antlr.Token) for _, ws := range whitespace { @@ -170,6 +211,33 @@ func commentsWithTrailingNewlines(comments []antlr.Token, whitespace []antlr.Tok // Check if the next token is whitespace if ws, exists := whitespaceMap[nextTokenIndex]; exists { + // Check if the whitespace contains a newline + if strings.Contains(ws.GetText(), "\n") { + result[commentIndex] = len(strings.Split(ws.GetText(), "\n")) - 1 + } + } + } + + return result +} + +// Find comments that have leading newlines +func commentsWithLeadingNewlines(comments []antlr.Token, whitespace []antlr.Token) map[int]struct{} { + result := make(map[int]struct{}) + + whitespaceMap := make(map[int]antlr.Token) + for _, ws := range whitespace { + whitespaceMap[ws.GetTokenIndex()] = ws + } + + for _, comment := range comments { + commentIndex := comment.GetTokenIndex() + + // Find the immediate previous token index + prevTokenIndex := commentIndex - 1 + + // Check if the next token is whitespace + if ws, exists := whitespaceMap[prevTokenIndex]; exists { // Check if the whitespace contains a newline if strings.Contains(ws.GetText(), "\n") { result[commentIndex] = struct{}{}