From 8f1432e2a5031f2f04add9e0cd1cf0ff750c1e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:41:45 -0800 Subject: [PATCH 01/11] Prepare GenAPI so it can be used for a new assembly diff generator. --- .../ApiCompatServiceProvider.cs | 2 +- .../GenAPITask.cs | 25 +- .../Microsoft.DotNet.GenAPI.Tool/Program.cs | 25 +- .../CSharpFileBuilder.cs | 78 ++-- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 102 +---- .../GenAPIAppConfiguration.cs | 349 ++++++++++++++++++ .../SyntaxGeneratorExtensions.cs | 1 - .../Filtering/DocIdSymbolFilter.cs | 17 +- .../Rules/AttributesMustMatchTests.cs | 70 ++-- .../CSharpFileBuilderTests.cs | 88 ++--- 10 files changed, 501 insertions(+), 256 deletions(-) create mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs diff --git a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs index b5d95dd8debd..d633abe2ac17 100644 --- a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs +++ b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs @@ -35,7 +35,7 @@ public ApiCompatServiceProvider(Func logFa CompositeSymbolFilter attributeDataSymbolFilter = new(accessibilitySymbolFilter); if (excludeAttributesFiles is not null) { - attributeDataSymbolFilter.Add(new DocIdSymbolFilter(excludeAttributesFiles)); + attributeDataSymbolFilter.Add(DocIdSymbolFilter.GetFilterForDocIds(excludeAttributesFiles)); } ApiComparerSettings apiComparerSettings = new( diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs index 6348a3fb8d4d..68be887ece01 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs @@ -62,17 +62,20 @@ public class GenAPITask : TaskBase /// protected override void ExecuteCore() { - GenAPIApp.Run(new MSBuildLog(Log), - Assemblies!, - AssemblyReferences, - OutputPath, - HeaderFile, - ExceptionMessage, - ExcludeApiFiles, - ExcludeAttributesFiles, - RespectInternals, - IncludeAssemblyAttributes - ); + GenApiAppConfiguration c = GenApiAppConfiguration.GetBuilder() + .WithLogger(new MSBuildLog(Log)) + .WithAssembliesPaths(Assemblies) + .WithAssemblyReferencesPaths(AssemblyReferences) + .WithOutputPath(OutputPath) + .WithHeaderFilePath(HeaderFile) + .WithExceptionMessage(ExceptionMessage) + .WithApiExclusionFilePaths(ExcludeApiFiles) + .WithAttributeExclusionFilePaths(ExcludeAttributesFiles) + .WithRespectInternals(RespectInternals) + .WithIncludeAssemblyAttributes(IncludeAssemblyAttributes) + .Build(); + + GenAPIApp.Run(c); } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs index dd2edbcbbd94..57b9e4c38896 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs @@ -97,17 +97,20 @@ static int Main(string[] args) rootCommand.SetAction((ParseResult parseResult) => { - GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal), - parseResult.GetValue(assembliesOption)!, - parseResult.GetValue(assemblyReferencesOption), - parseResult.GetValue(outputPathOption), - parseResult.GetValue(headerFileOption), - parseResult.GetValue(exceptionMessageOption), - parseResult.GetValue(excludeApiFilesOption), - parseResult.GetValue(excludeAttributesFilesOption), - parseResult.GetValue(respectInternalsOption), - parseResult.GetValue(includeAssemblyAttributesOption) - ); + GenApiAppConfiguration c = GenApiAppConfiguration.GetBuilder() + .WithLogger(new ConsoleLog(MessageImportance.Normal)) + .WithAssembliesPaths(parseResult.GetValue(assembliesOption)!) + .WithAssemblyReferencesPaths(parseResult.GetValue(assemblyReferencesOption)) + .WithOutputPath(parseResult.GetValue(outputPathOption)) + .WithHeaderFilePath(parseResult.GetValue(headerFileOption)) + .WithExceptionMessage(parseResult.GetValue(exceptionMessageOption)) + .WithApiExclusionFilePaths(parseResult.GetValue(excludeApiFilesOption)) + .WithAttributeExclusionFilePaths(parseResult.GetValue(excludeAttributesFilesOption)) + .WithRespectInternals(parseResult.GetValue(respectInternalsOption)) + .WithIncludeAssemblyAttributes(parseResult.GetValue(includeAssemblyAttributesOption)) + .Build(); + + GenAPIApp.Run(c); }); return rootCommand.Parse(args).Invoke(); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs index c4a287843beb..7c186dad1526 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; @@ -14,8 +13,6 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Simplification; using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; -using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.GenAPI.SyntaxRewriter; namespace Microsoft.DotNet.GenAPI @@ -23,48 +20,34 @@ namespace Microsoft.DotNet.GenAPI /// /// Processes assembly symbols to build corresponding structures in C# language. /// - public sealed class CSharpFileBuilder : IAssemblySymbolWriter, IDisposable + public sealed class CSharpFileBuilder : IAssemblySymbolWriter { - private readonly ILog _logger; + private readonly GenApiAppConfiguration _c; private readonly TextWriter _textWriter; - private readonly ISymbolFilter _symbolFilter; - private readonly ISymbolFilter _attributeDataSymbolFilter; - private readonly string? _exceptionMessage; - private readonly bool _includeAssemblyAttributes; private readonly AdhocWorkspace _adhocWorkspace; private readonly SyntaxGenerator _syntaxGenerator; - private readonly IEnumerable _metadataReferences; - - public CSharpFileBuilder(ILog logger, - ISymbolFilter symbolFilter, - ISymbolFilter attributeDataSymbolFilter, - TextWriter textWriter, - string? exceptionMessage, - bool includeAssemblyAttributes, - IEnumerable metadataReferences) + + public CSharpFileBuilder(GenApiAppConfiguration configuration, TextWriter textWriter) { - _logger = logger; + _c = configuration; _textWriter = textWriter; - _symbolFilter = symbolFilter; - _attributeDataSymbolFilter = attributeDataSymbolFilter; - _exceptionMessage = exceptionMessage; - _includeAssemblyAttributes = includeAssemblyAttributes; _adhocWorkspace = new AdhocWorkspace(); _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); - _metadataReferences = metadataReferences; } /// public void WriteAssembly(IAssemblySymbol assemblySymbol) { + _textWriter.Write(_c.Header); + CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable); Project project = _adhocWorkspace.AddProject(ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), assemblySymbol.Name, assemblySymbol.Name, LanguageNames.CSharp, compilationOptions: compilationOptions)); - project = project.AddMetadataReferences(_metadataReferences); + project = project.AddMetadataReferences(_c.Loader.MetadataReferences); - IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_symbolFilter.Include); + IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_c.SymbolFilter.Include); List namespaceSyntaxNodes = []; foreach (INamespaceSymbol namespaceSymbol in namespaceSymbols.Order()) @@ -80,9 +63,9 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes) .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation) .Rewrite(new TypeDeclarationCSharpSyntaxRewriter()) - .Rewrite(new BodyBlockCSharpSyntaxRewriter(_exceptionMessage)); + .Rewrite(new BodyBlockCSharpSyntaxRewriter(_c.ExceptionMessage)); - if (_includeAssemblyAttributes) + if (_c.IncludeAssemblyAttributes) { compilationUnit = GenerateAssemblyAttributes(assemblySymbol, compilationUnit); } @@ -103,7 +86,7 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) { SyntaxNode namespaceNode = _syntaxGenerator.NamespaceDeclaration(namespaceSymbol.ToDisplayString()); - IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_symbolFilter.Include); + IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_c.SymbolFilter.Include); if (!typeMembers.Any()) { return null; @@ -112,8 +95,8 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) foreach (INamedTypeSymbol typeMember in typeMembers.Order()) { SyntaxNode typeDeclaration = _syntaxGenerator - .DeclarationExt(typeMember, _symbolFilter) - .AddMemberAttributes(_syntaxGenerator, typeMember, _attributeDataSymbolFilter); + .DeclarationExt(typeMember, _c.SymbolFilter) + .AddMemberAttributes(_syntaxGenerator, typeMember, _c.AttributeDataSymbolFilter); typeDeclaration = Visit(typeDeclaration, typeMember); @@ -148,7 +131,7 @@ private bool HidesBaseMember(ISymbol member) // If they're methods, compare their names and signatures. return baseType.GetMembers(member.Name) - .Any(baseMember => _symbolFilter.Include(baseMember) && + .Any(baseMember => _c.SymbolFilter.Include(baseMember) && (baseMember.Kind != SymbolKind.Method || method.SignatureEquals((IMethodSymbol)baseMember))); } @@ -157,7 +140,7 @@ private bool HidesBaseMember(ISymbol member) // If they're indexers, compare their signatures. return baseType.GetMembers(member.Name) .Any(baseMember => baseMember is IPropertySymbol baseProperty && - _symbolFilter.Include(baseMember) && + _c.SymbolFilter.Include(baseMember) && (prop.GetMethod.SignatureEquals(baseProperty.GetMethod) || prop.SetMethod.SignatureEquals(baseProperty.SetMethod))); } @@ -165,21 +148,21 @@ private bool HidesBaseMember(ISymbol member) { // For all other kinds of members, compare their names. return baseType.GetMembers(member.Name) - .Any(_symbolFilter.Include); + .Any(_c.SymbolFilter.Include); } } private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) { - IEnumerable members = namedType.GetMembers().Where(_symbolFilter.Include); + IEnumerable members = namedType.GetMembers().Where(_c.SymbolFilter.Include); // If it's a value type if (namedType.TypeKind == TypeKind.Struct) { - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_symbolFilter, _attributeDataSymbolFilter)); + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_c.SymbolFilter, _c.AttributeDataSymbolFilter)); } - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_symbolFilter)); + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_c.SymbolFilter)); foreach (ISymbol member in members.Order()) { @@ -187,15 +170,15 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) { // If the method is ExplicitInterfaceImplementation and is derived from an interface that was filtered out, we must filter out it as well. if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && - method.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol) || + method.ExplicitInterfaceImplementations.Any(m => !_c.SymbolFilter.Include(m.ContainingSymbol) || // if explicit interface implementation method has inaccessible type argument - m.ContainingType.HasInaccessibleTypeArgument(_symbolFilter))) + m.ContainingType.HasInaccessibleTypeArgument(_c.SymbolFilter))) { continue; } // Filter out default constructors since these will be added automatically - if (method.IsImplicitDefaultConstructor(_symbolFilter)) + if (method.IsImplicitDefaultConstructor(_c.SymbolFilter)) { continue; } @@ -203,14 +186,14 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) // If the property is derived from an interface that was filter out, we must filtered out it either. if (member is IPropertySymbol property && !property.ExplicitInterfaceImplementations.IsEmpty && - property.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol))) + property.ExplicitInterfaceImplementations.Any(m => !_c.SymbolFilter.Include(m.ContainingSymbol))) { continue; } SyntaxNode memberDeclaration = _syntaxGenerator - .DeclarationExt(member, _symbolFilter) - .AddMemberAttributes(_syntaxGenerator, member, _attributeDataSymbolFilter); + .DeclarationExt(member, _c.SymbolFilter) + .AddMemberAttributes(_syntaxGenerator, member, _c.AttributeDataSymbolFilter); if (member is INamedTypeSymbol nestedTypeSymbol) { @@ -243,7 +226,7 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) { // When assembly references aren't available, assembly attributes with foreign types won't be resolved. - ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_attributeDataSymbolFilter); + ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_c.AttributeDataSymbolFilter); // Emit assembly attributes from the IAssemblySymbol List attributeSyntaxNodes = attributes @@ -280,7 +263,7 @@ private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNo private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) { - foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_symbolFilter.Include)) + foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_c.SymbolFilter.Include)) { if (symbol.TypeKind != TypeKind.Error) { @@ -293,7 +276,7 @@ private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assem } else { - _logger.LogWarning(string.Format( + _c.Logger.LogWarning(string.Format( Resources.ResolveTypeForwardFailed, symbol.ToDisplayString(), $"{symbol.ContainingAssembly.Name}.dll")); @@ -341,8 +324,5 @@ private OptionSet DefineFormattingOptions() .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInAnonymousTypes, false) .WithChangedOption(CSharpFormattingOptions.NewLineForClausesInQuery, false); } - - /// - public void Dispose() => _textWriter.Dispose(); } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index 51bf68c42017..268fb1113116 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -6,9 +6,7 @@ #endif using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; -using Microsoft.DotNet.GenAPI.Filtering; namespace Microsoft.DotNet.GenAPI { @@ -21,84 +19,32 @@ public static class GenAPIApp /// /// Initialize and run Roslyn-based GenAPI tool. /// - public static void Run(ILog logger, - string[] assemblies, - string[]? assemblyReferences, - string? outputPath, - string? headerFile, - string? exceptionMessage, - string[]? excludeApiFiles, - string[]? excludeAttributesFiles, - bool respectInternals, - bool includeAssemblyAttributes) + public static void Run(GenApiAppConfiguration c) { - bool resolveAssemblyReferences = assemblyReferences?.Length > 0; - - // Create, configure and execute the assembly loader. - AssemblySymbolLoader loader = new(resolveAssemblyReferences, respectInternals); - if (assemblyReferences is not null) - { - loader.AddReferenceSearchPaths(assemblyReferences); - } - IReadOnlyList assemblySymbols = loader.LoadAssemblies(assemblies); - - string headerFileText = ReadHeaderFile(headerFile); - - AccessibilitySymbolFilter accessibilitySymbolFilter = new( - respectInternals, - includeEffectivelyPrivateSymbols: true, - includeExplicitInterfaceImplementationSymbols: true); - - // Configure the symbol filter - CompositeSymbolFilter symbolFilter = new(); - if (excludeApiFiles is not null) - { - symbolFilter.Add(new DocIdSymbolFilter(excludeApiFiles)); - } - symbolFilter.Add(new ImplicitSymbolFilter()); - symbolFilter.Add(accessibilitySymbolFilter); - - // Configure the attribute data symbol filter - CompositeSymbolFilter attributeDataSymbolFilter = new(); - if (excludeAttributesFiles is not null) - { - attributeDataSymbolFilter.Add(new DocIdSymbolFilter(excludeAttributesFiles)); - } - attributeDataSymbolFilter.Add(accessibilitySymbolFilter); - - // Invoke the CSharpFileBuilder for each directly loaded assembly. - foreach (IAssemblySymbol? assemblySymbol in assemblySymbols) + // Invoke an assembly symbol writer for each directly loaded assembly. + foreach (IAssemblySymbol? assemblySymbol in c.AssemblySymbols) { if (assemblySymbol is null) continue; - using TextWriter textWriter = GetTextWriter(outputPath, assemblySymbol.Name); - textWriter.Write(headerFileText); - - using CSharpFileBuilder fileBuilder = new(logger, - symbolFilter, - attributeDataSymbolFilter, - textWriter, - exceptionMessage, - includeAssemblyAttributes, - loader.MetadataReferences); - - fileBuilder.WriteAssembly(assemblySymbol); + using TextWriter textWriter = GetTextWriter(c.OutputPath, assemblySymbol.Name); + IAssemblySymbolWriter writer = new CSharpFileBuilder(c, textWriter); + writer.WriteAssembly(assemblySymbol); } - if (loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) + if (c.Loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) { foreach (Diagnostic warning in roslynDiagnostics) { - logger.LogWarning(warning.Id, warning.ToString()); + c.Logger.LogWarning(warning.Id, warning.ToString()); } } - if (loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) + if (c.Loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) { foreach (AssemblyLoadWarning warning in loadWarnings) { - logger.LogWarning(warning.DiagnosticId, warning.Message); + c.Logger.LogWarning(warning.DiagnosticId, warning.Message); } } } @@ -119,33 +65,5 @@ private static TextWriter GetTextWriter(string? outputDirPath, string assemblyNa return File.CreateText(outputDirPath); } - - // Read the header file if specified, or use default one. - private static string ReadHeaderFile(string? headerFile) - { - const string defaultFileHeader = """ - //------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - //------------------------------------------------------------------------------ - - """; - - string header = !string.IsNullOrEmpty(headerFile) ? - File.ReadAllText(headerFile) : - defaultFileHeader; - -#if NET - header = header.ReplaceLineEndings(); -#else - header = Regex.Replace(header, @"\r\n|\n\r|\n|\r", Environment.NewLine); -#endif - - return header; - } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs new file mode 100644 index 000000000000..260b27f87007 --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs @@ -0,0 +1,349 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET +using System.Text.RegularExpressions; +#endif +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.GenAPI.Filtering; + +namespace Microsoft.DotNet.GenAPI; + +public enum OutputType +{ + Console, + Files, + Diff +} + +public class GenApiAppConfiguration +{ + private GenApiAppConfiguration() + { + } + + [AllowNull] + public ILog Logger { get; private set; } + public OutputType OutputType { get; private set; } = OutputType.Console; + public string? OutputPath { get; private set; } + [AllowNull] + public string Header { get; private set; } + public string? ExceptionMessage { get; private set; } + public bool IncludeAssemblyAttributes { get; private set; } + [AllowNull] + public AssemblySymbolLoader Loader { get; private set; } + [AllowNull] + public IReadOnlyList AssemblySymbols { get; private set; } + [AllowNull] + public CompositeSymbolFilter SymbolFilter { get; private set; } + [AllowNull] + public CompositeSymbolFilter AttributeDataSymbolFilter { get; private set; } + + public static Builder GetBuilder() => new Builder(); + + public class Builder + { + private ILog? _logger = null; + private string[]? _assembliesPaths = null; + private string[]? _assemblyReferencesPaths = null; + private OutputType _outputType = OutputType.Console; + private string? _outputPath = null; + private (string, Stream)[]? _assemblyStreams; + private string? _header = null; + private string? _exceptionMessage = null; + private string[]? _apiExclusionFilePaths = null; + private string[]? _attributeExclusionFilePaths = null; + bool _includeEffectivelyPrivateSymbols = true; + bool _includeExplicitInterfaceImplementationSymbols = true; + bool _respectInternals = false; + bool _includeAssemblyAttributes = false; + + public Builder WithLogger(ILog logger) + { + _logger = logger; + return this; + } + + public Builder WithAssembliesPaths(params string[]? assembliesPaths) + { + if (assembliesPaths == null) + { + return this; + } + if (_assemblyStreams != null) + { + throw new InvalidOperationException("Cannot specify both assembly paths and streams."); + } + foreach (string path in assembliesPaths) + { + ThrowIfFileSystemEntryNotFound(nameof(assembliesPaths), path); + } + _assembliesPaths = assembliesPaths; + return this; + } + + public Builder WithAssemblyStreams(params (string, Stream)[]? assemblyStreams) + { + if (assemblyStreams == null) + { + return this; + } + if (_assembliesPaths != null) + { + throw new InvalidOperationException("Cannot specify both assembly paths and streams."); + } + + _assemblyStreams = assemblyStreams; + return this; + } + + public Builder WithAssemblyReferencesPaths(params string[]? assemblyReferencesPaths) + { + if (assemblyReferencesPaths == null) + { + return this; + } + foreach (string path in assemblyReferencesPaths) + { + ThrowIfFileSystemEntryNotFound(nameof(assemblyReferencesPaths), path); + } + _assemblyReferencesPaths = assemblyReferencesPaths; + return this; + } + + public Builder WithOutputPath(string? outputPath) + { + if (outputPath == null) + { + return this; + } + ThrowIfDirectoryNotFound(nameof(outputPath), outputPath); + _outputPath = outputPath; + _outputType = OutputType.Files; + return this; + } + + public Builder WithHeaderFilePath(string? headerFilePath) + { + if (headerFilePath == null) + { + return this; + } + ThrowIfFileNotFound(nameof(headerFilePath), headerFilePath); + return WithHeader(File.ReadAllText(headerFilePath)); + } + + public Builder WithHeader(string? header) + { + if (header == null) + { + return this; + } + _header = header; + return this; + } + + public Builder WithExceptionMessage(string? exceptionMessage) + { + if (exceptionMessage == null) + { + return this; + } + _exceptionMessage = exceptionMessage; + return this; + } + + public Builder WithApiExclusionFilePaths(params string[]? apiExclusionFilePaths) + { + if (apiExclusionFilePaths == null) + { + return this; + } + + foreach (string path in apiExclusionFilePaths) + { + ThrowIfPathIsDirectory(nameof(apiExclusionFilePaths), path); + } + _apiExclusionFilePaths = apiExclusionFilePaths; + return this; + } + + public Builder WithAttributeExclusionFilePaths(params string[]? attributeExclusionFilePaths) + { + if (attributeExclusionFilePaths == null) + { + return this; + } + + foreach (string path in attributeExclusionFilePaths) + { + ThrowIfPathIsDirectory(nameof(attributeExclusionFilePaths), path); + } + _attributeExclusionFilePaths = attributeExclusionFilePaths; + return this; + } + + public Builder WithIncludeEffectivelyPrivateSymbols(bool includeEffectivelyPrivateSymbols) + { + _includeEffectivelyPrivateSymbols = includeEffectivelyPrivateSymbols; + return this; + } + + public Builder WithIncludeExplicitInterfaceImplementationSymbols(bool includeExplicitInterfaceImplementationSymbols) + { + _includeExplicitInterfaceImplementationSymbols = includeExplicitInterfaceImplementationSymbols; + return this; + } + + public Builder WithRespectInternals(bool respectInternals) + { + _respectInternals = respectInternals; + return this; + } + + public Builder WithIncludeAssemblyAttributes(bool includeAssemblyAttributes) + { + _includeAssemblyAttributes = includeAssemblyAttributes; + return this; + } + + public GenApiAppConfiguration Build() + { + AssemblySymbolLoader loader; + IReadOnlyList assemblySymbols; + + if (_assembliesPaths?.Length > 0) + { + bool resolveAssemblyReferences = _assemblyReferencesPaths?.Count() > 0; + loader = new(resolveAssemblyReferences, _respectInternals); + if (_assemblyReferencesPaths?.Count() > 0) + { + loader.AddReferenceSearchPaths(_assemblyReferencesPaths); + } + assemblySymbols = loader.LoadAssemblies(_assembliesPaths); + } + else if (_assemblyStreams?.Count() > 0) + { + loader = new(resolveAssemblyReferences: true, includeInternalSymbols: _respectInternals); + loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); + loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); + List symbols = []; + foreach ((string assemblyName, Stream assemblyStream) in _assemblyStreams) + { + if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) + { + symbols.Add(assemblySymbol); + } + } + assemblySymbols = symbols.AsReadOnly(); + } + else + { + throw new InvalidOperationException("No assemblies were specified, either from files or from streams."); + } + + AccessibilitySymbolFilter accessibilitySymbolFilter = new( + _respectInternals, + includeEffectivelyPrivateSymbols: _includeEffectivelyPrivateSymbols, + includeExplicitInterfaceImplementationSymbols: _includeExplicitInterfaceImplementationSymbols); + + // Configure the symbol filter + CompositeSymbolFilter symbolFilter = new(); + if (_apiExclusionFilePaths?.Count() > 0) + { + symbolFilter.Add(DocIdSymbolFilter.GetFilterForDocIds(_apiExclusionFilePaths)); + } + symbolFilter.Add(new ImplicitSymbolFilter()); + symbolFilter.Add(accessibilitySymbolFilter); + + // Configure the attribute data symbol filter + CompositeSymbolFilter attributeDataSymbolFilter = new(); + if (_attributeExclusionFilePaths?.Count() > 0) + { + attributeDataSymbolFilter.Add(DocIdSymbolFilter.GetFilterForDocIds(_attributeExclusionFilePaths)); + } + attributeDataSymbolFilter.Add(accessibilitySymbolFilter); + + return new GenApiAppConfiguration() + { + Logger = _logger ?? new ConsoleLog(MessageImportance.Normal), + OutputType = _outputType, + OutputPath = _outputPath, + Header = GetHeader(), + ExceptionMessage = _exceptionMessage, + IncludeAssemblyAttributes = _includeAssemblyAttributes, + Loader = loader, + AssemblySymbols = assemblySymbols, + SymbolFilter = symbolFilter, + AttributeDataSymbolFilter = attributeDataSymbolFilter + }; + + } + + private string GetHeader() + { + const string defaultFileHeader = """ + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + """; + + if (_header != null) + { +#if NET + _header = header.ReplaceLineEndings(); +#else + _header = Regex.Replace(_header, @"\r\n|\n\r|\n|\r", Environment.NewLine); +#endif + return _header; + } + + return defaultFileHeader; + } + + private void ThrowIfFileNotFound(string argumentName, string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"The {argumentName} file was not found: {filePath}"); + } + } + + private void ThrowIfDirectoryNotFound(string argumentName, string directoryPath) + { + if (!Directory.Exists(directoryPath)) + { + throw new DirectoryNotFoundException($"The {argumentName} directory was not found: {directoryPath}"); + } + } + + private void ThrowIfPathIsDirectory(string argumentName, string path) + { + if (Directory.Exists(path)) + { + throw new DirectoryNotFoundException($"The {argumentName} is a directory, not a file: {path}"); + } + } + + private void ThrowIfFileSystemEntryNotFound(string argumentName, string path) + { + if (Directory.Exists(path) || File.Exists(path)) + { + return; + } + + throw new FileNotFoundException($"The {argumentName} is not a valid file or directory: {path}"); + } + } +} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs index 1201e4db2336..c6ed188a2426 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Transactions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs index 3bb8ba16fac6..2642f5df3920 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs @@ -9,9 +9,15 @@ namespace Microsoft.DotNet.ApiSymbolExtensions.Filtering /// Implements the logic of filtering out api. /// Reads the file with the list of attributes, types, members in DocId format. /// - public class DocIdSymbolFilter(string[] docIdsToExcludeFiles) : ISymbolFilter + public class DocIdSymbolFilter : ISymbolFilter { - private readonly HashSet _docIdsToExclude = new(ReadDocIdsAttributes(docIdsToExcludeFiles)); + private readonly HashSet _docIdsToExclude; + + public static DocIdSymbolFilter GetFilterForDocIds(params string[] docIdsToExclude) + => new DocIdSymbolFilter(ReadDocIdsAttributes(docIdsToExclude)); + + private DocIdSymbolFilter(IEnumerable docIdsToExclude) + => _docIdsToExclude = [.. docIdsToExclude]; /// /// Determines whether the should be included. @@ -29,10 +35,15 @@ public bool Include(ISymbol symbol) return true; } - private static IEnumerable ReadDocIdsAttributes(IEnumerable docIdsToExcludeFiles) + private static IEnumerable ReadDocIdsAttributes(params string[] docIdsToExcludeFiles) { foreach (string docIdsToExcludeFile in docIdsToExcludeFiles) { + if (string.IsNullOrWhiteSpace(docIdsToExcludeFile)) + { + continue; + } + foreach (string id in File.ReadAllLines(docIdsToExcludeFile)) { #if NET diff --git a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs index 11d9b98aea4f..53c9fca44297 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs @@ -20,7 +20,7 @@ public class AttributesMustMatchTests * - ReturnValues * - Constructors * - Generic Parameters - * + * * Grouped into: * - Type * - Member @@ -29,7 +29,7 @@ public class AttributesMustMatchTests private static readonly TestRuleFactory s_ruleFactory = new((settings, context) => new AttributesMustMatch(settings, context)); private static ISymbolFilter GetAccessibilityAndAttributeSymbolFiltersAsComposite(params string[] excludeAttributeFiles) => - new CompositeSymbolFilter().Add(new AccessibilitySymbolFilter(false)).Add(new DocIdSymbolFilter(excludeAttributeFiles)); + new CompositeSymbolFilter().Add(new AccessibilitySymbolFilter(false)).Add(DocIdSymbolFilter.GetFilterForDocIds(excludeAttributeFiles)); public static TheoryData TypesCases => new() { @@ -39,7 +39,7 @@ private static ISymbolFilter GetAccessibilityAndAttributeSymbolFiltersAsComposit namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -56,7 +56,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -77,7 +77,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -94,7 +94,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -116,7 +116,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -133,7 +133,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -156,7 +156,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -173,7 +173,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -196,7 +196,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -212,7 +212,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -311,7 +311,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -337,7 +337,7 @@ public void F() {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -370,7 +370,7 @@ public void F() {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -396,7 +396,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -429,7 +429,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -457,7 +457,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -492,7 +492,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -518,7 +518,7 @@ public First() {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -551,7 +551,7 @@ public First() {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -577,7 +577,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -610,7 +610,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -634,7 +634,7 @@ public void F([Bar] int v, [Foo(""S"", A = true, B = 0)] string s) {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -666,7 +666,7 @@ public void F([Baz] int v, [Foo(""T"")] string s) {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -687,7 +687,7 @@ public class First<[Bar] T1, [Foo(""S"", A = true, B = 0)] T2> {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -716,7 +716,7 @@ public class First<[Baz] T1, [Foo(""T"")] T2> {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -740,7 +740,7 @@ public class First { namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -792,7 +792,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -815,7 +815,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -832,7 +832,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -1366,7 +1366,7 @@ public void TestExclusionsFilteredOut() namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -1382,7 +1382,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -1418,7 +1418,7 @@ public void AttributesExcludedButMembersValidated() namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} @@ -1433,7 +1433,7 @@ public class First {} namespace CompatTests { using System; - + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] public class FooAttribute : Attribute { public FooAttribute(String s) {} diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index 68009dbd4f0e..e4450779a365 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -32,44 +32,26 @@ private void RunTest(string original, bool includeEffectivelyPrivateSymbols = true, bool includeExplicitInterfaceImplementationSymbols = true, bool allowUnsafe = false, + string excludeApiFile = null, string excludedAttributeFile = null, [CallerMemberName] string assemblyName = "") { - StringWriter stringWriter = new(); - - // Configure symbol filters - AccessibilitySymbolFilter accessibilitySymbolFilter = new( - includeInternalSymbols, - includeEffectivelyPrivateSymbols, - includeExplicitInterfaceImplementationSymbols); - - CompositeSymbolFilter symbolFilter = new CompositeSymbolFilter() - .Add(new ImplicitSymbolFilter()) - .Add(accessibilitySymbolFilter); - - CompositeSymbolFilter attributeDataSymbolFilter = new(); - if (excludedAttributeFile is not null) - { - attributeDataSymbolFilter.Add(new DocIdSymbolFilter(new string[] { excludedAttributeFile })); - } - attributeDataSymbolFilter.Add(accessibilitySymbolFilter); - - IAssemblySymbolWriter csharpFileBuilder = new CSharpFileBuilder( - new ConsoleLog(MessageImportance.Low), - symbolFilter, - attributeDataSymbolFilter, - stringWriter, - null, - false, - MetadataReferences); + using StringWriter stringWriter = new(); using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(original, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName); - AssemblySymbolLoader assemblySymbolLoader = new(resolveAssemblyReferences: true, includeInternalSymbols: includeInternalSymbols); - assemblySymbolLoader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); - assemblySymbolLoader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); - IAssemblySymbol assemblySymbol = assemblySymbolLoader.LoadAssembly(assemblyName, assemblyStream); - csharpFileBuilder.WriteAssembly(assemblySymbol); + GenApiAppConfiguration c = GenApiAppConfiguration.GetBuilder() + .WithLogger(new ConsoleLog(MessageImportance.Low)) + .WithAssemblyStreams((assemblyName, assemblyStream)) + .WithRespectInternals(includeInternalSymbols) + .WithIncludeEffectivelyPrivateSymbols(includeEffectivelyPrivateSymbols) + .WithIncludeExplicitInterfaceImplementationSymbols(includeExplicitInterfaceImplementationSymbols) + .WithApiExclusionFilePaths(excludeApiFile) + .WithAttributeExclusionFilePaths(excludedAttributeFile) + .Build(); + + IAssemblySymbolWriter writer = new CSharpFileBuilder(c, stringWriter); + writer.WriteAssembly(c.AssemblySymbols.First()); StringBuilder stringBuilder = stringWriter.GetStringBuilder(); string resultedString = stringBuilder.ToString(); @@ -231,7 +213,7 @@ public void TestRecordDeclaration() { RunTest(original: """ namespace Foo - { + { public record RecordClass; public record RecordClass1(int i); public record RecordClass2(string s, int i); @@ -240,7 +222,7 @@ public record DerivedRecord2(string x, int i, double d) : RecordClass2(default(s public record DerivedRecord3(string x, int i, double d) : RecordClass2(default(string)!, i); public record DerivedRecord4(double d) : RecordClass2(default(string)!, default); public record DerivedRecord5() : RecordClass2(default(string)!, default); - + public record RecordClassWithMethods(int i) { public void DoSomething() { } @@ -345,11 +327,11 @@ public void TestRecordStructDeclaration() RunTest(original: """ namespace Foo { - - public record struct RecordStruct; + + public record struct RecordStruct; public record struct RecordStruct1(int i); public record struct RecordStruct2(string s, int i); - + public record struct RecordStructWithMethods(int i) { public void DoSomething() { } @@ -367,10 +349,10 @@ public record struct RecordStructWithConstructors(int i) public RecordStructWithConstructors() : this(1) { } public RecordStructWithConstructors(string s) : this(int.Parse(s)) { } } - + } """, - expected: """ + expected: """ namespace Foo { public partial struct RecordStruct : System.IEquatable @@ -1644,12 +1626,12 @@ public class B { public B(int i) {} } - + public class C : B { internal C() : base(0) {} } - + public class D : B { internal D(int i) : base(i) {} @@ -1672,7 +1654,7 @@ public partial class B { public B(int i) {} } - + public partial class C : B { internal C() : base(default) {} @@ -1702,12 +1684,12 @@ public class B { public B(int i) {} } - + public class C : B { internal C() : base(0) {} } - + public class D : B { internal D(int i) : base(i) {} @@ -1781,8 +1763,8 @@ namespace A public partial class B { protected B() {} - } - + } + public partial class C : B { internal C() {} @@ -1935,7 +1917,7 @@ public class B : A public class D { } public class Id { } - + public class V { } } """, @@ -2828,7 +2810,7 @@ public class Foo : System.Collections.ICollection, System.Collections.Generic } } - + """, // https://github.com/dotnet/sdk/issues/32195 tracks interface expansion expected: """ @@ -2909,7 +2891,7 @@ namespace N { public ref struct C where T : unmanaged { - public required (string? k, dynamic v, nint n) X { get; init; } + public required (string? k, dynamic v, nint n) X { get; init; } } public static class E @@ -2918,7 +2900,7 @@ public static void M(this object c, scoped System.ReadOnlySpan values) { } } } """, - expected: """ + expected: """ namespace N { public ref partial struct C @@ -2982,7 +2964,7 @@ public void TestExplicitInterfaceNonGenericCollections() namespace a { #pragma warning disable CS8597 - + public partial class MyStringCollection : ICollection, IEnumerable, IList { public int Count { get { throw null; } } @@ -3006,7 +2988,7 @@ public void RemoveAt(int index) { } void ICollection.CopyTo(Array array, int index) { } IEnumerator IEnumerable.GetEnumerator() { throw null; } int IList.Add(object? value) { throw null; } - bool IList.Contains(object? value) { throw null; } + bool IList.Contains(object? value) { throw null; } int IList.IndexOf(object? value) { throw null; } void IList.Insert(int index, object? value) { } void IList.Remove(object? value) { } @@ -3015,7 +2997,7 @@ void IList.Remove(object? value) { } #pragma warning restore CS8597 } """, - expected: """ + expected: """ namespace a { public partial class MyStringCollection : System.Collections.ICollection, System.Collections.IEnumerable, System.Collections.IList From bc754a635f6b6081e99ef97adf4e265189471e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:20:49 -0800 Subject: [PATCH 02/11] Pass to CSharpFileBuilder constructor only the necessary arguments instead of the whole config. Rename the config instance variables to something more meaningful instead of c or _c. Fix the build failure happening under #if NET. --- .../GenAPITask.cs | 4 +- .../Microsoft.DotNet.GenAPI.Tool/Program.cs | 4 +- .../CSharpFileBuilder.cs | 73 ++++++++++++------- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 24 +++--- ...onfiguration.cs => GenAPIConfiguration.cs} | 10 +-- .../CSharpFileBuilderTests.cs | 15 ++-- 6 files changed, 81 insertions(+), 49 deletions(-) rename src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/{GenAPIAppConfiguration.cs => GenAPIConfiguration.cs} (98%) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs index 68be887ece01..9de86051d036 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs @@ -62,7 +62,7 @@ public class GenAPITask : TaskBase /// protected override void ExecuteCore() { - GenApiAppConfiguration c = GenApiAppConfiguration.GetBuilder() + GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() .WithLogger(new MSBuildLog(Log)) .WithAssembliesPaths(Assemblies) .WithAssemblyReferencesPaths(AssemblyReferences) @@ -75,7 +75,7 @@ protected override void ExecuteCore() .WithIncludeAssemblyAttributes(IncludeAssemblyAttributes) .Build(); - GenAPIApp.Run(c); + GenAPIApp.Run(config); } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs index 57b9e4c38896..6c6ed5e870b7 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs @@ -97,7 +97,7 @@ static int Main(string[] args) rootCommand.SetAction((ParseResult parseResult) => { - GenApiAppConfiguration c = GenApiAppConfiguration.GetBuilder() + GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() .WithLogger(new ConsoleLog(MessageImportance.Normal)) .WithAssembliesPaths(parseResult.GetValue(assembliesOption)!) .WithAssemblyReferencesPaths(parseResult.GetValue(assemblyReferencesOption)) @@ -110,7 +110,7 @@ static int Main(string[] args) .WithIncludeAssemblyAttributes(parseResult.GetValue(includeAssemblyAttributesOption)) .Build(); - GenAPIApp.Run(c); + GenAPIApp.Run(config); }); return rootCommand.Parse(args).Invoke(); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs index 7c186dad1526..72c3595dd43b 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -13,6 +13,8 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Simplification; using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.GenAPI.SyntaxRewriter; namespace Microsoft.DotNet.GenAPI @@ -22,15 +24,34 @@ namespace Microsoft.DotNet.GenAPI /// public sealed class CSharpFileBuilder : IAssemblySymbolWriter { - private readonly GenApiAppConfiguration _c; + private readonly ILog _logger; private readonly TextWriter _textWriter; + private readonly AssemblySymbolLoader _loader; + private readonly CompositeSymbolFilter _symbolFilter; + private readonly CompositeSymbolFilter _attributeDataSymbolFilter; + private readonly string _header; + private readonly string? _exceptionMessage; + private readonly bool _includeAssemblyAttributes; private readonly AdhocWorkspace _adhocWorkspace; private readonly SyntaxGenerator _syntaxGenerator; - public CSharpFileBuilder(GenApiAppConfiguration configuration, TextWriter textWriter) + public CSharpFileBuilder(ILog logger, + TextWriter textWriter, + AssemblySymbolLoader loader, + CompositeSymbolFilter symbolFilter, + CompositeSymbolFilter attributeDataSymbolFilter, + string header, + string? exceptionMessage, + bool includeAssemblyAttributes) { - _c = configuration; + _logger = logger; _textWriter = textWriter; + _loader = loader; + _symbolFilter = symbolFilter; + _attributeDataSymbolFilter = attributeDataSymbolFilter; + _header = header; + _exceptionMessage = exceptionMessage; + _includeAssemblyAttributes = includeAssemblyAttributes; _adhocWorkspace = new AdhocWorkspace(); _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); } @@ -38,16 +59,16 @@ public CSharpFileBuilder(GenApiAppConfiguration configuration, TextWriter textWr /// public void WriteAssembly(IAssemblySymbol assemblySymbol) { - _textWriter.Write(_c.Header); + _textWriter.Write(_header); CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable); Project project = _adhocWorkspace.AddProject(ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), assemblySymbol.Name, assemblySymbol.Name, LanguageNames.CSharp, compilationOptions: compilationOptions)); - project = project.AddMetadataReferences(_c.Loader.MetadataReferences); + project = project.AddMetadataReferences(_loader.MetadataReferences); - IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_c.SymbolFilter.Include); + IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_symbolFilter.Include); List namespaceSyntaxNodes = []; foreach (INamespaceSymbol namespaceSymbol in namespaceSymbols.Order()) @@ -63,9 +84,9 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes) .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation) .Rewrite(new TypeDeclarationCSharpSyntaxRewriter()) - .Rewrite(new BodyBlockCSharpSyntaxRewriter(_c.ExceptionMessage)); + .Rewrite(new BodyBlockCSharpSyntaxRewriter(_exceptionMessage)); - if (_c.IncludeAssemblyAttributes) + if (_includeAssemblyAttributes) { compilationUnit = GenerateAssemblyAttributes(assemblySymbol, compilationUnit); } @@ -86,7 +107,7 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) { SyntaxNode namespaceNode = _syntaxGenerator.NamespaceDeclaration(namespaceSymbol.ToDisplayString()); - IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_c.SymbolFilter.Include); + IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_symbolFilter.Include); if (!typeMembers.Any()) { return null; @@ -95,8 +116,8 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) foreach (INamedTypeSymbol typeMember in typeMembers.Order()) { SyntaxNode typeDeclaration = _syntaxGenerator - .DeclarationExt(typeMember, _c.SymbolFilter) - .AddMemberAttributes(_syntaxGenerator, typeMember, _c.AttributeDataSymbolFilter); + .DeclarationExt(typeMember, _symbolFilter) + .AddMemberAttributes(_syntaxGenerator, typeMember, _attributeDataSymbolFilter); typeDeclaration = Visit(typeDeclaration, typeMember); @@ -131,7 +152,7 @@ private bool HidesBaseMember(ISymbol member) // If they're methods, compare their names and signatures. return baseType.GetMembers(member.Name) - .Any(baseMember => _c.SymbolFilter.Include(baseMember) && + .Any(baseMember => _symbolFilter.Include(baseMember) && (baseMember.Kind != SymbolKind.Method || method.SignatureEquals((IMethodSymbol)baseMember))); } @@ -140,7 +161,7 @@ private bool HidesBaseMember(ISymbol member) // If they're indexers, compare their signatures. return baseType.GetMembers(member.Name) .Any(baseMember => baseMember is IPropertySymbol baseProperty && - _c.SymbolFilter.Include(baseMember) && + _symbolFilter.Include(baseMember) && (prop.GetMethod.SignatureEquals(baseProperty.GetMethod) || prop.SetMethod.SignatureEquals(baseProperty.SetMethod))); } @@ -148,21 +169,21 @@ private bool HidesBaseMember(ISymbol member) { // For all other kinds of members, compare their names. return baseType.GetMembers(member.Name) - .Any(_c.SymbolFilter.Include); + .Any(_symbolFilter.Include); } } private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) { - IEnumerable members = namedType.GetMembers().Where(_c.SymbolFilter.Include); + IEnumerable members = namedType.GetMembers().Where(_symbolFilter.Include); // If it's a value type if (namedType.TypeKind == TypeKind.Struct) { - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_c.SymbolFilter, _c.AttributeDataSymbolFilter)); + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_symbolFilter, _attributeDataSymbolFilter)); } - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_c.SymbolFilter)); + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_symbolFilter)); foreach (ISymbol member in members.Order()) { @@ -170,15 +191,15 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) { // If the method is ExplicitInterfaceImplementation and is derived from an interface that was filtered out, we must filter out it as well. if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && - method.ExplicitInterfaceImplementations.Any(m => !_c.SymbolFilter.Include(m.ContainingSymbol) || + method.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol) || // if explicit interface implementation method has inaccessible type argument - m.ContainingType.HasInaccessibleTypeArgument(_c.SymbolFilter))) + m.ContainingType.HasInaccessibleTypeArgument(_symbolFilter))) { continue; } // Filter out default constructors since these will be added automatically - if (method.IsImplicitDefaultConstructor(_c.SymbolFilter)) + if (method.IsImplicitDefaultConstructor(_symbolFilter)) { continue; } @@ -186,14 +207,14 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) // If the property is derived from an interface that was filter out, we must filtered out it either. if (member is IPropertySymbol property && !property.ExplicitInterfaceImplementations.IsEmpty && - property.ExplicitInterfaceImplementations.Any(m => !_c.SymbolFilter.Include(m.ContainingSymbol))) + property.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol))) { continue; } SyntaxNode memberDeclaration = _syntaxGenerator - .DeclarationExt(member, _c.SymbolFilter) - .AddMemberAttributes(_syntaxGenerator, member, _c.AttributeDataSymbolFilter); + .DeclarationExt(member, _symbolFilter) + .AddMemberAttributes(_syntaxGenerator, member, _attributeDataSymbolFilter); if (member is INamedTypeSymbol nestedTypeSymbol) { @@ -226,7 +247,7 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) { // When assembly references aren't available, assembly attributes with foreign types won't be resolved. - ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_c.AttributeDataSymbolFilter); + ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_attributeDataSymbolFilter); // Emit assembly attributes from the IAssemblySymbol List attributeSyntaxNodes = attributes @@ -263,7 +284,7 @@ private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNo private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) { - foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_c.SymbolFilter.Include)) + foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_symbolFilter.Include)) { if (symbol.TypeKind != TypeKind.Error) { @@ -276,7 +297,7 @@ private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assem } else { - _c.Logger.LogWarning(string.Format( + _logger.LogWarning(string.Format( Resources.ResolveTypeForwardFailed, symbol.ToDisplayString(), $"{symbol.ContainingAssembly.Name}.dll")); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index 268fb1113116..679f770461ec 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -6,7 +6,6 @@ #endif using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.GenAPI { @@ -19,32 +18,39 @@ public static class GenAPIApp /// /// Initialize and run Roslyn-based GenAPI tool. /// - public static void Run(GenApiAppConfiguration c) + public static void Run(GenAPIConfiguration config) { // Invoke an assembly symbol writer for each directly loaded assembly. - foreach (IAssemblySymbol? assemblySymbol in c.AssemblySymbols) + foreach (IAssemblySymbol? assemblySymbol in config.AssemblySymbols) { if (assemblySymbol is null) continue; - using TextWriter textWriter = GetTextWriter(c.OutputPath, assemblySymbol.Name); - IAssemblySymbolWriter writer = new CSharpFileBuilder(c, textWriter); + using TextWriter textWriter = GetTextWriter(config.OutputPath, assemblySymbol.Name); + IAssemblySymbolWriter writer = new CSharpFileBuilder(config.Logger, + textWriter, + config.Loader, + config.SymbolFilter, + config.AttributeDataSymbolFilter, + config.Header, + config.ExceptionMessage, + config.IncludeAssemblyAttributes); writer.WriteAssembly(assemblySymbol); } - if (c.Loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) + if (config.Loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) { foreach (Diagnostic warning in roslynDiagnostics) { - c.Logger.LogWarning(warning.Id, warning.ToString()); + config.Logger.LogWarning(warning.Id, warning.ToString()); } } - if (c.Loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) + if (config.Loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) { foreach (AssemblyLoadWarning warning in loadWarnings) { - c.Logger.LogWarning(warning.DiagnosticId, warning.Message); + config.Logger.LogWarning(warning.DiagnosticId, warning.Message); } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs similarity index 98% rename from src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs rename to src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs index 260b27f87007..e0ebadecb49a 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIAppConfiguration.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs @@ -21,9 +21,9 @@ public enum OutputType Diff } -public class GenApiAppConfiguration +public class GenAPIConfiguration { - private GenApiAppConfiguration() + private GenAPIConfiguration() { } @@ -212,7 +212,7 @@ public Builder WithIncludeAssemblyAttributes(bool includeAssemblyAttributes) return this; } - public GenApiAppConfiguration Build() + public GenAPIConfiguration Build() { AssemblySymbolLoader loader; IReadOnlyList assemblySymbols; @@ -269,7 +269,7 @@ public GenApiAppConfiguration Build() } attributeDataSymbolFilter.Add(accessibilitySymbolFilter); - return new GenApiAppConfiguration() + return new GenAPIConfiguration() { Logger = _logger ?? new ConsoleLog(MessageImportance.Normal), OutputType = _outputType, @@ -302,7 +302,7 @@ private string GetHeader() if (_header != null) { #if NET - _header = header.ReplaceLineEndings(); + _header = _header.ReplaceLineEndings(); #else _header = Regex.Replace(_header, @"\r\n|\n\r|\n|\r", Environment.NewLine); #endif diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index e4450779a365..458ef5361fe3 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -6,11 +6,9 @@ using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.ApiSymbolExtensions.Tests; -using Microsoft.DotNet.GenAPI.Filtering; namespace Microsoft.DotNet.GenAPI.Tests { @@ -40,7 +38,7 @@ private void RunTest(string original, using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(original, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName); - GenApiAppConfiguration c = GenApiAppConfiguration.GetBuilder() + GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() .WithLogger(new ConsoleLog(MessageImportance.Low)) .WithAssemblyStreams((assemblyName, assemblyStream)) .WithRespectInternals(includeInternalSymbols) @@ -50,8 +48,15 @@ private void RunTest(string original, .WithAttributeExclusionFilePaths(excludedAttributeFile) .Build(); - IAssemblySymbolWriter writer = new CSharpFileBuilder(c, stringWriter); - writer.WriteAssembly(c.AssemblySymbols.First()); + IAssemblySymbolWriter writer = new CSharpFileBuilder(config.Logger, + stringWriter, + config.Loader, + config.SymbolFilter, + config.AttributeDataSymbolFilter, + config.Header, + config.ExceptionMessage, + config.IncludeAssemblyAttributes); + writer.WriteAssembly(config.AssemblySymbols.First()); StringBuilder stringBuilder = stringWriter.GetStringBuilder(); string resultedString = stringBuilder.ToString(); From 9c60ce554bec00fc0fb0ad31355a77a825d32e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:25:21 -0800 Subject: [PATCH 03/11] Rename DocIdSymbolFilter.GetFilterForDocIds to DocIdSymbolFilter.Create. --- .../ApiCompatServiceProvider.cs | 2 +- .../GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs | 4 ++-- .../Filtering/DocIdSymbolFilter.cs | 2 +- .../Rules/AttributesMustMatchTests.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs index d633abe2ac17..ec1e87c562eb 100644 --- a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs +++ b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs @@ -35,7 +35,7 @@ public ApiCompatServiceProvider(Func logFa CompositeSymbolFilter attributeDataSymbolFilter = new(accessibilitySymbolFilter); if (excludeAttributesFiles is not null) { - attributeDataSymbolFilter.Add(DocIdSymbolFilter.GetFilterForDocIds(excludeAttributesFiles)); + attributeDataSymbolFilter.Add(DocIdSymbolFilter.Create(excludeAttributesFiles)); } ApiComparerSettings apiComparerSettings = new( diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs index e0ebadecb49a..3468b6781159 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs @@ -256,7 +256,7 @@ public GenAPIConfiguration Build() CompositeSymbolFilter symbolFilter = new(); if (_apiExclusionFilePaths?.Count() > 0) { - symbolFilter.Add(DocIdSymbolFilter.GetFilterForDocIds(_apiExclusionFilePaths)); + symbolFilter.Add(DocIdSymbolFilter.Create(_apiExclusionFilePaths)); } symbolFilter.Add(new ImplicitSymbolFilter()); symbolFilter.Add(accessibilitySymbolFilter); @@ -265,7 +265,7 @@ public GenAPIConfiguration Build() CompositeSymbolFilter attributeDataSymbolFilter = new(); if (_attributeExclusionFilePaths?.Count() > 0) { - attributeDataSymbolFilter.Add(DocIdSymbolFilter.GetFilterForDocIds(_attributeExclusionFilePaths)); + attributeDataSymbolFilter.Add(DocIdSymbolFilter.Create(_attributeExclusionFilePaths)); } attributeDataSymbolFilter.Add(accessibilitySymbolFilter); diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs index 2642f5df3920..8dec5072d924 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs @@ -13,7 +13,7 @@ public class DocIdSymbolFilter : ISymbolFilter { private readonly HashSet _docIdsToExclude; - public static DocIdSymbolFilter GetFilterForDocIds(params string[] docIdsToExclude) + public static DocIdSymbolFilter Create(params string[] docIdsToExclude) => new DocIdSymbolFilter(ReadDocIdsAttributes(docIdsToExclude)); private DocIdSymbolFilter(IEnumerable docIdsToExclude) diff --git a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs index 53c9fca44297..5642297313df 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs @@ -29,7 +29,7 @@ public class AttributesMustMatchTests private static readonly TestRuleFactory s_ruleFactory = new((settings, context) => new AttributesMustMatch(settings, context)); private static ISymbolFilter GetAccessibilityAndAttributeSymbolFiltersAsComposite(params string[] excludeAttributeFiles) => - new CompositeSymbolFilter().Add(new AccessibilitySymbolFilter(false)).Add(DocIdSymbolFilter.GetFilterForDocIds(excludeAttributeFiles)); + new CompositeSymbolFilter().Add(new AccessibilitySymbolFilter(false)).Add(DocIdSymbolFilter.Create(excludeAttributeFiles)); public static TheoryData TypesCases => new() { From d8190fafd960bb4feabac0da5a1fe4d417691c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:30:43 -0800 Subject: [PATCH 04/11] Load assemblies as dictionary instead of list, where the key is the assembly name. --- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 9 +++---- .../GenAPIConfiguration.cs | 13 +++++----- .../AssemblySymbolLoader.cs | 25 ++++++++++++++++++- .../IAssemblySymbolLoader.cs | 13 +++++++--- .../CSharpFileBuilderTests.cs | 2 +- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index 679f770461ec..dbd4cd19e61b 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -21,12 +21,9 @@ public static class GenAPIApp public static void Run(GenAPIConfiguration config) { // Invoke an assembly symbol writer for each directly loaded assembly. - foreach (IAssemblySymbol? assemblySymbol in config.AssemblySymbols) + foreach (KeyValuePair kvp in config.AssemblySymbols) { - if (assemblySymbol is null) - continue; - - using TextWriter textWriter = GetTextWriter(config.OutputPath, assemblySymbol.Name); + using TextWriter textWriter = GetTextWriter(config.OutputPath, kvp.Key); IAssemblySymbolWriter writer = new CSharpFileBuilder(config.Logger, textWriter, config.Loader, @@ -35,7 +32,7 @@ public static void Run(GenAPIConfiguration config) config.Header, config.ExceptionMessage, config.IncludeAssemblyAttributes); - writer.WriteAssembly(assemblySymbol); + writer.WriteAssembly(kvp.Value); } if (config.Loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs index 3468b6781159..297b9d605be7 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs @@ -38,7 +38,7 @@ private GenAPIConfiguration() [AllowNull] public AssemblySymbolLoader Loader { get; private set; } [AllowNull] - public IReadOnlyList AssemblySymbols { get; private set; } + public IReadOnlyDictionary AssemblySymbols { get; private set; } [AllowNull] public CompositeSymbolFilter SymbolFilter { get; private set; } [AllowNull] @@ -215,7 +215,7 @@ public Builder WithIncludeAssemblyAttributes(bool includeAssemblyAttributes) public GenAPIConfiguration Build() { AssemblySymbolLoader loader; - IReadOnlyList assemblySymbols; + IReadOnlyDictionary assemblySymbols; if (_assembliesPaths?.Length > 0) { @@ -225,22 +225,23 @@ public GenAPIConfiguration Build() { loader.AddReferenceSearchPaths(_assemblyReferencesPaths); } - assemblySymbols = loader.LoadAssemblies(_assembliesPaths); + assemblySymbols = loader.LoadAssembliesAsDictionary(_assembliesPaths); } else if (_assemblyStreams?.Count() > 0) { loader = new(resolveAssemblyReferences: true, includeInternalSymbols: _respectInternals); loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); - List symbols = []; + Dictionary symbols = []; foreach ((string assemblyName, Stream assemblyStream) in _assemblyStreams) { if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) { - symbols.Add(assemblySymbol); + symbols.Add(assemblyName, assemblySymbol); } } - assemblySymbols = symbols.AsReadOnly(); + + assemblySymbols = symbols; } else { diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs index 6d7dea1bc8ae..8ec3fa304c10 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs @@ -15,7 +15,7 @@ namespace Microsoft.DotNet.ApiSymbolExtensions /// public class AssemblySymbolLoader : IAssemblySymbolLoader { - // Dictionary that holds the paths to help loading dependencies. Keys will be assembly name and + // Dictionary that holds the paths to help loading dependencies. Keys will be assembly name and // value are the containing folder. private readonly Dictionary _referencePathFiles = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _referencePathDirectories = new(StringComparer.OrdinalIgnoreCase); @@ -109,6 +109,29 @@ public bool HasLoadWarnings(out IReadOnlyList warnings) return assemblySymbols; } + /// + public IReadOnlyDictionary LoadAssembliesAsDictionary(params string[] paths) + { + // First resolve all assemblies that are passed in and create metadata references out of them. + // Reference assemblies of the passed in assemblies that themselves are passed in, will be skipped to be resolved, + // as they are resolved as part of the loop below. + ImmutableHashSet fileNames = paths.Select(path => Path.GetFileName(path)).ToImmutableHashSet(); + List assembliesToReturn = LoadFromPaths(paths, fileNames); + + // Create IAssemblySymbols out of the MetadataReferences. + // Doing this after resolving references to make sure that references are available. + Dictionary assemblySymbols = []; + foreach (MetadataReference metadataReference in assembliesToReturn) + { + if(_cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference) is IAssemblySymbol assemblySymbol) + { + assemblySymbols.Add(assemblySymbol.Name, assemblySymbol); + } + } + + return assemblySymbols; + } + /// public IReadOnlyList LoadAssembliesFromArchive(string archivePath, IReadOnlyList relativePaths) { diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs index cfb78f0a13d1..013584f3d2b3 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs @@ -34,12 +34,19 @@ public interface IAssemblySymbolLoader bool HasLoadWarnings(out IReadOnlyList warnings); /// - /// Loads a list of assemblies and gets its corresponding from the specified paths. + /// Loads a set of assemblies from the filesystem and gets their corresponding instances as a list. /// /// List of paths to load binaries from. Can be full paths to binaries or directories. - /// The list of resolved . + /// The list of resolved instances, which can be resolved to . IReadOnlyList LoadAssemblies(params string[] paths); + /// + /// Loads a set of assemblies from the filesystem and gets their corresponding instances as a dictionary. + /// + /// List of paths to load binaries from. Can be full paths to binaries or directories. + /// The dictionary of resolved instances,excluding those which resolved as . + IReadOnlyDictionary LoadAssembliesAsDictionary(params string[] paths); + /// /// Loads assemblies from an archive based on the given relative paths. /// @@ -60,7 +67,7 @@ public interface IAssemblySymbolLoader /// /// The name to use to resolve the assembly. /// The stream to read the metadata from. - /// representing the given . If an + /// representing the given . If an /// assembly with the same was already loaded, the previously loaded assembly is returned. IAssemblySymbol? LoadAssembly(string name, Stream stream); diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index 458ef5361fe3..44d2ddfd3070 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -56,7 +56,7 @@ private void RunTest(string original, config.Header, config.ExceptionMessage, config.IncludeAssemblyAttributes); - writer.WriteAssembly(config.AssemblySymbols.First()); + writer.WriteAssembly(config.AssemblySymbols.First().Value); StringBuilder stringBuilder = stringWriter.GetStringBuilder(); string resultedString = stringBuilder.ToString(); From 12864714ce4daecb1fddefe7d68c133ec2b73573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:22:57 -0800 Subject: [PATCH 05/11] Simplify GenAPIConfiguration --- Directory.Packages.props | 1 + eng/Version.Details.xml | 4 + eng/Versions.props | 1 + .../ApiCompatServiceProvider.cs | 2 +- .../GenAPITask.cs | 17 +- .../Microsoft.DotNet.GenAPI.Tool/Program.cs | 19 +- .../CSharpFileBuilder.cs | 30 +- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 38 +- .../GenAPIConfiguration.cs | 383 ++++++++---------- .../IAssemblySymbolWriter.cs | 18 +- .../AssemblySymbolLoader.cs | 2 +- .../Filtering/DocIdSymbolFilter.cs | 35 +- .../IAssemblySymbolLoader.cs | 2 +- ...osoft.DotNet.ApiCompatibility.Tests.csproj | 5 +- .../Rules/AttributesMustMatchTests.cs | 2 +- .../CSharpFileBuilderTests.cs | 40 +- 16 files changed, 307 insertions(+), 292 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 37f12c935a34..cde36c909357 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,6 +28,7 @@ + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a968beb23d97..9cf15a52ea6e 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -131,6 +131,10 @@ https://github.com/dotnet/roslyn d7b0a8c4b320a592e6b81dc5a40bc724cd8b71ba + + https://github.com/dotnet/roslyn + d7b0a8c4b320a592e6b81dc5a40bc724cd8b71ba + https://github.com/dotnet/roslyn d7b0a8c4b320a592e6b81dc5a40bc724cd8b71ba diff --git a/eng/Versions.props b/eng/Versions.props index b8c6971ef6bc..1f0ab5410739 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -215,6 +215,7 @@ 4.13.0-3.25056.21 4.13.0-3.25056.21 4.13.0-3.25056.21 + 4.13.0-3.25056.21 4.13.0-3.25056.21 4.13.0-3.25056.21 diff --git a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs index ec1e87c562eb..4b28606b5099 100644 --- a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs +++ b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs @@ -35,7 +35,7 @@ public ApiCompatServiceProvider(Func logFa CompositeSymbolFilter attributeDataSymbolFilter = new(accessibilitySymbolFilter); if (excludeAttributesFiles is not null) { - attributeDataSymbolFilter.Add(DocIdSymbolFilter.Create(excludeAttributesFiles)); + attributeDataSymbolFilter.Add(DocIdSymbolFilter.CreateFromFiles(excludeAttributesFiles)); } ApiComparerSettings apiComparerSettings = new( diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs index 9de86051d036..062f37d10275 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs @@ -63,19 +63,20 @@ public class GenAPITask : TaskBase protected override void ExecuteCore() { GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() - .WithLogger(new MSBuildLog(Log)) .WithAssembliesPaths(Assemblies) .WithAssemblyReferencesPaths(AssemblyReferences) - .WithOutputPath(OutputPath) - .WithHeaderFilePath(HeaderFile) - .WithExceptionMessage(ExceptionMessage) - .WithApiExclusionFilePaths(ExcludeApiFiles) - .WithAttributeExclusionFilePaths(ExcludeAttributesFiles) .WithRespectInternals(RespectInternals) - .WithIncludeAssemblyAttributes(IncludeAssemblyAttributes) .Build(); - GenAPIApp.Run(config); + GenAPIApp.Run(new MSBuildLog(Log), + config.AssemblySymbols, + OutputPath, + config.Loader, + GenAPIConfiguration.GetSymbolFilterFromFiles(ExcludeApiFiles, respectInternals: RespectInternals), + GenAPIConfiguration.GetAttributeFilterFromPaths(ExcludeAttributesFiles, respectInternals: RespectInternals), + GenAPIConfiguration.GetFormattedHeader(HeaderFile), + ExceptionMessage, + IncludeAssemblyAttributes); } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs index 6c6ed5e870b7..545f164a1ce6 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs @@ -98,19 +98,22 @@ static int Main(string[] args) rootCommand.SetAction((ParseResult parseResult) => { GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() - .WithLogger(new ConsoleLog(MessageImportance.Normal)) .WithAssembliesPaths(parseResult.GetValue(assembliesOption)!) .WithAssemblyReferencesPaths(parseResult.GetValue(assemblyReferencesOption)) - .WithOutputPath(parseResult.GetValue(outputPathOption)) - .WithHeaderFilePath(parseResult.GetValue(headerFileOption)) - .WithExceptionMessage(parseResult.GetValue(exceptionMessageOption)) - .WithApiExclusionFilePaths(parseResult.GetValue(excludeApiFilesOption)) - .WithAttributeExclusionFilePaths(parseResult.GetValue(excludeAttributesFilesOption)) .WithRespectInternals(parseResult.GetValue(respectInternalsOption)) - .WithIncludeAssemblyAttributes(parseResult.GetValue(includeAssemblyAttributesOption)) .Build(); - GenAPIApp.Run(config); + bool respectInternals = parseResult.GetValue(respectInternalsOption); + + GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal), + config.AssemblySymbols, + parseResult.GetValue(outputPathOption), + config.Loader, + GenAPIConfiguration.GetSymbolFilterFromFiles(parseResult.GetValue(excludeApiFilesOption), respectInternals), + GenAPIConfiguration.GetAttributeFilterFromPaths(parseResult.GetValue(excludeAttributesFilesOption), respectInternals), + GenAPIConfiguration.GetFormattedHeader(parseResult.GetValue(headerFileOption)), + parseResult.GetValue(exceptionMessageOption), + parseResult.GetValue(includeAssemblyAttributesOption)); }); return rootCommand.Parse(args).Invoke(); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs index 72c3595dd43b..383253746f4e 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -27,22 +27,24 @@ public sealed class CSharpFileBuilder : IAssemblySymbolWriter private readonly ILog _logger; private readonly TextWriter _textWriter; private readonly AssemblySymbolLoader _loader; - private readonly CompositeSymbolFilter _symbolFilter; - private readonly CompositeSymbolFilter _attributeDataSymbolFilter; + private readonly ISymbolFilter _symbolFilter; + private readonly ISymbolFilter _attributeDataSymbolFilter; private readonly string _header; private readonly string? _exceptionMessage; private readonly bool _includeAssemblyAttributes; private readonly AdhocWorkspace _adhocWorkspace; private readonly SyntaxGenerator _syntaxGenerator; + private readonly IEnumerable? _metadataReferences; public CSharpFileBuilder(ILog logger, TextWriter textWriter, AssemblySymbolLoader loader, - CompositeSymbolFilter symbolFilter, - CompositeSymbolFilter attributeDataSymbolFilter, + ISymbolFilter symbolFilter, + ISymbolFilter attributeDataSymbolFilter, string header, string? exceptionMessage, - bool includeAssemblyAttributes) + bool includeAssemblyAttributes, + IEnumerable? metadataReferences = null) { _logger = logger; _textWriter = textWriter; @@ -54,19 +56,26 @@ public CSharpFileBuilder(ILog logger, _includeAssemblyAttributes = includeAssemblyAttributes; _adhocWorkspace = new AdhocWorkspace(); _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); + _metadataReferences = metadataReferences; } /// public void WriteAssembly(IAssemblySymbol assemblySymbol) { _textWriter.Write(_header); + Document document = GetDocumentForAssembly(assemblySymbol); + GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); + } + /// + public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) + { CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, - nullableContextOptions: NullableContextOptions.Enable); + nullableContextOptions: NullableContextOptions.Enable); Project project = _adhocWorkspace.AddProject(ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), assemblySymbol.Name, assemblySymbol.Name, LanguageNames.CSharp, compilationOptions: compilationOptions)); - project = project.AddMetadataReferences(_loader.MetadataReferences); + project = project.AddMetadataReferences(_metadataReferences ?? _loader.MetadataReferences); IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_symbolFilter.Include); List namespaceSyntaxNodes = []; @@ -98,11 +107,12 @@ public void WriteAssembly(IAssemblySymbol assemblySymbol) document = Simplifier.ReduceAsync(document).Result; document = Formatter.FormatAsync(document, DefineFormattingOptions()).Result; - document.GetSyntaxRootAsync().Result! - .Rewrite(new SingleLineStatementCSharpSyntaxRewriter()) - .WriteTo(_textWriter); + return document; } + /// + public SyntaxNode GetFormattedRootNodeForDocument(Document document) => document.GetSyntaxRootAsync().Result!.Rewrite(new SingleLineStatementCSharpSyntaxRewriter()); + private SyntaxNode? Visit(INamespaceSymbol namespaceSymbol) { SyntaxNode namespaceNode = _syntaxGenerator.NamespaceDeclaration(namespaceSymbol.ToDisplayString()); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index dbd4cd19e61b..a93384c6c8de 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -6,6 +6,8 @@ #endif using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.GenAPI { @@ -18,36 +20,44 @@ public static class GenAPIApp /// /// Initialize and run Roslyn-based GenAPI tool. /// - public static void Run(GenAPIConfiguration config) + public static void Run(ILog logger, + Dictionary assemblySymbols, + string? outputPath, + AssemblySymbolLoader loader, + ISymbolFilter symbolFilter, + ISymbolFilter attributeSymbolFilter, + string header, + string? exceptionMessage, + bool includeAssemblyAttributes) { // Invoke an assembly symbol writer for each directly loaded assembly. - foreach (KeyValuePair kvp in config.AssemblySymbols) + foreach (KeyValuePair kvp in assemblySymbols) { - using TextWriter textWriter = GetTextWriter(config.OutputPath, kvp.Key); - IAssemblySymbolWriter writer = new CSharpFileBuilder(config.Logger, + using TextWriter textWriter = GetTextWriter(outputPath, kvp.Key); + IAssemblySymbolWriter writer = new CSharpFileBuilder(logger, textWriter, - config.Loader, - config.SymbolFilter, - config.AttributeDataSymbolFilter, - config.Header, - config.ExceptionMessage, - config.IncludeAssemblyAttributes); + loader, + symbolFilter, + attributeSymbolFilter, + header, + exceptionMessage, + includeAssemblyAttributes); writer.WriteAssembly(kvp.Value); } - if (config.Loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) + if (loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) { foreach (Diagnostic warning in roslynDiagnostics) { - config.Logger.LogWarning(warning.Id, warning.ToString()); + logger.LogWarning(warning.Id, warning.ToString()); } } - if (config.Loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) + if (loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) { foreach (AssemblyLoadWarning warning in loadWarnings) { - config.Logger.LogWarning(warning.DiagnosticId, warning.Message); + logger.LogWarning(warning.DiagnosticId, warning.Message); } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs index 297b9d605be7..224b9771f27c 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs @@ -9,65 +9,135 @@ using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; -using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.GenAPI.Filtering; +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; +using System.Diagnostics; namespace Microsoft.DotNet.GenAPI; -public enum OutputType -{ - Console, - Files, - Diff -} - public class GenAPIConfiguration { private GenAPIConfiguration() { } - [AllowNull] - public ILog Logger { get; private set; } - public OutputType OutputType { get; private set; } = OutputType.Console; - public string? OutputPath { get; private set; } - [AllowNull] - public string Header { get; private set; } - public string? ExceptionMessage { get; private set; } - public bool IncludeAssemblyAttributes { get; private set; } [AllowNull] public AssemblySymbolLoader Loader { get; private set; } [AllowNull] - public IReadOnlyDictionary AssemblySymbols { get; private set; } - [AllowNull] - public CompositeSymbolFilter SymbolFilter { get; private set; } - [AllowNull] - public CompositeSymbolFilter AttributeDataSymbolFilter { get; private set; } + public Dictionary AssemblySymbols { get; private set; } public static Builder GetBuilder() => new Builder(); + public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFilePaths, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + apiExclusionFilePaths?.Count() > 0 ? + DocIdSymbolFilter.CreateFromFiles(apiExclusionFilePaths) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); + } + + public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + apiExclusionList?.Count() > 0 ? + DocIdSymbolFilter.CreateFromDocIDs(apiExclusionList) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); + } + + public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclusionFilePaths, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + attributeExclusionFilePaths?.Count() > 0 ? + DocIdSymbolFilter.CreateFromFiles(attributeExclusionFilePaths) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); + } + + public static ISymbolFilter GetAttributeFilterFromList(string[]? attributeExclusionList, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + attributeExclusionList?.Count() > 0 ? + DocIdSymbolFilter.CreateFromDocIDs(attributeExclusionList) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); + } + + private static ISymbolFilter GetCompositeSymbolFilter(DocIdSymbolFilter? customFilter, + bool respectInternals, + bool includeEffectivelyPrivateSymbols, + bool includeExplicitInterfaceImplementationSymbols, + bool withImplicitSymbolFilter) + { + AccessibilitySymbolFilter accessibilitySymbolFilter = new( + respectInternals, + includeEffectivelyPrivateSymbols, + includeExplicitInterfaceImplementationSymbols); + + CompositeSymbolFilter filter = new(); + + if (customFilter != null) + { + filter.Add(customFilter); + } + if (withImplicitSymbolFilter) + { + filter.Add(new ImplicitSymbolFilter()); + } + + filter.Add(accessibilitySymbolFilter); + + return filter; + } + + + public static string GetFormattedHeader(string? customHeader = null) + { + const string defaultFileHeader = """ + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + """; + + if (customHeader != null) + { +#if NET + return customHeader.ReplaceLineEndings(); +#else + return Regex.Replace(customHeader, @"\r\n|\n\r|\n|\r", Environment.NewLine); +#endif + } + + return defaultFileHeader; + } + public class Builder { - private ILog? _logger = null; private string[]? _assembliesPaths = null; private string[]? _assemblyReferencesPaths = null; - private OutputType _outputType = OutputType.Console; - private string? _outputPath = null; - private (string, Stream)[]? _assemblyStreams; - private string? _header = null; - private string? _exceptionMessage = null; - private string[]? _apiExclusionFilePaths = null; - private string[]? _attributeExclusionFilePaths = null; - bool _includeEffectivelyPrivateSymbols = true; - bool _includeExplicitInterfaceImplementationSymbols = true; - bool _respectInternals = false; - bool _includeAssemblyAttributes = false; - - public Builder WithLogger(ILog logger) - { - _logger = logger; - return this; - } + private (string, string)[]? _assemblyTexts; + private bool _allowUnsafe = false; + private bool _respectInternals = false; public Builder WithAssembliesPaths(params string[]? assembliesPaths) { @@ -75,7 +145,7 @@ public Builder WithAssembliesPaths(params string[]? assembliesPaths) { return this; } - if (_assemblyStreams != null) + if (_assemblyTexts != null) { throw new InvalidOperationException("Cannot specify both assembly paths and streams."); } @@ -87,9 +157,9 @@ public Builder WithAssembliesPaths(params string[]? assembliesPaths) return this; } - public Builder WithAssemblyStreams(params (string, Stream)[]? assemblyStreams) + public Builder WithAssemblyTexts(params (string, string)[]? assemblyTexts) { - if (assemblyStreams == null) + if (assemblyTexts == null) { return this; } @@ -98,7 +168,7 @@ public Builder WithAssemblyStreams(params (string, Stream)[]? assemblyStreams) throw new InvalidOperationException("Cannot specify both assembly paths and streams."); } - _assemblyStreams = assemblyStreams; + _assemblyTexts = assemblyTexts; return this; } @@ -116,87 +186,9 @@ public Builder WithAssemblyReferencesPaths(params string[]? assemblyReferencesPa return this; } - public Builder WithOutputPath(string? outputPath) - { - if (outputPath == null) - { - return this; - } - ThrowIfDirectoryNotFound(nameof(outputPath), outputPath); - _outputPath = outputPath; - _outputType = OutputType.Files; - return this; - } - - public Builder WithHeaderFilePath(string? headerFilePath) - { - if (headerFilePath == null) - { - return this; - } - ThrowIfFileNotFound(nameof(headerFilePath), headerFilePath); - return WithHeader(File.ReadAllText(headerFilePath)); - } - - public Builder WithHeader(string? header) - { - if (header == null) - { - return this; - } - _header = header; - return this; - } - - public Builder WithExceptionMessage(string? exceptionMessage) - { - if (exceptionMessage == null) - { - return this; - } - _exceptionMessage = exceptionMessage; - return this; - } - - public Builder WithApiExclusionFilePaths(params string[]? apiExclusionFilePaths) - { - if (apiExclusionFilePaths == null) - { - return this; - } - - foreach (string path in apiExclusionFilePaths) - { - ThrowIfPathIsDirectory(nameof(apiExclusionFilePaths), path); - } - _apiExclusionFilePaths = apiExclusionFilePaths; - return this; - } - - public Builder WithAttributeExclusionFilePaths(params string[]? attributeExclusionFilePaths) + public Builder WithAllowUnsafe(bool allowUnsafe) { - if (attributeExclusionFilePaths == null) - { - return this; - } - - foreach (string path in attributeExclusionFilePaths) - { - ThrowIfPathIsDirectory(nameof(attributeExclusionFilePaths), path); - } - _attributeExclusionFilePaths = attributeExclusionFilePaths; - return this; - } - - public Builder WithIncludeEffectivelyPrivateSymbols(bool includeEffectivelyPrivateSymbols) - { - _includeEffectivelyPrivateSymbols = includeEffectivelyPrivateSymbols; - return this; - } - - public Builder WithIncludeExplicitInterfaceImplementationSymbols(bool includeExplicitInterfaceImplementationSymbols) - { - _includeExplicitInterfaceImplementationSymbols = includeExplicitInterfaceImplementationSymbols; + _allowUnsafe = allowUnsafe; return this; } @@ -206,137 +198,112 @@ public Builder WithRespectInternals(bool respectInternals) return this; } - public Builder WithIncludeAssemblyAttributes(bool includeAssemblyAttributes) - { - _includeAssemblyAttributes = includeAssemblyAttributes; - return this; - } - public GenAPIConfiguration Build() { AssemblySymbolLoader loader; - IReadOnlyDictionary assemblySymbols; + Dictionary assemblySymbols; if (_assembliesPaths?.Length > 0) { bool resolveAssemblyReferences = _assemblyReferencesPaths?.Count() > 0; - loader = new(resolveAssemblyReferences, _respectInternals); - if (_assemblyReferencesPaths?.Count() > 0) + loader = new AssemblySymbolLoader(resolveAssemblyReferences, _respectInternals); + if (_assemblyReferencesPaths?.Length > 0) { loader.AddReferenceSearchPaths(_assemblyReferencesPaths); } - assemblySymbols = loader.LoadAssembliesAsDictionary(_assembliesPaths); + assemblySymbols = new Dictionary(loader.LoadAssembliesAsDictionary(_assembliesPaths)); } - else if (_assemblyStreams?.Count() > 0) + else if (_assemblyTexts?.Count() > 0) { - loader = new(resolveAssemblyReferences: true, includeInternalSymbols: _respectInternals); + loader = new AssemblySymbolLoader(resolveAssemblyReferences: true, includeInternalSymbols: _respectInternals); loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); - Dictionary symbols = []; - foreach ((string assemblyName, Stream assemblyStream) in _assemblyStreams) + + assemblySymbols = new Dictionary(); + foreach ((string assemblyName, string assemblyText) in _assemblyTexts) { + using Stream assemblyStream = EmitAssemblyStreamFromSyntax(assemblyText, enableNullable: true, allowUnsafe: _allowUnsafe, assemblyName: assemblyName); if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) { - symbols.Add(assemblyName, assemblySymbol); + assemblySymbols.Add(assemblyName, assemblySymbol); } } - - assemblySymbols = symbols; } else { throw new InvalidOperationException("No assemblies were specified, either from files or from streams."); } - AccessibilitySymbolFilter accessibilitySymbolFilter = new( - _respectInternals, - includeEffectivelyPrivateSymbols: _includeEffectivelyPrivateSymbols, - includeExplicitInterfaceImplementationSymbols: _includeExplicitInterfaceImplementationSymbols); - - // Configure the symbol filter - CompositeSymbolFilter symbolFilter = new(); - if (_apiExclusionFilePaths?.Count() > 0) - { - symbolFilter.Add(DocIdSymbolFilter.Create(_apiExclusionFilePaths)); - } - symbolFilter.Add(new ImplicitSymbolFilter()); - symbolFilter.Add(accessibilitySymbolFilter); - - // Configure the attribute data symbol filter - CompositeSymbolFilter attributeDataSymbolFilter = new(); - if (_attributeExclusionFilePaths?.Count() > 0) - { - attributeDataSymbolFilter.Add(DocIdSymbolFilter.Create(_attributeExclusionFilePaths)); - } - attributeDataSymbolFilter.Add(accessibilitySymbolFilter); - return new GenAPIConfiguration() { - Logger = _logger ?? new ConsoleLog(MessageImportance.Normal), - OutputType = _outputType, - OutputPath = _outputPath, - Header = GetHeader(), - ExceptionMessage = _exceptionMessage, - IncludeAssemblyAttributes = _includeAssemblyAttributes, Loader = loader, - AssemblySymbols = assemblySymbols, - SymbolFilter = symbolFilter, - AttributeDataSymbolFilter = attributeDataSymbolFilter + AssemblySymbols = assemblySymbols }; - } - private string GetHeader() + + private static IEnumerable> DiagnosticOptions { get; } = new[] { - const string defaultFileHeader = """ - //------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - //------------------------------------------------------------------------------ + // Suppress warning for unused events. + new KeyValuePair("CS0067", ReportDiagnostic.Suppress) + }; - """; + private static IEnumerable DefaultReferences { get; } = new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DynamicAttribute).Assembly.Location), + }; + + public static Stream EmitAssemblyStreamFromSyntax(string syntax, + bool enableNullable = false, + byte[]? publicKey = null, + [CallerMemberName] string assemblyName = "", + bool allowUnsafe = false) + { + CSharpCompilation compilation = CreateCSharpCompilationFromSyntax([syntax], assemblyName, enableNullable, publicKey, allowUnsafe); - if (_header != null) - { -#if NET - _header = _header.ReplaceLineEndings(); -#else - _header = Regex.Replace(_header, @"\r\n|\n\r|\n|\r", Environment.NewLine); -#endif - return _header; - } + Debug.Assert(compilation.GetDiagnostics().IsEmpty); - return defaultFileHeader; + MemoryStream stream = new(); + compilation.Emit(stream); + stream.Seek(0, SeekOrigin.Begin); + return stream; } - private void ThrowIfFileNotFound(string argumentName, string filePath) + private static SyntaxTree GetSyntaxTree(string syntax) { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"The {argumentName} file was not found: {filePath}"); - } + return CSharpSyntaxTree.ParseText(syntax, ParseOptions); } - private void ThrowIfDirectoryNotFound(string argumentName, string directoryPath) + private static CSharpParseOptions ParseOptions { get; } = new CSharpParseOptions(preprocessorSymbols: +#if NETFRAMEWORK + new string[] { "NETFRAMEWORK" } +#else + Array.Empty() +#endif + ); + + private static CSharpCompilation CreateCSharpCompilationFromSyntax(IEnumerable syntax, string name, bool enableNullable, byte[]? publicKey, bool allowUnsafe) { - if (!Directory.Exists(directoryPath)) - { - throw new DirectoryNotFoundException($"The {argumentName} directory was not found: {directoryPath}"); - } + CSharpCompilation compilation = CreateCSharpCompilation(name, enableNullable, publicKey, allowUnsafe); + IEnumerable syntaxTrees = syntax.Select(s => GetSyntaxTree(s)); + return compilation.AddSyntaxTrees(syntaxTrees); } - private void ThrowIfPathIsDirectory(string argumentName, string path) + private static CSharpCompilation CreateCSharpCompilation(string name, bool enableNullable, byte[]? publicKey, bool allowUnsafe) { - if (Directory.Exists(path)) - { - throw new DirectoryNotFoundException($"The {argumentName} is a directory, not a file: {path}"); - } + bool publicSign = publicKey != null ? true : false; + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, + publicSign: publicSign, + cryptoPublicKey: publicSign ? publicKey!.ToImmutableArray() : default, + nullableContextOptions: enableNullable ? NullableContextOptions.Enable : NullableContextOptions.Disable, + allowUnsafe: allowUnsafe, + specificDiagnosticOptions: DiagnosticOptions); + + return CSharpCompilation.Create(name, options: compilationOptions, references: DefaultReferences); } + private void ThrowIfFileSystemEntryNotFound(string argumentName, string path) { if (Directory.Exists(path) || File.Exists(path)) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs index a739ca961575..48600836e63e 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs @@ -11,9 +11,23 @@ namespace Microsoft.DotNet.GenAPI public interface IAssemblySymbolWriter { /// - /// Process a given assembly symbol. + /// Write a given assembly symbol to the instance's desired output. /// - /// representing the loaded assembly. + /// An assembly symbol representing the loaded assembly. void WriteAssembly(IAssemblySymbol assemblySymbol); + + /// + /// Returns the configured source code document for the specified assembly symbol. + /// + /// The assembly symbol that represents the loaded assembly. + /// The source code document instance of the specified assembly symbol. + Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol); + + /// + /// Returns the formatted root syntax node for the specified document. + /// + /// A source code document instance. + /// The root syntax node of the specified document. + public SyntaxNode GetFormattedRootNodeForDocument(Document document); } } diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs index 8ec3fa304c10..29be3b23d690 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs @@ -110,7 +110,7 @@ public bool HasLoadWarnings(out IReadOnlyList warnings) } /// - public IReadOnlyDictionary LoadAssembliesAsDictionary(params string[] paths) + public IDictionary LoadAssembliesAsDictionary(params string[] paths) { // First resolve all assemblies that are passed in and create metadata references out of them. // Reference assemblies of the passed in assemblies that themselves are passed in, will be skipped to be resolved, diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs index 8dec5072d924..4a5435448fb3 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs @@ -13,8 +13,11 @@ public class DocIdSymbolFilter : ISymbolFilter { private readonly HashSet _docIdsToExclude; - public static DocIdSymbolFilter Create(params string[] docIdsToExclude) - => new DocIdSymbolFilter(ReadDocIdsAttributes(docIdsToExclude)); + public static DocIdSymbolFilter CreateFromFiles(params string[] filesWithDocIdsToExclude) + => new DocIdSymbolFilter(ReadDocIdsFromFiles(filesWithDocIdsToExclude)); + + public static DocIdSymbolFilter CreateFromDocIDs(params string[] docIdsToExclude) + => new DocIdSymbolFilter(ReadDocIdsFromList(docIdsToExclude)); private DocIdSymbolFilter(IEnumerable docIdsToExclude) => _docIdsToExclude = [.. docIdsToExclude]; @@ -35,7 +38,22 @@ public bool Include(ISymbol symbol) return true; } - private static IEnumerable ReadDocIdsAttributes(params string[] docIdsToExcludeFiles) + private static IEnumerable ReadDocIdsFromList(params string[] ids) + { + foreach (string id in ids) + { +#if NET + if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith('#') && !id.StartsWith("//")) +#else + if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith("#") && !id.StartsWith("//")) +#endif + { + yield return id.Trim(); + } + } + } + + private static IEnumerable ReadDocIdsFromFiles(params string[] docIdsToExcludeFiles) { foreach (string docIdsToExcludeFile in docIdsToExcludeFiles) { @@ -44,16 +62,9 @@ private static IEnumerable ReadDocIdsAttributes(params string[] docIdsTo continue; } - foreach (string id in File.ReadAllLines(docIdsToExcludeFile)) + foreach (string docId in ReadDocIdsFromList(File.ReadAllLines(docIdsToExcludeFile))) { -#if NET - if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith('#') && !id.StartsWith("//")) -#else - if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith("#") && !id.StartsWith("//")) -#endif - { - yield return id.Trim(); - } + yield return docId; } } } diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs index 013584f3d2b3..6cf2b3e88d38 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs @@ -45,7 +45,7 @@ public interface IAssemblySymbolLoader /// /// List of paths to load binaries from. Can be full paths to binaries or directories. /// The dictionary of resolved instances,excluding those which resolved as . - IReadOnlyDictionary LoadAssembliesAsDictionary(params string[] paths); + IDictionary LoadAssembliesAsDictionary(params string[] paths); /// /// Loads assemblies from an archive based on the given relative paths. diff --git a/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj b/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj index e2c0ccd07842..d41363a5174f 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj @@ -16,8 +16,9 @@ - + + - + diff --git a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs index 5642297313df..221bb16cdedb 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs @@ -29,7 +29,7 @@ public class AttributesMustMatchTests private static readonly TestRuleFactory s_ruleFactory = new((settings, context) => new AttributesMustMatch(settings, context)); private static ISymbolFilter GetAccessibilityAndAttributeSymbolFiltersAsComposite(params string[] excludeAttributeFiles) => - new CompositeSymbolFilter().Add(new AccessibilitySymbolFilter(false)).Add(DocIdSymbolFilter.Create(excludeAttributeFiles)); + new CompositeSymbolFilter().Add(new AccessibilitySymbolFilter(false)).Add(DocIdSymbolFilter.CreateFromFiles(excludeAttributeFiles)); public static TheoryData TypesCases => new() { diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index 44d2ddfd3070..611af509b9db 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; -using Microsoft.DotNet.ApiSymbolExtensions.Tests; namespace Microsoft.DotNet.GenAPI.Tests { @@ -30,32 +29,29 @@ private void RunTest(string original, bool includeEffectivelyPrivateSymbols = true, bool includeExplicitInterfaceImplementationSymbols = true, bool allowUnsafe = false, - string excludeApiFile = null, - string excludedAttributeFile = null, + string[] excludedAttributeList = null, [CallerMemberName] string assemblyName = "") { using StringWriter stringWriter = new(); - using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(original, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName); - GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() - .WithLogger(new ConsoleLog(MessageImportance.Low)) - .WithAssemblyStreams((assemblyName, assemblyStream)) + .WithAssemblyTexts((assemblyName, original)) + .WithAllowUnsafe(allowUnsafe) .WithRespectInternals(includeInternalSymbols) - .WithIncludeEffectivelyPrivateSymbols(includeEffectivelyPrivateSymbols) - .WithIncludeExplicitInterfaceImplementationSymbols(includeExplicitInterfaceImplementationSymbols) - .WithApiExclusionFilePaths(excludeApiFile) - .WithAttributeExclusionFilePaths(excludedAttributeFile) .Build(); - IAssemblySymbolWriter writer = new CSharpFileBuilder(config.Logger, - stringWriter, - config.Loader, - config.SymbolFilter, - config.AttributeDataSymbolFilter, - config.Header, - config.ExceptionMessage, - config.IncludeAssemblyAttributes); + + IAssemblySymbolWriter writer = new CSharpFileBuilder( + new ConsoleLog(MessageImportance.Low), + stringWriter, + config.Loader, + GenAPIConfiguration.GetSymbolFilterFromList([], includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + GenAPIConfiguration.GetAttributeFilterFromList(excludedAttributeList, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + header: string.Empty, + exceptionMessage: null, + includeAssemblyAttributes: false, + MetadataReferences); + writer.WriteAssembly(config.AssemblySymbols.First().Value); StringBuilder stringBuilder = stringWriter.GetStringBuilder(); @@ -2748,10 +2744,6 @@ public class PublicClass { } [Fact] public void TestAttributesExcludedWithFilter() { - using TempDirectory root = new(); - string filePath = Path.Combine(root.DirPath, "exclusions.txt"); - File.WriteAllText(filePath, "T:A.AnyTestAttribute"); - RunTest(original: """ namespace A { @@ -2787,7 +2779,7 @@ public partial class PublicClass } """, includeInternalSymbols: false, - excludedAttributeFile: filePath); + excludedAttributeList: ["T:A.AnyTestAttribute"]); } [Fact] From 3666a3a7652ba7c47b2303f316b76fb6b6abd82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:19:19 -0800 Subject: [PATCH 06/11] Use factory methods instead of fluent. Get rid of GenAPIConfiguration, split it into two separate classes. --- .../GenAPITask.cs | 22 +- .../Microsoft.DotNet.GenAPI.Tool/Program.cs | 31 +- .../AssemblyLoaderFactory.cs | 36 ++ .../CSharpAssemblyVisitor.cs | 341 +++++++++++++++++ .../CSharpFileBuilder.cs | 345 ++---------------- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 35 +- .../GenAPIConfiguration.cs | 317 ---------------- .../IAssemblySymbolWriter.cs | 14 - .../IAssemblyVisitor.cs | 23 ++ .../SymbolFilterFactory.cs | 86 +++++ ...osoft.DotNet.ApiCompatibility.Tests.csproj | 1 + .../CSharpFileBuilderTests.cs | 17 +- .../TestAssemblyLoaderFactory.cs | 36 ++ 13 files changed, 606 insertions(+), 698 deletions(-) create mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs create mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs delete mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs create mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs create mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs create mode 100644 test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs index 062f37d10275..cc383611c490 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.NET.Build.Tasks; @@ -62,20 +64,20 @@ public class GenAPITask : TaskBase /// protected override void ExecuteCore() { - GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() - .WithAssembliesPaths(Assemblies) - .WithAssemblyReferencesPaths(AssemblyReferences) - .WithRespectInternals(RespectInternals) - .Build(); + (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = AssemblyLoaderFactory.CreateFromFiles( + assembliesPaths: Assemblies ?? throw new NullReferenceException("Assemblies cannot be null."), + assemblyReferencesPaths: AssemblyReferences, + RespectInternals); GenAPIApp.Run(new MSBuildLog(Log), - config.AssemblySymbols, + loader, + assemblySymbols, OutputPath, - config.Loader, - GenAPIConfiguration.GetSymbolFilterFromFiles(ExcludeApiFiles, respectInternals: RespectInternals), - GenAPIConfiguration.GetAttributeFilterFromPaths(ExcludeAttributesFiles, respectInternals: RespectInternals), - GenAPIConfiguration.GetFormattedHeader(HeaderFile), + HeaderFile, ExceptionMessage, + ExcludeApiFiles, + ExcludeAttributesFiles, + RespectInternals, IncludeAssemblyAttributes); } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs index 545f164a1ce6..1dc8055b936d 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs @@ -5,6 +5,8 @@ using System.CommandLine.Parsing; using System.Diagnostics; using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.GenAPI.Tool @@ -97,23 +99,24 @@ static int Main(string[] args) rootCommand.SetAction((ParseResult parseResult) => { - GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() - .WithAssembliesPaths(parseResult.GetValue(assembliesOption)!) - .WithAssemblyReferencesPaths(parseResult.GetValue(assemblyReferencesOption)) - .WithRespectInternals(parseResult.GetValue(respectInternalsOption)) - .Build(); - bool respectInternals = parseResult.GetValue(respectInternalsOption); + (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = AssemblyLoaderFactory.CreateFromFiles( + assembliesPaths: parseResult.GetValue(assembliesOption) ?? throw new NullReferenceException("No assemblies provided."), + assemblyReferencesPaths: parseResult.GetValue(assemblyReferencesOption), + respectInternals); + GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal), - config.AssemblySymbols, - parseResult.GetValue(outputPathOption), - config.Loader, - GenAPIConfiguration.GetSymbolFilterFromFiles(parseResult.GetValue(excludeApiFilesOption), respectInternals), - GenAPIConfiguration.GetAttributeFilterFromPaths(parseResult.GetValue(excludeAttributesFilesOption), respectInternals), - GenAPIConfiguration.GetFormattedHeader(parseResult.GetValue(headerFileOption)), - parseResult.GetValue(exceptionMessageOption), - parseResult.GetValue(includeAssemblyAttributesOption)); + loader, + assemblySymbols, + parseResult.GetValue(outputPathOption), + parseResult.GetValue(headerFileOption), + parseResult.GetValue(exceptionMessageOption), + parseResult.GetValue(excludeApiFilesOption), + parseResult.GetValue(excludeAttributesFilesOption), + respectInternals, + parseResult.GetValue(includeAssemblyAttributesOption) + ); }); return rootCommand.Parse(args).Invoke(); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs new file mode 100644 index 000000000000..ff6006501258 --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; +using Microsoft.DotNet.GenAPI.Filtering; + +namespace Microsoft.DotNet.GenAPI; + +public class AssemblyLoaderFactory +{ + public static (IAssemblySymbolLoader, Dictionary) CreateFromFiles(string[] assembliesPaths, string[]? assemblyReferencesPaths, bool respectInternals = false) + { + AssemblySymbolLoader loader; + Dictionary assemblySymbols; + + if (assembliesPaths.Length == 0) + { + CreateWithNoAssemblies(); + } + + bool atLeastOneReferencePath = assemblyReferencesPaths?.Count() > 0; + loader = new AssemblySymbolLoader(resolveAssemblyReferences: atLeastOneReferencePath, respectInternals); + if (atLeastOneReferencePath) + { + loader.AddReferenceSearchPaths(assemblyReferencesPaths!); + } + assemblySymbols = new Dictionary(loader.LoadAssembliesAsDictionary(assembliesPaths)); + + return (loader, assemblySymbols); + } + + public static (IAssemblySymbolLoader, Dictionary) CreateWithNoAssemblies(bool respectInternals = false) => + (new AssemblySymbolLoader(resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary()); +} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs new file mode 100644 index 000000000000..5d0893ac1a4d --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs @@ -0,0 +1,341 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Formatting; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.GenAPI.SyntaxRewriter; + +namespace Microsoft.DotNet.GenAPI; + +public class CSharpAssemblyVisitor : IAssemblyVisitor +{ + private readonly ILog _logger; + private readonly IAssemblySymbolLoader _loader; + private readonly ISymbolFilter _symbolFilter; + private readonly ISymbolFilter _attributeDataSymbolFilter; + private readonly string? _exceptionMessage; + private readonly bool _includeAssemblyAttributes; + private readonly AdhocWorkspace _adhocWorkspace; + private readonly SyntaxGenerator _syntaxGenerator; + private readonly IEnumerable? _metadataReferences; + + public CSharpAssemblyVisitor(ILog logger, + IAssemblySymbolLoader loader, + ISymbolFilter symbolFilter, + ISymbolFilter attributeDataSymbolFilter, + string? exceptionMessage, + bool includeAssemblyAttributes, + IEnumerable? metadataReferences = null) + { + _logger = logger; + _loader = loader; + _symbolFilter = symbolFilter; + _attributeDataSymbolFilter = attributeDataSymbolFilter; + _exceptionMessage = exceptionMessage; + _includeAssemblyAttributes = includeAssemblyAttributes; + _adhocWorkspace = new AdhocWorkspace(); + _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); + _metadataReferences = metadataReferences; + } + + /// + public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) + { + CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, + nullableContextOptions: NullableContextOptions.Enable); + Project project = _adhocWorkspace.AddProject(ProjectInfo.Create( + ProjectId.CreateNewId(), VersionStamp.Create(), assemblySymbol.Name, assemblySymbol.Name, LanguageNames.CSharp, + compilationOptions: compilationOptions)); + project = project.AddMetadataReferences(_metadataReferences ?? _loader.MetadataReferences); + + IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_symbolFilter.Include); + List namespaceSyntaxNodes = []; + + foreach (INamespaceSymbol namespaceSymbol in namespaceSymbols.Order()) + { + SyntaxNode? syntaxNode = Visit(namespaceSymbol); + + if (syntaxNode is not null) + { + namespaceSyntaxNodes.Add(syntaxNode); + } + } + + SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation) + .Rewrite(new TypeDeclarationCSharpSyntaxRewriter()) + .Rewrite(new BodyBlockCSharpSyntaxRewriter(_exceptionMessage)); + + if (_includeAssemblyAttributes) + { + compilationUnit = GenerateAssemblyAttributes(assemblySymbol, compilationUnit); + } + + compilationUnit = GenerateForwardedTypeAssemblyAttributes(assemblySymbol, compilationUnit); + compilationUnit = compilationUnit.NormalizeWhitespace(eol: Environment.NewLine); + + Document document = project.AddDocument(assemblySymbol.Name, compilationUnit); + document = Simplifier.ReduceAsync(document).Result; + document = Formatter.FormatAsync(document, DefineFormattingOptions()).Result; + + return document; + } + + /// + public SyntaxNode GetFormattedRootNodeForDocument(Document document) => document.GetSyntaxRootAsync().Result!.Rewrite(new SingleLineStatementCSharpSyntaxRewriter()); + + private SyntaxNode? Visit(INamespaceSymbol namespaceSymbol) + { + SyntaxNode namespaceNode = _syntaxGenerator.NamespaceDeclaration(namespaceSymbol.ToDisplayString()); + + IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_symbolFilter.Include); + if (!typeMembers.Any()) + { + return null; + } + + foreach (INamedTypeSymbol typeMember in typeMembers.Order()) + { + SyntaxNode typeDeclaration = _syntaxGenerator + .DeclarationExt(typeMember, _symbolFilter) + .AddMemberAttributes(_syntaxGenerator, typeMember, _attributeDataSymbolFilter); + + typeDeclaration = Visit(typeDeclaration, typeMember); + + namespaceNode = _syntaxGenerator.AddMembers(namespaceNode, typeDeclaration); + } + + return namespaceNode; + } + + // Name hiding through inheritance occurs when classes or structs redeclare names that were inherited from base classes.This type of name hiding takes one of the following forms: + // - A constant, field, property, event, or type introduced in a class or struct hides all base class members with the same name. + // - A method introduced in a class or struct hides all non-method base class members with the same name, and all base class methods with the same signature(§7.6). + // - An indexer introduced in a class or struct hides all base class indexers with the same signature(§7.6) . + private bool HidesBaseMember(ISymbol member) + { + if (member.IsOverride) + { + return false; + } + + if (member.ContainingType.BaseType is not INamedTypeSymbol baseType) + { + return false; + } + + if (member is IMethodSymbol method) + { + if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation) + { + return false; + } + + // If they're methods, compare their names and signatures. + return baseType.GetMembers(member.Name) + .Any(baseMember => _symbolFilter.Include(baseMember) && + (baseMember.Kind != SymbolKind.Method || + method.SignatureEquals((IMethodSymbol)baseMember))); + } + else if (member is IPropertySymbol prop && prop.IsIndexer) + { + // If they're indexers, compare their signatures. + return baseType.GetMembers(member.Name) + .Any(baseMember => baseMember is IPropertySymbol baseProperty && + _symbolFilter.Include(baseMember) && + (prop.GetMethod.SignatureEquals(baseProperty.GetMethod) || + prop.SetMethod.SignatureEquals(baseProperty.SetMethod))); + } + else + { + // For all other kinds of members, compare their names. + return baseType.GetMembers(member.Name) + .Any(_symbolFilter.Include); + } + } + + private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) + { + IEnumerable members = namedType.GetMembers().Where(_symbolFilter.Include); + + // If it's a value type + if (namedType.TypeKind == TypeKind.Struct) + { + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_symbolFilter, _attributeDataSymbolFilter)); + } + + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_symbolFilter)); + + foreach (ISymbol member in members.Order()) + { + if (member is IMethodSymbol method) + { + // If the method is ExplicitInterfaceImplementation and is derived from an interface that was filtered out, we must filter it out as well. + if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && + method.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol) || + // if explicit interface implementation method has inaccessible type argument + m.ContainingType.HasInaccessibleTypeArgument(_symbolFilter))) + { + continue; + } + + // Filter out default constructors since these will be added automatically + if (method.IsImplicitDefaultConstructor(_symbolFilter)) + { + continue; + } + } + + // If the property is derived from an interface that was filtered out, we must not filter it out either. + if (member is IPropertySymbol property && !property.ExplicitInterfaceImplementations.IsEmpty && + property.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol))) + { + continue; + } + + SyntaxNode memberDeclaration = _syntaxGenerator + .DeclarationExt(member, _symbolFilter) + .AddMemberAttributes(_syntaxGenerator, member, _attributeDataSymbolFilter); + + if (member is INamedTypeSymbol nestedTypeSymbol) + { + memberDeclaration = Visit(memberDeclaration, nestedTypeSymbol); + } + + if (HidesBaseMember(member)) + { + DeclarationModifiers mods = _syntaxGenerator.GetModifiers(memberDeclaration); + memberDeclaration = _syntaxGenerator.WithModifiers(memberDeclaration, mods.WithIsNew(isNew: true)); + } + + try + { + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, memberDeclaration); + } + catch (InvalidOperationException e) + { + // re-throw the InvalidOperationException with the symbol that caused it. + throw new InvalidOperationException(string.Format(Resources.AddMemberThrowsException, + member.ToDisplayString(), + namedTypeNode, + e.Message)); + } + } + + return namedTypeNode; + } + + private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) + { + // When assembly references aren't available, assembly attributes with foreign types won't be resolved. + ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_attributeDataSymbolFilter); + + // Emit assembly attributes from the IAssemblySymbol + List attributeSyntaxNodes = attributes + .Where(attribute => !attribute.IsReserved()) + .Select(attribute => _syntaxGenerator.Attribute(attribute) + .WithTrailingTrivia(SyntaxFactory.LineFeed)) + .ToList(); + + // [assembly: System.Reflection.AssemblyVersion("x.x.x.x")] + if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(AssemblyVersionAttribute).FullName)) + { + attributeSyntaxNodes.Add(_syntaxGenerator.Attribute(typeof(AssemblyVersionAttribute).FullName!, + SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"{assembly.Identity.Version}\""))) + .WithTrailingTrivia(SyntaxFactory.LineFeed)); + } + + // [assembly: System.Runtime.CompilerServices.ReferenceAssembly] + if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(ReferenceAssemblyAttribute).FullName)) + { + attributeSyntaxNodes.Add(_syntaxGenerator.Attribute(typeof(ReferenceAssemblyAttribute).FullName!) + .WithTrailingTrivia(SyntaxFactory.LineFeed)); + } + + // [assembly: System.Reflection.AssemblyFlags((System.Reflection.AssemblyNameFlags)0x70)] + if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(AssemblyFlagsAttribute).FullName)) + { + attributeSyntaxNodes.Add(_syntaxGenerator.Attribute(typeof(AssemblyFlagsAttribute).FullName!, + SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName("(System.Reflection.AssemblyNameFlags)0x70"))) + .WithTrailingTrivia(SyntaxFactory.LineFeed)); + } + + return _syntaxGenerator.AddAttributes(compilationUnit, attributeSyntaxNodes); + } + + private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) + { + foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_symbolFilter.Include)) + { + if (symbol.TypeKind != TypeKind.Error) + { + // see https://github.com/dotnet/roslyn/issues/67341 + // GetForwardedTypes returns bound generics, but `typeof` requires unbound + TypeSyntax typeSyntaxNode = (TypeSyntax)_syntaxGenerator.TypeExpression(symbol.MakeUnboundIfGeneric()); + compilationUnit = _syntaxGenerator.AddAttributes(compilationUnit, + _syntaxGenerator.Attribute(typeof(TypeForwardedToAttribute).FullName!, + SyntaxFactory.TypeOfExpression(typeSyntaxNode)).WithTrailingTrivia(SyntaxFactory.LineFeed)); + } + else + { + _logger.LogWarning(string.Format( + Resources.ResolveTypeForwardFailed, + symbol.ToDisplayString(), + $"{symbol.ContainingAssembly.Name}.dll")); + } + } + + return compilationUnit; + } + + private static IEnumerable EnumerateNamespaces(IAssemblySymbol assemblySymbol) + { + Stack stack = new(); + stack.Push(assemblySymbol.GlobalNamespace); + + while (stack.Count > 0) + { + INamespaceSymbol current = stack.Pop(); + + yield return current; + + foreach (INamespaceSymbol subNamespace in current.GetNamespaceMembers()) + { + stack.Push(subNamespace); + } + } + } + + private OptionSet DefineFormattingOptions() + { + // TODO: consider to move configuration into file. + return _adhocWorkspace.Options + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInTypes, true) + .WithChangedOption(CSharpFormattingOptions.WrappingKeepStatementsOnSingleLine, true) + .WithChangedOption(CSharpFormattingOptions.WrappingPreserveSingleLine, true) + .WithChangedOption(CSharpFormattingOptions.IndentBlock, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInMethods, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInProperties, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInAccessors, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInAnonymousMethods, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInControlBlocks, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInAnonymousTypes, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInObjectCollectionArrayInitializers, false) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInLambdaExpressionBody, false) + .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInObjectInit, false) + .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInAnonymousTypes, false) + .WithChangedOption(CSharpFormattingOptions.NewLineForClausesInQuery, false); + } +} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs index 383253746f4e..54bbfdb580b9 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -1,21 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; -using System.Reflection; -using System.Runtime.CompilerServices; +#if !NET +using System.Text.RegularExpressions; +#endif using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Formatting; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Simplification; using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; -using Microsoft.DotNet.GenAPI.SyntaxRewriter; namespace Microsoft.DotNet.GenAPI { @@ -24,336 +16,57 @@ namespace Microsoft.DotNet.GenAPI /// public sealed class CSharpFileBuilder : IAssemblySymbolWriter { - private readonly ILog _logger; private readonly TextWriter _textWriter; - private readonly AssemblySymbolLoader _loader; - private readonly ISymbolFilter _symbolFilter; - private readonly ISymbolFilter _attributeDataSymbolFilter; - private readonly string _header; - private readonly string? _exceptionMessage; - private readonly bool _includeAssemblyAttributes; - private readonly AdhocWorkspace _adhocWorkspace; - private readonly SyntaxGenerator _syntaxGenerator; - private readonly IEnumerable? _metadataReferences; + private readonly string? _header; + private readonly IAssemblyVisitor _assemblyVisitor; public CSharpFileBuilder(ILog logger, TextWriter textWriter, - AssemblySymbolLoader loader, + IAssemblySymbolLoader loader, ISymbolFilter symbolFilter, ISymbolFilter attributeDataSymbolFilter, - string header, + string? header, string? exceptionMessage, bool includeAssemblyAttributes, IEnumerable? metadataReferences = null) { - _logger = logger; _textWriter = textWriter; - _loader = loader; - _symbolFilter = symbolFilter; - _attributeDataSymbolFilter = attributeDataSymbolFilter; _header = header; - _exceptionMessage = exceptionMessage; - _includeAssemblyAttributes = includeAssemblyAttributes; - _adhocWorkspace = new AdhocWorkspace(); - _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); - _metadataReferences = metadataReferences; + _assemblyVisitor = new CSharpAssemblyVisitor(logger, loader, symbolFilter, attributeDataSymbolFilter, exceptionMessage, includeAssemblyAttributes, metadataReferences); } /// public void WriteAssembly(IAssemblySymbol assemblySymbol) { - _textWriter.Write(_header); - Document document = GetDocumentForAssembly(assemblySymbol); - GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); + _textWriter.Write(GetFormattedHeader(_header)); + Document document = _assemblyVisitor.GetDocumentForAssembly(assemblySymbol); + _assemblyVisitor.GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); } - /// - public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) - { - CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, - nullableContextOptions: NullableContextOptions.Enable); - Project project = _adhocWorkspace.AddProject(ProjectInfo.Create( - ProjectId.CreateNewId(), VersionStamp.Create(), assemblySymbol.Name, assemblySymbol.Name, LanguageNames.CSharp, - compilationOptions: compilationOptions)); - project = project.AddMetadataReferences(_metadataReferences ?? _loader.MetadataReferences); - - IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_symbolFilter.Include); - List namespaceSyntaxNodes = []; - - foreach (INamespaceSymbol namespaceSymbol in namespaceSymbols.Order()) - { - SyntaxNode? syntaxNode = Visit(namespaceSymbol); - - if (syntaxNode is not null) - { - namespaceSyntaxNodes.Add(syntaxNode); - } - } - - SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes) - .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation) - .Rewrite(new TypeDeclarationCSharpSyntaxRewriter()) - .Rewrite(new BodyBlockCSharpSyntaxRewriter(_exceptionMessage)); - - if (_includeAssemblyAttributes) - { - compilationUnit = GenerateAssemblyAttributes(assemblySymbol, compilationUnit); - } - - compilationUnit = GenerateForwardedTypeAssemblyAttributes(assemblySymbol, compilationUnit); - compilationUnit = compilationUnit.NormalizeWhitespace(eol: Environment.NewLine); - - Document document = project.AddDocument(assemblySymbol.Name, compilationUnit); - document = Simplifier.ReduceAsync(document).Result; - document = Formatter.FormatAsync(document, DefineFormattingOptions()).Result; - - return document; - } - - /// - public SyntaxNode GetFormattedRootNodeForDocument(Document document) => document.GetSyntaxRootAsync().Result!.Rewrite(new SingleLineStatementCSharpSyntaxRewriter()); - - private SyntaxNode? Visit(INamespaceSymbol namespaceSymbol) - { - SyntaxNode namespaceNode = _syntaxGenerator.NamespaceDeclaration(namespaceSymbol.ToDisplayString()); - - IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_symbolFilter.Include); - if (!typeMembers.Any()) - { - return null; - } - - foreach (INamedTypeSymbol typeMember in typeMembers.Order()) - { - SyntaxNode typeDeclaration = _syntaxGenerator - .DeclarationExt(typeMember, _symbolFilter) - .AddMemberAttributes(_syntaxGenerator, typeMember, _attributeDataSymbolFilter); - - typeDeclaration = Visit(typeDeclaration, typeMember); - - namespaceNode = _syntaxGenerator.AddMembers(namespaceNode, typeDeclaration); - } - - return namespaceNode; - } - - // Name hiding through inheritance occurs when classes or structs redeclare names that were inherited from base classes.This type of name hiding takes one of the following forms: - // - A constant, field, property, event, or type introduced in a class or struct hides all base class members with the same name. - // - A method introduced in a class or struct hides all non-method base class members with the same name, and all base class methods with the same signature(§7.6). - // - An indexer introduced in a class or struct hides all base class indexers with the same signature(§7.6) . - private bool HidesBaseMember(ISymbol member) - { - if (member.IsOverride) - { - return false; - } - - if (member.ContainingType.BaseType is not INamedTypeSymbol baseType) - { - return false; - } - - if (member is IMethodSymbol method) - { - if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation) - { - return false; - } - - // If they're methods, compare their names and signatures. - return baseType.GetMembers(member.Name) - .Any(baseMember => _symbolFilter.Include(baseMember) && - (baseMember.Kind != SymbolKind.Method || - method.SignatureEquals((IMethodSymbol)baseMember))); - } - else if (member is IPropertySymbol prop && prop.IsIndexer) - { - // If they're indexers, compare their signatures. - return baseType.GetMembers(member.Name) - .Any(baseMember => baseMember is IPropertySymbol baseProperty && - _symbolFilter.Include(baseMember) && - (prop.GetMethod.SignatureEquals(baseProperty.GetMethod) || - prop.SetMethod.SignatureEquals(baseProperty.SetMethod))); - } - else - { - // For all other kinds of members, compare their names. - return baseType.GetMembers(member.Name) - .Any(_symbolFilter.Include); - } - } - - private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) - { - IEnumerable members = namedType.GetMembers().Where(_symbolFilter.Include); - - // If it's a value type - if (namedType.TypeKind == TypeKind.Struct) - { - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_symbolFilter, _attributeDataSymbolFilter)); - } - - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_symbolFilter)); - - foreach (ISymbol member in members.Order()) - { - if (member is IMethodSymbol method) - { - // If the method is ExplicitInterfaceImplementation and is derived from an interface that was filtered out, we must filter out it as well. - if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && - method.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol) || - // if explicit interface implementation method has inaccessible type argument - m.ContainingType.HasInaccessibleTypeArgument(_symbolFilter))) - { - continue; - } - - // Filter out default constructors since these will be added automatically - if (method.IsImplicitDefaultConstructor(_symbolFilter)) - { - continue; - } - } - - // If the property is derived from an interface that was filter out, we must filtered out it either. - if (member is IPropertySymbol property && !property.ExplicitInterfaceImplementations.IsEmpty && - property.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol))) - { - continue; - } - - SyntaxNode memberDeclaration = _syntaxGenerator - .DeclarationExt(member, _symbolFilter) - .AddMemberAttributes(_syntaxGenerator, member, _attributeDataSymbolFilter); - - if (member is INamedTypeSymbol nestedTypeSymbol) - { - memberDeclaration = Visit(memberDeclaration, nestedTypeSymbol); - } - - if (HidesBaseMember(member)) - { - DeclarationModifiers mods = _syntaxGenerator.GetModifiers(memberDeclaration); - memberDeclaration = _syntaxGenerator.WithModifiers(memberDeclaration, mods.WithIsNew(isNew: true)); - } - - try - { - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, memberDeclaration); - } - catch (InvalidOperationException e) - { - // re-throw the InvalidOperationException with the symbol that caused it. - throw new InvalidOperationException(string.Format(Resources.AddMemberThrowsException, - member.ToDisplayString(), - namedTypeNode, - e.Message)); - } - } - - return namedTypeNode; - } - - private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) + private static string GetFormattedHeader(string? customHeader) { - // When assembly references aren't available, assembly attributes with foreign types won't be resolved. - ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_attributeDataSymbolFilter); + const string defaultFileHeader = """ + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ - // Emit assembly attributes from the IAssemblySymbol - List attributeSyntaxNodes = attributes - .Where(attribute => !attribute.IsReserved()) - .Select(attribute => _syntaxGenerator.Attribute(attribute) - .WithTrailingTrivia(SyntaxFactory.LineFeed)) - .ToList(); + """; - // [assembly: System.Reflection.AssemblyVersion("x.x.x.x")] - if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(AssemblyVersionAttribute).FullName)) + if (customHeader != null) { - attributeSyntaxNodes.Add(_syntaxGenerator.Attribute(typeof(AssemblyVersionAttribute).FullName!, - SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"{assembly.Identity.Version}\""))) - .WithTrailingTrivia(SyntaxFactory.LineFeed)); +#if NET + return customHeader.ReplaceLineEndings(); +#else + return Regex.Replace(customHeader, @"\r\n|\n\r|\n|\r", Environment.NewLine); +#endif } - // [assembly: System.Runtime.CompilerServices.ReferenceAssembly] - if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(ReferenceAssemblyAttribute).FullName)) - { - attributeSyntaxNodes.Add(_syntaxGenerator.Attribute(typeof(ReferenceAssemblyAttribute).FullName!) - .WithTrailingTrivia(SyntaxFactory.LineFeed)); - } - - // [assembly: System.Reflection.AssemblyFlags((System.Reflection.AssemblyNameFlags)0x70)] - if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(AssemblyFlagsAttribute).FullName)) - { - attributeSyntaxNodes.Add(_syntaxGenerator.Attribute(typeof(AssemblyFlagsAttribute).FullName!, - SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName("(System.Reflection.AssemblyNameFlags)0x70"))) - .WithTrailingTrivia(SyntaxFactory.LineFeed)); - } - - return _syntaxGenerator.AddAttributes(compilationUnit, attributeSyntaxNodes); - } - - private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) - { - foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_symbolFilter.Include)) - { - if (symbol.TypeKind != TypeKind.Error) - { - // see https://github.com/dotnet/roslyn/issues/67341 - // GetForwardedTypes returns bound generics, but `typeof` requires unbound - TypeSyntax typeSyntaxNode = (TypeSyntax)_syntaxGenerator.TypeExpression(symbol.MakeUnboundIfGeneric()); - compilationUnit = _syntaxGenerator.AddAttributes(compilationUnit, - _syntaxGenerator.Attribute(typeof(TypeForwardedToAttribute).FullName!, - SyntaxFactory.TypeOfExpression(typeSyntaxNode)).WithTrailingTrivia(SyntaxFactory.LineFeed)); - } - else - { - _logger.LogWarning(string.Format( - Resources.ResolveTypeForwardFailed, - symbol.ToDisplayString(), - $"{symbol.ContainingAssembly.Name}.dll")); - } - } - - return compilationUnit; - } - - private static IEnumerable EnumerateNamespaces(IAssemblySymbol assemblySymbol) - { - Stack stack = new(); - stack.Push(assemblySymbol.GlobalNamespace); - - while (stack.Count > 0) - { - INamespaceSymbol current = stack.Pop(); - - yield return current; - - foreach (INamespaceSymbol subNamespace in current.GetNamespaceMembers()) - { - stack.Push(subNamespace); - } - } - } - - private OptionSet DefineFormattingOptions() - { - // TODO: consider to move configuration into file. - return _adhocWorkspace.Options - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInTypes, true) - .WithChangedOption(CSharpFormattingOptions.WrappingKeepStatementsOnSingleLine, true) - .WithChangedOption(CSharpFormattingOptions.WrappingPreserveSingleLine, true) - .WithChangedOption(CSharpFormattingOptions.IndentBlock, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInMethods, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInProperties, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInAccessors, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInAnonymousMethods, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInControlBlocks, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInAnonymousTypes, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInObjectCollectionArrayInitializers, false) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInLambdaExpressionBody, false) - .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInObjectInit, false) - .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInAnonymousTypes, false) - .WithChangedOption(CSharpFormattingOptions.NewLineForClausesInQuery, false); + return defaultFileHeader; } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index a93384c6c8de..a3096bf6dd9a 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -6,7 +6,6 @@ #endif using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.GenAPI @@ -21,27 +20,29 @@ public static class GenAPIApp /// Initialize and run Roslyn-based GenAPI tool. /// public static void Run(ILog logger, - Dictionary assemblySymbols, - string? outputPath, - AssemblySymbolLoader loader, - ISymbolFilter symbolFilter, - ISymbolFilter attributeSymbolFilter, - string header, - string? exceptionMessage, - bool includeAssemblyAttributes) + IAssemblySymbolLoader loader, + Dictionary assemblySymbols, + string? outputPath, + string? headerFile, + string? exceptionMessage, + string[]? excludeApiFiles, + string[]? excludeAttributesFiles, + bool respectInternals, + bool includeAssemblyAttributes) { + // Invoke an assembly symbol writer for each directly loaded assembly. foreach (KeyValuePair kvp in assemblySymbols) { using TextWriter textWriter = GetTextWriter(outputPath, kvp.Key); - IAssemblySymbolWriter writer = new CSharpFileBuilder(logger, - textWriter, - loader, - symbolFilter, - attributeSymbolFilter, - header, - exceptionMessage, - includeAssemblyAttributes); + CSharpFileBuilder writer = new(logger, + textWriter, + loader, + SymbolFilterFactory.GetSymbolFilterFromFiles(excludeApiFiles, respectInternals), + SymbolFilterFactory.GetAttributeFilterFromPaths(excludeAttributesFiles, respectInternals), + headerFile, + exceptionMessage, + includeAssemblyAttributes); writer.WriteAssembly(kvp.Value); } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs deleted file mode 100644 index 224b9771f27c..000000000000 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIConfiguration.cs +++ /dev/null @@ -1,317 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if !NET -using System.Text.RegularExpressions; -#endif -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis; -using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; -using Microsoft.DotNet.GenAPI.Filtering; -using Microsoft.CodeAnalysis.CSharp; -using System.Collections.Immutable; -using System.Diagnostics; - -namespace Microsoft.DotNet.GenAPI; - -public class GenAPIConfiguration -{ - private GenAPIConfiguration() - { - } - - [AllowNull] - public AssemblySymbolLoader Loader { get; private set; } - [AllowNull] - public Dictionary AssemblySymbols { get; private set; } - - public static Builder GetBuilder() => new Builder(); - - public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFilePaths, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - apiExclusionFilePaths?.Count() > 0 ? - DocIdSymbolFilter.CreateFromFiles(apiExclusionFilePaths) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); - } - - public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - apiExclusionList?.Count() > 0 ? - DocIdSymbolFilter.CreateFromDocIDs(apiExclusionList) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); - } - - public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclusionFilePaths, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - attributeExclusionFilePaths?.Count() > 0 ? - DocIdSymbolFilter.CreateFromFiles(attributeExclusionFilePaths) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); - } - - public static ISymbolFilter GetAttributeFilterFromList(string[]? attributeExclusionList, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - attributeExclusionList?.Count() > 0 ? - DocIdSymbolFilter.CreateFromDocIDs(attributeExclusionList) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); - } - - private static ISymbolFilter GetCompositeSymbolFilter(DocIdSymbolFilter? customFilter, - bool respectInternals, - bool includeEffectivelyPrivateSymbols, - bool includeExplicitInterfaceImplementationSymbols, - bool withImplicitSymbolFilter) - { - AccessibilitySymbolFilter accessibilitySymbolFilter = new( - respectInternals, - includeEffectivelyPrivateSymbols, - includeExplicitInterfaceImplementationSymbols); - - CompositeSymbolFilter filter = new(); - - if (customFilter != null) - { - filter.Add(customFilter); - } - if (withImplicitSymbolFilter) - { - filter.Add(new ImplicitSymbolFilter()); - } - - filter.Add(accessibilitySymbolFilter); - - return filter; - } - - - public static string GetFormattedHeader(string? customHeader = null) - { - const string defaultFileHeader = """ - //------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - //------------------------------------------------------------------------------ - - """; - - if (customHeader != null) - { -#if NET - return customHeader.ReplaceLineEndings(); -#else - return Regex.Replace(customHeader, @"\r\n|\n\r|\n|\r", Environment.NewLine); -#endif - } - - return defaultFileHeader; - } - - public class Builder - { - private string[]? _assembliesPaths = null; - private string[]? _assemblyReferencesPaths = null; - private (string, string)[]? _assemblyTexts; - private bool _allowUnsafe = false; - private bool _respectInternals = false; - - public Builder WithAssembliesPaths(params string[]? assembliesPaths) - { - if (assembliesPaths == null) - { - return this; - } - if (_assemblyTexts != null) - { - throw new InvalidOperationException("Cannot specify both assembly paths and streams."); - } - foreach (string path in assembliesPaths) - { - ThrowIfFileSystemEntryNotFound(nameof(assembliesPaths), path); - } - _assembliesPaths = assembliesPaths; - return this; - } - - public Builder WithAssemblyTexts(params (string, string)[]? assemblyTexts) - { - if (assemblyTexts == null) - { - return this; - } - if (_assembliesPaths != null) - { - throw new InvalidOperationException("Cannot specify both assembly paths and streams."); - } - - _assemblyTexts = assemblyTexts; - return this; - } - - public Builder WithAssemblyReferencesPaths(params string[]? assemblyReferencesPaths) - { - if (assemblyReferencesPaths == null) - { - return this; - } - foreach (string path in assemblyReferencesPaths) - { - ThrowIfFileSystemEntryNotFound(nameof(assemblyReferencesPaths), path); - } - _assemblyReferencesPaths = assemblyReferencesPaths; - return this; - } - - public Builder WithAllowUnsafe(bool allowUnsafe) - { - _allowUnsafe = allowUnsafe; - return this; - } - - public Builder WithRespectInternals(bool respectInternals) - { - _respectInternals = respectInternals; - return this; - } - - public GenAPIConfiguration Build() - { - AssemblySymbolLoader loader; - Dictionary assemblySymbols; - - if (_assembliesPaths?.Length > 0) - { - bool resolveAssemblyReferences = _assemblyReferencesPaths?.Count() > 0; - loader = new AssemblySymbolLoader(resolveAssemblyReferences, _respectInternals); - if (_assemblyReferencesPaths?.Length > 0) - { - loader.AddReferenceSearchPaths(_assemblyReferencesPaths); - } - assemblySymbols = new Dictionary(loader.LoadAssembliesAsDictionary(_assembliesPaths)); - } - else if (_assemblyTexts?.Count() > 0) - { - loader = new AssemblySymbolLoader(resolveAssemblyReferences: true, includeInternalSymbols: _respectInternals); - loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); - loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); - - assemblySymbols = new Dictionary(); - foreach ((string assemblyName, string assemblyText) in _assemblyTexts) - { - using Stream assemblyStream = EmitAssemblyStreamFromSyntax(assemblyText, enableNullable: true, allowUnsafe: _allowUnsafe, assemblyName: assemblyName); - if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) - { - assemblySymbols.Add(assemblyName, assemblySymbol); - } - } - } - else - { - throw new InvalidOperationException("No assemblies were specified, either from files or from streams."); - } - - return new GenAPIConfiguration() - { - Loader = loader, - AssemblySymbols = assemblySymbols - }; - } - - - private static IEnumerable> DiagnosticOptions { get; } = new[] - { - // Suppress warning for unused events. - new KeyValuePair("CS0067", ReportDiagnostic.Suppress) - }; - - private static IEnumerable DefaultReferences { get; } = new[] - { - MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DynamicAttribute).Assembly.Location), - }; - - public static Stream EmitAssemblyStreamFromSyntax(string syntax, - bool enableNullable = false, - byte[]? publicKey = null, - [CallerMemberName] string assemblyName = "", - bool allowUnsafe = false) - { - CSharpCompilation compilation = CreateCSharpCompilationFromSyntax([syntax], assemblyName, enableNullable, publicKey, allowUnsafe); - - Debug.Assert(compilation.GetDiagnostics().IsEmpty); - - MemoryStream stream = new(); - compilation.Emit(stream); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } - - private static SyntaxTree GetSyntaxTree(string syntax) - { - return CSharpSyntaxTree.ParseText(syntax, ParseOptions); - } - - private static CSharpParseOptions ParseOptions { get; } = new CSharpParseOptions(preprocessorSymbols: -#if NETFRAMEWORK - new string[] { "NETFRAMEWORK" } -#else - Array.Empty() -#endif - ); - - private static CSharpCompilation CreateCSharpCompilationFromSyntax(IEnumerable syntax, string name, bool enableNullable, byte[]? publicKey, bool allowUnsafe) - { - CSharpCompilation compilation = CreateCSharpCompilation(name, enableNullable, publicKey, allowUnsafe); - IEnumerable syntaxTrees = syntax.Select(s => GetSyntaxTree(s)); - return compilation.AddSyntaxTrees(syntaxTrees); - } - - private static CSharpCompilation CreateCSharpCompilation(string name, bool enableNullable, byte[]? publicKey, bool allowUnsafe) - { - bool publicSign = publicKey != null ? true : false; - var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, - publicSign: publicSign, - cryptoPublicKey: publicSign ? publicKey!.ToImmutableArray() : default, - nullableContextOptions: enableNullable ? NullableContextOptions.Enable : NullableContextOptions.Disable, - allowUnsafe: allowUnsafe, - specificDiagnosticOptions: DiagnosticOptions); - - return CSharpCompilation.Create(name, options: compilationOptions, references: DefaultReferences); - } - - - private void ThrowIfFileSystemEntryNotFound(string argumentName, string path) - { - if (Directory.Exists(path) || File.Exists(path)) - { - return; - } - - throw new FileNotFoundException($"The {argumentName} is not a valid file or directory: {path}"); - } - } -} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs index 48600836e63e..e093037b8a78 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs @@ -15,19 +15,5 @@ public interface IAssemblySymbolWriter /// /// An assembly symbol representing the loaded assembly. void WriteAssembly(IAssemblySymbol assemblySymbol); - - /// - /// Returns the configured source code document for the specified assembly symbol. - /// - /// The assembly symbol that represents the loaded assembly. - /// The source code document instance of the specified assembly symbol. - Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol); - - /// - /// Returns the formatted root syntax node for the specified document. - /// - /// A source code document instance. - /// The root syntax node of the specified document. - public SyntaxNode GetFormattedRootNodeForDocument(Document document); } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs new file mode 100644 index 000000000000..a47bd466035f --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DotNet.GenAPI; + +public interface IAssemblyVisitor +{ + /// + /// Returns the configured source code document for the specified assembly symbol. + /// + /// The assembly symbol that represents the loaded assembly. + /// The source code document instance of the specified assembly symbol. + Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol); + + /// + /// Returns the formatted root syntax node for the specified document. + /// + /// A source code document instance. + /// The root syntax node of the specified document. + public SyntaxNode GetFormattedRootNodeForDocument(Document document); +} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs new file mode 100644 index 000000000000..5a49f9fb46a6 --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.GenAPI.Filtering; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; + +namespace Microsoft.DotNet.GenAPI; + +public static class SymbolFilterFactory +{ + public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFilePaths, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + apiExclusionFilePaths?.Count() > 0 ? + DocIdSymbolFilter.CreateFromFiles(apiExclusionFilePaths) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); + } + + public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + apiExclusionList?.Count() > 0 ? + DocIdSymbolFilter.CreateFromDocIDs(apiExclusionList) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); + } + + public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclusionFilePaths, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + attributeExclusionFilePaths?.Count() > 0 ? + DocIdSymbolFilter.CreateFromFiles(attributeExclusionFilePaths) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); + } + + public static ISymbolFilter GetAttributeFilterFromList(string[]? attributeExclusionList, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + attributeExclusionList?.Count() > 0 ? + DocIdSymbolFilter.CreateFromDocIDs(attributeExclusionList) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); + } + + private static ISymbolFilter GetCompositeSymbolFilter(DocIdSymbolFilter? customFilter, + bool respectInternals, + bool includeEffectivelyPrivateSymbols, + bool includeExplicitInterfaceImplementationSymbols, + bool withImplicitSymbolFilter) + { + AccessibilitySymbolFilter accessibilitySymbolFilter = new( + respectInternals, + includeEffectivelyPrivateSymbols, + includeExplicitInterfaceImplementationSymbols); + + CompositeSymbolFilter filter = new(); + + if (customFilter != null) + { + filter.Add(customFilter); + } + if (withImplicitSymbolFilter) + { + filter.Add(new ImplicitSymbolFilter()); + } + + filter.Add(accessibilitySymbolFilter); + + return filter; + } +} diff --git a/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj b/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj index d41363a5174f..a5b1a26c8789 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index 611af509b9db..e3b7691f230d 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; @@ -34,25 +35,21 @@ private void RunTest(string original, { using StringWriter stringWriter = new(); - GenAPIConfiguration config = GenAPIConfiguration.GetBuilder() - .WithAssemblyTexts((assemblyName, original)) - .WithAllowUnsafe(allowUnsafe) - .WithRespectInternals(includeInternalSymbols) - .Build(); - + (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = TestAssemblyLoaderFactory + .CreateFromTexts(assemblyTexts: [(assemblyName, original)], respectInternals: includeInternalSymbols, allowUnsafe); IAssemblySymbolWriter writer = new CSharpFileBuilder( new ConsoleLog(MessageImportance.Low), stringWriter, - config.Loader, - GenAPIConfiguration.GetSymbolFilterFromList([], includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), - GenAPIConfiguration.GetAttributeFilterFromList(excludedAttributeList, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + loader, + SymbolFilterFactory.GetSymbolFilterFromList([], includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + SymbolFilterFactory.GetAttributeFilterFromList(excludedAttributeList, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), header: string.Empty, exceptionMessage: null, includeAssemblyAttributes: false, MetadataReferences); - writer.WriteAssembly(config.AssemblySymbols.First().Value); + writer.WriteAssembly(assemblySymbols.First().Value); StringBuilder stringBuilder = stringWriter.GetStringBuilder(); string resultedString = stringBuilder.ToString(); diff --git a/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs new file mode 100644 index 000000000000..be09c47c5f00 --- /dev/null +++ b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Tests; + +namespace Microsoft.DotNet.GenAPI.Tests; + +public class TestAssemblyLoaderFactory +{ + public static (IAssemblySymbolLoader, Dictionary) CreateFromTexts((string, string)[] assemblyTexts, bool respectInternals = false, bool allowUnsafe = false) + { + if (assemblyTexts.Length == 0) + { + return AssemblyLoaderFactory.CreateWithNoAssemblies(respectInternals); + } + + AssemblySymbolLoader loader = new(resolveAssemblyReferences: true, includeInternalSymbols: respectInternals); + loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); + loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); + + Dictionary assemblySymbols = new(); + foreach ((string assemblyName, string assemblyText) in assemblyTexts) + { + using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(assemblyText, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName); + if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) + { + assemblySymbols.Add(assemblyName, assemblySymbol); + } + } + + return (loader, assemblySymbols); + } +} From a3a2c70c7e09d0157b10b0aa0dfcaa43e694bb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:19:57 -0800 Subject: [PATCH 07/11] Address feedback and make some other improvements. --- .../AssemblyLoaderFactory.cs | 15 ++++++++ .../CSharpAssemblyVisitor.cs | 29 ++++++++++++--- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 17 ++------- .../SymbolFilterFactory.cs | 35 +++++++++++++++++++ .../TypeDeclarationCSharpSyntaxRewriter.cs | 12 +++++-- .../AssemblySymbolLoader.cs | 35 +++++++++++++++++++ .../IAssemblySymbolLoader.cs | 15 ++++++++ 7 files changed, 137 insertions(+), 21 deletions(-) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs index ff6006501258..e48f33eb4da1 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs @@ -8,8 +8,18 @@ namespace Microsoft.DotNet.GenAPI; +/// +/// Class that facilitates the creation of an assembly symbol loader and its corresponding assembly symbols. +/// public class AssemblyLoaderFactory { + /// + /// Creates an assembly symbol loader and its corresponding assembly symbols from the given DLL files in the filesystem. + /// + /// A collection of paths where the assembly DLLs should be searched. + /// An optional collection of paths where the assembly references should be searched. + /// Whether to include internal symbols or not. + /// A tuple containing the assembly symbol loader and a dictionary of assembly symbols. public static (IAssemblySymbolLoader, Dictionary) CreateFromFiles(string[] assembliesPaths, string[]? assemblyReferencesPaths, bool respectInternals = false) { AssemblySymbolLoader loader; @@ -31,6 +41,11 @@ public static (IAssemblySymbolLoader, Dictionary) Creat return (loader, assemblySymbols); } + /// + /// Creates a default assembly symbol loader and a dictionary of assembly symbols. + /// + /// Whether to include internal symbols or not. + /// A tuple containing the assembly symbol loader and a dictionary of assembly symbols. public static (IAssemblySymbolLoader, Dictionary) CreateWithNoAssemblies(bool respectInternals = false) => (new AssemblySymbolLoader(resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary()); } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs index 5d0893ac1a4d..ecb74c24132b 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs @@ -19,6 +19,9 @@ namespace Microsoft.DotNet.GenAPI; +/// +/// A class that visits a collection of specified assemblies and generates the corresponding C# document and syntax trees. +/// public class CSharpAssemblyVisitor : IAssemblyVisitor { private readonly ILog _logger; @@ -30,14 +33,30 @@ public class CSharpAssemblyVisitor : IAssemblyVisitor private readonly AdhocWorkspace _adhocWorkspace; private readonly SyntaxGenerator _syntaxGenerator; private readonly IEnumerable? _metadataReferences; - + private readonly bool _addPartialModifier; + private readonly bool _hideImplicitDefaultConstructors; + + /// + /// Initializes a new instance of the class. + /// + /// The logger to use. + /// The assembly symbol loader to use. + /// The symbol filter to use. + /// The attribute data symbol filter to use. + /// The optional exception message to use. + /// Whether to include assembly attributes or not. + /// The metadata references to use. The default value is . + /// Whether to add the partial modifier or not. The default value is . + /// Whether to hide implicit default constructors or not. The default value is . public CSharpAssemblyVisitor(ILog logger, IAssemblySymbolLoader loader, ISymbolFilter symbolFilter, ISymbolFilter attributeDataSymbolFilter, string? exceptionMessage, bool includeAssemblyAttributes, - IEnumerable? metadataReferences = null) + IEnumerable? metadataReferences = null, + bool addPartialModifier = true, + bool hideImplicitDefaultConstructors = true) { _logger = logger; _loader = loader; @@ -48,6 +67,8 @@ public CSharpAssemblyVisitor(ILog logger, _adhocWorkspace = new AdhocWorkspace(); _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); _metadataReferences = metadataReferences; + _addPartialModifier = addPartialModifier; + _hideImplicitDefaultConstructors = hideImplicitDefaultConstructors; } /// @@ -75,7 +96,7 @@ public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes) .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation) - .Rewrite(new TypeDeclarationCSharpSyntaxRewriter()) + .Rewrite(new TypeDeclarationCSharpSyntaxRewriter(_addPartialModifier)) .Rewrite(new BodyBlockCSharpSyntaxRewriter(_exceptionMessage)); if (_includeAssemblyAttributes) @@ -192,7 +213,7 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) } // Filter out default constructors since these will be added automatically - if (method.IsImplicitDefaultConstructor(_symbolFilter)) + if (_hideImplicitDefaultConstructors && method.IsImplicitDefaultConstructor(_symbolFilter)) { continue; } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index a3096bf6dd9a..dc1518df5a8e 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -46,21 +46,8 @@ public static void Run(ILog logger, writer.WriteAssembly(kvp.Value); } - if (loader.HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) - { - foreach (Diagnostic warning in roslynDiagnostics) - { - logger.LogWarning(warning.Id, warning.ToString()); - } - } - - if (loader.HasLoadWarnings(out IReadOnlyList loadWarnings)) - { - foreach (AssemblyLoadWarning warning in loadWarnings) - { - logger.LogWarning(warning.DiagnosticId, warning.Message); - } - } + loader.LogAllDiagnostics(logger); + loader.LogAllWarnings(logger); } // Creates a TextWriter capable of writing into Console or a cs file. diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs index 5a49f9fb46a6..894e59e11c40 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs @@ -7,8 +7,19 @@ namespace Microsoft.DotNet.GenAPI; +/// +/// Factory class that creates composite filters based on the given exclusion lists. +/// public static class SymbolFilterFactory { + /// + /// Creates a symbol filter based on the given exclusion file paths. + /// + /// A collection of paths where the exclusion files should be searched. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the symbol filter. public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFilePaths, bool respectInternals = false, bool includeEffectivelyPrivateSymbols = true, @@ -21,6 +32,14 @@ public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFileP return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); } + /// + /// Creates a symbol filter based on the given exclusion list. + /// + /// A collection of exclusion list. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the symbol filter. public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, bool respectInternals = false, bool includeEffectivelyPrivateSymbols = true, @@ -33,6 +52,14 @@ public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); } + /// + /// Creates an attribute filter based on the given exclusion file paths. + /// + /// A collection of paths where the exclusion files should be searched. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the attribute filter. public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclusionFilePaths, bool respectInternals = false, bool includeEffectivelyPrivateSymbols = true, @@ -45,6 +72,14 @@ public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclu return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); } + /// + /// Creates an attribute filter based on the given exclusion list. + /// + /// A collection of exclusion list. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the attribute filter. public static ISymbolFilter GetAttributeFilterFromList(string[]? attributeExclusionList, bool respectInternals = false, bool includeEffectivelyPrivateSymbols = true, diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs index 80a061eb461f..bf8ba53e4adb 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs @@ -17,6 +17,14 @@ namespace Microsoft.DotNet.GenAPI.SyntaxRewriter /// public class TypeDeclarationCSharpSyntaxRewriter : CSharpSyntaxRewriter { + private readonly bool _addPartialModifier; + + /// + /// Initializes a new instance of the class, and optionally allows deciding whether to insert the partial modifier for types or not. + /// + /// Determines whether to insert the partial modifier for types or not. + public TypeDeclarationCSharpSyntaxRewriter(bool addPartialModifier = true) => _addPartialModifier = addPartialModifier; + /// public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { @@ -83,7 +91,7 @@ public class TypeDeclarationCSharpSyntaxRewriter : CSharpSyntaxRewriter } } - private static T? VisitCommonTypeDeclaration(T? node) where T : TypeDeclarationSyntax + private T? VisitCommonTypeDeclaration(T? node) where T : TypeDeclarationSyntax { if (node == null) { @@ -91,7 +99,7 @@ public class TypeDeclarationCSharpSyntaxRewriter : CSharpSyntaxRewriter } node = RemoveBaseType(node, "global::System.Object"); - return AddPartialModifier(node); + return _addPartialModifier ? AddPartialModifier(node) : node; } private static T? AddPartialModifier(T? node) where T : TypeDeclarationSyntax => diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs index 29be3b23d690..ad68c63dc515 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs @@ -7,6 +7,7 @@ using System.Reflection.PortableExecutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.ApiSymbolExtensions { @@ -279,6 +280,40 @@ public IEnumerable LoadMatchingAssemblies(IEnumerable + public void LogAllDiagnostics(ILog logger, string? customMessage = null) + { + if (HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) + { + if (!string.IsNullOrEmpty(customMessage)) + { + logger.LogWarning(customMessage!); + } + + foreach (Diagnostic warning in roslynDiagnostics) + { + logger.LogWarning(warning.Id, warning.ToString()); + } + } + } + + /// + public void LogAllWarnings(ILog logger, string? customMessage = null) + { + if (HasLoadWarnings(out IReadOnlyList loadWarnings)) + { + if (!string.IsNullOrEmpty(customMessage)) + { + logger.LogWarning(customMessage!); + } + + foreach (AssemblyLoadWarning warning in loadWarnings) + { + logger.LogWarning(warning.DiagnosticId, warning.Message); + } + } + } + /// public IEnumerable MetadataReferences => _cSharpCompilation.References; diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs index 6cf2b3e88d38..937f08476675 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.ApiSymbolExtensions { @@ -90,6 +91,20 @@ public interface IAssemblySymbolLoader /// The list of matching assemblies represented as . IEnumerable LoadMatchingAssemblies(IEnumerable fromAssemblies, IEnumerable searchPaths, bool validateMatchingIdentity = true, bool warnOnMissingAssemblies = true); + /// + /// + /// + /// + /// + void LogAllDiagnostics(ILog logger, string? customMessage = null); + + /// + /// + /// + /// + /// + void LogAllWarnings(ILog logger, string? customMessage = null); + /// /// The list of metadata references represented as . /// From 2006571929641158b6e1ea6f38ef1898ceeac71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:55:52 -0800 Subject: [PATCH 08/11] Remove unnecessary factory class and pass logger where it was missing. --- .../ApiCompatServiceProvider.cs | 2 +- .../GenAPITask.cs | 6 ++- .../Microsoft.DotNet.GenAPI.Tool/Program.cs | 7 ++- .../AssemblyLoaderFactory.cs | 51 ------------------- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 4 +- .../AssemblySymbolLoader.cs | 47 +++++++++++++---- .../AssemblySymbolLoaderFactory.cs | 7 ++- .../IAssemblySymbolLoader.cs | 14 +++-- ...eworkInPackageValidatorIntegrationTests.cs | 2 +- .../ValidatePackageTargetIntegrationTests.cs | 2 +- .../Rules/AssemblyIdentityMustMatchTests.cs | 9 ++-- .../AssemblySymbolLoaderTests.cs | 27 +++++----- .../CSharpFileBuilderTests.cs | 4 +- .../TestAssemblyLoaderFactory.cs | 7 +-- 14 files changed, 91 insertions(+), 98 deletions(-) delete mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs diff --git a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs index 4b28606b5099..f351aa68e114 100644 --- a/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs +++ b/src/Compatibility/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ApiCompatServiceProvider.cs @@ -49,7 +49,7 @@ public ApiCompatServiceProvider(Func logFa return new ApiCompatRunner(SuppressibleLog, SuppressionEngine, new ApiComparerFactory(ruleFactory(SuppressibleLog), apiComparerSettings), - new AssemblySymbolLoaderFactory(respectInternals)); + new AssemblySymbolLoaderFactory(SuppressibleLog, respectInternals)); }); } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs index cc383611c490..38fed527c774 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs @@ -64,12 +64,14 @@ public class GenAPITask : TaskBase /// protected override void ExecuteCore() { - (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = AssemblyLoaderFactory.CreateFromFiles( + ILog logger = new MSBuildLog(Log); + (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = AssemblySymbolLoader.CreateFromFiles( + logger, assembliesPaths: Assemblies ?? throw new NullReferenceException("Assemblies cannot be null."), assemblyReferencesPaths: AssemblyReferences, RespectInternals); - GenAPIApp.Run(new MSBuildLog(Log), + GenAPIApp.Run(logger, loader, assemblySymbols, OutputPath, diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs index 1dc8055b936d..a401b52166b2 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs @@ -101,12 +101,15 @@ static int Main(string[] args) { bool respectInternals = parseResult.GetValue(respectInternalsOption); - (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = AssemblyLoaderFactory.CreateFromFiles( + ILog logger = new ConsoleLog(MessageImportance.Normal); + + (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = AssemblySymbolLoader.CreateFromFiles( + logger, assembliesPaths: parseResult.GetValue(assembliesOption) ?? throw new NullReferenceException("No assemblies provided."), assemblyReferencesPaths: parseResult.GetValue(assemblyReferencesOption), respectInternals); - GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal), + GenAPIApp.Run(logger, loader, assemblySymbols, parseResult.GetValue(outputPathOption), diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs deleted file mode 100644 index e48f33eb4da1..000000000000 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/AssemblyLoaderFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; -using Microsoft.DotNet.GenAPI.Filtering; - -namespace Microsoft.DotNet.GenAPI; - -/// -/// Class that facilitates the creation of an assembly symbol loader and its corresponding assembly symbols. -/// -public class AssemblyLoaderFactory -{ - /// - /// Creates an assembly symbol loader and its corresponding assembly symbols from the given DLL files in the filesystem. - /// - /// A collection of paths where the assembly DLLs should be searched. - /// An optional collection of paths where the assembly references should be searched. - /// Whether to include internal symbols or not. - /// A tuple containing the assembly symbol loader and a dictionary of assembly symbols. - public static (IAssemblySymbolLoader, Dictionary) CreateFromFiles(string[] assembliesPaths, string[]? assemblyReferencesPaths, bool respectInternals = false) - { - AssemblySymbolLoader loader; - Dictionary assemblySymbols; - - if (assembliesPaths.Length == 0) - { - CreateWithNoAssemblies(); - } - - bool atLeastOneReferencePath = assemblyReferencesPaths?.Count() > 0; - loader = new AssemblySymbolLoader(resolveAssemblyReferences: atLeastOneReferencePath, respectInternals); - if (atLeastOneReferencePath) - { - loader.AddReferenceSearchPaths(assemblyReferencesPaths!); - } - assemblySymbols = new Dictionary(loader.LoadAssembliesAsDictionary(assembliesPaths)); - - return (loader, assemblySymbols); - } - - /// - /// Creates a default assembly symbol loader and a dictionary of assembly symbols. - /// - /// Whether to include internal symbols or not. - /// A tuple containing the assembly symbol loader and a dictionary of assembly symbols. - public static (IAssemblySymbolLoader, Dictionary) CreateWithNoAssemblies(bool respectInternals = false) => - (new AssemblySymbolLoader(resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary()); -} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index dc1518df5a8e..2666a5ab9c1d 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -46,8 +46,8 @@ public static void Run(ILog logger, writer.WriteAssembly(kvp.Value); } - loader.LogAllDiagnostics(logger); - loader.LogAllWarnings(logger); + loader.LogAllDiagnostics(); + loader.LogAllWarnings(); } // Creates a TextWriter capable of writing into Console or a cs file. diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs index ad68c63dc515..77e15ea89c09 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs @@ -16,6 +16,7 @@ namespace Microsoft.DotNet.ApiSymbolExtensions /// public class AssemblySymbolLoader : IAssemblySymbolLoader { + private readonly ILog _logger; // Dictionary that holds the paths to help loading dependencies. Keys will be assembly name and // value are the containing folder. private readonly Dictionary _referencePathFiles = new(StringComparer.OrdinalIgnoreCase); @@ -35,13 +36,41 @@ public class AssemblySymbolLoader : IAssemblySymbolLoader /// public const string AssemblyReferenceNotFoundErrorCode = "CP1002"; + /// + /// Creates an assembly symbol loader and its corresponding assembly symbols from the given DLL files in the filesystem. + /// + /// The logger instance to use for message logging. + /// A collection of paths where the assembly DLLs should be searched. + /// An optional collection of paths where the assembly references should be searched. + /// Whether to include internal symbols or not. + /// A tuple containing an assembly symbol loader and its corresponding dictionary of assembly symbols. + public static (AssemblySymbolLoader, Dictionary) CreateFromFiles(ILog logger, string[] assembliesPaths, string[]? assemblyReferencesPaths, bool respectInternals = false) + { + if (assembliesPaths.Length == 0) + { + return (new AssemblySymbolLoader(logger, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary()); + } + + bool atLeastOneReferencePath = assemblyReferencesPaths?.Count() > 0; + AssemblySymbolLoader loader = new(logger, resolveAssemblyReferences: atLeastOneReferencePath, respectInternals); + if (atLeastOneReferencePath) + { + loader.AddReferenceSearchPaths(assemblyReferencesPaths!); + } + Dictionary dictionary = new(loader.LoadAssembliesAsDictionary(assembliesPaths)); + + return (loader, dictionary); + } + /// /// Creates a new instance of the class. /// + /// The logger instance to use for message logging. /// True to attempt to load references for loaded assemblies from the locations specified with . Default is false. /// True to include all internal metadata for assemblies loaded. Default is false which only includes public and some internal metadata. - public AssemblySymbolLoader(bool resolveAssemblyReferences = false, bool includeInternalSymbols = false) + public AssemblySymbolLoader(ILog logger, bool resolveAssemblyReferences = false, bool includeInternalSymbols = false) { + _logger = logger; _loadedAssemblies = []; CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable, metadataImportOptions: includeInternalSymbols ? MetadataImportOptions.Internal : MetadataImportOptions.Public); @@ -281,35 +310,35 @@ public IEnumerable LoadMatchingAssemblies(IEnumerable - public void LogAllDiagnostics(ILog logger, string? customMessage = null) + public void LogAllDiagnostics(string? headerMessage = null) { if (HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) { - if (!string.IsNullOrEmpty(customMessage)) + if (!string.IsNullOrEmpty(headerMessage)) { - logger.LogWarning(customMessage!); + _logger.LogWarning(headerMessage!); } foreach (Diagnostic warning in roslynDiagnostics) { - logger.LogWarning(warning.Id, warning.ToString()); + _logger.LogWarning(warning.Id, warning.ToString()); } } } /// - public void LogAllWarnings(ILog logger, string? customMessage = null) + public void LogAllWarnings(string? headerMessage = null) { if (HasLoadWarnings(out IReadOnlyList loadWarnings)) { - if (!string.IsNullOrEmpty(customMessage)) + if (!string.IsNullOrEmpty(headerMessage)) { - logger.LogWarning(customMessage!); + _logger.LogWarning(headerMessage!); } foreach (AssemblyLoadWarning warning in loadWarnings) { - logger.LogWarning(warning.DiagnosticId, warning.Message); + _logger.LogWarning(warning.DiagnosticId, warning.Message); } } } diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoaderFactory.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoaderFactory.cs index 2df44a418f9f..b2b2b1075667 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoaderFactory.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoaderFactory.cs @@ -1,16 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.DotNet.ApiSymbolExtensions.Logging; + namespace Microsoft.DotNet.ApiSymbolExtensions { /// /// Factory to create an AssemblySymbolLoader /// + /// The logger instance to use for message logging. /// True to include internal API when reading assemblies from the created. - public sealed class AssemblySymbolLoaderFactory(bool includeInternalSymbols = false) : IAssemblySymbolLoaderFactory + public sealed class AssemblySymbolLoaderFactory(ILog logger, bool includeInternalSymbols = false) : IAssemblySymbolLoaderFactory { /// public IAssemblySymbolLoader Create(bool shouldResolveReferences) => - new AssemblySymbolLoader(shouldResolveReferences, includeInternalSymbols); + new AssemblySymbolLoader(logger, shouldResolveReferences, includeInternalSymbols); } } diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs index 937f08476675..23fdfc160759 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs @@ -92,18 +92,16 @@ public interface IAssemblySymbolLoader IEnumerable LoadMatchingAssemblies(IEnumerable fromAssemblies, IEnumerable searchPaths, bool validateMatchingIdentity = true, bool warnOnMissingAssemblies = true); /// - /// + /// Logs all diagnostics that were emitted during the loading of the assemblies. /// - /// - /// - void LogAllDiagnostics(ILog logger, string? customMessage = null); + /// Optional custom message to prepend to the diagnostics. + void LogAllDiagnostics(string? headerMessage = null); /// - /// + /// Logs all warnings that were emitted during the loading of the assemblies. /// - /// - /// - void LogAllWarnings(ILog logger, string? customMessage = null); + /// Optional custom message to prepend to the warnings. + void LogAllWarnings(string? headerMessage = null); /// /// The list of metadata references represented as . diff --git a/test/Microsoft.DotNet.ApiCompat.IntegrationTests/CompatibleFrameworkInPackageValidatorIntegrationTests.cs b/test/Microsoft.DotNet.ApiCompat.IntegrationTests/CompatibleFrameworkInPackageValidatorIntegrationTests.cs index 91a896625fd5..f2c1acde2421 100644 --- a/test/Microsoft.DotNet.ApiCompat.IntegrationTests/CompatibleFrameworkInPackageValidatorIntegrationTests.cs +++ b/test/Microsoft.DotNet.ApiCompat.IntegrationTests/CompatibleFrameworkInPackageValidatorIntegrationTests.cs @@ -27,7 +27,7 @@ public CompatibleFrameworkInPackageValidatorIntegrationTests(ITestOutputHelper l new ApiCompatRunner(log, new SuppressionEngine(), new ApiComparerFactory(new RuleFactory(log)), - new AssemblySymbolLoaderFactory())); + new AssemblySymbolLoaderFactory(log))); return (log, validator); } diff --git a/test/Microsoft.DotNet.ApiCompat.IntegrationTests/Task/ValidatePackageTargetIntegrationTests.cs b/test/Microsoft.DotNet.ApiCompat.IntegrationTests/Task/ValidatePackageTargetIntegrationTests.cs index 6bbb8beb8362..028fddd8d262 100644 --- a/test/Microsoft.DotNet.ApiCompat.IntegrationTests/Task/ValidatePackageTargetIntegrationTests.cs +++ b/test/Microsoft.DotNet.ApiCompat.IntegrationTests/Task/ValidatePackageTargetIntegrationTests.cs @@ -29,7 +29,7 @@ public ValidatePackageTargetIntegrationTests(ITestOutputHelper log) : base(log) new ApiCompatRunner(log, new SuppressionEngine(), new ApiComparerFactory(new RuleFactory(log)), - new AssemblySymbolLoaderFactory())); + new AssemblySymbolLoaderFactory(log))); return (log, validator); } diff --git a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AssemblyIdentityMustMatchTests.cs b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AssemblyIdentityMustMatchTests.cs index ca9fe2ca9a06..64f09bd6da92 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AssemblyIdentityMustMatchTests.cs +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AssemblyIdentityMustMatchTests.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.DotNet.ApiCompatibility.Tests; using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.ApiSymbolExtensions.Tests; namespace Microsoft.DotNet.ApiCompatibility.Rules.Tests @@ -191,13 +192,16 @@ public void RetargetableFlagSet(bool strictMode) [assembly: AssemblyFlags(AssemblyNameFlags.Retargetable)] "; + ILog logger = new ConsoleLog(MessageImportance.High); + // Emitting the assembly to a physical location to workaround: // https://github.com/dotnet/roslyn/issues/54836 + string leftAssembly = SymbolFactory.EmitAssemblyFromSyntax(syntax, publicKey: _publicKey); string rightAssembly = SymbolFactory.EmitAssemblyFromSyntax(syntax); - IAssemblySymbol leftSymbol = new AssemblySymbolLoader().LoadAssembly(leftAssembly); - IAssemblySymbol rightSymbol = new AssemblySymbolLoader().LoadAssembly(rightAssembly); + IAssemblySymbol leftSymbol = new AssemblySymbolLoader(logger).LoadAssembly(leftAssembly); + IAssemblySymbol rightSymbol = new AssemblySymbolLoader(logger).LoadAssembly(rightAssembly); Assert.True(leftSymbol.Identity.IsRetargetable); Assert.True(rightSymbol.Identity.IsRetargetable); @@ -210,4 +214,3 @@ public void RetargetableFlagSet(bool strictMode) } } } - diff --git a/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs b/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs index bb31747720b9..4091987d73cc 100644 --- a/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs +++ b/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs @@ -6,12 +6,15 @@ using System.Collections.Concurrent; using System.Reflection; using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.ApiSymbolExtensions.Tests { public class AssemblySymbolLoaderTests : SdkTest { + private ILog _logger = new ConsoleLog(MessageImportance.High); + public AssemblySymbolLoaderTests(ITestOutputHelper log) : base(log) { } private const string SimpleAssemblySourceContents = @" @@ -89,14 +92,14 @@ private TestAssetInfo GetAsset(TestAssetsManager manager) [Fact] public void LoadAssembly_Throws() { - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); Assert.Throws(() => loader.LoadAssembly(Guid.NewGuid().ToString("N").Substring(0, 8))); } [Fact] public void LoadAssemblyFromSourceFiles_Throws() { - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable paths = new[] { Guid.NewGuid().ToString("N") }; Assert.Throws(() => loader.LoadAssemblyFromSourceFiles(paths, "assembly1", Array.Empty())); Assert.Throws("filePaths", () => loader.LoadAssemblyFromSourceFiles(Array.Empty(), "assembly1", Array.Empty())); @@ -106,7 +109,7 @@ public void LoadAssemblyFromSourceFiles_Throws() [Fact] public void LoadMatchingAssemblies_Throws() { - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable paths = new[] { Guid.NewGuid().ToString("N") }; IAssemblySymbol assembly = SymbolFactory.GetAssemblyFromSyntax("namespace MyNamespace { class Foo { } }"); @@ -119,7 +122,7 @@ public void LoadMatchingAssembliesWarns() IAssemblySymbol assembly = SymbolFactory.GetAssemblyFromSyntax("namespace MyNamespace { class Foo { } }"); IEnumerable paths = new[] { AppContext.BaseDirectory }; - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable symbols = loader.LoadMatchingAssemblies(new[] { assembly }, paths); Assert.Empty(symbols); Assert.True(loader.HasLoadWarnings(out IReadOnlyList warnings)); @@ -152,7 +155,7 @@ public void LoadMatchingAssembliesSameIdentitySucceeds() .Should() .Pass(); - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable matchingAssemblies = loader.LoadMatchingAssemblies(new[] { fromAssembly }, new[] { outputDirectory }); Assert.Single(matchingAssemblies); @@ -167,7 +170,7 @@ public void LoadMatchingAssemblies_DifferentIdentity(bool validateIdentities) var assetInfo = GetSimpleTestAsset(); IAssemblySymbol fromAssembly = SymbolFactory.GetAssemblyFromSyntax(SimpleAssemblySourceContents, assemblyName: assetInfo.TestAsset.TestProject.Name); - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable matchingAssemblies = loader.LoadMatchingAssemblies(new[] { fromAssembly }, new[] { assetInfo.OutputDirectory }, validateMatchingIdentity: validateIdentities); if (validateIdentities) @@ -194,7 +197,7 @@ public void LoadMatchingAssemblies_DifferentIdentity(bool validateIdentities) public void LoadsSimpleAssemblyFromDirectory() { var assetInfo = GetSimpleTestAsset(); - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable symbols = loader.LoadAssemblies(assetInfo.OutputDirectory); Assert.Single(symbols); @@ -212,7 +215,7 @@ public void LoadsSimpleAssemblyFromDirectory() public void LoadSimpleAssemblyFullPath() { var assetInfo = GetSimpleTestAsset(); - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IAssemblySymbol symbol = loader.LoadAssembly(Path.Combine(assetInfo.OutputDirectory, assetInfo.TestAsset.TestProject.Name + ".dll")); IEnumerable types = symbol.GlobalNamespace @@ -246,7 +249,7 @@ public void LoadsMultipleAssembliesFromDirectory() .Should() .Pass(); - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); IEnumerable symbols = loader.LoadAssemblies(outputDirectory); Assert.Equal(2, symbols.Count()); @@ -263,7 +266,7 @@ public void LoadsMultipleAssembliesFromDirectory() public void LoadAssemblyResolveReferences_WarnsWhenEnabled(bool resolveReferences) { var assetInfo = GetSimpleTestAsset(); - AssemblySymbolLoader loader = new(resolveAssemblyReferences: resolveReferences); + AssemblySymbolLoader loader = new(_logger, resolveAssemblyReferences: resolveReferences); loader.LoadAssembly(Path.Combine(assetInfo.OutputDirectory, assetInfo.TestAsset.TestProject.Name + ".dll")); if (resolveReferences) @@ -295,7 +298,7 @@ public void LoadAssemblyResolveReferences_WarnsWhenEnabled(bool resolveReference public void LoadAssembliesShouldResolveReferencesNoWarnings() { var assetInfo = GetSimpleTestAsset(); - AssemblySymbolLoader loader = new(resolveAssemblyReferences: true); + AssemblySymbolLoader loader = new(_logger, resolveAssemblyReferences: true); // AddReferenceSearchDirectories should be able to handle directories as well as full path to assemblies. loader.AddReferenceSearchPaths(Path.GetDirectoryName(typeof(string).Assembly.Location)); loader.AddReferenceSearchPaths(Path.GetFullPath(typeof(string).Assembly.Location)); @@ -314,7 +317,7 @@ public void LoadAssemblyFromStreamNoWarns() { var assetInfo = GetSimpleTestAsset(); TestProject testProject = assetInfo.TestAsset.TestProject; - AssemblySymbolLoader loader = new(); + AssemblySymbolLoader loader = new(_logger); using FileStream stream = File.OpenRead(Path.Combine(assetInfo.OutputDirectory, testProject.Name + ".dll")); IAssemblySymbol symbol = loader.LoadAssembly(testProject.Name, stream); diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index e3b7691f230d..cdf6dd2c2071 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -14,6 +14,8 @@ namespace Microsoft.DotNet.GenAPI.Tests { public class CSharpFileBuilderTests { + private readonly ILog _logger = new ConsoleLog(MessageImportance.High); + class AllowAllFilter : ISymbolFilter { public bool Include(ISymbol symbol) => true; @@ -36,7 +38,7 @@ private void RunTest(string original, using StringWriter stringWriter = new(); (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = TestAssemblyLoaderFactory - .CreateFromTexts(assemblyTexts: [(assemblyName, original)], respectInternals: includeInternalSymbols, allowUnsafe); + .CreateFromTexts(_logger, assemblyTexts: [(assemblyName, original)], respectInternals: includeInternalSymbols, allowUnsafe); IAssemblySymbolWriter writer = new CSharpFileBuilder( new ConsoleLog(MessageImportance.Low), diff --git a/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs index be09c47c5f00..8f7fde2cfe2a 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs @@ -4,20 +4,21 @@ using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; using Microsoft.DotNet.ApiSymbolExtensions.Tests; namespace Microsoft.DotNet.GenAPI.Tests; public class TestAssemblyLoaderFactory { - public static (IAssemblySymbolLoader, Dictionary) CreateFromTexts((string, string)[] assemblyTexts, bool respectInternals = false, bool allowUnsafe = false) + public static (IAssemblySymbolLoader, Dictionary) CreateFromTexts(ILog logger, (string, string)[] assemblyTexts, bool respectInternals = false, bool allowUnsafe = false) { if (assemblyTexts.Length == 0) { - return AssemblyLoaderFactory.CreateWithNoAssemblies(respectInternals); + return (new AssemblySymbolLoader(logger, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary()); } - AssemblySymbolLoader loader = new(resolveAssemblyReferences: true, includeInternalSymbols: respectInternals); + AssemblySymbolLoader loader = new(logger, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals); loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!); loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!); From a42e09385f2f842f877e13e267de1f165bcabab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:14:26 -0800 Subject: [PATCH 09/11] Merge CompositeSymbolFilter changes with main --- .../Filtering/CompositeSymbolFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs index d1b8cf29f899..4d784165a307 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs @@ -14,7 +14,7 @@ public sealed class CompositeSymbolFilter(params IEnumerable filt /// /// List on inner filters. /// - public List Filters { get; } = new(filters); + public List Filters { get; } = [.. filters]; /// /// Determines whether the should be included. From b385c9c921e0964dd5bd1238dd9948aa2ca6e8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:20:24 -0800 Subject: [PATCH 10/11] Do not set addPartialModifier to true by default in TypeDeclarationCSharpSyntaxRewriter --- .../TypeDeclarationCSharpSyntaxRewriter.cs | 14 ++++++-------- .../TypeDeclarationCSharpSyntaxRewriterTests.cs | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs index bf8ba53e4adb..a3232cdf391f 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs @@ -15,15 +15,13 @@ namespace Microsoft.DotNet.GenAPI.SyntaxRewriter /// - adds partial keyword /// - remove Object from a list of base types. /// - public class TypeDeclarationCSharpSyntaxRewriter : CSharpSyntaxRewriter + /// + /// Initializes a new instance of the class, and optionally allows deciding whether to insert the partial modifier for types or not. + /// + /// Determines whether to insert the partial modifier for types or not. + public class TypeDeclarationCSharpSyntaxRewriter(bool addPartialModifier) : CSharpSyntaxRewriter { - private readonly bool _addPartialModifier; - - /// - /// Initializes a new instance of the class, and optionally allows deciding whether to insert the partial modifier for types or not. - /// - /// Determines whether to insert the partial modifier for types or not. - public TypeDeclarationCSharpSyntaxRewriter(bool addPartialModifier = true) => _addPartialModifier = addPartialModifier; + private readonly bool _addPartialModifier = addPartialModifier; /// public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) diff --git a/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriterTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriterTests.cs index c2f498c1651c..f33ea9ec90e4 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriterTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriterTests.cs @@ -10,7 +10,7 @@ public class TypeDeclarationCSharpSyntaxRewriterTests : CSharpSyntaxRewriterTest [Fact] public void TestRemoveSystemObjectAsBaseClass() { - CompareSyntaxTree(new TypeDeclarationCSharpSyntaxRewriter(), + CompareSyntaxTree(new TypeDeclarationCSharpSyntaxRewriter(addPartialModifier: true), original: """ namespace A { @@ -32,7 +32,7 @@ partial class B [Fact] public void TestAddPartialKeyword() { - CompareSyntaxTree(new TypeDeclarationCSharpSyntaxRewriter(), + CompareSyntaxTree(new TypeDeclarationCSharpSyntaxRewriter(addPartialModifier: true), original: """ namespace A { @@ -54,7 +54,7 @@ partial interface D { } [Fact] public void TestPartialTypeDeclaration() { - CompareSyntaxTree(new TypeDeclarationCSharpSyntaxRewriter(), + CompareSyntaxTree(new TypeDeclarationCSharpSyntaxRewriter(addPartialModifier: true), original: """ namespace A { From a7358fc59121ad2fe96fda220bba4f2cfb306e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20S=C3=A1nchez=20L=C3=B3pez?= <1175054+carlossanlop@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:00:00 -0800 Subject: [PATCH 11/11] Address latest batch of feedback: Rename the wrongly named CSharpAssemblyVisitor class, remove its interface. Move the static methods from SymbolFilterFactory to CompositeSymbolFilter, and delete SymbolFilterFactory. Move TryGetRecordConstructor static extension method to a higher assembly. Move the NullableAttributes.cs dependency to the same higher assembly. Move ImplicitSymbolFilter to a higher assembly. Adjust the tests accordingly. --- ....cs => CSharpAssemblyDocumentGenerator.cs} | 20 ++- .../CSharpFileBuilder.cs | 8 +- .../Filtering/ImplicitSymbolFilter.cs | 87 ------------- .../Microsoft.DotNet.GenAPI/GenAPIApp.cs | 5 +- .../IAssemblyVisitor.cs | 23 ---- .../INamedTypeSymbolExtensions.cs | 35 +---- .../Microsoft.DotNet.GenAPI.csproj | 4 - .../SymbolFilterFactory.cs | 121 ------------------ .../SyntaxGeneratorExtensions.cs | 1 + .../Filtering/CompositeSymbolFilter.cs | 107 ++++++++++++++++ .../Filtering/ImplicitSymbolFilter.cs | 84 ++++++++++++ ...icrosoft.DotNet.ApiSymbolExtensions.csproj | 1 + .../SymbolExtensions.cs | 29 +++++ .../CSharpFileBuilderTests.cs | 4 +- 14 files changed, 248 insertions(+), 281 deletions(-) rename src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/{CSharpAssemblyVisitor.cs => CSharpAssemblyDocumentGenerator.cs} (95%) delete mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Filtering/ImplicitSymbolFilter.cs delete mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs delete mode 100644 src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs create mode 100644 src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/ImplicitSymbolFilter.cs diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs similarity index 95% rename from src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs rename to src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs index ecb74c24132b..7a5b105297c5 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyVisitor.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs @@ -20,9 +20,9 @@ namespace Microsoft.DotNet.GenAPI; /// -/// A class that visits a collection of specified assemblies and generates the corresponding C# document and syntax trees. +/// A class that generates the C# document and syntax trees of a specified collection of assemblies. /// -public class CSharpAssemblyVisitor : IAssemblyVisitor +public sealed class CSharpAssemblyDocumentGenerator { private readonly ILog _logger; private readonly IAssemblySymbolLoader _loader; @@ -37,7 +37,7 @@ public class CSharpAssemblyVisitor : IAssemblyVisitor private readonly bool _hideImplicitDefaultConstructors; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The logger to use. /// The assembly symbol loader to use. @@ -48,7 +48,7 @@ public class CSharpAssemblyVisitor : IAssemblyVisitor /// The metadata references to use. The default value is . /// Whether to add the partial modifier or not. The default value is . /// Whether to hide implicit default constructors or not. The default value is . - public CSharpAssemblyVisitor(ILog logger, + public CSharpAssemblyDocumentGenerator(ILog logger, IAssemblySymbolLoader loader, ISymbolFilter symbolFilter, ISymbolFilter attributeDataSymbolFilter, @@ -71,7 +71,11 @@ public CSharpAssemblyVisitor(ILog logger, _hideImplicitDefaultConstructors = hideImplicitDefaultConstructors; } - /// + /// + /// Returns the configured source code document for the specified assembly symbol. + /// + /// The assembly symbol that represents the loaded assembly. + /// The source code document instance of the specified assembly symbol. public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) { CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, @@ -114,7 +118,11 @@ public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) return document; } - /// + /// + /// Returns the formatted root syntax node for the specified document. + /// + /// A source code document instance. + /// The root syntax node of the specified document. public SyntaxNode GetFormattedRootNodeForDocument(Document document) => document.GetSyntaxRootAsync().Result!.Rewrite(new SingleLineStatementCSharpSyntaxRewriter()); private SyntaxNode? Visit(INamespaceSymbol namespaceSymbol) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs index 54bbfdb580b9..ec571332ca36 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -18,7 +18,7 @@ public sealed class CSharpFileBuilder : IAssemblySymbolWriter { private readonly TextWriter _textWriter; private readonly string? _header; - private readonly IAssemblyVisitor _assemblyVisitor; + private readonly CSharpAssemblyDocumentGenerator _docGenerator; public CSharpFileBuilder(ILog logger, TextWriter textWriter, @@ -32,15 +32,15 @@ public CSharpFileBuilder(ILog logger, { _textWriter = textWriter; _header = header; - _assemblyVisitor = new CSharpAssemblyVisitor(logger, loader, symbolFilter, attributeDataSymbolFilter, exceptionMessage, includeAssemblyAttributes, metadataReferences); + _docGenerator = new CSharpAssemblyDocumentGenerator(logger, loader, symbolFilter, attributeDataSymbolFilter, exceptionMessage, includeAssemblyAttributes, metadataReferences); } /// public void WriteAssembly(IAssemblySymbol assemblySymbol) { _textWriter.Write(GetFormattedHeader(_header)); - Document document = _assemblyVisitor.GetDocumentForAssembly(assemblySymbol); - _assemblyVisitor.GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); + Document document = _docGenerator.GetDocumentForAssembly(assemblySymbol); + _docGenerator.GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); } private static string GetFormattedHeader(string? customHeader) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Filtering/ImplicitSymbolFilter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Filtering/ImplicitSymbolFilter.cs deleted file mode 100644 index 48d2cb66d46e..000000000000 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Filtering/ImplicitSymbolFilter.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; - -namespace Microsoft.DotNet.GenAPI.Filtering -{ - /// - /// Filter out implicitly generated members for properties, events, etc. - /// - public class ImplicitSymbolFilter : ISymbolFilter - { - /// - /// Determines whether implicitly generated symbols should be included. - /// - /// to evaluate. - /// True to include the or false to filter it out. - public bool Include(ISymbol symbol) - { - if (symbol is IMethodSymbol method) - { - if (method.IsImplicitlyDeclared || - method.Kind == SymbolKind.NamedType || - method.MethodKind == MethodKind.PropertyGet || - method.MethodKind == MethodKind.PropertySet || - method.MethodKind == MethodKind.EventAdd || - method.MethodKind == MethodKind.EventRemove || - method.MethodKind == MethodKind.EventRaise || - method.MethodKind == MethodKind.DelegateInvoke) - { - return false; - } - - // If the method is an explicitly implemented getter or setter, exclude it. - // https://github.com/dotnet/roslyn/issues/53911 - if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && - method.ExplicitInterfaceImplementations.Any(m => m is { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet })) - { - return false; - } - } - - if (symbol is ITypeSymbol type) - { - if (type.DeclaredAccessibility == Accessibility.Internal && ( - // exclude the compiler generated `` type - type.Name == "" || - // exclude any types which the compiler embedded - marked with EmbeddedAttribute. - // these will be generated by the compiler when compiling C# syntax that requires them. - type.GetAttributes().Any(a => a.AttributeClass?.Name == "EmbeddedAttribute" && a.AttributeClass?.ContainingNamespace.ToDisplayString() == "Microsoft.CodeAnalysis"))) - { - return false; - } - } - - // exclude compiler-synthesized members on record - if (symbol.ContainingType is { IsRecord: true }) - { - if (symbol.IsCompilerGenerated()) - { - return false; - } - - // see if we can identify the record parameter syntax by locating the compiler generated constructor - if (symbol.ContainingType.TryGetRecordConstructor(out IMethodSymbol? recordConstructor)) - { - // exclude the compiler generated constructor - if (SymbolEqualityComparer.Default.Equals(symbol, recordConstructor)) - { - return false; - } - - // exclude the compiler generated properties - if (symbol is IPropertySymbol) - { - // Exclude members with the same name as the record constructor's parameters - return !recordConstructor.Parameters.Any(p => p.Name == symbol.Name); - } - } - } - - return true; - } - } -} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index 2666a5ab9c1d..b8e3625069e1 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -6,6 +6,7 @@ #endif using Microsoft.CodeAnalysis; using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; namespace Microsoft.DotNet.GenAPI @@ -38,8 +39,8 @@ public static void Run(ILog logger, CSharpFileBuilder writer = new(logger, textWriter, loader, - SymbolFilterFactory.GetSymbolFilterFromFiles(excludeApiFiles, respectInternals), - SymbolFilterFactory.GetAttributeFilterFromPaths(excludeAttributesFiles, respectInternals), + CompositeSymbolFilter.GetSymbolFilterFromFiles(excludeApiFiles, respectInternals), + CompositeSymbolFilter.GetAttributeFilterFromPaths(excludeAttributesFiles, respectInternals), headerFile, exceptionMessage, includeAssemblyAttributes); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs deleted file mode 100644 index a47bd466035f..000000000000 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblyVisitor.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.DotNet.GenAPI; - -public interface IAssemblyVisitor -{ - /// - /// Returns the configured source code document for the specified assembly symbol. - /// - /// The assembly symbol that represents the loaded assembly. - /// The source code document instance of the specified assembly symbol. - Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol); - - /// - /// Returns the formatted root syntax node for the specified document. - /// - /// A source code document instance. - /// The root syntax node of the specified document. - public SyntaxNode GetFormattedRootNodeForDocument(Document document); -} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/INamedTypeSymbolExtensions.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/INamedTypeSymbolExtensions.cs index fdbee7992df1..4ef1ecedcf6d 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/INamedTypeSymbolExtensions.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/INamedTypeSymbolExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -70,14 +69,14 @@ private static SyntaxList FromAttributeData(IEnumerable attrs, bool isReadonly) + private static SyntaxNode CreateDummyField(string type, string fieldName, SyntaxList attrs, bool isReadonly) { List modifiers = new() { SyntaxFactory.Token(SyntaxKind.PrivateKeyword) }; if (isReadonly) modifiers.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)); SyntaxNode declaration = SyntaxFactory.FieldDeclaration( SyntaxFactory.VariableDeclaration( - SyntaxFactory.ParseTypeName(typ)) + SyntaxFactory.ParseTypeName(type)) .WithVariables( SyntaxFactory.SingletonSeparatedList( SyntaxFactory.VariableDeclarator( @@ -224,7 +223,7 @@ static bool IncludeInternalSymbols(ISymbolFilter filter) => yield return constructor; } - // Synthesize a base class initializer. + // Synthesize a base class initializer. public static ConstructorInitializerSyntax GenerateBaseConstructorInitializer(this IMethodSymbol baseTypeConstructor) { return SyntaxFactory.ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, baseTypeConstructor.CreateDefaultArgumentList()); @@ -248,33 +247,5 @@ public static ArgumentListSyntax CreateDefaultArgumentList(this IMethodSymbol me return argumentList; } - - // Locates constructor generated by the compiler for `record Foo(...)` syntax - // If the type is a record and the compiler generated constructor is found it will be returned, otherwise null. - // The compiler will not generate a constructor in the case where the user defined it themself without using an argument list - // in the record declaration, or if the record has no parameters. - public static bool TryGetRecordConstructor(this INamedTypeSymbol type, [NotNullWhen(true)] out IMethodSymbol? recordConstructor) - { - if (!type.IsRecord) - { - recordConstructor = null; - return false; - } - - // Locate the compiler generated Deconstruct method. - var deconstructMethod = (IMethodSymbol?)type.GetMembers("Deconstruct") - .FirstOrDefault(m => m is IMethodSymbol && m.IsCompilerGenerated()); - - // Locate the compiler generated constructor by matching parameters to Deconstruct - since we cannot locate it with an attribute. - recordConstructor = (IMethodSymbol?)type.GetMembers(".ctor") - .FirstOrDefault(m => m is IMethodSymbol method && - method.MethodKind == MethodKind.Constructor && - (deconstructMethod == null ? - method.Parameters.IsEmpty : - method.Parameters.Select(p => p.Type).SequenceEqual( - deconstructMethod.Parameters.Select(p => p.Type), SymbolEqualityComparer.Default))); - - return recordConstructor != null; - } } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj index 1966bb66ab05..0c382c85d75f 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj @@ -4,10 +4,6 @@ $(NetToolMinimum);$(NetFrameworkToolCurrent) - - - - diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs deleted file mode 100644 index 894e59e11c40..000000000000 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SymbolFilterFactory.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.DotNet.GenAPI.Filtering; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; - -namespace Microsoft.DotNet.GenAPI; - -/// -/// Factory class that creates composite filters based on the given exclusion lists. -/// -public static class SymbolFilterFactory -{ - /// - /// Creates a symbol filter based on the given exclusion file paths. - /// - /// A collection of paths where the exclusion files should be searched. - /// Whether to include internal symbols or not. - /// Whether to include effectively private symbols or not. - /// Whether to include explicit interface implementation symbols or not. - /// An instance of the symbol filter. - public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFilePaths, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - apiExclusionFilePaths?.Count() > 0 ? - DocIdSymbolFilter.CreateFromFiles(apiExclusionFilePaths) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); - } - - /// - /// Creates a symbol filter based on the given exclusion list. - /// - /// A collection of exclusion list. - /// Whether to include internal symbols or not. - /// Whether to include effectively private symbols or not. - /// Whether to include explicit interface implementation symbols or not. - /// An instance of the symbol filter. - public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - apiExclusionList?.Count() > 0 ? - DocIdSymbolFilter.CreateFromDocIDs(apiExclusionList) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); - } - - /// - /// Creates an attribute filter based on the given exclusion file paths. - /// - /// A collection of paths where the exclusion files should be searched. - /// Whether to include internal symbols or not. - /// Whether to include effectively private symbols or not. - /// Whether to include explicit interface implementation symbols or not. - /// An instance of the attribute filter. - public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclusionFilePaths, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - attributeExclusionFilePaths?.Count() > 0 ? - DocIdSymbolFilter.CreateFromFiles(attributeExclusionFilePaths) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); - } - - /// - /// Creates an attribute filter based on the given exclusion list. - /// - /// A collection of exclusion list. - /// Whether to include internal symbols or not. - /// Whether to include effectively private symbols or not. - /// Whether to include explicit interface implementation symbols or not. - /// An instance of the attribute filter. - public static ISymbolFilter GetAttributeFilterFromList(string[]? attributeExclusionList, - bool respectInternals = false, - bool includeEffectivelyPrivateSymbols = true, - bool includeExplicitInterfaceImplementationSymbols = true) - { - DocIdSymbolFilter? docIdSymbolFilter = - attributeExclusionList?.Count() > 0 ? - DocIdSymbolFilter.CreateFromDocIDs(attributeExclusionList) : null; - - return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); - } - - private static ISymbolFilter GetCompositeSymbolFilter(DocIdSymbolFilter? customFilter, - bool respectInternals, - bool includeEffectivelyPrivateSymbols, - bool includeExplicitInterfaceImplementationSymbols, - bool withImplicitSymbolFilter) - { - AccessibilitySymbolFilter accessibilitySymbolFilter = new( - respectInternals, - includeEffectivelyPrivateSymbols, - includeExplicitInterfaceImplementationSymbols); - - CompositeSymbolFilter filter = new(); - - if (customFilter != null) - { - filter.Add(customFilter); - } - if (withImplicitSymbolFilter) - { - filter.Add(new ImplicitSymbolFilter()); - } - - filter.Add(accessibilitySymbolFilter); - - return filter; - } -} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs index c6ed188a2426..3af277f65c4a 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; namespace Microsoft.DotNet.GenAPI diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs index 4d784165a307..24fda7630790 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs @@ -16,6 +16,113 @@ public sealed class CompositeSymbolFilter(params IEnumerable filt /// public List Filters { get; } = [.. filters]; + /// + /// Creates a composite filter to exclude APIs using the DocIDs provided in the specifed file paths. + /// + /// A collection of paths where the exclusion files should be searched. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the symbol filter. + public static ISymbolFilter GetSymbolFilterFromFiles(string[]? apiExclusionFilePaths, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + apiExclusionFilePaths?.Count() > 0 ? + DocIdSymbolFilter.CreateFromFiles(apiExclusionFilePaths) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); + } + + /// + /// Creates a composite filter to exclude APIs using the DocIDs provided in the specifed list. + /// + /// A collection of exclusion list. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the symbol filter. + public static ISymbolFilter GetSymbolFilterFromList(string[]? apiExclusionList, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + apiExclusionList?.Count() > 0 ? + DocIdSymbolFilter.CreateFromDocIDs(apiExclusionList) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: true); + } + + /// + /// Creates an composite filter to exclude attributes using the DocID provided in the specified file paths. + /// + /// A collection of paths where the exclusion files should be searched. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the attribute filter. + public static ISymbolFilter GetAttributeFilterFromPaths(string[]? attributeExclusionFilePaths, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + attributeExclusionFilePaths?.Count() > 0 ? + DocIdSymbolFilter.CreateFromFiles(attributeExclusionFilePaths) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); + } + + /// + /// Creates an composite filter to exclude attributes using the DocID provided in the specified list. + /// + /// A collection of exclusion list. + /// Whether to include internal symbols or not. + /// Whether to include effectively private symbols or not. + /// Whether to include explicit interface implementation symbols or not. + /// An instance of the attribute filter. + public static ISymbolFilter GetAttributeFilterFromList(string[]? attributeExclusionList, + bool respectInternals = false, + bool includeEffectivelyPrivateSymbols = true, + bool includeExplicitInterfaceImplementationSymbols = true) + { + DocIdSymbolFilter? docIdSymbolFilter = + attributeExclusionList?.Count() > 0 ? + DocIdSymbolFilter.CreateFromDocIDs(attributeExclusionList) : null; + + return GetCompositeSymbolFilter(docIdSymbolFilter, respectInternals, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols, withImplicitSymbolFilter: false); + } + + private static ISymbolFilter GetCompositeSymbolFilter(DocIdSymbolFilter? customFilter, + bool respectInternals, + bool includeEffectivelyPrivateSymbols, + bool includeExplicitInterfaceImplementationSymbols, + bool withImplicitSymbolFilter) + { + AccessibilitySymbolFilter accessibilitySymbolFilter = new( + respectInternals, + includeEffectivelyPrivateSymbols, + includeExplicitInterfaceImplementationSymbols); + + CompositeSymbolFilter filter = new(); + + if (customFilter != null) + { + filter.Add(customFilter); + } + if (withImplicitSymbolFilter) + { + filter.Add(new ImplicitSymbolFilter()); + } + + filter.Add(accessibilitySymbolFilter); + + return filter; + } + /// /// Determines whether the should be included. /// diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/ImplicitSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/ImplicitSymbolFilter.cs new file mode 100644 index 000000000000..cbb4ed95f228 --- /dev/null +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/ImplicitSymbolFilter.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DotNet.ApiSymbolExtensions.Filtering; + +/// +/// Filter out implicitly generated members for properties, events, etc. +/// +public class ImplicitSymbolFilter : ISymbolFilter +{ + /// + /// Determines whether implicitly generated symbols should be included. + /// + /// to evaluate. + /// True to include the or false to filter it out. + public bool Include(ISymbol symbol) + { + if (symbol is IMethodSymbol method) + { + if (method.IsImplicitlyDeclared || + method.Kind == SymbolKind.NamedType || + method.MethodKind == MethodKind.PropertyGet || + method.MethodKind == MethodKind.PropertySet || + method.MethodKind == MethodKind.EventAdd || + method.MethodKind == MethodKind.EventRemove || + method.MethodKind == MethodKind.EventRaise || + method.MethodKind == MethodKind.DelegateInvoke) + { + return false; + } + + // If the method is an explicitly implemented getter or setter, exclude it. + // https://github.com/dotnet/roslyn/issues/53911 + if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && + method.ExplicitInterfaceImplementations.Any(m => m is { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet })) + { + return false; + } + } + + if (symbol is ITypeSymbol type) + { + if (type.DeclaredAccessibility == Accessibility.Internal && ( + // exclude the compiler generated `` type + type.Name == "" || + // exclude any types which the compiler embedded - marked with EmbeddedAttribute. + // these will be generated by the compiler when compiling C# syntax that requires them. + type.GetAttributes().Any(a => a.AttributeClass?.Name == "EmbeddedAttribute" && a.AttributeClass?.ContainingNamespace.ToDisplayString() == "Microsoft.CodeAnalysis"))) + { + return false; + } + } + + // exclude compiler-synthesized members on record + if (symbol.ContainingType is { IsRecord: true }) + { + if (symbol.IsCompilerGenerated()) + { + return false; + } + + // see if we can identify the record parameter syntax by locating the compiler generated constructor + if (symbol.ContainingType.TryGetRecordConstructor(out IMethodSymbol? recordConstructor)) + { + // exclude the compiler generated constructor + if (SymbolEqualityComparer.Default.Equals(symbol, recordConstructor)) + { + return false; + } + + // exclude the compiler generated properties + if (symbol is IPropertySymbol) + { + // Exclude members with the same name as the record constructor's parameters + return !recordConstructor.Parameters.Any(p => p.Name == symbol.Name); + } + } + } + + return true; + } +} diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Microsoft.DotNet.ApiSymbolExtensions.csproj b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Microsoft.DotNet.ApiSymbolExtensions.csproj index c531b3bd79a7..c9d256170375 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Microsoft.DotNet.ApiSymbolExtensions.csproj +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Microsoft.DotNet.ApiSymbolExtensions.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/SymbolExtensions.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/SymbolExtensions.cs index 8e7e44b929bc..c8a9b5239cb7 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/SymbolExtensions.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/SymbolExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; namespace Microsoft.DotNet.ApiSymbolExtensions @@ -126,5 +127,33 @@ public static bool IsEventAdderOrRemover(this IMethodSymbol method) => method.MethodKind == MethodKind.EventRemove || method.Name.StartsWith("add_", StringComparison.Ordinal) || method.Name.StartsWith("remove_", StringComparison.Ordinal); + + // Locates constructor generated by the compiler for `record Foo(...)` syntax + // If the type is a record and the compiler generated constructor is found it will be returned, otherwise null. + // The compiler will not generate a constructor in the case where the user defined it themself without using an argument list + // in the record declaration, or if the record has no parameters. + public static bool TryGetRecordConstructor(this INamedTypeSymbol type, [NotNullWhen(true)] out IMethodSymbol? recordConstructor) + { + if (!type.IsRecord) + { + recordConstructor = null; + return false; + } + + // Locate the compiler generated Deconstruct method. + var deconstructMethod = (IMethodSymbol?)type.GetMembers("Deconstruct") + .FirstOrDefault(m => m is IMethodSymbol && m.IsCompilerGenerated()); + + // Locate the compiler generated constructor by matching parameters to Deconstruct - since we cannot locate it with an attribute. + recordConstructor = (IMethodSymbol?)type.GetMembers(".ctor") + .FirstOrDefault(m => m is IMethodSymbol method && + method.MethodKind == MethodKind.Constructor && + (deconstructMethod == null ? + method.Parameters.IsEmpty : + method.Parameters.Select(p => p.Type).SequenceEqual( + deconstructMethod.Parameters.Select(p => p.Type), SymbolEqualityComparer.Default))); + + return recordConstructor != null; + } } } diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index cdf6dd2c2071..3f5d4a5cc3ad 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -44,8 +44,8 @@ private void RunTest(string original, new ConsoleLog(MessageImportance.Low), stringWriter, loader, - SymbolFilterFactory.GetSymbolFilterFromList([], includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), - SymbolFilterFactory.GetAttributeFilterFromList(excludedAttributeList, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + CompositeSymbolFilter.GetSymbolFilterFromList([], includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + CompositeSymbolFilter.GetAttributeFilterFromList(excludedAttributeList, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), header: string.Empty, exceptionMessage: null, includeAssemblyAttributes: false,