Skip to content

Commit d45e56f

Browse files
authored
Merge pull request #1177 from microsoft/improveInputErrorReporting
Improve input error reporting
2 parents a37a0b4 + 94dca70 commit d45e56f

File tree

4 files changed

+92
-29
lines changed

4 files changed

+92
-29
lines changed

src/Microsoft.Windows.CsWin32/SourceGenerator.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,22 @@ public class SourceGenerator : ISourceGenerator
130130
"Configuration",
131131
DiagnosticSeverity.Warning,
132132
isEnabledByDefault: true);
133+
134+
public static readonly DiagnosticDescriptor NonUniqueMetadataInputs = new(
135+
InputProjectionErrorId,
136+
InputProjectionErrorTitle,
137+
"The metadata projections input into CsWin32 must have unique names. The name \"{0}\" is used more than once.",
138+
"Configuration",
139+
DiagnosticSeverity.Error,
140+
isEnabledByDefault: true);
133141
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
134142

143+
private const string InputProjectionErrorId = "PInvoke010";
144+
private const string InputProjectionErrorTitle = "Input projection error";
145+
135146
private const string NativeMethodsTxtAdditionalFileName = "NativeMethods.txt";
136147
private const string NativeMethodsJsonAdditionalFileName = "NativeMethods.json";
148+
137149
private static readonly char[] ZeroWhiteSpace = new char[]
138150
{
139151
'\uFEFF', // ZERO WIDTH NO-BREAK SPACE (U+FEFF)
@@ -199,7 +211,14 @@ public void Execute(GeneratorExecutionContext context)
199211
}
200212

201213
Docs? docs = ParseDocs(context);
202-
SuperGenerator superGenerator = SuperGenerator.Combine(CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)));
214+
Generator[] generators = CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)).ToArray();
215+
if (TryFindNonUniqueValue(generators, g => g.InputAssemblyName, StringComparer.OrdinalIgnoreCase, out (Generator Item, string Value) nonUniqueGenerator))
216+
{
217+
context.ReportDiagnostic(Diagnostic.Create(NonUniqueMetadataInputs, null, nonUniqueGenerator.Value));
218+
return;
219+
}
220+
221+
SuperGenerator superGenerator = SuperGenerator.Combine(generators);
203222
try
204223
{
205224
foreach (AdditionalText nativeMethodsTxtFile in nativeMethodsTxtFiles)
@@ -346,6 +365,23 @@ private static string AssembleFullExceptionMessage(Exception ex)
346365
return sb.ToString();
347366
}
348367

