|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
| 3 | +using System.Collections.Immutable; |
3 | 4 | using System.Linq; |
4 | | -using System.Text; |
5 | | -using System.Text.RegularExpressions; |
6 | 5 | using Microsoft.CodeAnalysis; |
7 | 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; |
8 | 7 |
|
9 | | -namespace DotnetAutomaticInterface |
| 8 | +namespace DotnetAutomaticInterface; |
| 9 | + |
| 10 | +/// <summary> |
| 11 | +/// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs |
| 12 | +/// </summary> |
| 13 | +/// <remarks> |
| 14 | +/// Enhancements or additional Roslyn-related extension methods should be placed in <see cref="RoslynExtensionsAutomaticInterface"/> |
| 15 | +/// </remarks> |
| 16 | +public static class RoslynExtensions |
10 | 17 | { |
11 | | - /// <summary> |
12 | | - /// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs |
13 | | - /// </summary> |
14 | | - public static class RoslynExtensions |
| 18 | + private static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type) |
15 | 19 | { |
16 | | - private static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type) |
17 | | - { |
18 | | - var current = type; |
19 | | - while (current != null) |
20 | | - { |
21 | | - yield return current; |
22 | | - current = current.BaseType; |
23 | | - } |
24 | | - } |
25 | | - |
26 | | - public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type) |
| 20 | + var current = type; |
| 21 | + while (current != null) |
27 | 22 | { |
28 | | - return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()); |
| 23 | + yield return current; |
| 24 | + current = current.BaseType; |
29 | 25 | } |
| 26 | + } |
30 | 27 |
|
31 | | - public static string GetClassName(this ClassDeclarationSyntax proxy) |
32 | | - { |
33 | | - return proxy.Identifier.Text; |
34 | | - } |
35 | | - |
36 | | - /// <summary> |
37 | | - /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2 |
38 | | - /// </summary> |
39 | | - public static string GetWhereStatement( |
40 | | - this ITypeParameterSymbol typeParameterSymbol, |
41 | | - SymbolDisplayFormat typeDisplayFormat, |
42 | | - List<string> generatedInterfaceNames |
43 | | - ) |
44 | | - { |
45 | | - var result = $"where {typeParameterSymbol.Name} : "; |
46 | | - |
47 | | - var constraints = new List<string>(); |
48 | | - |
49 | | - if (typeParameterSymbol.HasReferenceTypeConstraint) |
50 | | - { |
51 | | - constraints.Add("class"); |
52 | | - } |
53 | | - |
54 | | - if (typeParameterSymbol.HasValueTypeConstraint) |
55 | | - { |
56 | | - constraints.Add("struct"); |
57 | | - } |
58 | | - |
59 | | - if (typeParameterSymbol.HasNotNullConstraint) |
60 | | - { |
61 | | - constraints.Add("notnull"); |
62 | | - } |
63 | | - |
64 | | - constraints.AddRange( |
65 | | - typeParameterSymbol.ConstraintTypes.Select(t => |
66 | | - t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames) |
67 | | - ) |
68 | | - ); |
69 | | - |
70 | | - // The new() constraint must be last |
71 | | - if (typeParameterSymbol.HasConstructorConstraint) |
72 | | - { |
73 | | - constraints.Add("new()"); |
74 | | - } |
| 28 | + public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type) |
| 29 | + { |
| 30 | + return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()); |
| 31 | + } |
75 | 32 |
|
76 | | - if (constraints.Count == 0) |
77 | | - { |
78 | | - return ""; |
79 | | - } |
| 33 | + public static string GetClassName(this ClassDeclarationSyntax proxy) |
| 34 | + { |
| 35 | + return proxy.Identifier.Text; |
| 36 | + } |
80 | 37 |
|
81 | | - result += string.Join(", ", constraints); |
| 38 | + /// <summary> |
| 39 | + /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2 |
| 40 | + /// </summary> |
| 41 | + public static string GetWhereStatement( |
| 42 | + this ITypeParameterSymbol typeParameterSymbol, |
| 43 | + SymbolDisplayFormat typeDisplayFormat |
| 44 | + ) |
| 45 | + { |
| 46 | + var result = $"where {typeParameterSymbol.Name} : "; |
82 | 47 |
|
83 | | - return result; |
84 | | - } |
| 48 | + var constraints = new List<string>(); |
85 | 49 |
|
86 | | - public static string ToDisplayString( |
87 | | - this IParameterSymbol symbol, |
88 | | - SymbolDisplayFormat displayFormat, |
89 | | - bool nullableContextEnabled, |
90 | | - List<string> generatedInterfaceNames |
91 | | - ) |
| 50 | + if (typeParameterSymbol.HasReferenceTypeConstraint) |
92 | 51 | { |
93 | | - string? RenderTypeSymbolWithNullableAnnotation(SymbolDisplayPart part) => |
94 | | - part.Symbol is ITypeSymbol typeSymbol |
95 | | - ? typeSymbol |
96 | | - .WithNullableAnnotation(NullableAnnotation.Annotated) |
97 | | - .ToDisplayString(displayFormat) |
98 | | - : null; |
99 | | - |
100 | | - // Special case for reference parameters with default value null (e.g. string x = null) - the nullable |
101 | | - // context isn't applied automatically, so it must be forced explicitly |
102 | | - var forceNullableAnnotation = |
103 | | - nullableContextEnabled |
104 | | - && symbol |
105 | | - is { |
106 | | - Type.IsReferenceType: true, |
107 | | - HasExplicitDefaultValue: true, |
108 | | - ExplicitDefaultValue: null |
109 | | - } |
110 | | - && symbol.NullableAnnotation != NullableAnnotation.Annotated; |
111 | | - |
112 | | - return ToDisplayString( |
113 | | - symbol, |
114 | | - displayFormat, |
115 | | - generatedInterfaceNames, |
116 | | - forceNullableAnnotation ? RenderTypeSymbolWithNullableAnnotation : null |
117 | | - ); |
| 52 | + constraints.Add("class"); |
118 | 53 | } |
119 | 54 |
|
120 | | - public static string ToDisplayString( |
121 | | - this ITypeSymbol symbol, |
122 | | - SymbolDisplayFormat displayFormat, |
123 | | - List<string> generatedInterfaceNames |
124 | | - ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames); |
125 | | - |
126 | | - /// <summary> |
127 | | - /// Wraps <see cref="ITypeSymbol.ToDisplayString(Microsoft.CodeAnalysis.SymbolDisplayFormat?)" /> with custom resolution for generated types |
128 | | - /// </summary> |
129 | | - private static string ToDisplayString( |
130 | | - this ISymbol symbol, |
131 | | - SymbolDisplayFormat displayFormat, |
132 | | - List<string> generatedInterfaceNames, |
133 | | - Func<SymbolDisplayPart, string?>? customRenderDisplayPart = null |
134 | | - ) |
| 55 | + if (typeParameterSymbol.HasValueTypeConstraint) |
135 | 56 | { |
136 | | - var displayStringBuilder = new StringBuilder(); |
137 | | - |
138 | | - var displayParts = GetDisplayParts(symbol, displayFormat); |
139 | | - |
140 | | - foreach (var part in displayParts) |
141 | | - { |
142 | | - if (part.Kind == SymbolDisplayPartKind.ErrorTypeName) |
143 | | - { |
144 | | - var unrecognisedName = part.ToString(); |
145 | | - |
146 | | - var inferredName = ReplaceWithInferredInterfaceName( |
147 | | - unrecognisedName, |
148 | | - generatedInterfaceNames |
149 | | - ); |
150 | | - |
151 | | - displayStringBuilder.Append(inferredName); |
152 | | - } |
153 | | - else |
154 | | - { |
155 | | - var customRender = customRenderDisplayPart?.Invoke(part); |
156 | | - displayStringBuilder.Append(customRender ?? part.ToString()); |
157 | | - } |
158 | | - } |
159 | | - |
160 | | - return displayStringBuilder.ToString(); |
| 57 | + constraints.Add("struct"); |
161 | 58 | } |
162 | 59 |
|
163 | | - /// <summary> |
164 | | - /// The same as <see cref="ISymbol.ToDisplayParts"/> but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child |
165 | | - /// </summary> |
166 | | - private static IEnumerable<SymbolDisplayPart> GetDisplayParts( |
167 | | - ISymbol symbol, |
168 | | - SymbolDisplayFormat displayFormat |
169 | | - ) |
| 60 | + if (typeParameterSymbol.HasNotNullConstraint) |
170 | 61 | { |
171 | | - var cache = new List<SymbolDisplayPart>(); |
172 | | - |
173 | | - foreach (var part in symbol.ToDisplayParts(displayFormat)) |
174 | | - { |
175 | | - if (cache.Count == 0) |
176 | | - { |
177 | | - cache.Add(part); |
178 | | - continue; |
179 | | - } |
180 | | - |
181 | | - var previousPart = cache.Last(); |
182 | | - |
183 | | - if ( |
184 | | - IsPartQualificationPunctuation(previousPart) |
185 | | - ^ IsPartQualificationPunctuation(part) |
186 | | - ) |
187 | | - { |
188 | | - cache.Add(part); |
189 | | - } |
190 | | - else |
191 | | - { |
192 | | - yield return CombineQualifiedTypeParts(cache); |
193 | | - cache.Clear(); |
194 | | - cache.Add(part); |
195 | | - } |
196 | | - } |
197 | | - |
198 | | - if (cache.Count > 0) |
199 | | - { |
200 | | - yield return CombineQualifiedTypeParts(cache); |
201 | | - } |
202 | | - |
203 | | - static SymbolDisplayPart CombineQualifiedTypeParts( |
204 | | - ICollection<SymbolDisplayPart> qualifiedTypeParts |
205 | | - ) |
206 | | - { |
207 | | - var qualifiedType = qualifiedTypeParts.Last(); |
| 62 | + constraints.Add("notnull"); |
| 63 | + } |
208 | 64 |
|
209 | | - return qualifiedTypeParts.Count == 1 |
210 | | - ? qualifiedType |
211 | | - : new SymbolDisplayPart( |
212 | | - qualifiedType.Kind, |
213 | | - qualifiedType.Symbol, |
214 | | - string.Join("", qualifiedTypeParts) |
215 | | - ); |
216 | | - } |
| 65 | + constraints.AddRange( |
| 66 | + typeParameterSymbol.ConstraintTypes.Select(t => t.ToDisplayString(typeDisplayFormat)) |
| 67 | + ); |
217 | 68 |
|
218 | | - static bool IsPartQualificationPunctuation(SymbolDisplayPart part) => |
219 | | - part.ToString() is "." or "::"; |
| 69 | + // The new() constraint must be last |
| 70 | + if (typeParameterSymbol.HasConstructorConstraint) |
| 71 | + { |
| 72 | + constraints.Add("new()"); |
220 | 73 | } |
221 | 74 |
|
222 | | - private static string ReplaceWithInferredInterfaceName( |
223 | | - string unrecognisedName, |
224 | | - List<string> generatedInterfaceNames |
225 | | - ) |
| 75 | + if (constraints.Count == 0) |
226 | 76 | { |
227 | | - var matches = generatedInterfaceNames |
228 | | - .Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$")) |
229 | | - .ToList(); |
| 77 | + return ""; |
| 78 | + } |
230 | 79 |
|
231 | | - if (matches.Count != 1) |
232 | | - { |
233 | | - // Either there's no match or an ambiguous match - we can't safely infer the interface name. |
234 | | - // This is very much a "best effort" approach - if there are two interfaces with the same name, |
235 | | - // there's no obvious way to work out which one the symbol is referring to. |
236 | | - return unrecognisedName; |
237 | | - } |
| 80 | + result += string.Join(", ", constraints); |
238 | 81 |
|
239 | | - return matches[0]; |
240 | | - } |
| 82 | + return result; |
241 | 83 | } |
242 | 84 | } |
0 commit comments