Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate types when referenced projects declare them internally #87

Merged
2 commits merged into from
Feb 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public class Generator : IDisposable
/// <summary>
/// Initializes a new instance of the <see cref="Generator"/> class.
/// </summary>
/// <param name="metadataLibraryStream">The stream to the winmd metadata to generate APIs from.</param>
/// <param name="metadataLibraryStream">The stream to the winmd metadata to generate APIs from. This will be disposed of with the <see cref="Generator"/>.</param>
/// <param name="options">Options that influence the result of generation.</param>
/// <param name="compilation">The compilation that the generated code will be added to.</param>
/// <param name="parseOptions">The parse options that will be used for the generated code.</param>
Expand Down Expand Up @@ -1460,6 +1460,35 @@ private bool IsCompilerGenerated(TypeDefinition typeDef)
return isCompilerGenerated;
}

private ISymbol? FindSymbolIfAlreadyAvailable(string fullyQualifiedMetadataName)
{
if (this.compilation is object)
{
if (this.compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName) is { } ownSymbol)
{
// This assembly defines it.
return ownSymbol;
}

foreach (var reference in this.compilation.References)
{
if (this.compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol referencedAssembly)
{
if (referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName) is { } externalSymbol)
{
if (this.compilation.IsSymbolAccessibleWithin(externalSymbol, this.compilation.Assembly))
{
// A referenced assembly declares this symbol and it is accessible to our own.
return externalSymbol;
}
}
}
}
}

return null;
}

private MemberDeclarationSyntax? CreateInteropType(TypeDefinitionHandle typeDefHandle)
{
TypeDefinition typeDef = this.mr.GetTypeDefinition(typeDefHandle);
Expand All @@ -1468,12 +1497,13 @@ private bool IsCompilerGenerated(TypeDefinition typeDef)
return null;
}

// Skip if the compilation already defines this type.
// Skip if the compilation already defines this type or can access it from elsewhere.
string name = this.mr.GetString(typeDef.Name);
string ns = this.mr.GetString(typeDef.Namespace);
string fullyQualifiedName = this.Namespace + "." + name;
if (this.compilation?.GetTypeByMetadataName(fullyQualifiedName) is object)
if (this.FindSymbolIfAlreadyAvailable(fullyQualifiedName) is object)
{
// The type already exists either in this project or a referenced one.
return null;
}

Expand Down
3 changes: 3 additions & 0 deletions test/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ dotnet_diagnostic.CA2007.severity = none

# SA1310: Field names should not contain underscore
dotnet_diagnostic.SA1310.severity = silent

# SA1133: Do not combine attributes
dotnet_diagnostic.SA1133.severity = silent
45 changes: 39 additions & 6 deletions test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class GeneratorTests : IDisposable, IAsyncLifetime
public GeneratorTests(ITestOutputHelper logger)
{
this.logger = logger;
this.metadataStream = File.OpenRead(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"));
this.metadataStream = OpenMetadata();

this.parseOptions = CSharpParseOptions.Default
.WithDocumentationMode(DocumentationMode.Diagnose)
Expand Down Expand Up @@ -228,9 +228,38 @@ public void FullGeneration()
this.AssertNoDiagnostics(logGeneratedCode: false);
}

[Theory, PairwiseData]
public void ProjectReferenceBetweenTwoGeneratingProjects(bool internalsVisibleTo)
{
CSharpCompilation referencedProject = this.compilation
.WithAssemblyName("refdProj");
if (internalsVisibleTo)
{
referencedProject = referencedProject.AddSyntaxTrees(
CSharpSyntaxTree.ParseText($@"[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute(""{this.compilation.AssemblyName}"")]", this.parseOptions));
}

using var referencedGenerator = new Generator(OpenMetadata(), compilation: referencedProject, parseOptions: this.parseOptions);
Assert.True(referencedGenerator.TryGenerate("LockWorkStation", CancellationToken.None));
referencedProject = this.AddGeneratedCode(referencedProject, referencedGenerator);
this.AssertNoDiagnostics(referencedProject);

// Now produce more code in a referencing project that includes at least one of the same types as generated in the referenced project.
this.compilation = this.compilation.AddReferences(referencedProject.ToMetadataReference());
this.generator = new Generator(this.metadataStream, compilation: this.compilation, parseOptions: this.parseOptions);
Assert.True(this.generator.TryGenerate("LockWorkStation", CancellationToken.None));
this.CollectGeneratedCode(this.generator);
this.AssertNoDiagnostics(logGeneratedCode: false);
}

private static ImmutableArray<Diagnostic> FilterDiagnostics(ImmutableArray<Diagnostic> diagnostics) => diagnostics.Where(d => d.Severity > DiagnosticSeverity.Hidden).ToImmutableArray();

private void CollectGeneratedCode(Generator generator)
private static FileStream OpenMetadata()
{
return File.OpenRead(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!, "Windows.Win32.winmd"));
}

private CSharpCompilation AddGeneratedCode(CSharpCompilation compilation, Generator generator)
{
var compilationUnits = generator.GetCompilationUnits(CancellationToken.None);
var syntaxTrees = new List<SyntaxTree>(compilationUnits.Count);
Expand All @@ -241,25 +270,29 @@ private void CollectGeneratedCode(Generator generator)
syntaxTrees.Add(CSharpSyntaxTree.ParseText(unit.Value.ToFullString(), this.parseOptions, path: unit.Key));
}

this.compilation = this.compilation.AddSyntaxTrees(syntaxTrees);
return compilation.AddSyntaxTrees(syntaxTrees);
}

private void CollectGeneratedCode(Generator generator) => this.compilation = this.AddGeneratedCode(this.compilation, generator);

private MethodDeclarationSyntax? FindGeneratedMethod(string name) => this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>()).FirstOrDefault(md => md.Identifier.ValueText == name);

private BaseTypeDeclarationSyntax? FindGeneratedType(string name) => this.compilation.SyntaxTrees.SelectMany(st => st.GetRoot().DescendantNodes().OfType<BaseTypeDeclarationSyntax>()).FirstOrDefault(md => md.Identifier.ValueText == name);

private bool IsMethodGenerated(string name) => this.FindGeneratedMethod(name) is object;

private void AssertNoDiagnostics(bool logGeneratedCode = true)
private void AssertNoDiagnostics(bool logGeneratedCode = true) => this.AssertNoDiagnostics(this.compilation, logGeneratedCode);

private void AssertNoDiagnostics(CSharpCompilation compilation, bool logGeneratedCode = true)
{
var diagnostics = FilterDiagnostics(this.compilation.GetDiagnostics());
var diagnostics = FilterDiagnostics(compilation.GetDiagnostics());
this.LogDiagnostics(diagnostics);

var emitDiagnostics = ImmutableArray<Diagnostic>.Empty;
bool? emitSuccessful = null;
if (diagnostics.IsEmpty)
{
var emitResult = this.compilation.Emit(peStream: Stream.Null, xmlDocumentationStream: Stream.Null);
var emitResult = compilation.Emit(peStream: Stream.Null, xmlDocumentationStream: Stream.Null);
emitSuccessful = emitResult.Success;
emitDiagnostics = FilterDiagnostics(emitResult.Diagnostics);
this.LogDiagnostics(emitDiagnostics);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageReference Include="System.Reflection.Metadata" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="Xunit.Combinatorial" Version="1.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
</ItemGroup>

Expand Down