368+
private static bool TryFindNonUniqueValue<T, TValue>(IEnumerable<T> sequence, Func<T, TValue> valueSelector, IEqualityComparer<TValue> comparer, out (T Item, TValue Value) nonUniqueValue)
369+
{
370+
HashSet<TValue> seenValues = new(comparer);
371+
nonUniqueValue = default;
372+
foreach (T item in sequence)
373+
{
374+
TValue value = valueSelector(item);
375+
if (!seenValues.Add(value))
376+
{
377+
nonUniqueValue = (item, value);
378+
return true;
379+
}
380+
}
381+
382+
return false;
383+
}
384+
349385
private static IReadOnlyList<string> CollectMetadataPaths(GeneratorExecutionContext context)
350386
{
351387
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32InputMetadataPaths", out string? delimitedMetadataBasePaths) ||

test/Microsoft.Windows.CsWin32.Tests/CSharpSourceGeneratorVerifier.cs

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ public Test([CallerFilePath] string? testFile = null, [CallerMemberName] string?
2121

2222
this.ReferenceAssemblies = MyReferenceAssemblies.NetStandard20;
2323
this.TestState.Sources.Add(string.Empty);
24-
this.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", ConstructGlobalConfigString()));
2524
}
2625

2726
public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.CSharp9;
@@ -31,15 +30,11 @@ public Test([CallerFilePath] string? testFile = null, [CallerMemberName] string?
3130
[StringSyntax(StringSyntaxAttribute.Json)]
3231
public string? NativeMethodsJson { get; set; }
3332

34-
protected override IEnumerable<Type> GetSourceGenerators()
35-
{
36-
yield return typeof(SourceGenerator);
37-
}
33+
public GeneratorConfiguration GeneratorConfiguration { get; set; } = GeneratorConfiguration.Default;
3834

39-
protected override ParseOptions CreateParseOptions()
40-
{
41-
return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(this.LanguageVersion);
42-
}
35+
protected override IEnumerable<Type> GetSourceGenerators() => [typeof(SourceGenerator)];
36+
37+
protected override ParseOptions CreateParseOptions() => ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(this.LanguageVersion);
4338

4439
protected override CompilationOptions CreateCompilationOptions()
4540
{
@@ -62,26 +57,9 @@ protected override Task RunImplAsync(CancellationToken cancellationToken)
6257
this.TestState.AdditionalFiles.Add(("NativeMethods.json", this.NativeMethodsJson));
6358
}
6459

65-
return base.RunImplAsync(cancellationToken);
66-
}
60+
this.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", this.GeneratorConfiguration.ToGlobalConfigString()));
6761

68-
private static string ConstructGlobalConfigString(bool omitDocs = false)
69-
{
70-
StringBuilder globalConfigBuilder = new();
71-
globalConfigBuilder.AppendLine("is_global = true");
72-
globalConfigBuilder.AppendLine();
73-
globalConfigBuilder.AppendLine($"build_property.CsWin32InputMetadataPaths = {JoinAssemblyMetadata("ProjectionMetadataWinmd")}");
74-
if (!omitDocs)
75-
{
76-
globalConfigBuilder.AppendLine($"build_property.CsWin32InputDocPaths = {JoinAssemblyMetadata("ProjectionDocs")}");
77-
}
78-
79-
return globalConfigBuilder.ToString();
80-
81-
static string JoinAssemblyMetadata(string name)
82-
{
83-
return string.Join(";", typeof(GeneratorTests).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().Where(metadata => metadata.Key == name).Select(metadata => metadata.Value));
84-
}
62+
return base.RunImplAsync(cancellationToken);
8563
}
8664
}
8765
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
internal record GeneratorConfiguration
5+
{
6+
internal static GeneratorConfiguration Default { get; } = new();
7+
8+
internal ImmutableArray<string> InputMetadataPaths { get; init; } = CollectAssemblyMetadata("ProjectionMetadataWinmd");
9+
10+
internal ImmutableArray<string> InputDocPaths { get; init; } = CollectAssemblyMetadata("ProjectionDocs");
11+
12+
internal string ToGlobalConfigString()
13+
{
14+
StringBuilder globalConfigBuilder = new();
15+
globalConfigBuilder.AppendLine("is_global = true");
16+
globalConfigBuilder.AppendLine();
17+
AddPathsProperty("CsWin32InputMetadataPaths", this.InputMetadataPaths);
18+
AddPathsProperty("CsWin32InputDocPaths", this.InputDocPaths);
19+
20+
return globalConfigBuilder.ToString();
21+
22+
void AddPathsProperty(string name, ImmutableArray<string> paths)
23+
{
24+
if (!paths.IsEmpty)
25+
{
26+
globalConfigBuilder.AppendLine($"build_property.{name} = {string.Join("|", paths)}");
27+
}
28+
}
29+
}
30+
31+
private static ImmutableArray<string> CollectAssemblyMetadata(string name) => [.. typeof(GeneratorTests).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().Where(metadata => metadata.Key == name && metadata.Value is not null).Select(metadata => metadata.Value)];
32+
}

test/Microsoft.Windows.CsWin32.Tests/SourceGeneratorTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,21 @@ public async Task MissingSystemMemoryReference_WithGeneratedCode_Net60()
6969
NativeMethodsTxt = "CreateFile",
7070
}.RunAsync();
7171
}
72+
73+
[Fact]
74+
public async Task NonUniqueWinmdProjectionNames()
75+
{
76+
await new VerifyCS.Test
77+
{
78+
NativeMethodsTxt = "CreateFile",
79+
GeneratorConfiguration = GeneratorConfiguration.Default with
80+
{
81+
InputMetadataPaths = GeneratorConfiguration.Default.InputMetadataPaths.AddRange(GeneratorConfiguration.Default.InputMetadataPaths),
82+
},
83+
ExpectedDiagnostics =
84+
{
85+
new DiagnosticResult(SourceGenerator.NonUniqueMetadataInputs.Id, DiagnosticSeverity.Error),
86+
},
87+
}.RunAsync();
88+
}
7289
}

0 commit comments

Comments
 (0)