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 + } + } +}