From 28d95d8a4c00a87dfe3e404832649410ebc28dbe Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 15:23:08 +0200 Subject: [PATCH 1/5] Improve C# comments filtering. --- embedding/commentfilter/config.go | 2 +- embedding/commentfilter/csharp_filter.go | 381 ++++++++++++++++++ embedding/commentfilter/csharp_filter_test.go | 51 +++ 3 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 embedding/commentfilter/csharp_filter.go diff --git a/embedding/commentfilter/config.go b/embedding/commentfilter/config.go index 56a662c..03f1891 100644 --- a/embedding/commentfilter/config.go +++ b/embedding/commentfilter/config.go @@ -37,7 +37,7 @@ var filtersByExtension = map[string]filterEntry{ ".groovy": filterConfig(MarkerCommentFilter{Syntax: javaSyntax}, allModes), // C# - ".cs": filterConfig(MarkerCommentFilter{Syntax: csharpSyntax}, allModes), + ".cs": filterConfig(CSharpCommentFilter{}, allModes), // C/C++ ".c": filterConfig(MarkerCommentFilter{Syntax: cStyleSyntax}, regularModes), diff --git a/embedding/commentfilter/csharp_filter.go b/embedding/commentfilter/csharp_filter.go new file mode 100644 index 0000000..5169492 --- /dev/null +++ b/embedding/commentfilter/csharp_filter.go @@ -0,0 +1,381 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package commentfilter + +import "strings" + +const ( + csharpInterpolatedVerbatimStringStart = `$@"` + csharpVerbatimInterpolatedStringStart = `@$"` + csharpVerbatimStringStart = `@"` + csharpInterpolatedStringStart = `$"` + csharpEscapedQuote = `""` + csharpEscapedOpenBrace = `{{` + csharpEscapedCloseBrace = `}}` +) + +// CSharpCommentFilter filters C# comments while preserving string literal text. +type CSharpCommentFilter struct{} + +// csharpState tracks C# lexical state that can span source lines. +type csharpState struct { + // blockActive reports whether scanning is inside a block comment. + blockActive bool + + // blockKeep reports whether the active block comment should be retained. + blockKeep bool + + // stringActive reports whether scanning is inside a string. + stringActive bool + + // stringVerbatim reports whether the active string uses verbatim escaping. + stringVerbatim bool + + // stringInterpolated reports whether the active string has interpolation holes. + stringInterpolated bool + + // interpolationDepth is the active brace depth inside an interpolation expression. + interpolationDepth int +} + +// csharpLineFilter filters one C# source line. +type csharpLineFilter struct { + // line is the source line being filtered. + line string + + // mode selects which comments to retain. + mode Mode + + // state tracks C# constructs across lines. + state *csharpState + + // result accumulates the filtered source line. + result strings.Builder + + // position is the current byte index in line. + position int + + // hadComment reports whether the line contained a recognized comment. + hadComment bool +} + +// Filter removes or preserves C# comments according to mode. +// +// Parameters: +// lines - provides C# source lines. +// mode - selects comments to retain. +// +// Returns filtered source lines. +func (CSharpCommentFilter) Filter(lines []string, mode Mode) []string { + var filtered []string + state := csharpState{} + for _, line := range lines { + filteredLine, hadComment := filterCSharpLine(line, mode, &state) + if hadComment && strings.TrimSpace(filteredLine) == "" { + continue + } + filtered = append(filtered, filteredLine) + } + + return filtered +} + +// filterCSharpLine removes or preserves recognized C# comments from one line. +func filterCSharpLine(line string, mode Mode, state *csharpState) (string, bool) { + filter := csharpLineFilter{ + line: line, + mode: mode, + state: state, + } + + return filter.filterLine() +} + +// filterLine walks the current line until it reaches its end or a line comment. +func (f *csharpLineFilter) filterLine() (string, bool) { + for f.position < len(f.line) { + if f.consumeActiveBlock() { + continue + } + if f.consumeStringInterpolation() { + continue + } + if f.consumeStringText() { + continue + } + if f.consumeStringStart() { + continue + } + if comment := f.consumeComment(); comment.consumed { + if comment.stopLine { + break + } + + continue + } + f.consumeCodeByte() + } + + f.closeSingleLineStringAtLineEnd() + + return f.result.String(), f.hadComment +} + +// consumeActiveBlock consumes text while the scanner is inside a block comment. +func (f *csharpLineFilter) consumeActiveBlock() bool { + if !f.state.blockActive { + return false + } + f.hadComment = true + end := strings.Index(f.line[f.position:], cStyleBlockCommentEnd) + if end < 0 { + if f.state.blockKeep { + f.result.WriteString(f.line[f.position:]) + } + f.position = len(f.line) + + return true + } + endPosition := f.position + end + len(cStyleBlockCommentEnd) + if f.state.blockKeep { + f.result.WriteString(f.line[f.position:endPosition]) + } + f.position = endPosition + f.state.blockActive = false + + return true +} + +// consumeStringInterpolation filters code inside an active interpolation expression. +func (f *csharpLineFilter) consumeStringInterpolation() bool { + if !f.state.stringActive || f.state.interpolationDepth == 0 { + return false + } + for f.position < len(f.line) { + if f.consumeActiveBlock() { + continue + } + if f.consumeInterpolationString() { + continue + } + if comment := f.consumeComment(); comment.consumed { + if comment.stopLine { + return true + } + + continue + } + switch f.line[f.position] { + case '{': + f.state.interpolationDepth++ + f.consumeCodeByte() + case '}': + f.state.interpolationDepth-- + f.consumeCodeByte() + if f.state.interpolationDepth == 0 { + return true + } + default: + f.consumeCodeByte() + } + } + + return true +} + +// consumeInterpolationString copies a string literal inside interpolation code. +func (f *csharpLineFilter) consumeInterpolationString() bool { + token, verbatim, found := csharpStringTokenAt(f.line, f.position) + if !found { + return false + } + f.result.WriteString(token) + f.position += len(token) + for f.position < len(f.line) { + switch { + case verbatim && strings.HasPrefix(f.line[f.position:], csharpEscapedQuote): + f.result.WriteString(csharpEscapedQuote) + f.position += len(csharpEscapedQuote) + case !verbatim && f.line[f.position] == '\\': + f.writeEscapedByte() + case f.line[f.position] == '"': + f.consumeCodeByte() + + return true + default: + f.consumeCodeByte() + } + } + + return true +} + +// consumeStringText copies active string text without scanning comment markers inside it. +func (f *csharpLineFilter) consumeStringText() bool { + if !f.state.stringActive || f.state.interpolationDepth > 0 { + return false + } + for f.position < len(f.line) { + switch { + case f.state.stringVerbatim && strings.HasPrefix(f.line[f.position:], csharpEscapedQuote): + f.result.WriteString(csharpEscapedQuote) + f.position += len(csharpEscapedQuote) + case !f.state.stringVerbatim && f.line[f.position] == '\\': + f.writeEscapedByte() + case f.line[f.position] == '"': + f.consumeCodeByte() + f.closeString() + + return true + case f.startsEscapedInterpolationBrace(): + f.result.WriteString(f.line[f.position : f.position+2]) + f.position += 2 + case f.state.stringInterpolated && f.line[f.position] == '{': + f.consumeCodeByte() + f.state.interpolationDepth = 1 + + return true + default: + f.consumeCodeByte() + } + } + + return true +} + +// consumeStringStart starts a C# string literal at the current position. +func (f *csharpLineFilter) consumeStringStart() bool { + token, verbatim, found := csharpStringTokenAt(f.line, f.position) + if !found { + return false + } + interpolated := strings.HasPrefix(token, "$") || strings.HasPrefix(token, "@$") + f.startString(token, verbatim, interpolated) + + return true +} + +// csharpStringTokenAt returns a C# string opener at position. +func csharpStringTokenAt(line string, position int) (string, bool, bool) { + switch { + case strings.HasPrefix(line[position:], csharpInterpolatedVerbatimStringStart): + return csharpInterpolatedVerbatimStringStart, true, true + case strings.HasPrefix(line[position:], csharpVerbatimInterpolatedStringStart): + return csharpVerbatimInterpolatedStringStart, true, true + case strings.HasPrefix(line[position:], csharpVerbatimStringStart): + return csharpVerbatimStringStart, true, true + case strings.HasPrefix(line[position:], csharpInterpolatedStringStart): + return csharpInterpolatedStringStart, false, true + case line[position] == '"': + return `"`, false, true + default: + return "", false, false + } +} + +// startString records the active string literal and copies its opening token. +func (f *csharpLineFilter) startString(token string, verbatim bool, interpolated bool) { + f.result.WriteString(token) + f.position += len(token) + f.state.stringActive = true + f.state.stringVerbatim = verbatim + f.state.stringInterpolated = interpolated +} + +// startsEscapedInterpolationBrace reports whether the position starts {{ or }} in string text. +func (f *csharpLineFilter) startsEscapedInterpolationBrace() bool { + if !f.state.stringInterpolated || f.position+1 >= len(f.line) { + return false + } + + return strings.HasPrefix(f.line[f.position:], csharpEscapedOpenBrace) || + strings.HasPrefix(f.line[f.position:], csharpEscapedCloseBrace) +} + +// closeSingleLineStringAtLineEnd clears invalid single-line strings at end of line. +func (f *csharpLineFilter) closeSingleLineStringAtLineEnd() { + if f.state.stringActive && !f.state.stringVerbatim && f.state.interpolationDepth == 0 { + f.closeString() + } +} + +// closeString clears the active C# string state. +func (f *csharpLineFilter) closeString() { + f.state.stringActive = false + f.state.stringVerbatim = false + f.state.stringInterpolated = false + f.state.interpolationDepth = 0 +} + +// consumeComment consumes a C# comment when one starts at the scanner position. +func (f *csharpLineFilter) consumeComment() commentConsumeResult { + if strings.HasPrefix(f.line[f.position:], cStyleDocCommentStart) { + f.startBlockComment(f.mode == RetainDocumentation) + + return commentConsumeResult{consumed: true} + } + if strings.HasPrefix(f.line[f.position:], cStyleBlockCommentStart) { + f.startBlockComment(f.mode == RetainBlock || f.mode == RetainRegular) + + return commentConsumeResult{consumed: true} + } + if strings.HasPrefix(f.line[f.position:], "///") { + f.hadComment = true + if f.mode == RetainDocumentation { + f.result.WriteString(f.line[f.position:]) + } + f.position = len(f.line) + + return commentConsumeResult{consumed: true, stopLine: true} + } + if strings.HasPrefix(f.line[f.position:], "//") { + f.hadComment = true + if f.mode == RetainInline || f.mode == RetainRegular { + f.result.WriteString(f.line[f.position:]) + } + f.position = len(f.line) + + return commentConsumeResult{consumed: true, stopLine: true} + } + + return commentConsumeResult{} +} + +// startBlockComment records the active block comment and whether to keep it. +func (f *csharpLineFilter) startBlockComment(keep bool) { + f.hadComment = true + f.state.blockActive = true + f.state.blockKeep = keep +} + +// writeEscapedByte copies an escaped byte pair from a regular string. +func (f *csharpLineFilter) writeEscapedByte() { + f.result.WriteByte(f.line[f.position]) + f.position++ + if f.position < len(f.line) { + f.result.WriteByte(f.line[f.position]) + f.position++ + } +} + +// consumeCodeByte copies one source byte. +func (f *csharpLineFilter) consumeCodeByte() { + f.result.WriteByte(f.line[f.position]) + f.position++ +} diff --git a/embedding/commentfilter/csharp_filter_test.go b/embedding/commentfilter/csharp_filter_test.go index 5d8df8e..6b6d3a5 100644 --- a/embedding/commentfilter/csharp_filter_test.go +++ b/embedding/commentfilter/csharp_filter_test.go @@ -54,4 +54,55 @@ var _ = Describe("C#", func() { assertFiltered("Api.cs", RetainInline, lines, expected) }) + + It("should strip comments without treating verbatim string text as comments", func() { + lines := []string{ + "// header comment", + `var uri = @"https://example.com/*not-comment*/"; // inline comment`, + `var block = @"Keep // marker`, + `and /* marker */ text"; /* trailing block */`, + } + + expected := []string{ + `var uri = @"https://example.com/*not-comment*/"; `, + `var block = @"Keep // marker`, + `and /* marker */ text"; `, + } + + assertFiltered("Api.cs", RetainNone, lines, expected) + }) + + It("should strip comments without treating interpolated string text as comments", func() { + lines := []string{ + `var message = $"Keep // and /* markers */ {Format(value /* real comment */)}";`, + `var escaped = $"Keep {{ // text }} and {value}"; // inline comment`, + `var nested = $"Value {Format("/* not comment */")} // still text"; // inline comment`, + } + + expected := []string{ + `var message = $"Keep // and /* markers */ {Format(value )}";`, + `var escaped = $"Keep {{ // text }} and {value}"; `, + `var nested = $"Value {Format("/* not comment */")} // still text"; `, + } + + assertFiltered("Api.cs", RetainNone, lines, expected) + }) + + It("should strip comments without treating verbatim interpolated string text as comments", func() { + lines := []string{ + `var path = $@"C:\Temp\// not comment {name /* real comment */}";`, + `var template = @$"Keep /* marker */ and ""// marker""`, + `with {Format(value // real comment`, + `)}"; // trailing comment`, + } + + expected := []string{ + `var path = $@"C:\Temp\// not comment {name }";`, + `var template = @$"Keep /* marker */ and ""// marker""`, + `with {Format(value `, + `)}"; `, + } + + assertFiltered("Api.cs", RetainNone, lines, expected) + }) }) From 090cb303dbdb99fc2962efc7133b7400e48618f1 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 15:35:51 +0200 Subject: [PATCH 2/5] Improve interpolation handling. --- embedding/commentfilter/csharp_filter.go | 34 +++++++++++++++++++ embedding/commentfilter/csharp_filter_test.go | 2 ++ 2 files changed, 36 insertions(+) diff --git a/embedding/commentfilter/csharp_filter.go b/embedding/commentfilter/csharp_filter.go index 5169492..ef8d7a5 100644 --- a/embedding/commentfilter/csharp_filter.go +++ b/embedding/commentfilter/csharp_filter.go @@ -52,6 +52,9 @@ type csharpState struct { // interpolationDepth is the active brace depth inside an interpolation expression. interpolationDepth int + + // interpolationFormat reports whether scanning is inside interpolation format text. + interpolationFormat bool } // csharpLineFilter filters one C# source line. @@ -171,6 +174,13 @@ func (f *csharpLineFilter) consumeStringInterpolation() bool { if f.consumeActiveBlock() { continue } + if f.consumeInterpolationFormat() { + if f.state.interpolationDepth == 0 { + return true + } + + continue + } if f.consumeInterpolationString() { continue } @@ -199,6 +209,29 @@ func (f *csharpLineFilter) consumeStringInterpolation() bool { return true } +// consumeInterpolationFormat copies C# format text after a top-level interpolation colon. +func (f *csharpLineFilter) consumeInterpolationFormat() bool { + if !f.state.interpolationFormat { + if f.state.interpolationDepth != 1 || f.line[f.position] != ':' { + return false + } + f.state.interpolationFormat = true + f.consumeCodeByte() + } + for f.position < len(f.line) { + if f.line[f.position] == '}' { + f.consumeCodeByte() + f.state.interpolationFormat = false + f.state.interpolationDepth = 0 + + return true + } + f.consumeCodeByte() + } + + return true +} + // consumeInterpolationString copies a string literal inside interpolation code. func (f *csharpLineFilter) consumeInterpolationString() bool { token, verbatim, found := csharpStringTokenAt(f.line, f.position) @@ -321,6 +354,7 @@ func (f *csharpLineFilter) closeString() { f.state.stringVerbatim = false f.state.stringInterpolated = false f.state.interpolationDepth = 0 + f.state.interpolationFormat = false } // consumeComment consumes a C# comment when one starts at the scanner position. diff --git a/embedding/commentfilter/csharp_filter_test.go b/embedding/commentfilter/csharp_filter_test.go index 6b6d3a5..bcc5ec7 100644 --- a/embedding/commentfilter/csharp_filter_test.go +++ b/embedding/commentfilter/csharp_filter_test.go @@ -77,12 +77,14 @@ var _ = Describe("C#", func() { `var message = $"Keep // and /* markers */ {Format(value /* real comment */)}";`, `var escaped = $"Keep {{ // text }} and {value}"; // inline comment`, `var nested = $"Value {Format("/* not comment */")} // still text"; // inline comment`, + `var url = $"{scheme://example.com/*path*/}"; // inline comment`, } expected := []string{ `var message = $"Keep // and /* markers */ {Format(value )}";`, `var escaped = $"Keep {{ // text }} and {value}"; `, `var nested = $"Value {Format("/* not comment */")} // still text"; `, + `var url = $"{scheme://example.com/*path*/}"; `, } assertFiltered("Api.cs", RetainNone, lines, expected) From d5de56b7471174e0fa27b919329b634e757d7276 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 15:39:10 +0200 Subject: [PATCH 3/5] Improve nested interpolation handling. --- embedding/commentfilter/config.go | 12 -- embedding/commentfilter/csharp_filter.go | 128 +++++++++++------- embedding/commentfilter/csharp_filter_test.go | 14 ++ 3 files changed, 96 insertions(+), 58 deletions(-) diff --git a/embedding/commentfilter/config.go b/embedding/commentfilter/config.go index 03f1891..0b023d6 100644 --- a/embedding/commentfilter/config.go +++ b/embedding/commentfilter/config.go @@ -107,18 +107,6 @@ var javaSyntax = CommentMarker{ QuoteChars: "\"'", } -var csharpSyntax = CommentMarker{ - Inline: []string{"//"}, - Block: []BlockMarker{ - {Start: cStyleBlockCommentStart, End: cStyleBlockCommentEnd}, - }, - Documentation: DocumentationMarker{ - Inline: []string{"///"}, - Block: []BlockMarker{{Start: cStyleDocCommentStart, End: cStyleBlockCommentEnd}}, - }, - QuoteChars: jsQuoteChars, -} - var cStyleSyntax = CommentMarker{ Inline: []string{"//"}, Block: []BlockMarker{ diff --git a/embedding/commentfilter/csharp_filter.go b/embedding/commentfilter/csharp_filter.go index ef8d7a5..7758a9b 100644 --- a/embedding/commentfilter/csharp_filter.go +++ b/embedding/commentfilter/csharp_filter.go @@ -122,6 +122,9 @@ func (f *csharpLineFilter) filterLine() (string, bool) { if f.consumeStringText() { continue } + if f.consumeCharacterLiteral() { + continue + } if f.consumeStringStart() { continue } @@ -171,44 +174,59 @@ func (f *csharpLineFilter) consumeStringInterpolation() bool { return false } for f.position < len(f.line) { - if f.consumeActiveBlock() { - continue - } - if f.consumeInterpolationFormat() { + if f.consumeInterpolationSegment() { if f.state.interpolationDepth == 0 { return true } continue } - if f.consumeInterpolationString() { - continue - } - if comment := f.consumeComment(); comment.consumed { - if comment.stopLine { - return true - } - - continue - } - switch f.line[f.position] { - case '{': - f.state.interpolationDepth++ - f.consumeCodeByte() - case '}': - f.state.interpolationDepth-- - f.consumeCodeByte() - if f.state.interpolationDepth == 0 { - return true - } - default: - f.consumeCodeByte() + if f.consumeInterpolationCodeByte() { + return true } } return true } +// consumeInterpolationSegment consumes multi-byte interpolation content. +func (f *csharpLineFilter) consumeInterpolationSegment() bool { + if f.consumeActiveBlock() { + return true + } + if f.consumeInterpolationFormat() { + return true + } + if f.consumeInterpolationString() { + return true + } + if comment := f.consumeComment(); comment.consumed { + return true + } + + return false +} + +// consumeInterpolationCodeByte copies expression code and reports whether interpolation closed. +func (f *csharpLineFilter) consumeInterpolationCodeByte() bool { + switch f.line[f.position] { + case '{': + f.state.interpolationDepth++ + f.consumeCodeByte() + + return false + case '}': + f.state.interpolationDepth-- + f.consumeCodeByte() + + return f.state.interpolationDepth == 0 + default: + f.consumeCodeByte() + + return false + } +} + // consumeInterpolationFormat copies C# format text after a top-level interpolation colon. func (f *csharpLineFilter) consumeInterpolationFormat() bool { if !f.state.interpolationFormat { @@ -265,29 +283,47 @@ func (f *csharpLineFilter) consumeStringText() bool { return false } for f.position < len(f.line) { - switch { - case f.state.stringVerbatim && strings.HasPrefix(f.line[f.position:], csharpEscapedQuote): - f.result.WriteString(csharpEscapedQuote) - f.position += len(csharpEscapedQuote) - case !f.state.stringVerbatim && f.line[f.position] == '\\': - f.writeEscapedByte() - case f.line[f.position] == '"': - f.consumeCodeByte() - f.closeString() - + if f.consumeStringTextSegment() { return true - case f.startsEscapedInterpolationBrace(): - f.result.WriteString(f.line[f.position : f.position+2]) - f.position += 2 - case f.state.stringInterpolated && f.line[f.position] == '{': - f.consumeCodeByte() - f.state.interpolationDepth = 1 - - return true - default: - f.consumeCodeByte() } + f.consumeCodeByte() + } + + return true +} + +// consumeStringTextSegment consumes special syntax inside active string text. +func (f *csharpLineFilter) consumeStringTextSegment() bool { + switch { + case f.state.stringVerbatim && strings.HasPrefix(f.line[f.position:], csharpEscapedQuote): + f.result.WriteString(csharpEscapedQuote) + f.position += len(csharpEscapedQuote) + case !f.state.stringVerbatim && f.line[f.position] == '\\': + f.writeEscapedByte() + case f.line[f.position] == '"': + f.consumeCodeByte() + f.closeString() + case f.startsEscapedInterpolationBrace(): + f.result.WriteString(f.line[f.position : f.position+2]) + f.position += 2 + case f.state.stringInterpolated && f.line[f.position] == '{': + f.consumeCodeByte() + f.state.interpolationDepth = 1 + default: + return false + } + + return true +} + +// consumeCharacterLiteral copies a C# character literal. +func (f *csharpLineFilter) consumeCharacterLiteral() bool { + quoteEnd := quotedSegmentEnd(f.line, f.position, "'") + if quoteEnd <= f.position { + return false } + f.result.WriteString(f.line[f.position:quoteEnd]) + f.position = quoteEnd return true } diff --git a/embedding/commentfilter/csharp_filter_test.go b/embedding/commentfilter/csharp_filter_test.go index bcc5ec7..e77edc8 100644 --- a/embedding/commentfilter/csharp_filter_test.go +++ b/embedding/commentfilter/csharp_filter_test.go @@ -55,6 +55,20 @@ var _ = Describe("C#", func() { assertFiltered("Api.cs", RetainInline, lines, expected) }) + It("should strip comments after character literals", func() { + lines := []string{ + `var quote = '"'; // inline comment`, + `var slash = '/'; // inline comment`, + } + + expected := []string{ + `var quote = '"'; `, + `var slash = '/'; `, + } + + assertFiltered("Api.cs", RetainNone, lines, expected) + }) + It("should strip comments without treating verbatim string text as comments", func() { lines := []string{ "// header comment", From 5edd4975b1751861d8580b4b90650e3631382010 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 15:43:39 +0200 Subject: [PATCH 4/5] Improve doc. --- embedding/commentfilter/csharp_filter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/embedding/commentfilter/csharp_filter.go b/embedding/commentfilter/csharp_filter.go index 7758a9b..8242baf 100644 --- a/embedding/commentfilter/csharp_filter.go +++ b/embedding/commentfilter/csharp_filter.go @@ -250,7 +250,8 @@ func (f *csharpLineFilter) consumeInterpolationFormat() bool { return true } -// consumeInterpolationString copies a string literal inside interpolation code. +// consumeInterpolationString copies a line-local string literal inside interpolation code. +// Nested multi-line verbatim strings intentionally resume as interpolation code on the next line. func (f *csharpLineFilter) consumeInterpolationString() bool { token, verbatim, found := csharpStringTokenAt(f.line, f.position) if !found { From 6b129df704db0f37f4d7e6cd39d26a0892709794 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 15:55:30 +0200 Subject: [PATCH 5/5] Improve doc. --- embedding/commentfilter/csharp_filter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/embedding/commentfilter/csharp_filter.go b/embedding/commentfilter/csharp_filter.go index 8242baf..0ec705c 100644 --- a/embedding/commentfilter/csharp_filter.go +++ b/embedding/commentfilter/csharp_filter.go @@ -228,6 +228,8 @@ func (f *csharpLineFilter) consumeInterpolationCodeByte() bool { } // consumeInterpolationFormat copies C# format text after a top-level interpolation colon. +// This lightweight scanner does not track parentheses inside interpolation expressions, +// so a ternary colon in parentheses is treated as format text. func (f *csharpLineFilter) consumeInterpolationFormat() bool { if !f.state.interpolationFormat { if f.state.interpolationDepth != 1 || f.line[f.position] != ':' {