diff --git a/internal/lint/code/comments.go b/internal/lint/code/comments.go index 330966c3..b3808d7a 100644 --- a/internal/lint/code/comments.go +++ b/internal/lint/code/comments.go @@ -1,8 +1,10 @@ package code import ( + "bytes" "context" "sort" + "strings" sitter "github.com/smacker/go-tree-sitter" ) @@ -16,6 +18,72 @@ type Comment struct { Scope string } +// doneMerging determines when we should *stop* concatenating line-scoped +// comments. +func doneMerging(curr, prev Comment) bool { + if prev.Line != curr.Line-1 { + // If the comments aren't on consecutive lines, don't merge them. + return true + } else if prev.Offset != curr.Offset { + // If the comments aren't at the same offset, don't merge them. + return true + } + return false +} + +func coalesce(comments []Comment) []Comment { + var joined []Comment + + tBuf := bytes.Buffer{} + sBuf := bytes.Buffer{} + + for i, comment := range comments { + offset := comment.Offset + if i > 0 { + offset += comments[i-1].Offset + } + + if comment.Scope == "text.comment.block" { //nolint:gocritic + joined = append(joined, comment) + } else if i == 0 || doneMerging(comment, comments[i-1]) { + if tBuf.Len() > 0 { + // We have comments to merge ... + last := joined[len(joined)-1] + + last.Text += tBuf.String() + last.Source += ("\n" + sBuf.String()) + + joined[len(joined)-1] = last + + tBuf.Reset() + sBuf.Reset() + } + joined = append(joined, comment) + } else { + tBuf.WriteString(comment.Text) + sBuf.WriteString(comment.Source + "\n") + } + } + + if tBuf.Len() > 0 { + last := joined[len(joined)-1] + + last.Text += tBuf.String() + last.Source += ("\n" + sBuf.String()) + + joined[len(joined)-1] = last + + tBuf.Reset() + sBuf.Reset() + } + + for i, comment := range joined { + joined[i].Text = strings.TrimLeft(comment.Text, " ") + } + + return joined +} + // GetComments returns all comments in the given source code. func GetComments(source []byte, lang *Language) ([]Comment, error) { var comments []Comment @@ -43,5 +111,5 @@ func GetComments(source []byte, lang *Language) ([]Comment, error) { }) } - return comments, nil + return coalesce(comments), nil } diff --git a/internal/lint/code/go.go b/internal/lint/code/go.go index 8ed53ff7..0f07a4d6 100644 --- a/internal/lint/code/go.go +++ b/internal/lint/code/go.go @@ -8,7 +8,7 @@ import ( func Go() *Language { return &Language{ - Delims: regexp.MustCompile(`// ?|/\* ?| ?\*/`), + Delims: regexp.MustCompile(`//|/\*|\*/`), Parser: golang.GetLanguage(), Queries: []string{`(comment)+ @comment`}, Padding: cStyle, diff --git a/internal/lint/code/py.go b/internal/lint/code/py.go index 3394c79f..e21f13f6 100644 --- a/internal/lint/code/py.go +++ b/internal/lint/code/py.go @@ -8,7 +8,7 @@ import ( func Python() *Language { return &Language{ - Delims: regexp.MustCompile(`#\s?|\s*"""\s*|\s*'''\s*`), + Delims: regexp.MustCompile(`#|"""|'''`), Parser: python.GetLanguage(), Queries: []string{ `(comment)+ @comment`, @@ -18,11 +18,11 @@ func Python() *Language { (#offset! @docstring 0 3 0 -3))`, // Class docstring `((class_definition - body: (block . (expression_statement (string) @rst))) - (#offset! @rst 0 3 0 -3))`, + body: (block . (expression_statement (string) @docstring))) + (#offset! @docstring 0 3 0 -3))`, // Module docstring - `((module . (expression_statement (string) @rst)) - (#offset! @rst 0 3 0 -3))`, + `((module . (expression_statement (string) @docstring)) + (#offset! @docstring 0 3 0 -3))`, }, } } diff --git a/internal/lint/code/query.go b/internal/lint/code/query.go index 105841f9..d4138598 100644 --- a/internal/lint/code/query.go +++ b/internal/lint/code/query.go @@ -1,6 +1,7 @@ package code import ( + "bytes" "strings" sitter "github.com/smacker/go-tree-sitter" @@ -38,6 +39,14 @@ func (qe *QueryEngine) run(q *sitter.Query, source []byte) []Comment { scope := "text.comment.line" if strings.Count(cText, "\n") > 1 { scope = "text.comment.block" + + buf := bytes.Buffer{} + for _, line := range strings.Split(cText, "\n") { + buf.WriteString(strings.TrimLeft(line, " ")) + buf.WriteString("\n") + } + + cText = buf.String() } comments = append(comments, Comment{ diff --git a/internal/lint/code/rs.go b/internal/lint/code/rs.go index c95dadc5..fc4ff181 100644 --- a/internal/lint/code/rs.go +++ b/internal/lint/code/rs.go @@ -8,7 +8,7 @@ import ( func Rust() *Language { return &Language{ - Delims: regexp.MustCompile(`/{2,3}\s?`), + Delims: regexp.MustCompile(`/{2,3}`), Parser: rust.GetLanguage(), Queries: []string{`(line_comment)+ @comment`}, } diff --git a/testdata/comments/in/2.py b/testdata/comments/in/2.py index c0c1fe8f..3cac2c98 100644 --- a/testdata/comments/in/2.py +++ b/testdata/comments/in/2.py @@ -3,7 +3,13 @@ def FIXME(): """ - FIXME: + FIXME: this is *mardown*. + + ```python + print("FIXME: this is *python*.") + ``` + + New line. """ print(""" FIXME: This should *not* be linted. diff --git a/testdata/comments/out/0.json b/testdata/comments/out/0.json index 2478b242..f6def023 100644 --- a/testdata/comments/out/0.json +++ b/testdata/comments/out/0.json @@ -1,53 +1,18 @@ [ { - "Text": "\nPackage lint implments Vale's syntax-aware linting functionality.\n\nThe package is split into core linting logic (this file), source code\n(code.go), and markup (markup.go).\n", + "Text": "\nPackage lint implments Vale's syntax-aware linting functionality.\n\nThe package is split into core linting logic (this file), source code\n(code.go), and markup (markup.go).\n\n", "Source": "/*\nPackage lint implments Vale's syntax-aware linting functionality.\n\nThe package is split into core linting logic (this file), source code\n(code.go), and markup (markup.go).\n*/", "Line": 1, "Offset": 0, "Scope": "text.comment.block" }, { - "Text": "Println formats using the default formats for its oprands and writes to", - "Source": "// Println formats using the default formats for its oprands and writes to", + "Text": "Println formats using the default formats for its oprands and writes to standard output. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and any write error encountered.", + "Source": "// Println formats using the default formats for its oprands and writes to\n// standard output.\n//\n// Spaces are always added between operands and a newline is appended.\n//\n// It returns the number of bytes written and any write error encountered.\n", "Line": 11, "Offset": 0, "Scope": "text.comment.line" }, - { - "Text": "standard output.", - "Source": "// standard output.", - "Line": 12, - "Offset": 0, - "Scope": "text.comment.line" - }, - { - "Text": "", - "Source": "//", - "Line": 13, - "Offset": 0, - "Scope": "text.comment.line" - }, - { - "Text": "Spaces are always added between operands and a newline is appended.", - "Source": "// Spaces are always added between operands and a newline is appended.", - "Line": 14, - "Offset": 0, - "Scope": "text.comment.line" - }, - { - "Text": "", - "Source": "//", - "Line": 15, - "Offset": 0, - "Scope": "text.comment.line" - }, - { - "Text": "It returns the number of bytes written and any write error encountered.", - "Source": "// It returns the number of bytes written and any write error encountered.", - "Line": 16, - "Offset": 0, - "Scope": "text.comment.line" - }, { "Text": "foo bar", "Source": "// foo bar", diff --git a/testdata/comments/out/1.json b/testdata/comments/out/1.json index af5185c1..a6eee7c1 100644 --- a/testdata/comments/out/1.json +++ b/testdata/comments/out/1.json @@ -14,122 +14,17 @@ "Scope": "text.comment.line" }, { - "Text": "Returns a person with the name given them\n", - "Source": "/// Returns a person with the name given them\n", + "Text": "Returns a person with the name given them\n\n # Arguments\n\n * `foof` - A string slice that holds the name of the person\n\n # Examples\n\n ```rust\n You can have rust code between fences inside the comments\n If you pass --test to `rustdoc`, it will even test it for you!\n use doc::Person;\n let person = Person::new(\"name\");\n ```\n", + "Source": "/// Returns a person with the name given them\n\n///\n\n/// # Arguments\n\n///\n\n/// * `foof` - A string slice that holds the name of the person\n\n///\n\n/// # Examples\n\n///\n\n/// ```rust\n\n/// // You can have rust code between fences inside the comments\n\n/// // If you pass --test to `rustdoc`, it will even test it for you!\n\n/// use doc::Person;\n\n/// let person = Person::new(\"name\");\n\n/// ```\n\n", "Line": 10, "Offset": 4, "Scope": "text.comment.line" }, { - "Text": "", - "Source": "///\n", - "Line": 11, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "# Arguments\n", - "Source": "/// # Arguments\n", - "Line": 12, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "", - "Source": "///\n", - "Line": 13, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "* `foof` - A string slice that holds the name of the person\n", - "Source": "/// * `foof` - A string slice that holds the name of the person\n", - "Line": 14, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "", - "Source": "///\n", - "Line": 15, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "# Examples\n", - "Source": "/// # Examples\n", - "Line": 16, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "", - "Source": "///\n", - "Line": 17, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "```rust\n", - "Source": "/// ```rust\n", - "Line": 18, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "You can have rust code between fences inside the comments\n", - "Source": "/// // You can have rust code between fences inside the comments\n", - "Line": 19, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "If you pass --test to `rustdoc`, it will even test it for you!\n", - "Source": "/// // If you pass --test to `rustdoc`, it will even test it for you!\n", - "Line": 20, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "use doc::Person;\n", - "Source": "/// use doc::Person;\n", - "Line": 21, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "let person = Person::new(\"name\");\n", - "Source": "/// let person = Person::new(\"name\");\n", - "Line": 22, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "```\n", - "Source": "/// ```\n", - "Line": 23, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "Gives a friendly hello!\n", - "Source": "/// Gives a friendly hello!\n", + "Text": "Gives a friendly hello!\n\n Says \"Hello, [name]\" to the `Person` it is called on.\n", + "Source": "/// Gives a friendly hello!\n\n///\n\n/// Says \"Hello, [name]\" to the `Person` it is called on.\n\n", "Line": 30, "Offset": 4, "Scope": "text.comment.line" - }, - { - "Text": "", - "Source": "///\n", - "Line": 31, - "Offset": 4, - "Scope": "text.comment.line" - }, - { - "Text": "Says \"Hello, [name]\" to the `Person` it is called on.\n", - "Source": "/// Says \"Hello, [name]\" to the `Person` it is called on.\n", - "Line": 32, - "Offset": 4, - "Scope": "text.comment.line" } ] \ No newline at end of file diff --git a/testdata/comments/out/2.json b/testdata/comments/out/2.json index 0f977f2f..99f11882 100644 --- a/testdata/comments/out/2.json +++ b/testdata/comments/out/2.json @@ -7,44 +7,44 @@ "Scope": "text.comment.line" }, { - "Text": "FIXME:", - "Source": "\"\"\"\n FIXME:\n \"\"\"", + "Text": "\nFIXME: this is *mardown*.\n\n```python\nprint(\"FIXME: this is *python*.\")\n```\n\nNew line.\n\n", + "Source": "\"\"\"\n FIXME: this is *mardown*.\n\n ```python\n print(\"FIXME: this is *python*.\")\n ```\n\n New line.\n \"\"\"", "Line": 5, "Offset": 4, - "Scope": "text.comment.line" + "Scope": "text.comment.block" }, { "Text": "XXX: This should be flagged!", "Source": "# XXX: This should be flagged!", - "Line": 13, + "Line": 19, "Offset": 0, "Scope": "text.comment.line" }, { "Text": "XXX:", "Source": "# XXX:", - "Line": 15, + "Line": 21, "Offset": 14, "Scope": "text.comment.line" }, { "Text": "NOTE:", "Source": "# NOTE:", - "Line": 16, + "Line": 22, "Offset": 12, "Scope": "text.comment.line" }, { - "Text": "NOTE:", + "Text": "\nNOTE:\n\n", "Source": "\"\"\"\n NOTE:\n \"\"\"", - "Line": 30, + "Line": 36, "Offset": 4, - "Scope": "text.comment.line" + "Scope": "text.comment.block" }, { - "Text": "NOTE This is the start of a block.\n\n TODO: Assume that a file is modified since an invalid timestamp as per RFC\n 2616, section 14.25. GMT", + "Text": "NOTE This is the start of a block.\n\nTODO: Assume that a file is modified since an invalid timestamp as per RFC\n2616, section 14.25. GMT\n\n", "Source": "\"\"\"NOTE This is the start of a block.\n\n TODO: Assume that a file is modified since an invalid timestamp as per RFC\n 2616, section 14.25. GMT\n \"\"\"", - "Line": 39, + "Line": 45, "Offset": 4, "Scope": "text.comment.block" }