From a3da43ec064798d66822b3ce9c6cbb7fc2d33213 Mon Sep 17 00:00:00 2001
From: Eljees <3.14hell@gmail.com>
Date: Fri, 25 Sep 2026 10:57:15 +0300
Subject: [PATCH] feat(code): parse C# comments with the C# grammar
`.cs` and `.csx` files were parsed with the C grammar, which does not know C#'s verbatim,
raw or interpolated strings. A comment after `@"C:\temp\"` was swallowed by the string, and
a `//` inside a `"""` raw string literal was linted as a comment.
Add a C# extractor on the grammar go-tree-sitter already vendors, and map `.cs`, `.csx`
and the csharp and c# notebook kernels to it. `// ReSharper disable|restore` and
`// NOSONAR` are treated as directives.
Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>
Assisted-by: Claude Code
---
internal/core/format.go | 2 +-
internal/core/notebook.go | 4 ++--
internal/lint/code/cs.go | 27 +++++++++++++++++++++++++++
internal/lint/code/doc.go | 1 +
internal/lint/code/lang.go | 2 ++
testdata/comments/in/12.cs | 16 ++++++++++++++++
testdata/comments/out/12.json | 30 ++++++++++++++++++++++++++++++
testdata/e2e/lint.yaml | 11 +++++++++++
testdata/fixtures/formats/test.cs | 19 +++++++++++++++++++
9 files changed, 109 insertions(+), 3 deletions(-)
create mode 100644 internal/lint/code/cs.go
create mode 100644 testdata/comments/in/12.cs
create mode 100644 testdata/comments/out/12.json
create mode 100644 testdata/fixtures/formats/test.cs
diff --git a/internal/core/format.go b/internal/core/format.go
index 0359fc48..ed0b14d3 100755
--- a/internal/core/format.go
+++ b/internal/core/format.go
@@ -59,7 +59,7 @@ var FormatByExtension = map[string][]string{
`\.(?:clj|cljs|cljc|cljd)$`: {".clj", "code"},
`\.(?:cpp|cc|c|cp|cxx|c\+\+|h|hpp|h\+\+)$`: {".cpp", "code"},
`\.(?:css)$`: {".css", "code"},
- `\.(?:cs|csx)$`: {".c", "code"},
+ `\.(?:cs|csx)$`: {".cs", "code"},
`\.(?:dita)$`: {".dita", "markup"},
`\.(?:ex|exs)$`: {".ex", "code"},
`\.(?:go)$`: {".go", "code"},
diff --git a/internal/core/notebook.go b/internal/core/notebook.go
index 593713f9..851fcf07 100644
--- a/internal/core/notebook.go
+++ b/internal/core/notebook.go
@@ -47,8 +47,8 @@ var kernelExts = map[string]string{
"elixir": ".ex",
"clojure": ".clj",
"perl": ".r",
- "csharp": ".c",
- "c#": ".c",
+ "csharp": ".cs",
+ "c#": ".cs",
"scala": ".c",
"powershell": ".ps1",
}
diff --git a/internal/lint/code/cs.go b/internal/lint/code/cs.go
new file mode 100644
index 00000000..3c13f8ce
--- /dev/null
+++ b/internal/lint/code/cs.go
@@ -0,0 +1,27 @@
+package code
+
+import (
+ "regexp"
+
+ "github.com/smacker/go-tree-sitter/csharp"
+ "github.com/vale-cli/vale/v3/internal/core"
+)
+
+// CSharp extracts `//` line comments, `///` XML documentation comments and
+// `/* */` block comments, all of which the grammar parses as `comment`.
+func CSharp() *Language {
+ return &Language{
+ Delims: regexp.MustCompile(`///?|/\*|\*/`),
+ Prefix: cStylePrefix,
+ Parser: csharp.GetLanguage(),
+ Queries: []core.Scope{
+ {Name: "", Expr: "(comment) @comment", Type: ""},
+ },
+ // `///` is listed so that a documentation comment's padding matches
+ // what Delims takes off; cStyle alone stops at `//`.
+ Padding: func(s string) int {
+ return computePadding(s, []string{"//", "///", "/*"})
+ },
+ Directive: csharpDirective,
+ }
+}
diff --git a/internal/lint/code/doc.go b/internal/lint/code/doc.go
index 71d6997a..00dfe92b 100644
--- a/internal/lint/code/doc.go
+++ b/internal/lint/code/doc.go
@@ -225,5 +225,6 @@ var (
javaDirective = regexp.MustCompile(`^//[ \t]*(?:NOPMD|NOSONAR|CHECKSTYLE|noinspection|@formatter:|\$NON-NLS)`)
kotlinDirective = regexp.MustCompile(`^//[ \t]*(?:ktlint|NOSONAR|noinspection|@formatter:)`)
cDirective = regexp.MustCompile(`^//[ \t]*(?:NOLINT|clang-format|cppcheck-suppress)|^/\*[ \t]*(?:NOLINT|clang-format)`)
+ csharpDirective = regexp.MustCompile(`^//[ \t]*(?:ReSharper (?:disable|restore)\b|NOSONAR)`)
rubyDirective = regexp.MustCompile(`^#[ \t]*(?:rubocop:|frozen_string_literal:|encoding:|typed:|sorbet:|-\*-)`)
)
diff --git a/internal/lint/code/lang.go b/internal/lint/code/lang.go
index a5555ee9..de595dc6 100644
--- a/internal/lint/code/lang.go
+++ b/internal/lint/code/lang.go
@@ -88,6 +88,8 @@ func newLanguage(ext string) (*Language, error) {
return Cpp(), nil
case ".c":
return C(), nil
+ case ".cs":
+ return CSharp(), nil
case ".js", ".jsx":
return JavaScript(), nil
case ".hs":
diff --git a/testdata/comments/in/12.cs b/testdata/comments/in/12.cs
new file mode 100644
index 00000000..ff627f6d
--- /dev/null
+++ b/testdata/comments/in/12.cs
@@ -0,0 +1,16 @@
+// A line comment
+// continued on the next line.
+namespace Demo
+{
+ /// An XML documentation comment.
+ class Paths
+ {
+ static string Dir = @"C:\temp\"; /* A block comment after a verbatim string. */
+ static string Raw = """
+ // Not a comment: inside a raw string literal.
+ """;
+ static string Url = $"{Dir}// not a comment";
+ // ReSharper disable once UnusedMember.Local
+ static int x = 1; // A trailing comment.
+ }
+}
diff --git a/testdata/comments/out/12.json b/testdata/comments/out/12.json
new file mode 100644
index 00000000..4679cf27
--- /dev/null
+++ b/testdata/comments/out/12.json
@@ -0,0 +1,30 @@
+[
+ {
+ "Text": "A line comment\ncontinued on the next line.\n",
+ "Source": "// A line comment\n// continued on the next line.\n",
+ "Line": 1,
+ "Offset": 0,
+ "Scope": "text.comment.line"
+ },
+ {
+ "Text": "\u003csummary\u003eAn XML documentation comment.\u003c/summary\u003e",
+ "Source": "/// \u003csummary\u003eAn XML documentation comment.\u003c/summary\u003e",
+ "Line": 5,
+ "Offset": 4,
+ "Scope": "text.comment.line"
+ },
+ {
+ "Text": "A block comment after a verbatim string. ",
+ "Source": "/* A block comment after a verbatim string. */",
+ "Line": 8,
+ "Offset": 41,
+ "Scope": "text.comment.line"
+ },
+ {
+ "Text": "A trailing comment.",
+ "Source": "// A trailing comment.",
+ "Line": 14,
+ "Offset": 26,
+ "Scope": "text.comment.line"
+ }
+]
\ No newline at end of file
diff --git a/testdata/e2e/lint.yaml b/testdata/e2e/lint.yaml
index f70769f1..f7a3c596 100644
--- a/testdata/e2e/lint.yaml
+++ b/testdata/e2e/lint.yaml
@@ -53,6 +53,17 @@ cases:
test.kt:17:4:vale.Annotations:'XXX' left in text
test.kt:21:5:vale.Annotations:'NOTE' left in text
+ - name: csharp
+ args: test.cs
+ exit: 0
+ want: |
+ test.cs:1:4:vale.Annotations:'NOTE' left in text
+ test.cs:2:4:vale.Annotations:'XXX' left in text
+ test.cs:5:18:vale.Annotations:'TODO' left in text
+ test.cs:8:12:vale.Annotations:'NOTE' left in text
+ test.cs:15:37:vale.Annotations:'FIXME' left in text
+ test.cs:16:33:vale.Annotations:'XXX' left in text
+
- name: proto
args: test.proto
exit: 0
diff --git a/testdata/fixtures/formats/test.cs b/testdata/fixtures/formats/test.cs
new file mode 100644
index 00000000..b8446072
--- /dev/null
+++ b/testdata/fixtures/formats/test.cs
@@ -0,0 +1,19 @@
+// NOTE: a line comment
+// XXX: continued on the next line
+namespace Test
+{
+ /// TODO: an XML documentation comment.
+ public class Test
+ {
+ /* NOTE: a block comment */
+ private const string Raw = """
+ // TODO: a raw string, not a comment
+ """;
+
+ public string Greet(string name)
+ {
+ var s = @"C:\temp\"; // FIXME: a comment after a verbatim string
+ return s + name; // XXX: a trailing comment
+ }
+ }
+}