Skip to content

Commit

Permalink
Merge pull request #1177 from microsoft/improveInputErrorReporting
Browse files Browse the repository at this point in the history
Improve input error reporting
  • Loading branch information
AArnott committed May 10, 2024
2 parents a37a0b4 + 94dca70 commit d45e56f
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 29 deletions.
38 changes: 37 additions & 1 deletion src/Microsoft.Windows.CsWin32/SourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,22 @@ public class SourceGenerator : ISourceGenerator
"Configuration",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor NonUniqueMetadataInputs = new(
InputProjectionErrorId,
InputProjectionErrorTitle,
"The metadata projections input into CsWin32 must have unique names. The name \"{0}\" is used more than once.",
"Configuration",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

private const string InputProjectionErrorId = "PInvoke010";
private const string InputProjectionErrorTitle = "Input projection error";

private const string NativeMethodsTxtAdditionalFileName = "NativeMethods.txt";
private const string NativeMethodsJsonAdditionalFileName = "NativeMethods.json";

private static readonly char[] ZeroWhiteSpace = new char[]
{
'\uFEFF', // ZERO WIDTH NO-BREAK SPACE (U+FEFF)
Expand Down Expand Up @@ -199,7 +211,14 @@ public void Execute(GeneratorExecutionContext context)
}

Docs? docs = ParseDocs(context);
SuperGenerator superGenerator = SuperGenerator.Combine(CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)));
Generator[] generators = CollectMetadataPaths(context).Select(path => new Generator(path, docs, options, compilation, parseOptions)).ToArray();
if (TryFindNonUniqueValue(generators, g => g.InputAssemblyName, StringComparer.OrdinalIgnoreCase, out (Generator Item, string Value) nonUniqueGenerator))
{
context.ReportDiagnostic(Diagnostic.Create(NonUniqueMetadataInputs, null, nonUniqueGenerator.Value));
return;
}

SuperGenerator superGenerator = SuperGenerator.Combine(generators);
try
{
foreach (AdditionalText nativeMethodsTxtFile in nativeMethodsTxtFiles)
Expand Down Expand Up @@ -346,6 +365,23 @@ private static string AssembleFullExceptionMessage(Exception ex)
return sb.ToString();
}

private static bool TryFindNonUniqueValue<T, TValue>(IEnumerable<T> sequence, Func<T, TValue> valueSelector, IEqualityComparer<TValue> comparer, out (T Item, TValue Value) nonUniqueValue)
{
HashSet<TValue> seenValues = new(comparer);
nonUniqueValue = default;
foreach (T item in sequence)
{
TValue value = valueSelector(item);
if (!seenValues.Add(value))
{
nonUniqueValue = (item, value);
return true;
}
}

return false;
}

private static IReadOnlyList<string> CollectMetadataPaths(GeneratorExecutionContext context)
{
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.CsWin32InputMetadataPaths", out string? delimitedMetadataBasePaths) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public Test([CallerFilePath] string? testFile = null, [CallerMemberName] string?

this.ReferenceAssemblies = MyReferenceAssemblies.NetStandard20;
this.TestState.Sources.Add(string.Empty);
this.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", ConstructGlobalConfigString()));
}

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

protected override IEnumerable<Type> GetSourceGenerators()
{
yield return typeof(SourceGenerator);
}
public GeneratorConfiguration GeneratorConfiguration { get; set; } = GeneratorConfiguration.Default;

protected override ParseOptions CreateParseOptions()
{
return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(this.LanguageVersion);
}
protected override IEnumerable<Type> GetSourceGenerators() => [typeof(SourceGenerator)];

protected override ParseOptions CreateParseOptions() => ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(this.LanguageVersion);

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

return base.RunImplAsync(cancellationToken);
}
this.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", this.GeneratorConfiguration.ToGlobalConfigString()));

private static string ConstructGlobalConfigString(bool omitDocs = false)
{
StringBuilder globalConfigBuilder = new();
globalConfigBuilder.AppendLine("is_global = true");
globalConfigBuilder.AppendLine();
globalConfigBuilder.AppendLine($"build_property.CsWin32InputMetadataPaths = {JoinAssemblyMetadata("ProjectionMetadataWinmd")}");
if (!omitDocs)
{
globalConfigBuilder.AppendLine($"build_property.CsWin32InputDocPaths = {JoinAssemblyMetadata("ProjectionDocs")}");
}

return globalConfigBuilder.ToString();

static string JoinAssemblyMetadata(string name)
{
return string.Join(";", typeof(GeneratorTests).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().Where(metadata => metadata.Key == name).Select(metadata => metadata.Value));
}
return base.RunImplAsync(cancellationToken);
}
}
}
32 changes: 32 additions & 0 deletions test/Microsoft.Windows.CsWin32.Tests/GeneratorConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

internal record GeneratorConfiguration
{
internal static GeneratorConfiguration Default { get; } = new();

internal ImmutableArray<string> InputMetadataPaths { get; init; } = CollectAssemblyMetadata("ProjectionMetadataWinmd");

internal ImmutableArray<string> InputDocPaths { get; init; } = CollectAssemblyMetadata("ProjectionDocs");

internal string ToGlobalConfigString()
{
StringBuilder globalConfigBuilder = new();
globalConfigBuilder.AppendLine("is_global = true");
globalConfigBuilder.AppendLine();
AddPathsProperty("CsWin32InputMetadataPaths", this.InputMetadataPaths);
AddPathsProperty("CsWin32InputDocPaths", this.InputDocPaths);

return globalConfigBuilder.ToString();

void AddPathsProperty(string name, ImmutableArray<string> paths)
{
if (!paths.IsEmpty)
{
globalConfigBuilder.AppendLine($"build_property.{name} = {string.Join("|", paths)}");
}
}
}

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)];
}
17 changes: 17 additions & 0 deletions test/Microsoft.Windows.CsWin32.Tests/SourceGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,21 @@ public async Task MissingSystemMemoryReference_WithGeneratedCode_Net60()
NativeMethodsTxt = "CreateFile",
}.RunAsync();
}

[Fact]
public async Task NonUniqueWinmdProjectionNames()
{
await new VerifyCS.Test
{
NativeMethodsTxt = "CreateFile",
GeneratorConfiguration = GeneratorConfiguration.Default with
{
InputMetadataPaths = GeneratorConfiguration.Default.InputMetadataPaths.AddRange(GeneratorConfiguration.Default.InputMetadataPaths),
},
ExpectedDiagnostics =
{
new DiagnosticResult(SourceGenerator.NonUniqueMetadataInputs.Id, DiagnosticSeverity.Error),
},
}.RunAsync();
}
}

0 comments on commit d45e56f

Please sign in to comment.