Skip to content

Commit c154ac2

Browse files
authored
Keep the extracted text one line per source line (#1135)
1 parent 8fe9804 commit c154ac2

6 files changed

Lines changed: 100 additions & 25 deletions

File tree

‎internal/lint/code/comments.go‎

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package code
33
import (
44
"bytes"
55
"context"
6-
"fmt"
76
"sort"
87
"strings"
98

@@ -50,20 +49,37 @@ func doneMerging(curr, prev Comment) bool {
5049
return false
5150
}
5251

53-
func addSourceLine(line string, atEnd bool) string {
54-
if line == "" {
55-
return "\n\n"
56-
}
52+
// appendLine adds one line to a pending run of line comments.
53+
//
54+
// One newline per line, so the run ends up with exactly as many lines as the
55+
// source it came from. A blank line comment -- `//` with nothing after it --
56+
// is empty and contributes only its newline; giving it two ended that line
57+
// twice and put a line in the extracted text that the source doesn't have.
58+
// See #1022.
59+
func appendLine(line string) string {
60+
// The space after the delimiter belongs to the delimiter, and the padding
61+
// added when an alert is mapped back already counts it. Trimming only the
62+
// lines that still needed a newline left it on the ones that didn't --
63+
// Rust's `///`, whose node content carries its own -- where it was then
64+
// counted twice. The first line of a run is trimmed by the caller.
65+
line = strings.TrimLeft(line, " ")
66+
return strings.TrimRight(line, "\n") + "\n"
67+
}
5768

58-
if !strings.HasPrefix(line, "\n") && !atEnd {
59-
line = strings.TrimLeft(line, " ")
60-
line = fmt.Sprintf("\n%s", line)
61-
} else if !strings.HasSuffix(line, "\n") && atEnd {
62-
line = strings.TrimLeft(line, " ")
63-
line = fmt.Sprintf("%s\n", line)
69+
// attachRun joins a pending run of lines to the text it belongs to.
70+
//
71+
// The run needs the line above it terminated, which is a question about that
72+
// text and not about the run: a comment whose node content already carried its
73+
// newline -- Rust's `//!`, for one -- is terminated, and adding another puts a
74+
// blank line between the two that the source doesn't have. Keying this off the
75+
// run instead lost the terminator whenever the run began with a blank comment
76+
// line, which cost a line in the other direction.
77+
func attachRun(text, run string) string {
78+
if !strings.HasSuffix(text, "\n") {
79+
text += "\n"
6480
}
6581

66-
return line
82+
return text + run
6783
}
6884

6985
func coalesce(comments []Comment) []Comment {
@@ -78,8 +94,8 @@ func coalesce(comments []Comment) []Comment {
7894
if tBuf.Len() > 0 {
7995
last := joined[len(joined)-1]
8096

81-
last.Text += addSourceLine(tBuf.String(), false)
82-
last.Source += addSourceLine(sBuf.String(), false)
97+
last.Text = attachRun(last.Text, tBuf.String())
98+
last.Source = attachRun(last.Source, sBuf.String())
8399

84100
joined[len(joined)-1] = last
85101

@@ -100,8 +116,8 @@ func coalesce(comments []Comment) []Comment {
100116
flush()
101117
joined = append(joined, comment)
102118
} else {
103-
tBuf.WriteString(addSourceLine(comment.Text, true))
104-
sBuf.WriteString(addSourceLine(comment.Source, true))
119+
tBuf.WriteString(appendLine(comment.Text))
120+
sBuf.WriteString(appendLine(comment.Source))
105121
}
106122
}
107123

‎internal/lint/code/comments_test.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ import (
88
"testing"
99
)
1010

11+
func TestAppendLineNormalizesTrailingNewlines(t *testing.T) {
12+
tests := map[string]string{
13+
"": "\n",
14+
"text": "text\n",
15+
"text\n": "text\n",
16+
"text\n\n": "text\n",
17+
" text\n\n": "text\n",
18+
}
19+
20+
for input, expected := range tests {
21+
if actual := appendLine(input); actual != expected {
22+
t.Errorf("appendLine(%q) = %q, want %q", input, actual, expected)
23+
}
24+
}
25+
}
26+
1127
var testDir = "../../../testdata/comments"
1228
var binDir = "../../../bin"
1329

‎testdata/comments/out/0.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"Scope": "text.comment.block"
88
},
99
{
10-
"Text": "Println formats using the default formats for its oprands and writes to\nstandard output.\n\n\nSpaces are always added between operands and a newline is appended.\n\n\nIt returns the number of bytes written and any write error encountered.\n",
10+
"Text": "Println formats using the default formats for its oprands and writes to\nstandard output.\n\nSpaces are always added between operands and a newline is appended.\n\nIt returns the number of bytes written and any write error encountered.\n",
1111
"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",
1212
"Line": 11,
1313
"Offset": 0,

‎testdata/comments/out/1.json‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[
22
{
3-
"Text": "This module defines the set of command line arguments that ripgrep supports,\nincluding some light validation.\n\n\nThis module is purposely written in a bare-bones way, since it is included\nin ripgrep's build.rs file as a way to generate a man page and completion\nfiles for common shells.\n\n\nThe only other place that ripgrep deals with clap is in src/args.rs, which\nis where we read clap's configuration from the end user's arguments and turn\nit into a ripgrep-specific configuration type that is not coupled with clap.\n",
3+
"Text": "This module defines the set of command line arguments that ripgrep supports,\nincluding some light validation.\n\nThis module is purposely written in a bare-bones way, since it is included\nin ripgrep's build.rs file as a way to generate a man page and completion\nfiles for common shells.\n\nThe only other place that ripgrep deals with clap is in src/args.rs, which\nis where we read clap's configuration from the end user's arguments and turn\nit into a ripgrep-specific configuration type that is not coupled with clap.\n",
44
"Source": "// This module defines the set of command line arguments that ripgrep supports,\n// including some light validation.\n//\n// This module is purposely written in a bare-bones way, since it is included\n// in ripgrep's build.rs file as a way to generate a man page and completion\n// files for common shells.\n//\n// The only other place that ripgrep deals with clap is in src/args.rs, which\n// is where we read clap's configuration from the end user's arguments and turn\n// it into a ripgrep-specific configuration type that is not coupled with clap.\n",
55
"Line": 1,
66
"Offset": 0,
77
"Scope": "text.comment.line"
88
},
99
{
10-
"Text": "A human being is representd here\n\n A human being is representd here\n",
11-
"Source": "/// A human being is representd here\n\n///\n/// A human being is representd here\n",
10+
"Text": "A human being is representd here\n\nA human being is representd here\n",
11+
"Source": "/// A human being is representd here\n///\n/// A human being is representd here\n",
1212
"Line": 13,
1313
"Offset": 0,
1414
"Scope": "text.comment.line"
@@ -21,15 +21,15 @@
2121
"Scope": "text.comment.line"
2222
},
2323
{
24-
"Text": "Returns a person with the name given them\n\n # Arguments\n\n * `foof` - A string slice doof that holds the nme of the person\n\n # Exmples\n\n ```\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",
25-
"Source": "/// Returns a person with the name given them\n\n///\n/// # Arguments\n///\n/// * `foof` - A string slice doof that holds the nme of the person\n///\n/// # Exmples\n///\n/// ```\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",
24+
"Text": "Returns a person with the name given them\n\n# Arguments\n\n* `foof` - A string slice doof that holds the nme of the person\n\n# Exmples\n\n```\nYou can have rust code between fences inside the comments\nIf you pass --test to `rustdoc`, it will even test it for you!\nuse doc::Person;\nlet person = Person::new(\"name\");\n```\n",
25+
"Source": "/// Returns a person with the name given them\n///\n/// # Arguments\n///\n/// * `foof` - A string slice doof that holds the nme of the person\n///\n/// # Exmples\n///\n/// ```\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",
2626
"Line": 22,
2727
"Offset": 4,
2828
"Scope": "text.comment.line"
2929
},
3030
{
31-
"Text": "Gives a friendly hello!\n\n Says \"Hello, [name]\" to the `Person` it is called on.\n",
32-
"Source": "/// Gives a friendly hello!\n\n///\n/// Says \"Hello, [name]\" to the `Person` it is called on.\n",
31+
"Text": "Gives a friendly hello!\n\nSays \"Hello, [name]\" to the `Person` it is called on.\n",
32+
"Source": "/// Gives a friendly hello!\n///\n/// Says \"Hello, [name]\" to the `Person` it is called on.\n",
3333
"Line": 42,
3434
"Offset": 4,
3535
"Scope": "text.comment.line"

‎testdata/comments/out/5.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
3-
"Text": "This Deployment runs our API component\n\nTo increase the TODO number of replicas that run,\nchange the value of the spec.replicas field.\n\n\nAlways use a value higher than 1 to ensure\nmultiple replicas are running, as this\nguarantees redundancy if one instance fails.\n",
3+
"Text": "This Deployment runs our API component\n\nTo increase the TODO number of replicas that run,\nchange the value of the spec.replicas field.\n\nAlways use a value higher than 1 to ensure\nmultiple replicas are running, as this\nguarantees redundancy if one instance fails.\n",
44
"Source": "# This Deployment runs our API component\n#\n# To increase the TODO number of replicas that run,\n# change the value of the spec.replicas field.\n#\n# Always use a value higher than 1 to ensure\n# multiple replicas are running, as this\n# guarantees redundancy if one instance fails.\n",
55
"Line": 1,
66
"Offset": 0,

‎testdata/e2e/fragments.yaml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,49 @@ cases:
5555
test2.rs:409:38:Vale.Spelling:Did you really mean 'RGArg'?
5656
test2.rs:2860:34:Vale.Spelling:Did you really mean 'conlicts'?
5757
58+
- name: blank-comment-line
59+
about: |
60+
#1022: a blank line comment produced two lines in the extracted text
61+
where the source has one, so every alert below it was reported a line
62+
too far down with its column collapsed to 1.
63+
files:
64+
.vale.ini: |
65+
StylesPath = styles
66+
MinAlertLevel = suggestion
67+
68+
[*.{proto,go}]
69+
BasedOnStyles = T
70+
styles/T/Eror.yml: |
71+
extends: existence
72+
message: "Found '%s'."
73+
level: error
74+
tokens:
75+
- Eror
76+
test.proto: |
77+
syntax = "proto3";
78+
79+
// Correct.
80+
// Correct.
81+
// Eror.
82+
//
83+
// Eror.
84+
message Message {}
85+
test.go: |
86+
package main
87+
88+
// Eror.
89+
//
90+
//
91+
// Eror.
92+
func main() {}
93+
args: .
94+
exit: 1
95+
want: |
96+
test.go:3:4:T.Eror:Found 'Eror'.
97+
test.go:6:4:T.Eror:Found 'Eror'.
98+
test.proto:5:4:T.Eror:Found 'Eror'.
99+
test.proto:7:4:T.Eror:Found 'Eror'.
100+
58101
- name: tab-indented-block
59102
about: |
60103
#1130: only spaces were stripped when dedenting a block comment, so one

0 commit comments

Comments
 (0)