From 801c8ec98e95ea8735834e48b9986123df29755b Mon Sep 17 00:00:00 2001 From: Rudy Regazzoni Date: Thu, 4 Dec 2025 15:22:38 +0100 Subject: [PATCH 1/2] cmd/cover: exclude commented-out code from coverage instrumentation --- src/cmd/cover/cover.go | 215 +++++++++++++++++++++++++++- src/cmd/cover/cover_test.go | 273 ++++++++++++++++++++++++++++++++++++ 2 files changed, 486 insertions(+), 2 deletions(-) diff --git a/src/cmd/cover/cover.go b/src/cmd/cover/cover.go index 9207fa0e8b0987..9e5afd87492b25 100644 --- a/src/cmd/cover/cover.go +++ b/src/cmd/cover/cover.go @@ -266,6 +266,197 @@ type File struct { pkg *Package } +// BlockSegment represents a segment of a basic block that can be either +// executable code or commented-out code. +type BlockSegment struct { + start token.Pos + end token.Pos + hasCode bool // true if this segment contains executable code +} + +// blockSplitter holds the state for splitting a block by comments. +// It tracks cursor position, line state, and section boundaries +// while scanning through source code character by character. +type blockSplitter struct { + // Source information + file *token.File + startOffset int + + // Accumulated results + segments []BlockSegment + + // Cursor position + i int + indexStartCurrentLine int + currentOffset int + currentSegmentStart token.Pos + + // Section state (persists across lines) + inCommentedSection bool + + // Line state (reset each line) + lineHasCode bool + lineHasComment bool + + // Cursor state (tracks what we're inside) + inSingleLineComment bool + inMultiLineComment bool + inString bool + inRawString bool +} + +// processLine handles end-of-line processing: computes state transitions +// and decides whether to create a new segment based on code/comment boundaries. +func (bs *blockSplitter) processLine() { + lineStart := bs.currentOffset + lineEnd := bs.currentOffset + (bs.i - bs.indexStartCurrentLine) + + if bs.inCommentedSection && bs.lineHasCode { + // End of commented section, start of code section + segmentEnd := bs.file.Pos(lineStart) + bs.segments = append(bs.segments, BlockSegment{ + start: bs.currentSegmentStart, + end: segmentEnd, + hasCode: false, + }) + bs.currentSegmentStart = bs.file.Pos(lineStart) + bs.inCommentedSection = false + } else if !bs.inCommentedSection && !bs.lineHasCode && bs.lineHasComment { + // End of code section, start of commented section + segmentEnd := bs.file.Pos(lineStart) + if bs.currentSegmentStart < segmentEnd { + bs.segments = append(bs.segments, BlockSegment{ + start: bs.currentSegmentStart, + end: segmentEnd, + hasCode: true, + }) + } + bs.currentSegmentStart = bs.file.Pos(lineStart) + bs.inCommentedSection = true + } + + bs.currentOffset = lineEnd +} + +// resetLineState resets line-specific state for a new line. +func (bs *blockSplitter) resetLineState() { + bs.indexStartCurrentLine = bs.i + bs.lineHasComment = bs.inMultiLineComment + bs.lineHasCode = false + bs.inSingleLineComment = false +} + +// inStringOrComment returns true if currently inside a string or comment. +func (bs *blockSplitter) inStringOrComment() bool { + return bs.inString || bs.inRawString || bs.inSingleLineComment || bs.inMultiLineComment +} + +// splitBlockByComments analyzes a block range and splits it into segments, +// separating executable code from any commented lines. +// To do this, it reads character by character the original source code. +func (f *File) splitBlockByComments(start, end token.Pos) []BlockSegment { + startOffset := f.offset(start) + endOffset := f.offset(end) + + if startOffset >= endOffset || endOffset > len(f.content) { + return []BlockSegment{{start: start, end: end, hasCode: true}} + } + + originalSourceCode := f.content[startOffset:endOffset] + + bs := &blockSplitter{ + file: f.fset.File(start), + startOffset: startOffset, + currentOffset: startOffset, + currentSegmentStart: start, + } + + for bs.i < len(originalSourceCode) { + char := originalSourceCode[bs.i] + + if char == '\\' && bs.inString && bs.i+1 < len(originalSourceCode) { + bs.lineHasCode = true + bs.i += 2 // Skip escaped character + continue + } + + if char == '"' && !bs.inRawString && !bs.inSingleLineComment && !bs.inMultiLineComment { + bs.lineHasCode = true + bs.inString = !bs.inString + bs.i++ + continue + } + + if char == '`' && !bs.inString && !bs.inSingleLineComment && !bs.inMultiLineComment { + bs.lineHasCode = true + bs.inRawString = !bs.inRawString + bs.i++ + continue + } + + if char == '\n' { + bs.i++ + bs.processLine() + bs.resetLineState() + continue + } + + if bs.i+1 < len(originalSourceCode) { + nextChar := originalSourceCode[bs.i+1] + if char == '/' && nextChar == '/' && !bs.inString && !bs.inRawString && !bs.inMultiLineComment { + bs.inSingleLineComment = true + bs.lineHasComment = true + bs.i += 2 + continue + } + + if char == '/' && nextChar == '*' && !bs.inString && !bs.inRawString && !bs.inSingleLineComment { + bs.inMultiLineComment = true + bs.lineHasComment = true + bs.i += 2 + continue + } + + if char == '*' && nextChar == '/' && bs.inMultiLineComment { + bs.inMultiLineComment = false + bs.lineHasComment = true + bs.i += 2 + continue + } + } + + // If we matched nothing else and the char is not a whitespace, we are in normal code. + if !bs.lineHasCode && !isWhitespace(char) && !bs.inSingleLineComment && !bs.inMultiLineComment { + bs.lineHasCode = true + } + + bs.i++ + } + + // Process the last line if it doesn't end with a newline + bs.processLine() + + // Add the final segment + if bs.currentSegmentStart < end { + bs.segments = append(bs.segments, BlockSegment{ + start: bs.currentSegmentStart, + end: end, + hasCode: !bs.inCommentedSection, + }) + } + + // If no segments were created, return the original block as a code segment + if len(bs.segments) == 0 { + return []BlockSegment{{start: start, end: end, hasCode: true}} + } + + return bs.segments +} + +func isWhitespace(b byte) bool { + return b == ' ' || b == '\t' || b == '\n' || b == '\r' +} + // findText finds text in the original source, starting at pos. // It correctly skips over comments and assumes it need not // handle quoted strings. @@ -755,7 +946,14 @@ func (f *File) addCounters(pos, insertPos, blockEnd token.Pos, list []ast.Stmt, // Special case: make sure we add a counter to an empty block. Can't do this below // or we will add a counter to an empty statement list after, say, a return statement. if len(list) == 0 { - f.edit.Insert(f.offset(insertPos), f.newCounter(insertPos, blockEnd, 0)+";") + // Split the empty block by comments and only add counter if there's executable content + segments := f.splitBlockByComments(insertPos, blockEnd) + for _, segment := range segments { + if segment.hasCode { + f.edit.Insert(f.offset(segment.start), f.newCounter(segment.start, segment.end, 0)+";") + break // Only insert one counter per basic block + } + } return } // Make a copy of the list, as we may mutate it and should leave the @@ -805,7 +1003,20 @@ func (f *File) addCounters(pos, insertPos, blockEnd token.Pos, list []ast.Stmt, end = blockEnd } if pos != end { // Can have no source to cover if e.g. blocks abut. - f.edit.Insert(f.offset(insertPos), f.newCounter(pos, end, last)+";") + // Split the block by comments and create counters only for executable segments + segments := f.splitBlockByComments(pos, end) + for i, segment := range segments { + if segment.hasCode { + // Only create counter for executable code segments + // Insert counter at the beginning of the executable segment + insertOffset := f.offset(segment.start) + // For the first segment, use the original insertPos if it's before the segment + if i == 0 { + insertOffset = f.offset(insertPos) + } + f.edit.Insert(insertOffset, f.newCounter(segment.start, segment.end, last)+";") + } + } } list = list[last:] if len(list) == 0 { diff --git a/src/cmd/cover/cover_test.go b/src/cmd/cover/cover_test.go index 431c0560f6eb23..a01a02895bb1ee 100644 --- a/src/cmd/cover/cover_test.go +++ b/src/cmd/cover/cover_test.go @@ -19,6 +19,7 @@ import ( "os/exec" "path/filepath" "regexp" + "strconv" "strings" "sync" "testing" @@ -638,3 +639,275 @@ func main() { t.Errorf("unexpected success; want failure due to newline in file path") } } + +// TestCommentedOutCodeExclusion tests that all commented lines are excluded +// from coverage requirements using the simplified block splitting approach. +func TestCommentedOutCodeExclusion(t *testing.T) { + testenv.MustHaveGoBuild(t) + + // Create a test file with various types of comments mixed with real code + testSrc := `package main + + import "fmt" + + func main() { + fmt.Println("Start - should be covered") + + // This is a regular comment - should be excluded from coverage + x := 42 + + // Any comment line is excluded, regardless of content + // fmt.Println("commented code") + // TODO: implement this later + // BUG: fix this issue + + y := 0 + fmt.Println("After comments - should be covered") + + /* some comment */ + + fmt.Println("It's a trap /*") + z := 0 + + if x > 0 { + y = x * 2 + } else { + y = x - 2 + } + + z = 5 + + /* Multiline comments + with here + and here */ + + z1 := 0 + + z1 = 1 /* Multiline comments + where there is code at the beginning + and the end */ z1 = 2 + + z1 = 3; /* // */ z1 = 4 + + z1 = 5 /* // + // + // */ z1 = 6 + + /* + */ z1 = 7 /* + */ + + z1 = 8/* + before */ /* after + */z1 = 9 + + /* before */ z1 = 10 + /* before */ z1 = 10 /* after */ + z1 = 10 /* after */ + + fmt.Printf("Result: %d\n", z) + fmt.Printf("Result: %d\n", z1) + + s := ` + "`This is a multi-line raw string" + ` + // fake comment on line 2 + /* and fake comment on line 3 */ + and other` + "`" + ` + + s = ` + "`another multiline string" + ` + ` + "`" + ` // another trap + + fmt.Printf("%s", s) + + // More comments to exclude + // for i := 0; i < 10; i++ { + // fmt.Printf("Loop %d", i) + // } + + fmt.Printf("Result: %d\n", y) + // end comment + } + + func empty() { + + } + + func singleBlock() { + fmt.Printf("ResultSomething") + } + + func justComment() { + // comment + } + + func justMultilineComment() { + /* comment + again + until here */ + }` + + tmpdir := t.TempDir() + srcPath := filepath.Join(tmpdir, "test.go") + if err := os.WriteFile(srcPath, []byte(testSrc), 0666); err != nil { + t.Fatalf("writing test file: %v", err) + } + + // Test that our cover tool processes the file without errors + cmd := testenv.Command(t, testcover(t), "-mode=set", srcPath) + out, err := cmd.Output() + if err != nil { + t.Fatalf("cover failed: %v\nOutput: %s", err, out) + } + + // The output should contain the real code but preserve commented-out code as comments, we still need to get rid of the first line + outStr := string(out) + outStr = strings.Join(strings.SplitN(outStr, "\n", 2)[1:], "\n") + + segments := extractCounterPositionFromCode(outStr) + + if len(segments) != 18 { + t.Fatalf("expected 18 code segments, got %d", len(segments)) + } + + // Assert all segments contents, we expect to find all lines of code and no comments alone on a line + assertSegmentContent(t, segments[0], []string{`fmt.Println("Start - should be covered")`, `{`}) + assertSegmentContent(t, segments[1], []string{`x := 42`}) + assertSegmentContent(t, segments[2], []string{`y := 0`, `fmt.Println("After comments - should be covered")`}) + assertSegmentContent(t, segments[3], []string{`fmt.Println("It's a trap /*")`, `z := 0`, `if x > 0 {`}) + assertSegmentContent(t, segments[4], []string{`z = 5`}) + assertSegmentContent(t, segments[5], []string{`z1 := 0`, `z1 = 1 /* Multiline comments`}) + assertSegmentContent(t, segments[6], []string{`and the end */ z1 = 2`, `z1 = 3; /* // */ z1 = 4`, `z1 = 5 /* //`}) + assertSegmentContent(t, segments[7], []string{`// */ z1 = 6`}) + assertSegmentContent(t, segments[8], []string{`*/ z1 = 7 /*`}) + assertSegmentContent(t, segments[9], []string{`z1 = 8/*`}) + assertSegmentContent(t, segments[10], []string{`*/z1 = 9`, `/* before */ z1 = 10`, `/* before */ z1 = 10 /* after */`, `z1 = 10 /* after */`, + `fmt.Printf("Result: %d\n", z)`, `fmt.Printf("Result: %d\n", z1)`, `s := ` + "`" + `This is a multi-line raw string`, `// fake comment on line 2`, + `/* and fake comment on line 3 */`, `and other` + "`", `s = ` + "`" + `another multiline string`, "` // another trap", `fmt.Printf("%s", s)`}) + assertSegmentContent(t, segments[11], []string{`fmt.Printf("Result: %d\n", y)`}) + // Segment 12 is the if block in main() + assertSegmentContent(t, segments[12], []string{`{`, `y = x * 2`, `}`}) + // and segment 13 is the else block, extra curly brackets introduced by cover tool + assertSegmentContent(t, segments[13], []string{`{`, `{`, `y = x - 2`, `}`, `}`}) + // function empty() + assertSegmentContent(t, segments[14], []string{}) + // function singleBlock() + assertSegmentContent(t, segments[15], []string{`{`, `fmt.Printf("ResultSomething")`}) + // function justComment() + assertSegmentContent(t, segments[16], []string{}) + // function justMultilineComment() + assertSegmentContent(t, segments[17], []string{}) +} + +func assertSegmentContent(t *testing.T, segment CounterSegment, wantLines []string) { + code := segment.Code + + // removing counter + expectedCounter := fmt.Sprintf("GoCover.Count[%d] = 1;", segment.CounterIndex) + if strings.Contains(code, expectedCounter) { + code = strings.Replace(code, expectedCounter, "", 1) + } else { + t.Fatalf("expected code segment %d to contain the counter code '%q', but segment is: %q", segment.CounterIndex, expectedCounter, code) + } + + // removing expected strings + for _, wantLine := range wantLines { + if strings.Contains(code, wantLine) { + code = strings.Replace(code, wantLine, "", 1) + } else { + t.Fatalf("expected code segment %d to contain '%q', but segment is: %q", segment.CounterIndex, wantLine, code) + } + } + + // checking if only left is whitespace + code = strings.TrimSpace(code) + if code != "" { + t.Fatalf("expected code segment %d to contain only expected strings, leftover: %q", segment.CounterIndex, code) + } +} + +type CounterSegment struct { + StartLine int + EndLine int + StartColumn int + EndColumn int + CounterIndex int + Code string +} + +func extractCounterPositionFromCode(code string) []CounterSegment { + re := regexp.MustCompile(`(?P\d+), (?P\d+), (?P0x[0-9a-f]+), // \[(?P\d+)\]`) + // find all occurrences and extract captured parenthesis into a struct + matches := re.FindAllStringSubmatch(code, -1) + var matchResults []CounterSegment + for _, m := range matches { + packed := m[re.SubexpIndex("packedColumnDetail")] + var packedVal int + fmt.Sscanf(packed, "0x%x", &packedVal) + startCol := packedVal & 0xFFFF + endCol := (packedVal >> 16) & 0xFFFF + startLine, _ := strconv.Atoi(m[re.SubexpIndex("startLine")]) + endLine, _ := strconv.Atoi(m[re.SubexpIndex("endLine")]) + + // fix: if start line and end line are equals, increase end column by the number of chars in 'GoCover.Count[X] = 1;' + if startLine == endLine { + endCol += len("GoCover.Count[" + m[re.SubexpIndex("counterIndex")] + "] = 1;") + } + + counterIndex, _ := strconv.Atoi(m[re.SubexpIndex("counterIndex")]) + codeSegment := extractCodeFromSegmentDetail(startLine, endLine, startCol, endCol, code) + + matchResults = append(matchResults, CounterSegment{ + StartLine: startLine, + EndLine: endLine, + StartColumn: startCol, + EndColumn: endCol, + CounterIndex: counterIndex, + Code: codeSegment, + }) + } + return matchResults +} + +func extractCodeFromSegmentDetail(startLine int, endLine int, startCol int, endCol int, code string) string { + lines := strings.Split(code, "\n") + var codeSegment string + if startLine == endLine && startLine > 0 && startLine <= len(lines) { + // Single line segment + line := lines[startLine-1] + if startCol <= 0 { + startCol = 1 + } + if endCol > len(line) { + endCol = len(line) + } + + if startCol <= endCol { + // display details of next operation + codeSegment = line[startCol-1 : endCol] + } + } else if startLine > 0 && endLine <= len(lines) && startLine <= endLine { + // Multi-line segment + var segmentLines []string + for i := startLine; i <= endLine; i++ { + if i > len(lines) { + break + } + line := lines[i-1] + if i == startLine { + // First line: from startCol to end + if startCol > 0 && startCol <= len(line) { + segmentLines = append(segmentLines, line[startCol-1:]) + } + } else if i == endLine { + // Last line: from beginning to endCol + if endCol <= len(line) { + segmentLines = append(segmentLines, line[:endCol]) + } + } else { + // Middle lines: entire line + segmentLines = append(segmentLines, line) + } + } + codeSegment = strings.Join(segmentLines, "\n") + } + return codeSegment +} From f740d24ec0d6066c57496696e2f208530b1f532b Mon Sep 17 00:00:00 2001 From: Rudy Regazzoni Date: Fri, 5 Dec 2025 09:09:07 +0100 Subject: [PATCH 2/2] replace manual scanning with go/scanner call --- src/cmd/cover/cover.go | 221 ++++++++++++++--------------------------- 1 file changed, 74 insertions(+), 147 deletions(-) diff --git a/src/cmd/cover/cover.go b/src/cmd/cover/cover.go index 9e5afd87492b25..9749e318c83af9 100644 --- a/src/cmd/cover/cover.go +++ b/src/cmd/cover/cover.go @@ -13,6 +13,7 @@ import ( "fmt" "go/ast" "go/parser" + "go/scanner" "go/token" "internal/coverage" "internal/coverage/encodemeta" @@ -274,86 +275,10 @@ type BlockSegment struct { hasCode bool // true if this segment contains executable code } -// blockSplitter holds the state for splitting a block by comments. -// It tracks cursor position, line state, and section boundaries -// while scanning through source code character by character. -type blockSplitter struct { - // Source information - file *token.File - startOffset int - - // Accumulated results - segments []BlockSegment - - // Cursor position - i int - indexStartCurrentLine int - currentOffset int - currentSegmentStart token.Pos - - // Section state (persists across lines) - inCommentedSection bool - - // Line state (reset each line) - lineHasCode bool - lineHasComment bool - - // Cursor state (tracks what we're inside) - inSingleLineComment bool - inMultiLineComment bool - inString bool - inRawString bool -} - -// processLine handles end-of-line processing: computes state transitions -// and decides whether to create a new segment based on code/comment boundaries. -func (bs *blockSplitter) processLine() { - lineStart := bs.currentOffset - lineEnd := bs.currentOffset + (bs.i - bs.indexStartCurrentLine) - - if bs.inCommentedSection && bs.lineHasCode { - // End of commented section, start of code section - segmentEnd := bs.file.Pos(lineStart) - bs.segments = append(bs.segments, BlockSegment{ - start: bs.currentSegmentStart, - end: segmentEnd, - hasCode: false, - }) - bs.currentSegmentStart = bs.file.Pos(lineStart) - bs.inCommentedSection = false - } else if !bs.inCommentedSection && !bs.lineHasCode && bs.lineHasComment { - // End of code section, start of commented section - segmentEnd := bs.file.Pos(lineStart) - if bs.currentSegmentStart < segmentEnd { - bs.segments = append(bs.segments, BlockSegment{ - start: bs.currentSegmentStart, - end: segmentEnd, - hasCode: true, - }) - } - bs.currentSegmentStart = bs.file.Pos(lineStart) - bs.inCommentedSection = true - } - - bs.currentOffset = lineEnd -} - -// resetLineState resets line-specific state for a new line. -func (bs *blockSplitter) resetLineState() { - bs.indexStartCurrentLine = bs.i - bs.lineHasComment = bs.inMultiLineComment - bs.lineHasCode = false - bs.inSingleLineComment = false -} - -// inStringOrComment returns true if currently inside a string or comment. -func (bs *blockSplitter) inStringOrComment() bool { - return bs.inString || bs.inRawString || bs.inSingleLineComment || bs.inMultiLineComment -} - // splitBlockByComments analyzes a block range and splits it into segments, // separating executable code from any commented lines. -// To do this, it reads character by character the original source code. +// It uses go/scanner to tokenize the source and identify which lines +// contain executable code vs. only comments. func (f *File) splitBlockByComments(start, end token.Pos) []BlockSegment { startOffset := f.offset(start) endOffset := f.offset(end) @@ -362,99 +287,101 @@ func (f *File) splitBlockByComments(start, end token.Pos) []BlockSegment { return []BlockSegment{{start: start, end: end, hasCode: true}} } - originalSourceCode := f.content[startOffset:endOffset] + src := f.content[startOffset:endOffset] + origFile := f.fset.File(start) - bs := &blockSplitter{ - file: f.fset.File(start), - startOffset: startOffset, - currentOffset: startOffset, - currentSegmentStart: start, - } + // Create a new file set for scanning this block + fset := token.NewFileSet() + file := fset.AddFile("", -1, len(src)) - for bs.i < len(originalSourceCode) { - char := originalSourceCode[bs.i] + var s scanner.Scanner + s.Init(file, src, nil, scanner.ScanComments) - if char == '\\' && bs.inString && bs.i+1 < len(originalSourceCode) { - bs.lineHasCode = true - bs.i += 2 // Skip escaped character - continue - } + // Track which lines have code vs only comments + lineHasCode := make(map[int]bool) + lineHasComment := make(map[int]bool) - if char == '"' && !bs.inRawString && !bs.inSingleLineComment && !bs.inMultiLineComment { - bs.lineHasCode = true - bs.inString = !bs.inString - bs.i++ - continue + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break } - if char == '`' && !bs.inString && !bs.inSingleLineComment && !bs.inMultiLineComment { - bs.lineHasCode = true - bs.inRawString = !bs.inRawString - bs.i++ - continue + // Calculate start and end lines using the standard pattern from go/ast + // (e.g., ast.Comment.End() returns pos + len(text)) + startLine := file.Line(pos) + endLine := file.Line(pos + token.Pos(len(lit)) - 1) + if endLine < startLine { + endLine = startLine } - if char == '\n' { - bs.i++ - bs.processLine() - bs.resetLineState() - continue - } - - if bs.i+1 < len(originalSourceCode) { - nextChar := originalSourceCode[bs.i+1] - if char == '/' && nextChar == '/' && !bs.inString && !bs.inRawString && !bs.inMultiLineComment { - bs.inSingleLineComment = true - bs.lineHasComment = true - bs.i += 2 - continue - } - - if char == '/' && nextChar == '*' && !bs.inString && !bs.inRawString && !bs.inSingleLineComment { - bs.inMultiLineComment = true - bs.lineHasComment = true - bs.i += 2 - continue + if tok == token.COMMENT { + // Mark all lines spanned by this comment + for line := startLine; line <= endLine; line++ { + lineHasComment[line] = true } - - if char == '*' && nextChar == '/' && bs.inMultiLineComment { - bs.inMultiLineComment = false - bs.lineHasComment = true - bs.i += 2 - continue + } else { + // Mark all lines spanned by this token as having code + for line := startLine; line <= endLine; line++ { + lineHasCode[line] = true } } + } - // If we matched nothing else and the char is not a whitespace, we are in normal code. - if !bs.lineHasCode && !isWhitespace(char) && !bs.inSingleLineComment && !bs.inMultiLineComment { - bs.lineHasCode = true + // Build segments based on line transitions + // The scanner has already built the line table in file, so we can use + // file.LineStart() to get positions directly instead of manual calculation. + var segments []BlockSegment + var currentSegmentStart token.Pos = start + inCommentedSection := false + + totalLines := file.LineCount() + for line := 1; line <= totalLines; line++ { + hasCode := lineHasCode[line] + hasComment := lineHasComment[line] + + if inCommentedSection && hasCode { + // End of commented section, start of code section + lineOffset := file.Offset(file.LineStart(line)) + segmentEnd := origFile.Pos(startOffset + lineOffset) + segments = append(segments, BlockSegment{ + start: currentSegmentStart, + end: segmentEnd, + hasCode: false, + }) + currentSegmentStart = segmentEnd + inCommentedSection = false + } else if !inCommentedSection && !hasCode && hasComment { + // End of code section, start of commented section + lineOffset := file.Offset(file.LineStart(line)) + segmentEnd := origFile.Pos(startOffset + lineOffset) + if currentSegmentStart < segmentEnd { + segments = append(segments, BlockSegment{ + start: currentSegmentStart, + end: segmentEnd, + hasCode: true, + }) + } + currentSegmentStart = segmentEnd + inCommentedSection = true } - - bs.i++ } - // Process the last line if it doesn't end with a newline - bs.processLine() - // Add the final segment - if bs.currentSegmentStart < end { - bs.segments = append(bs.segments, BlockSegment{ - start: bs.currentSegmentStart, + if currentSegmentStart < end { + segments = append(segments, BlockSegment{ + start: currentSegmentStart, end: end, - hasCode: !bs.inCommentedSection, + hasCode: !inCommentedSection, }) } // If no segments were created, return the original block as a code segment - if len(bs.segments) == 0 { + if len(segments) == 0 { return []BlockSegment{{start: start, end: end, hasCode: true}} } - return bs.segments -} - -func isWhitespace(b byte) bool { - return b == ' ' || b == '\t' || b == '\n' || b == '\r' + return segments } // findText finds text in the original source, starting at pos.