userdiff: better method/property matching for C#

- Support multi-line methods by not requiring closing parenthesis. - Support multiple generics (comma was missing before). - Add missing `foreach`, `lock` and `fixed` keywords to skip over. - Remove `instanceof` keyword, which isn't C#. - Also detect non-method keywords not positioned at the start of a line. - Added tests; none existed before. The overall strategy is to focus more on what isn't expected for method/property definitions, instead of what is, but is fully optional. Signed-off-by: Steven Jeuris <steven.jeuris@gmail.com> Acked-by: Johannes Sixt <j6t@kdbg.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Steven Jeuris committed Apr 3, 2024 at 21:42 UTC ec0e3075d245afa3f3a848bd7bdc6376dea85fe0
21 files changed +352 -6
t/t4018/csharp-exclude-assignments new
+20
@@ -0,0 +1,20 @@
1 +class Example
2 +{
3 + string Method(int RIGHT)
4 + {
5 + var constantAssignment = "test";
6 + var methodAssignment = MethodCall();
7 + var multiLineMethodAssignment = MethodCall(
8 + );
9 + var multiLine = "first"
10 + + MethodCall()
11 + +
12 + ( MethodCall()
13 + )
14 + + MethodCall();
15 +
16 + return "ChangeMe";
17 + }
18 +
19 + string MethodCall(int a = 0, int b = 0) => "test";
20 +}
t/t4018/csharp-exclude-control-statements new
+34
@@ -0,0 +1,34 @@
1 +class Example
2 +{
3 + string Method(int RIGHT)
4 + {
5 + if (false)
6 + {
7 + return "out";
8 + }
9 + else { }
10 + if (true) MethodCall(
11 + );
12 + else MethodCall(
13 + );
14 + switch ("test")
15 + {
16 + case "one":
17 + return MethodCall(
18 + );
19 + case "two":
20 + break;
21 + }
22 + (int, int) tuple = (1, 4);
23 + switch (tuple)
24 + {
25 + case (1, 4):
26 + MethodCall();
27 + break;
28 + }
29 +
30 + return "ChangeMe";
31 + }
32 +
33 + string MethodCall(int a = 0, int b = 0) => "test";
34 +}
t/t4018/csharp-exclude-exceptions new
+29
@@ -0,0 +1,29 @@
1 +using System;
2 +
3 +class Example
4 +{
5 + string Method(int RIGHT)
6 + {
7 + try
8 + {
9 + throw new Exception("fail");
10 + }
11 + catch (Exception)
12 + {
13 + }
14 + finally
15 + {
16 + }
17 + try { } catch (Exception) {}
18 + try
19 + {
20 + throw GetException(
21 + );
22 + }
23 + catch (Exception) { }
24 +
25 + return "ChangeMe";
26 + }
27 +
28 + Exception GetException() => new Exception("fail");
29 +}
t/t4018/csharp-exclude-generic-method-calls new
+12
@@ -0,0 +1,12 @@
1 +class Example
2 +{
3 + string Method(int RIGHT)
4 + {
5 + GenericMethodCall<int, int>(
6 + );
7 +
8 + return "ChangeMe";
9 + }
10 +
11 + string GenericMethodCall<T, T2>() => "test";
12 +}
t/t4018/csharp-exclude-init-dispose new
+22
@@ -0,0 +1,22 @@
1 +using System;
2 +
3 +class Example : IDisposable
4 +{
5 + string Method(int RIGHT)
6 + {
7 + new Example();
8 + new Example(
9 + );
10 + new Example { };
11 + using (this)
12 + {
13 + }
14 + var def =
15 + this is default(
16 + Example);
17 +
18 + return "ChangeMe";
19 + }
20 +
21 + public void Dispose() {}
22 +}
t/t4018/csharp-exclude-iterations new
+26
@@ -0,0 +1,26 @@
1 +using System.Linq;
2 +
3 +class Example
4 +{
5 + string Method(int RIGHT)
6 + {
7 + do { } while (true);
8 + do MethodCall(
9 + ); while (true);
10 + while (true);
11 + while (true) {
12 + break;
13 + }
14 + for (int i = 0; i < 10; ++i)
15 + {
16 + }
17 + foreach (int i in Enumerable.Range(0, 10))
18 + {
19 + }
20 + int[] numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
21 +
22 + return "ChangeMe";
23 + }
24 +
25 + string MethodCall(int a = 0, int b = 0) => "test";
26 +}
t/t4018/csharp-exclude-method-calls new
+20
@@ -0,0 +1,20 @@
1 +class Example
2 +{
3 + string Method(int RIGHT)
4 + {
5 + MethodCall();
6 + MethodCall(1, 2);
7 + MethodCall(
8 + 1, 2);
9 + MethodCall(
10 + 1, 2,
11 + 3);
12 + MethodCall(
13 + 1, MethodCall(),
14 + 2);
15 +
16 + return "ChangeMe";
17 + }
18 +
19 + int MethodCall(int a = 0, int b = 0, int c = 0) => 42;
20 +}
t/t4018/csharp-exclude-other new
+18
@@ -0,0 +1,18 @@
1 +class Example
2 +{
3 + string Method(int RIGHT)
4 + {
5 + lock (this)
6 + {
7 + }
8 + unsafe
9 + {
10 + byte[] bytes = [1, 2, 3];
11 + fixed (byte* pointerToFirst = bytes)
12 + {
13 + }
14 + }
15 +
16 + return "ChangeMe";
17 + }
18 +}
t/t4018/csharp-method new
+10
@@ -0,0 +1,10 @@
1 +class Example
2 +{
3 + string Method(int RIGHT)
4 + {
5 + // Filler
6 + // Filler
7 +
8 + return "ChangeMe";
9 + }
10 +}
t/t4018/csharp-method-array new
+10
@@ -0,0 +1,10 @@
1 +class Example
2 +{
3 + string[] Method(int RIGHT)
4 + {
5 + // Filler
6 + // Filler
7 +
8 + return ["ChangeMe"];
9 + }
10 +}
t/t4018/csharp-method-explicit new
+12
@@ -0,0 +1,12 @@
1 +using System;
2 +
3 +class Example : IDisposable
4 +{
5 + void IDisposable.Dispose() // RIGHT
6 + {
7 + // Filler
8 + // Filler
9 +
10 + // ChangeMe
11 + }
12 +}
t/t4018/csharp-method-generics new
+11
@@ -0,0 +1,11 @@
1 +class Example<T1, T2>
2 +{
3 + Example<int, string> Method<TA, TB>(TA RIGHT, TB b)
4 + {
5 + // Filler
6 + // Filler
7 +
8 + // ChangeMe
9 + return null;
10 + }
11 +}
t/t4018/csharp-method-generics-alternate-spaces new
+11
@@ -0,0 +1,11 @@
1 +class Example<T1, T2>
2 +{
3 + Example<int,string> Method<TA ,TB>(TA RIGHT, TB b)
4 + {
5 + // Filler
6 + // Filler
7 +
8 + // ChangeMe
9 + return null;
10 + }
11 +}
t/t4018/csharp-method-modifiers new
+13
@@ -0,0 +1,13 @@
1 +using System.Threading.Tasks;
2 +
3 +class Example
4 +{
5 + static internal async Task Method(int RIGHT)
6 + {
7 + // Filler
8 + // Filler
9 +
10 + // ChangeMe
11 + await Task.Delay(1);
12 + }
13 +}
t/t4018/csharp-method-multiline new
+10
@@ -0,0 +1,10 @@
1 +class Example
2 +{
3 + string Method_RIGHT(
4 + int a,
5 + int b,
6 + int c)
7 + {
8 + return "ChangeMe";
9 + }
10 +}
t/t4018/csharp-method-params new
+10
@@ -0,0 +1,10 @@
1 +class Example
2 +{
3 + string Method(int RIGHT, int b, int c = 42)
4 + {
5 + // Filler
6 + // Filler
7 +
8 + return "ChangeMe";
9 + }
10 +}
t/t4018/csharp-method-special-chars new
+11
@@ -0,0 +1,11 @@
1 +class @Some_Type
2 +{
3 + @Some_Type @Method_With_Underscore(int RIGHT)
4 + {
5 + // Filler
6 + // Filler
7 +
8 + // ChangeMe
9 + return new @Some_Type();
10 + }
11 +}
t/t4018/csharp-method-with-spacing new
+10
@@ -0,0 +1,10 @@
1 +class Example
2 +{
3 + string Method ( int RIGHT )
4 + {
5 + // Filler
6 + // Filler
7 +
8 + return "ChangeMe";
9 + }
10 +}
t/t4018/csharp-property new
+11
@@ -0,0 +1,11 @@
1 +class Example
2 +{
3 + public bool RIGHT
4 + {
5 + get { return true; }
6 + set
7 + {
8 + // ChangeMe
9 + }
10 + }
11 +}
t/t4018/csharp-property-braces-same-line new
+10
@@ -0,0 +1,10 @@
1 +class Example
2 +{
3 + public bool RIGHT {
4 + get { return true; }
5 + set
6 + {
7 + // ChangeMe
8 + }
9 + }
10 +}
userdiff.c
+42 -6
@@ -90,12 +90,48 @@ PATTERNS("cpp",
90 "|\\.[0-9][0-9]*([Ee][-+]?[0-9]+)?[fFlL]?"
91 "|[-+*/<>%&^|=!]=|--|\\+\\+|<<=?|>>=?|&&|\\|\\||::|->\\*?|\\.\\*|<=>"),
92 PATTERNS("csharp",
93 - /* Keywords */
94 - "!^[ \t]*(do|while|for|if|else|instanceof|new|return|switch|case|throw|catch|using)\n"
95 - /* Methods and constructors */
96 - "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe|async)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[<>@._[:alnum:]]+[ \t]*\\(.*\\))[ \t]*$\n"
97 - /* Properties */
98 - "^[ \t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe)[ \t]+)*[][<>@.~_[:alnum:]]+[ \t]+[@._[:alnum:]]+)[ \t]*$\n"
93 + /*
94 + * Jump over reserved keywords which are illegal method names, but which
95 + * can be followed by parentheses without special characters in between,
96 + * making them look like methods.
97 + */
98 + "!(^|[ \t]+)" /* Start of line or whitespace. */
99 + "(do|while|for|foreach|if|else|new|default|return|switch|case|throw"
100 + "|catch|using|lock|fixed)"
101 + "([ \t(]+|$)\n" /* Whitespace, "(", or end of line. */
102 + /*
103 + * Methods/constructors:
104 + * The strategy is to identify a minimum of two groups (any combination
105 + * of keywords/type/name) before the opening parenthesis, and without
106 + * final unexpected characters, normally only used in ordinary statements.
107 + */
108 + "^[ \t]*" /* Remove leading whitespace. */
109 + "(" /* Start chunk header capture. */
110 + "(" /* First group. */
111 + "[][[:alnum:]@_.]" /* Name. */
112 + "(<[][[:alnum:]@_, \t<>]+>)?" /* Optional generic parameters. */
113 + ")+"
114 + "([ \t]+" /* Subsequent groups, prepended with space. */
115 + "([][[:alnum:]@_.](<[][[:alnum:]@_, \t<>]+>)?)+"
116 + ")+"
117 + "[ \t]*" /* Optional space before parameters start. */
118 + "\\(" /* Start of method parameters. */
119 + "[^;]*" /* Allow complex parameters, but exclude statements (;). */
120 + ")$\n" /* Close chunk header capture. */
121 + /*
122 + * Properties:
123 + * As with methods, expect a minimum of two groups. But, more trivial than
124 + * methods, the vast majority of properties long enough to be worth
125 + * showing a chunk header for don't include "=:;,()" on the line they are
126 + * defined, since they don't have a parameter list.
127 + */
128 + "^[ \t]*("
129 + "([][[:alnum:]@_.](<[][[:alnum:]@_, \t<>]+>)?)+"
130 + "([ \t]+"
131 + "([][[:alnum:]@_.](<[][[:alnum:]@_, \t<>]+>)?)+"
132 + ")+" /* Up to here, same as methods regex. */
133 + "[^;=:,()]*" /* Compared to methods, no parameter list allowed. */
134 + ")$\n"
135 /* Type definitions */
136 "^[ \t]*(((static|public|internal|private|protected|new|unsafe|sealed|abstract|partial)[ \t]+)*(class|enum|interface|struct|record)[ \t]+.*)$\n"
137 /* Namespace */