|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | + |
| 3 | +package processor |
| 4 | + |
| 5 | +import "testing" |
| 6 | + |
| 7 | +// Reported downstream by cs (github.com/boyter/cs), which vendors this package: |
| 8 | +// |
| 9 | +// panic: runtime error: index out of range [1411] with length 1411 |
| 10 | +// processor.blankState(...) workers.go:377 |
| 11 | +// |
| 12 | +// The trigger is a file whose final bytes are a docstring/ignore-escape quote |
| 13 | +// start token (Python """ or ''', C# @") with no trailing newline. |
| 14 | +// |
| 15 | +// verifyIgnoreEscape advances index past the matched start token |
| 16 | +// (index += len(Quotes[i].Start)) without bounding it against len(Content). |
| 17 | +// When the token ends the file, index lands exactly on len(Content), and |
| 18 | +// blankState then writes ContentByteType[index] out of bounds. |
| 19 | +// |
| 20 | +// The out-of-bounds write only fires when ClassifyContent is set (cs sets it; |
| 21 | +// scc's own CLI does not), but the same over-advance corrupts line counting for |
| 22 | +// every caller: the final line is skipped entirely. |
| 23 | + |
| 24 | +var docstringEOFCases = []struct { |
| 25 | + name string |
| 26 | + language string |
| 27 | + content string |
| 28 | + lines int |
| 29 | +}{ |
| 30 | + {"python triple double quote at eof", "Python", "def greet():\n return \"hi\"\n\"\"\"", 3}, |
| 31 | + {"python triple single quote at eof", "Python", "x = 1\n'''", 2}, |
| 32 | + {"python docstring is whole file", "Python", "\"\"\"", 1}, |
| 33 | + {"csharp ignore escape quote at eof", "C#", "class A {\n@\"", 2}, |
| 34 | + // Control: the same token followed by a newline has always worked. |
| 35 | + {"python docstring with trailing newline", "Python", "x = 1\n\"\"\"\n", 2}, |
| 36 | +} |
| 37 | + |
| 38 | +// A quote start token at EOF must not write past the end of ContentByteType. |
| 39 | +func TestCountStatsDocStringAtEOFNoPanic(t *testing.T) { |
| 40 | + ProcessConstants() |
| 41 | + |
| 42 | + for _, tc := range docstringEOFCases { |
| 43 | + t.Run(tc.name, func(t *testing.T) { |
| 44 | + fileJob := FileJob{Language: tc.language, ClassifyContent: true} |
| 45 | + fileJob.SetContent(tc.content) |
| 46 | + |
| 47 | + CountStats(&fileJob) |
| 48 | + |
| 49 | + if len(fileJob.ContentByteType) != len(fileJob.Content) { |
| 50 | + t.Errorf("Expected ContentByteType of length %d got %d", len(fileJob.Content), len(fileJob.ContentByteType)) |
| 51 | + } |
| 52 | + }) |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +// The same over-advance silently drops the last line for every caller, whether |
| 57 | +// or not content classification is enabled. |
| 58 | +func TestCountStatsDocStringAtEOFLineCount(t *testing.T) { |
| 59 | + ProcessConstants() |
| 60 | + |
| 61 | + for _, tc := range docstringEOFCases { |
| 62 | + t.Run(tc.name, func(t *testing.T) { |
| 63 | + fileJob := FileJob{Language: tc.language} |
| 64 | + fileJob.SetContent(tc.content) |
| 65 | + |
| 66 | + CountStats(&fileJob) |
| 67 | + |
| 68 | + if fileJob.Lines != int64(tc.lines) { |
| 69 | + t.Errorf("Expected %d lines got %d", tc.lines, fileJob.Lines) |
| 70 | + } |
| 71 | + if fileJob.Lines != fileJob.Code+fileJob.Comment+fileJob.Blank { |
| 72 | + t.Errorf("Expected lines %d to equal code %d + comment %d + blank %d", |
| 73 | + fileJob.Lines, fileJob.Code, fileJob.Comment, fileJob.Blank) |
| 74 | + } |
| 75 | + }) |
| 76 | + } |
| 77 | +} |
0 commit comments