diff --git a/src/Authoring/WinRT.SourceGenerator/DiagnosticUtils.cs b/src/Authoring/WinRT.SourceGenerator/DiagnosticUtils.cs index ee26dec01..04765f34f 100644 --- a/src/Authoring/WinRT.SourceGenerator/DiagnosticUtils.cs +++ b/src/Authoring/WinRT.SourceGenerator/DiagnosticUtils.cs @@ -45,7 +45,7 @@ private void CheckNamespaces() { WinRTSyntaxReceiver syntaxReceiver = (WinRTSyntaxReceiver)_context.SyntaxReceiver; - // Used to check for conflicitng namespace names + // Used to check for conflicting namespace names HashSet namespaceNames = new(); foreach (var @namespace in syntaxReceiver.Namespaces) diff --git a/src/Authoring/WinRT.SourceGenerator/Generator.cs b/src/Authoring/WinRT.SourceGenerator/Generator.cs index 3a9da59c2..92ab4551f 100644 --- a/src/Authoring/WinRT.SourceGenerator/Generator.cs +++ b/src/Authoring/WinRT.SourceGenerator/Generator.cs @@ -1,257 +1,266 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; -using System.Reflection.PortableExecutable; -using System.Text; - -namespace Generator -{ - public class ComponentGenerator - { - private Logger Logger { get; } - private readonly GeneratorExecutionContext context; - private string tempFolder; - - public ComponentGenerator(GeneratorExecutionContext context) - { - this.context = context; - Logger = new Logger(context); - } - - private string GetTempFolder(bool clearSourceFilesFromFolder = false) - { - if (string.IsNullOrEmpty(tempFolder) || !File.Exists(tempFolder)) - { - string outputDir = Path.Combine(Path.GetTempPath(), "CsWinRT", Path.GetRandomFileName()).TrimEnd('\\'); - Directory.CreateDirectory(outputDir); - tempFolder = outputDir; - Logger.Log("Created temp folder: " + tempFolder); - } - - if (clearSourceFilesFromFolder) - { - foreach (var file in Directory.GetFiles(tempFolder, "*.cs", SearchOption.TopDirectoryOnly)) - { - Logger.Log("Clearing " + file); - File.Delete(file); - } - } - - return tempFolder; - } - - private void GenerateSources() - { - string cswinrtExe = context.GetCsWinRTExe(); - string assemblyName = context.GetAssemblyName(); - string winmdFile = context.GetWinmdOutputFile(); - string outputDir = GetTempFolder(true); - string windowsMetadata = context.GetCsWinRTWindowsMetadata(); - string winmds = context.GetCsWinRTDependentMetadata(); - - string arguments = string.Format( - "-component -input \"{0}\" -input {1} -include {2} -output \"{3}\" -input {4} -verbose", - winmdFile, - windowsMetadata, - assemblyName, - outputDir, - winmds); - Logger.Log("Running " + cswinrtExe + " " + arguments); - - var processInfo = new ProcessStartInfo - { - FileName = cswinrtExe, - Arguments = arguments, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true - }; - - try - { - using var cswinrtProcess = Process.Start(processInfo); - Logger.Log(cswinrtProcess.StandardOutput.ReadToEnd()); - Logger.Log(cswinrtProcess.StandardError.ReadToEnd()); - cswinrtProcess.WaitForExit(); - - if (cswinrtProcess.ExitCode != 0) - { - throw new Win32Exception(cswinrtProcess.ExitCode); - } - - foreach (var file in Directory.GetFiles(outputDir, "*.cs", SearchOption.TopDirectoryOnly)) - { - Logger.Log("Adding " + file); - context.AddSource(Path.GetFileNameWithoutExtension(file), SourceText.From(File.ReadAllText(file), Encoding.UTF8)); - } - } - finally - { - if (!context.GetKeepGeneratedSources()) - { - Directory.Delete(outputDir, true); - } - } - } - - private void GenerateWinMD(MetadataBuilder metadataBuilder) - { - string outputFile = context.GetWinmdOutputFile(); - Logger.Log("Writing " + outputFile); - var managedPeBuilder = new ManagedPEBuilder( - new PEHeaderBuilder( - machine: Machine.I386, - imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll | Characteristics.Bit32Machine), - new MetadataRootBuilder(metadataBuilder, "WindowsRuntime 1.4"), - new BlobBuilder(), - flags: CorFlags.ILOnly); - - var peBlob = new BlobBuilder(); - managedPeBuilder.Serialize(peBlob); - - using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write); - peBlob.WriteContentTo(fs); - } - - private bool CatchWinRTDiagnostics() - { - string assemblyName = context.GetAssemblyName(); - WinRTComponentScanner winrtScanner = new(context, assemblyName); - winrtScanner.FindDiagnostics(); - return winrtScanner.Found(); - } - - public void Generate() - { - if (CatchWinRTDiagnostics()) - { - Logger.Log("Exiting early -- found errors in authored runtime component."); - Logger.Close(); - Environment.ExitCode = -1; - return; - } - - try - { - string assembly = context.GetAssemblyName(); - string version = context.GetAssemblyVersion(); - MetadataBuilder metadataBuilder = new MetadataBuilder(); - - var writer = new WinRTTypeWriter( - assembly, - version, - metadataBuilder, - Logger); - - WinRTSyntaxReceiver syntaxReceiver = (WinRTSyntaxReceiver)context.SyntaxReceiver; - Logger.Log("Found " + syntaxReceiver.Declarations.Count + " types"); - foreach (var declaration in syntaxReceiver.Declarations) - { - writer.Model = context.Compilation.GetSemanticModel(declaration.SyntaxTree); - writer.Visit(declaration); - } - writer.FinalizeGeneration(); - - GenerateWinMD(metadataBuilder); - if (!context.ShouldGenerateWinMDOnly()) - { - GenerateSources(); - } - } - catch (Exception e) - { - Logger.Log(e.ToString()); - if (e.InnerException != null) - { - Logger.Log(e.InnerException.ToString()); - } - Logger.Close(); - Environment.ExitCode = -2; - throw; - } - - Logger.Log("Done"); - Logger.Close(); - } - } - - [Generator] - public class SourceGenerator : ISourceGenerator - { - public void Execute(GeneratorExecutionContext context) - { - if (!context.IsCsWinRTComponent() && !context.ShouldGenerateWinMDOnly()) - { - System.Diagnostics.Debug.WriteLine($"Skipping component {context.GetAssemblyName()}"); - return; - } - - ComponentGenerator generator = new ComponentGenerator(context); - generator.Generate(); - } - - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new WinRTSyntaxReceiver()); - } - } - - class WinRTSyntaxReceiver : ISyntaxReceiver - { - public List Declarations = new(); - public List Namespaces = new(); - - private bool HasSomePublicTypes(SyntaxNode syntaxNode) - { - return syntaxNode.ChildNodes().OfType().Any(IsPublic); - } - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - // Store namespaces separately as we only need to look at them for diagnostics - // If we did store them in declarations, we would get duplicate entries in the WinMD, - // once from the namespace declaration and once from the member's declaration - if (syntaxNode is NamespaceDeclarationSyntax @namespace) - { - if (HasSomePublicTypes(syntaxNode)) - { - Namespaces.Add(@namespace); - } - - // Subsequent checks will fail, small performance boost to return now. - return; - } - - if (syntaxNode is not MemberDeclarationSyntax declaration || !IsPublic(declaration)) - { - return; - } - - if (syntaxNode is ClassDeclarationSyntax || - syntaxNode is InterfaceDeclarationSyntax || - syntaxNode is EnumDeclarationSyntax || - syntaxNode is DelegateDeclarationSyntax || - syntaxNode is StructDeclarationSyntax) - { - Declarations.Add(declaration); - } - } - - private bool IsPublic(MemberDeclarationSyntax member) - { - // We detect whether partial types are public using symbol information later. - return member.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword) || m.IsKind(SyntaxKind.PartialKeyword)); - } - } -} +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using System.Text; + +namespace Generator +{ + public class ComponentGenerator + { + private Logger Logger { get; } + private readonly GeneratorExecutionContext context; + private string tempFolder; + + public ComponentGenerator(GeneratorExecutionContext context) + { + this.context = context; + Logger = new Logger(context); + } + + private string GetTempFolder(bool clearSourceFilesFromFolder = false) + { + if (string.IsNullOrEmpty(tempFolder) || !File.Exists(tempFolder)) + { + string outputDir = Path.Combine(Path.GetTempPath(), "CsWinRT", Path.GetRandomFileName()).TrimEnd('\\'); + Directory.CreateDirectory(outputDir); + tempFolder = outputDir; + Logger.Log("Created temp folder: " + tempFolder); + } + + if (clearSourceFilesFromFolder) + { + foreach (var file in Directory.GetFiles(tempFolder, "*.cs", SearchOption.TopDirectoryOnly)) + { + Logger.Log("Clearing " + file); + File.Delete(file); + } + } + + return tempFolder; + } + + private void GenerateSources() + { + string cswinrtExe = context.GetCsWinRTExe(); + string assemblyName = context.GetAssemblyName(); + string winmdFile = context.GetWinmdOutputFile(); + string outputDir = GetTempFolder(true); + string windowsMetadata = context.GetCsWinRTWindowsMetadata(); + string winmds = context.GetCsWinRTDependentMetadata(); + + string arguments = string.Format( + "-component -input \"{0}\" -input {1} -include {2} -output \"{3}\" -input {4} -verbose", + winmdFile, + windowsMetadata, + assemblyName, + outputDir, + winmds); + Logger.Log("Running " + cswinrtExe + " " + arguments); + + var processInfo = new ProcessStartInfo + { + FileName = cswinrtExe, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true + }; + + try + { + using var cswinrtProcess = Process.Start(processInfo); + Logger.Log(cswinrtProcess.StandardOutput.ReadToEnd()); + Logger.Log(cswinrtProcess.StandardError.ReadToEnd()); + cswinrtProcess.WaitForExit(); + + if (cswinrtProcess.ExitCode != 0) + { + throw new Win32Exception(cswinrtProcess.ExitCode); + } + + foreach (var file in Directory.GetFiles(outputDir, "*.cs", SearchOption.TopDirectoryOnly)) + { + Logger.Log("Adding " + file); + context.AddSource(Path.GetFileNameWithoutExtension(file), SourceText.From(File.ReadAllText(file), Encoding.UTF8)); + } + } + finally + { + if (!context.GetKeepGeneratedSources()) + { + Directory.Delete(outputDir, true); + } + } + } + + private void GenerateWinMD(MetadataBuilder metadataBuilder) + { + string outputFile = context.GetWinmdOutputFile(); + Logger.Log("Writing " + outputFile); + var managedPeBuilder = new ManagedPEBuilder( + new PEHeaderBuilder( + machine: Machine.I386, + imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll | Characteristics.Bit32Machine), + new MetadataRootBuilder(metadataBuilder, "WindowsRuntime 1.4"), + new BlobBuilder(), + flags: CorFlags.ILOnly); + + var peBlob = new BlobBuilder(); + managedPeBuilder.Serialize(peBlob); + + using var fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write); + peBlob.WriteContentTo(fs); + } + + private bool CatchWinRTDiagnostics() + { + string assemblyName = context.GetAssemblyName(); + WinRTComponentScanner winrtScanner = new(context, assemblyName); + winrtScanner.FindDiagnostics(); + return winrtScanner.Found(); + } + + public void Generate() + { + if (CatchWinRTDiagnostics()) + { + Logger.Log("Exiting early -- found errors in authored runtime component."); + Logger.Close(); + Environment.ExitCode = -1; + return; + } + + try + { + string assembly = context.GetAssemblyName(); + string version = context.GetAssemblyVersion(); + MetadataBuilder metadataBuilder = new MetadataBuilder(); + + var writer = new WinRTTypeWriter( + assembly, + version, + metadataBuilder, + Logger); + + WinRTSyntaxReceiver syntaxReceiver = (WinRTSyntaxReceiver)context.SyntaxReceiver; + Logger.Log("Found " + syntaxReceiver.Declarations.Count + " types"); + foreach (var declaration in syntaxReceiver.Declarations) + { + writer.Model = context.Compilation.GetSemanticModel(declaration.SyntaxTree); + writer.Visit(declaration); + } + writer.FinalizeGeneration(); + + GenerateWinMD(metadataBuilder); + if (!context.ShouldGenerateWinMDOnly()) + { + GenerateSources(); + } + } + catch (Exception e) + { + Logger.Log(e.ToString()); + if (e.InnerException != null) + { + Logger.Log(e.InnerException.ToString()); + } + Logger.Close(); + Environment.ExitCode = -2; + throw; + } + + Logger.Log("Done"); + Logger.Close(); + } + } + + [Generator] + public class SourceGenerator : ISourceGenerator + { + public void Execute(GeneratorExecutionContext context) + { + if (!context.IsCsWinRTComponent() && !context.ShouldGenerateWinMDOnly()) + { + System.Diagnostics.Debug.WriteLine($"Skipping component {context.GetAssemblyName()}"); + return; + } + + ComponentGenerator generator = new ComponentGenerator(context); + generator.Generate(); + } + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new WinRTSyntaxReceiver()); + } + } + + class WinRTSyntaxReceiver : ISyntaxReceiver + { + public List Declarations = new(); + public List Namespaces = new(); + + private bool HasSomePublicTypes(SyntaxNode syntaxNode) + { + return syntaxNode.ChildNodes().OfType().Any(IsPublic); + } + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // Store namespaces separately as we only need to look at them for diagnostics + // If we did store them in declarations, we would get duplicate entries in the WinMD, + // once from the namespace declaration and once from the member's declaration + if (syntaxNode is NamespaceDeclarationSyntax @namespace) + { + // We only include the namespace if it has a public type as otherwise it won't + // be projected. For partial types, there would be one instance that we encounter + // which declares the accessibility and we will use that to determine the accessibility + // of the type for the purpose of determining whether to include the namespace. + if (HasSomePublicTypes(syntaxNode)) + { + Namespaces.Add(@namespace); + } + + // Subsequent checks will fail, small performance boost to return now. + return; + } + + if (syntaxNode is not MemberDeclarationSyntax declaration || !IsPublicOrPartial(declaration)) + { + return; + } + + if (syntaxNode is ClassDeclarationSyntax || + syntaxNode is InterfaceDeclarationSyntax || + syntaxNode is EnumDeclarationSyntax || + syntaxNode is DelegateDeclarationSyntax || + syntaxNode is StructDeclarationSyntax) + { + Declarations.Add(declaration); + } + } + + private bool IsPublic(MemberDeclarationSyntax member) + { + return member.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)); + } + + private bool IsPublicOrPartial(MemberDeclarationSyntax member) + { + // We detect whether partial types are public using symbol information later. + return member.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword) || m.IsKind(SyntaxKind.PartialKeyword)); + } + } +} diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index 2035d1b76..0c1a90f26 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -1569,4 +1569,28 @@ public partial struct PartialStruct { public double Z; } +} + +namespace AnotherNamespace +{ + internal partial class PartialClass3 + { + public void InternalFunction() + { + } + } + + partial class PartialClass3 + { + public void InternalFunction2() + { + } + } + + internal class InternalClass + { + public void InternalFunction() + { + } + } } \ No newline at end of file diff --git a/src/Tests/DiagnosticTests/NegativeData.cs b/src/Tests/DiagnosticTests/NegativeData.cs index 0177e1c59..cf8f450a0 100644 --- a/src/Tests/DiagnosticTests/NegativeData.cs +++ b/src/Tests/DiagnosticTests/NegativeData.cs @@ -43,6 +43,31 @@ namespace A public sealed class Blank { public Blank() {} } }"; + private const string UnrelatedNamespaceWithPublicPartialTypes = @" +namespace DiagnosticTests +{ + namespace Foo + { + public sealed class Dog + { + public int Woof { get; set; } + } + } +} +namespace Utilities +{ + public sealed partial class Sandwich + { + private int BreadCount { get; set; } + } + + partial class Sandwich + { + private int BreadCount2 { get; set; } + } +} +"; + // note the below test should only fail if the AssemblyName is "DiagnosticTests.A", this is valid under the default "DiagnosticTests" private const string NamespaceDifferByDot = @" namespace DiagnosticTests.A diff --git a/src/Tests/DiagnosticTests/PositiveData.cs b/src/Tests/DiagnosticTests/PositiveData.cs index 9bd060a98..90b673ac9 100644 --- a/src/Tests/DiagnosticTests/PositiveData.cs +++ b/src/Tests/DiagnosticTests/PositiveData.cs @@ -24,6 +24,30 @@ private class Sandwich } }"; + private const string Valid_UnrelatedNamespaceWithNoPublicTypes2 = @" +namespace DiagnosticTests +{ + namespace Foo + { + public sealed class Dog + { + public int Woof { get; set; } + } + } +} +namespace Utilities +{ + internal partial class Sandwich + { + private int BreadCount { get; set; } + } + + partial class Sandwich + { + private int BreadCount2 { get; set; } + } +}"; + private const string Valid_SubNamespacesWithOverlappingNames = @" namespace DiagnosticTests { diff --git a/src/Tests/DiagnosticTests/UnitTesting.cs b/src/Tests/DiagnosticTests/UnitTesting.cs index 0a793e6dd..d87cc3764 100644 --- a/src/Tests/DiagnosticTests/UnitTesting.cs +++ b/src/Tests/DiagnosticTests/UnitTesting.cs @@ -120,6 +120,7 @@ private static IEnumerable InvalidCases yield return new TestCaseData(PropertyNoGetter, WinRTRules.PrivateGetterRule).SetName("Property. No Get, public Setter"); // namespace tests yield return new TestCaseData(SameNameNamespacesDisjoint, WinRTRules.DisjointNamespaceRule).SetName("Namespace. isn't accessible without Test prefix, doesn't use type"); + yield return new TestCaseData(UnrelatedNamespaceWithPublicPartialTypes, WinRTRules.DisjointNamespaceRule).SetName("Namespace. Component has public types in different namespaces"); yield return new TestCaseData(NamespacesDifferByCase, WinRTRules.NamespacesDifferByCase).SetName("Namespace. names only differ by case"); yield return new TestCaseData(DisjointNamespaces, WinRTRules.DisjointNamespaceRule).SetName("Namespace. isn't accessible without Test prefix, doesn't use type"); yield return new TestCaseData(DisjointNamespaces2, WinRTRules.DisjointNamespaceRule).SetName("Namespace. using type from inaccessible namespace"); @@ -414,7 +415,8 @@ private static IEnumerable ValidCases { get { - yield return new TestCaseData(Valid_UnrelatedNamespaceWithNoPublicTypes).SetName("Valid. Namespace. Helper namespace with no public types"); + yield return new TestCaseData(Valid_UnrelatedNamespaceWithNoPublicTypes).SetName("Valid. Namespace. Helper namespace with no public types"); + yield return new TestCaseData(Valid_UnrelatedNamespaceWithNoPublicTypes2).SetName("Valid. Namespace. Helper namespace with partial types but aren't public"); yield return new TestCaseData(Valid_SubNamespacesWithOverlappingNames).SetName("Valid. Namespace. Overlapping namesepaces names is OK if in different namespaces"); yield return new TestCaseData(Valid_PrivateSetter).SetName("Valid. Property. Private Setter"); yield return new TestCaseData(Valid_RollYourOwnAsyncAction).SetName("Valid. AsyncInterfaces. Implementing your own IAsyncAction");