Skip to content

Commit 28df788

Browse files
committed
Simplify replacement of error type names with inferred names
- Change approach to use `ToDisplayParts`, which removes the need for regex parsing of generated code - Fix bug in `ReplaceWithInferredInterfaceName` where dots weren't being escaped - Add tests to ensure partially qualified references will be resolved correctly
1 parent 6ccd23a commit 28df788

6 files changed

+225
-63
lines changed

AutomaticInterface/AutomaticInterface/RoslynExtensions.cs

Lines changed: 81 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -86,104 +86,123 @@ public static string ToDisplayString(
8686
this IParameterSymbol symbol,
8787
SymbolDisplayFormat displayFormat,
8888
List<string> generatedInterfaceNames
89-
)
90-
{
91-
var parameterDisplayString = symbol.ToDisplayString(displayFormat);
92-
93-
var parameterTypeDisplayString = symbol.Type.ToDisplayString(
94-
displayFormat,
95-
generatedInterfaceNames
96-
);
97-
98-
// Replace the type part of the parameter definition - we don't try to generate the whole parameter definition
99-
// as it's quite complex - we leave that to Roslyn.
100-
return ParameterTypeMatcher.Replace(parameterDisplayString, parameterTypeDisplayString);
101-
}
89+
) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
10290

103-
/// <summary>
104-
/// Matches the type part of a parameter definition (Type name[ = defaultValue])
105-
/// </summary>
106-
private static readonly Regex ParameterTypeMatcher =
107-
new(@"[^\s=]+(?=\s\S+(\s?=\s?\S+)?$)", RegexOptions.Compiled);
91+
public static string ToDisplayString(
92+
this ITypeSymbol symbol,
93+
SymbolDisplayFormat displayFormat,
94+
List<string> generatedInterfaceNames
95+
) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
10896

