From 10e86929ab5064af928421f9d7ffec8660df2310 Mon Sep 17 00:00:00 2001
From: Joey Robichaud <jorobich@microsoft.com>
Date: Tue, 19 Dec 2023 21:18:12 -0800
Subject: [PATCH] Classify generic constraints

Fixes #301
---
 grammars/csharp.tmLanguage      | 18 ++++++++++
 grammars/csharp.tmLanguage.cson | 12 +++++++
 src/csharp.tmLanguage.yml       |  6 ++++
 test/class.tests.ts             | 59 +++++++++++++++++++++++++++++++++
 test/utils/tokenize.ts          |  6 ++++
 5 files changed, 101 insertions(+)

diff --git a/grammars/csharp.tmLanguage b/grammars/csharp.tmLanguage
index 2195f9f..90bc891 100644
--- a/grammars/csharp.tmLanguage
+++ b/grammars/csharp.tmLanguage
@@ -1566,6 +1566,24 @@
             <key>match</key>
             <string>\bstruct\b</string>
           </dict>
+          <dict>
+            <key>name</key>
+            <string>keyword.other.constraint.default.cs</string>
+            <key>match</key>
+            <string>\bdefault\b</string>
+          </dict>
+          <dict>
+            <key>name</key>
+            <string>keyword.other.constraint.notnull.cs</string>
+            <key>match</key>
+            <string>\bnotnull\b</string>
+          </dict>
+          <dict>
+            <key>name</key>
+            <string>keyword.other.constraint.unmanaged.cs</string>
+            <key>match</key>
+            <string>\bunmanaged\b</string>
+          </dict>
           <dict>
             <key>match</key>
             <string>(new)\s*(\()\s*(\))</string>
diff --git a/grammars/csharp.tmLanguage.cson b/grammars/csharp.tmLanguage.cson
index d8534ce..aa8cfdc 100644
--- a/grammars/csharp.tmLanguage.cson
+++ b/grammars/csharp.tmLanguage.cson
@@ -979,6 +979,18 @@ repository:
         name: "storage.type.struct.cs"
         match: "\\bstruct\\b"
       }
+      {
+        name: "keyword.other.constraint.default.cs"
+        match: "\\bdefault\\b"
+      }
+      {
+        name: "keyword.other.constraint.notnull.cs"
+        match: "\\bnotnull\\b"
+      }
+      {
+        name: "keyword.other.constraint.unmanaged.cs"
+        match: "\\bunmanaged\\b"
+      }
       {
         match: "(new)\\s*(\\()\\s*(\\))"
         captures:
diff --git a/src/csharp.tmLanguage.yml b/src/csharp.tmLanguage.yml
index d331b57..eb3ff8b 100644
--- a/src/csharp.tmLanguage.yml
+++ b/src/csharp.tmLanguage.yml
@@ -496,6 +496,12 @@ repository:
       match: \bclass\b
     - name: storage.type.struct.cs
       match: \bstruct\b
+    - name: keyword.other.constraint.default.cs
+      match: \bdefault\b
+    - name: keyword.other.constraint.notnull.cs
+      match: \bnotnull\b
+    - name: keyword.other.constraint.unmanaged.cs
+      match: \bunmanaged\b
     - match: (new)\s*(\()\s*(\))
       captures:
         '1': { name: keyword.operator.expression.new.cs }
diff --git a/test/class.tests.ts b/test/class.tests.ts
index a575cda..4849620 100644
--- a/test/class.tests.ts
+++ b/test/class.tests.ts
@@ -330,6 +330,65 @@ unsafe class C
                     Token.Punctuation.CloseBrace]);
             });
 
+            it(`class with various generic type constraints (#301)`, async () => {
+                const input = Input.InNamespace(`
+class NotNullContainer<T> where T : notnull
+{
+    private unsafe static void DisplaySize<T>()
+        where T : unmanaged { }
+    public override void M<T>(T? item)
+        where T : default { }
+}`, namespaceStyle);
+                const tokens = await tokenize(input);
+
+                tokens.should.deep.equal([
+                    Token.Keyword.Definition.Class,
+                    Token.Identifier.ClassName("NotNullContainer"),
+                    Token.Punctuation.TypeParameter.Begin,
+                    Token.Identifier.TypeParameterName("T"),
+                    Token.Punctuation.TypeParameter.End,
+                    Token.Keyword.Modifier.Where,
+                    Token.Identifier.TypeParameterName("T"),
+                    Token.Punctuation.Colon,
+                    Token.Keyword.Constraint.NotNull,
+                    Token.Punctuation.OpenBrace,
+                    Token.Keyword.Modifier.Private,
+                    Token.Keyword.Modifier.Unsafe,
+                    Token.Keyword.Modifier.Static,
+                    Token.PrimitiveType.Void,
+                    Token.Identifier.MethodName("DisplaySize"),
+                    Token.Punctuation.TypeParameter.Begin,
+                    Token.Identifier.TypeParameterName("T"),
+                    Token.Punctuation.TypeParameter.End,
+                    Token.Punctuation.OpenParen,
+                    Token.Punctuation.CloseParen,
+                    Token.Keyword.Modifier.Where,
+                    Token.Identifier.TypeParameterName("T"),
+                    Token.Punctuation.Colon,
+                    Token.Keyword.Constraint.Unmanaged,
+                    Token.Punctuation.OpenBrace,
+                    Token.Punctuation.CloseBrace,
+                    Token.Keyword.Modifier.Public,
+                    Token.Keyword.Modifier.Override,
+                    Token.PrimitiveType.Void,
+                    Token.Identifier.MethodName("M"),
+                    Token.Punctuation.TypeParameter.Begin,
+                    Token.Identifier.TypeParameterName("T"),
+                    Token.Punctuation.TypeParameter.End,
+                    Token.Punctuation.OpenParen,
+                    Token.Type("T"),
+                    Token.Punctuation.QuestionMark,
+                    Token.Identifier.ParameterName("item"),
+                    Token.Punctuation.CloseParen,
+                    Token.Keyword.Modifier.Where,
+                    Token.Identifier.TypeParameterName("T"),
+                    Token.Punctuation.Colon,
+                    Token.Keyword.Constraint.Default,
+                    Token.Punctuation.OpenBrace,
+                    Token.Punctuation.CloseBrace,
+                    Token.Punctuation.CloseBrace]);
+            });
+
             it(`primary constructor (${styleName} Namespace)`, async () => {
 
                 const input = Input.InNamespace(`
diff --git a/test/utils/tokenize.ts b/test/utils/tokenize.ts
index de33a85..71f950d 100644
--- a/test/utils/tokenize.ts
+++ b/test/utils/tokenize.ts
@@ -269,6 +269,12 @@ export namespace Token {
             export const When = createToken('when', 'keyword.control.conditional.when.cs');
         }
 
+        export namespace Constraint {
+            export const Default = createToken('default', 'keyword.other.constraint.default.cs');
+            export const NotNull = createToken('notnull', 'keyword.other.constraint.notnull.cs');
+            export const Unmanaged = createToken('unmanaged', 'keyword.other.constraint.unmanaged.cs');
+        }
+
         export namespace Context {
             export const Checked = createToken('checked', 'keyword.control.context.checked.cs');
             export const Fixed = createToken('fixed', 'keyword.control.context.fixed.cs');