10997
/// <summary>
11098
/// Wraps <see cref="ITypeSymbol.ToDisplayString(Microsoft.CodeAnalysis.SymbolDisplayFormat?)" /> with custom resolution for generated types
11199
/// </summary>
112-
/// <returns></returns>
113-
public static string ToDisplayString(
114-
this ITypeSymbol symbol,
100+
private static string ToDisplayString(
101+
this ISymbol symbol,
115102
SymbolDisplayFormat displayFormat,
116103
List<string> generatedInterfaceNames
117104
)
118105
{
119-
var builder = new StringBuilder();
106+
var displayStringBuilder = new StringBuilder();
120107

121-
AppendTypeSymbolDisplayString(symbol, displayFormat, generatedInterfaceNames, builder);
108+
var displayParts = GetDisplayParts(symbol, displayFormat);
109+
110+
foreach (var part in displayParts)
111+
{
112+
if (part.Kind == SymbolDisplayPartKind.ErrorTypeName)
113+
{
114+
var unrecognisedName = part.ToString();
115+
116+
var inferredName = ReplaceWithInferredInterfaceName(
117+
unrecognisedName,
118+
generatedInterfaceNames
119+
);
120+
121+
displayStringBuilder.Append(inferredName);
122+
}
123+
else
124+
{
125+
displayStringBuilder.Append(part);
126+
}
127+
}
122128

123-
return builder.ToString();
129+
return displayStringBuilder.ToString();
124130
}
125131

126-
private static void AppendTypeSymbolDisplayString(
127-
ITypeSymbol typeSymbol,
128-
SymbolDisplayFormat displayFormat,
129-
List<string> generatedInterfaceNames,
130-
StringBuilder builder
132+
/// <summary>
133+
/// The same as <see cref="ISymbol.ToDisplayParts"/> but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child
134+
/// </summary>
135+
private static IEnumerable<SymbolDisplayPart> GetDisplayParts(
136+
ISymbol symbol,
137+
SymbolDisplayFormat displayFormat
131138
)
132139
{
133-
if (typeSymbol is not IErrorTypeSymbol errorTypeSymbol)
140+
var cache = new List<SymbolDisplayPart>();
141+
142+
foreach (var part in symbol.ToDisplayParts(displayFormat))
134143
{
135-
// This symbol contains no unresolved types. Fall back to the default generation provided by Roslyn
136-
builder.Append(typeSymbol.ToDisplayString(displayFormat));
137-
return;
138-
}
144+
if (cache.Count == 0)
145+
{
146+
cache.Add(part);
147+
continue;
148+
}
139149

140-
var symbolName =
141-
InferGeneratedInterfaceName(errorTypeSymbol, generatedInterfaceNames)
142-
?? errorTypeSymbol.Name;
150+
var previousPart = cache.Last();
143151

144-
builder.Append(symbolName);
152+
if (
153+
IsPartQualificationPunctuation(previousPart)
154+
^ IsPartQualificationPunctuation(part)
155+
)
156+
{
157+
cache.Add(part);
158+
}
159+
else
160+
{
161+
yield return CombineQualifiedTypeParts(cache);
162+
cache.Clear();
163+
cache.Add(part);
164+
}
165+
}
145166

146-
if (errorTypeSymbol.IsGenericType)
167+
if (cache.Count > 0)
147168
{
148-
builder.Append('<');
169+
yield return CombineQualifiedTypeParts(cache);
170+
}
149171

150-
bool isFirstTypeArgument = true;
151-
foreach (var typeArgument in errorTypeSymbol.TypeArguments)
152-
{
153-
if (!isFirstTypeArgument)
154-
{
155-
builder.Append(", ");
156-
}
157-
158-
AppendTypeSymbolDisplayString(
159-
typeArgument,
160-
displayFormat,
161-
generatedInterfaceNames,
162-
builder
172+
static SymbolDisplayPart CombineQualifiedTypeParts(
173+
ICollection<SymbolDisplayPart> qualifiedTypeParts
174+
)
175+
{
176+
var qualifiedType = qualifiedTypeParts.Last();
177+
178+
return qualifiedTypeParts.Count == 1
179+
? qualifiedType
180+
: new SymbolDisplayPart(
181+
qualifiedType.Kind,
182+
qualifiedType.Symbol,
183+
string.Join("", qualifiedTypeParts)
163184
);
164-
165-
isFirstTypeArgument = false;
166-
}
167-
168-
builder.Append('>');
169185
}
186+
187+
static bool IsPartQualificationPunctuation(SymbolDisplayPart part) =>
188+
part.ToString() is "." or "::";
170189
}
171190

172-
private static string? InferGeneratedInterfaceName(
173-
IErrorTypeSymbol unrecognisedSymbol,
191+
private static string ReplaceWithInferredInterfaceName(
192+
string unrecognisedName,
174193
List<string> generatedInterfaceNames
175194
)
176195
{
177196
var matches = generatedInterfaceNames
178-
.Where(name => Regex.IsMatch(name, $"[.:]{unrecognisedSymbol.Name}$"))
197+
.Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$"))
179198
.ToList();
180199

181200
if (matches.Count != 1)
182201
{
183202
// Either there's no match or an ambiguous match - we can't safely infer the interface name.
184203
// This is very much a "best effort" approach - if there are two interfaces with the same name,
185204
// there's no obvious way to work out which one the symbol is referring to.
186-
return null;
205+
return unrecognisedName;
187206
}
188207

189208
return matches[0];

AutomaticInterface/Tests/Infrastructure.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public static string GenerateCode(string code)
2626
var sourceDiagnostics = compilation.GetDiagnostics();
2727
var sourceErrors = sourceDiagnostics
2828
.Where(d => d.Severity == DiagnosticSeverity.Error)
29-
.Where(x => x.Id != "CS0246") // missing references are ok
29+
.Where(x => x.Id != "CS0246" && x.Id != "CS0234") // missing references are ok
3030
.ToList();
3131

3232
Assert.Empty(sourceErrors);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//--------------------------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by a tool.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
6+
// </auto-generated>
7+
//--------------------------------------------------------------------------------------------------
8+
9+
namespace Processor
10+
{
11+
[global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
12+
public partial interface IModelProcessor
13+
{
14+
/// <inheritdoc cref="Processor.ModelProcessor.Template" />
15+
global::ModelsRoot.Models.IModel Template { get; }
16+
17+
/// <inheritdoc cref="Processor.ModelProcessor.Process(Models.IModel)" />
18+
global::ModelsRoot.Models.IModel Process(global::ModelsRoot.Models.IModel model);
19+
20+
/// <inheritdoc cref="Processor.ModelProcessor.ModelChanged" />
21+
event EventHandler<global::ModelsRoot.Models.IModel> ModelChanged;
22+
23+
}
24+
}
25+
26+
27+
//--------------------------------------------------------------------------------------------------
28+
// <auto-generated>
29+
// This code was generated by a tool.
30+
//
31+
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
32+
// </auto-generated>
33+
//--------------------------------------------------------------------------------------------------
34+
35+
namespace ModelsRoot.Models
36+
{
37+
[global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
38+
public partial interface IModel
39+
{
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//--------------------------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by a tool.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
6+
// </auto-generated>
7+
//--------------------------------------------------------------------------------------------------
8+
9+
namespace Root.Processor
10+
{
11+
[global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
12+
public partial interface IModelProcessor
13+
{
14+
/// <inheritdoc cref="Root.Processor.ModelProcessor.ProcessFullyQualified(Root.ModelsRoot.Models.IModel)" />
15+
global::Root.ModelsRoot.Models.IModel ProcessFullyQualified(global::Root.ModelsRoot.Models.IModel model);
16+
17+
/// <inheritdoc cref="Root.Processor.ModelProcessor.ProcessRelativeQualified(Root.ModelsRoot.Models.IModel)" />
18+
global::Root.ModelsRoot.Models.IModel ProcessRelativeQualified(global::Root.ModelsRoot.Models.IModel model);
19+
20+
}
21+
}
22+
23+
24+
//--------------------------------------------------------------------------------------------------
25+
// <auto-generated>
26+
// This code was generated by a tool.
27+
//
28+
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
29+
// </auto-generated>
30+
//--------------------------------------------------------------------------------------------------
31+
32+
namespace Root.ModelsRoot.Models
33+
{
34+
[global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
35+
public partial interface IModel
36+
{
37+
}
38+
}

AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,67 @@ public class Model<T>;
235235

236236
await Verify(Infrastructure.GenerateCode(code));
237237
}
238+
239+
[Fact]
240+
public async Task WorksWithQualifiedGeneratedInterfaceReferences()
241+
{
242+
const string code = """
243+
using AutomaticInterface;
244+
245+
namespace Processor
246+
{
247+
using ModelsRoot;
248+
249+
[GenerateAutomaticInterface]
250+
public class ModelProcessor : IModelProcessor
251+
{
252+
public Models.IModel Process(Models.IModel model) => null;
253+
254+
public event EventHandler<Models.IModel> ModelChanged;
255+
256+
public Models.IModel Template => null;
257+
}
258+
}
259+
260+
namespace ModelsRoot.Models
261+
{
262+
263+
[GenerateAutomaticInterface]
264+
public class Model : IModel;
265+
}
266+
""";
267+
268+
await Verify(Infrastructure.GenerateCode(code));
269+
}
270+
271+
[Fact]
272+
public async Task WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces()
273+
{
274+
const string code = """
275+
using AutomaticInterface;
276+
277+
namespace Root
278+
{
279+
namespace Processor
280+
{
281+
[GenerateAutomaticInterface]
282+
public class ModelProcessor : IModelProcessor
283+
{
284+
public Root.ModelsRoot.Models.IModel ProcessFullyQualified(Root.ModelsRoot.Models.IModel model) => null;
285+
286+
public ModelsRoot.Models.IModel ProcessRelativeQualified(ModelsRoot.Models.IModel model) => null;
287+
}
288+
}
289+
290+
namespace ModelsRoot.Models
291+
{
292+
293+
[GenerateAutomaticInterface]
294+
public class Model : IModel;
295+
}
296+
}
297+
""";
298+
299+
await Verify(Infrastructure.GenerateCode(code));
300+
}
238301
}

0 commit comments

Comments
 (0)