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 b5d95dd8debd..f351aa68e114 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.CreateFromFiles(excludeAttributesFiles)); } ApiComparerSettings apiComparerSettings = new( @@ -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 6348a3fb8d4d..38fed527c774 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,17 +64,23 @@ public class GenAPITask : TaskBase /// protected override void ExecuteCore() { - GenAPIApp.Run(new MSBuildLog(Log), - Assemblies!, - AssemblyReferences, - OutputPath, - HeaderFile, - ExceptionMessage, - ExcludeApiFiles, - ExcludeAttributesFiles, - RespectInternals, - IncludeAssemblyAttributes - ); + 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(logger, + loader, + assemblySymbols, + OutputPath, + 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 dd2edbcbbd94..a401b52166b2 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,15 +99,25 @@ static int Main(string[] args) rootCommand.SetAction((ParseResult parseResult) => { - GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal), - parseResult.GetValue(assembliesOption)!, - parseResult.GetValue(assemblyReferencesOption), + bool respectInternals = parseResult.GetValue(respectInternalsOption); + + 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(logger, + loader, + assemblySymbols, parseResult.GetValue(outputPathOption), parseResult.GetValue(headerFileOption), parseResult.GetValue(exceptionMessageOption), parseResult.GetValue(excludeApiFilesOption), parseResult.GetValue(excludeAttributesFilesOption), - parseResult.GetValue(respectInternalsOption), + respectInternals, parseResult.GetValue(includeAssemblyAttributesOption) ); }); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs new file mode 100644 index 000000000000..7a5b105297c5 --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs @@ -0,0 +1,370 @@ +// 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; + +/// +/// A class that generates the C# document and syntax trees of a specified collection of assemblies. +/// +public sealed class CSharpAssemblyDocumentGenerator +{ + 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; + 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 CSharpAssemblyDocumentGenerator(ILog logger, + IAssemblySymbolLoader loader, + ISymbolFilter symbolFilter, + ISymbolFilter attributeDataSymbolFilter, + string? exceptionMessage, + bool includeAssemblyAttributes, + IEnumerable? metadataReferences = null, + bool addPartialModifier = true, + bool hideImplicitDefaultConstructors = true) + { + _logger = logger; + _loader = loader; + _symbolFilter = symbolFilter; + _attributeDataSymbolFilter = attributeDataSymbolFilter; + _exceptionMessage = exceptionMessage; + _includeAssemblyAttributes = includeAssemblyAttributes; + _adhocWorkspace = new AdhocWorkspace(); + _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); + _metadataReferences = metadataReferences; + _addPartialModifier = addPartialModifier; + _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, + 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(_addPartialModifier)) + .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; + } + + /// + /// 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) + { + 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 (_hideImplicitDefaultConstructors && 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 c4a287843beb..ec571332ca36 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -1,348 +1,72 @@ // 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.Diagnostics; -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 { /// /// 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 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; + private readonly string? _header; + private readonly CSharpAssemblyDocumentGenerator _docGenerator; public CSharpFileBuilder(ILog logger, - ISymbolFilter symbolFilter, - ISymbolFilter attributeDataSymbolFilter, - TextWriter textWriter, - string? exceptionMessage, - bool includeAssemblyAttributes, - IEnumerable metadataReferences) + TextWriter textWriter, + IAssemblySymbolLoader loader, + ISymbolFilter symbolFilter, + ISymbolFilter attributeDataSymbolFilter, + string? header, + string? exceptionMessage, + bool includeAssemblyAttributes, + IEnumerable? metadataReferences = null) { - _logger = logger; _textWriter = textWriter; - _symbolFilter = symbolFilter; - _attributeDataSymbolFilter = attributeDataSymbolFilter; - _exceptionMessage = exceptionMessage; - _includeAssemblyAttributes = includeAssemblyAttributes; - _adhocWorkspace = new AdhocWorkspace(); - _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); - _metadataReferences = metadataReferences; + _header = header; + _docGenerator = new CSharpAssemblyDocumentGenerator(logger, loader, symbolFilter, attributeDataSymbolFilter, exceptionMessage, includeAssemblyAttributes, metadataReferences); } /// public void WriteAssembly(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); - - 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; - - document.GetSyntaxRootAsync().Result! - .Rewrite(new SingleLineStatementCSharpSyntaxRewriter()) - .WriteTo(_textWriter); - } - - 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; + _textWriter.Write(GetFormattedHeader(_header)); + Document document = _docGenerator.GetDocumentForAssembly(assemblySymbol); + _docGenerator.GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); } - 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); - } - } + return defaultFileHeader; } - - 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); - } - - /// - public void Dispose() => _textWriter.Dispose(); } } 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 51bf68c42017..b8e3625069e1 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -8,7 +8,6 @@ using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; -using Microsoft.DotNet.GenAPI.Filtering; namespace Microsoft.DotNet.GenAPI { @@ -22,8 +21,8 @@ public static class GenAPIApp /// Initialize and run Roslyn-based GenAPI tool. /// public static void Run(ILog logger, - string[] assemblies, - string[]? assemblyReferences, + IAssemblySymbolLoader loader, + Dictionary assemblySymbols, string? outputPath, string? headerFile, string? exceptionMessage, @@ -32,75 +31,24 @@ public static void Run(ILog logger, bool respectInternals, bool includeAssemblyAttributes) { - bool resolveAssemblyReferences = assemblyReferences?.Length > 0; - // Create, configure and execute the assembly loader. - AssemblySymbolLoader loader = new(resolveAssemblyReferences, respectInternals); - if (assemblyReferences is not null) + // Invoke an assembly symbol writer for each directly loaded assembly. + foreach (KeyValuePair kvp in assemblySymbols) { - loader.AddReferenceSearchPaths(assemblyReferences); + using TextWriter textWriter = GetTextWriter(outputPath, kvp.Key); + CSharpFileBuilder writer = new(logger, + textWriter, + loader, + CompositeSymbolFilter.GetSymbolFilterFromFiles(excludeApiFiles, respectInternals), + CompositeSymbolFilter.GetAttributeFilterFromPaths(excludeAttributesFiles, respectInternals), + headerFile, + exceptionMessage, + includeAssemblyAttributes); + writer.WriteAssembly(kvp.Value); } - 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) - { - 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); - } - - 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(); + loader.LogAllWarnings(); } // Creates a TextWriter capable of writing into Console or a cs file. @@ -119,33 +67,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/IAssemblySymbolWriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs index a739ca961575..e093037b8a78 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/IAssemblySymbolWriter.cs @@ -11,9 +11,9 @@ 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); } } 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/SyntaxGeneratorExtensions.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs index 1201e4db2336..3af277f65c4a 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs @@ -1,11 +1,11 @@ // 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; using Microsoft.CodeAnalysis.Editing; +using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; namespace Microsoft.DotNet.GenAPI diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs index 80a061eb461f..a3232cdf391f 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/TypeDeclarationCSharpSyntaxRewriter.cs @@ -15,8 +15,14 @@ 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 = addPartialModifier; + /// public override SyntaxNode? VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { @@ -83,7 +89,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 +97,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 6d7dea1bc8ae..77e15ea89c09 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 { @@ -15,7 +16,8 @@ namespace Microsoft.DotNet.ApiSymbolExtensions /// public class AssemblySymbolLoader : IAssemblySymbolLoader { - // Dictionary that holds the paths to help loading dependencies. Keys will be assembly name and + 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); private readonly HashSet _referencePathDirectories = new(StringComparer.OrdinalIgnoreCase); @@ -34,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); @@ -109,6 +139,29 @@ public bool HasLoadWarnings(out IReadOnlyList warnings) return assemblySymbols; } + /// + 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, + // 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) { @@ -256,6 +309,40 @@ public IEnumerable LoadMatchingAssemblies(IEnumerable + public void LogAllDiagnostics(string? headerMessage = null) + { + if (HasRoslynDiagnostics(out IReadOnlyList roslynDiagnostics)) + { + if (!string.IsNullOrEmpty(headerMessage)) + { + _logger.LogWarning(headerMessage!); + } + + foreach (Diagnostic warning in roslynDiagnostics) + { + _logger.LogWarning(warning.Id, warning.ToString()); + } + } + } + + /// + public void LogAllWarnings(string? headerMessage = null) + { + if (HasLoadWarnings(out IReadOnlyList loadWarnings)) + { + if (!string.IsNullOrEmpty(headerMessage)) + { + _logger.LogWarning(headerMessage!); + } + + foreach (AssemblyLoadWarning warning in loadWarnings) + { + _logger.LogWarning(warning.DiagnosticId, warning.Message); + } + } + } + /// public IEnumerable MetadataReferences => _cSharpCompilation.References; 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/Filtering/CompositeSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs index d1b8cf29f899..24fda7630790 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/CompositeSymbolFilter.cs @@ -14,7 +14,114 @@ public sealed class CompositeSymbolFilter(params IEnumerable filt /// /// List on inner filters. /// - public List Filters { get; } = new(filters); + 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/DocIdSymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs index 3bb8ba16fac6..4a5435448fb3 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/Filtering/DocIdSymbolFilter.cs @@ -9,9 +9,18 @@ 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 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]; /// /// Determines whether the should be included. @@ -29,20 +38,33 @@ public bool Include(ISymbol symbol) return true; } - private static IEnumerable ReadDocIdsAttributes(IEnumerable docIdsToExcludeFiles) + private static IEnumerable ReadDocIdsFromList(params string[] ids) { - foreach (string docIdsToExcludeFile in docIdsToExcludeFiles) + foreach (string id in ids) { - foreach (string id in File.ReadAllLines(docIdsToExcludeFile)) - { #if NET - if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith('#') && !id.StartsWith("//")) + if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith('#') && !id.StartsWith("//")) #else - if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith("#") && !id.StartsWith("//")) + if (!string.IsNullOrWhiteSpace(id) && !id.StartsWith("#") && !id.StartsWith("//")) #endif - { - yield return id.Trim(); - } + { + yield return id.Trim(); + } + } + } + + private static IEnumerable ReadDocIdsFromFiles(params string[] docIdsToExcludeFiles) + { + foreach (string docIdsToExcludeFile in docIdsToExcludeFiles) + { + if (string.IsNullOrWhiteSpace(docIdsToExcludeFile)) + { + continue; + } + + foreach (string docId in ReadDocIdsFromList(File.ReadAllLines(docIdsToExcludeFile))) + { + yield return docId; } } } 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/IAssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/IAssemblySymbolLoader.cs index cfb78f0a13d1..23fdfc160759 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 { @@ -34,12 +35,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 . + IDictionary LoadAssembliesAsDictionary(params string[] paths); + /// /// Loads assemblies from an archive based on the given relative paths. /// @@ -60,7 +68,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); @@ -83,6 +91,18 @@ public interface IAssemblySymbolLoader /// The list of matching assemblies represented as . IEnumerable LoadMatchingAssemblies(IEnumerable fromAssemblies, IEnumerable searchPaths, bool validateMatchingIdentity = true, bool warnOnMissingAssemblies = true); + /// + /// Logs all diagnostics that were emitted during the loading of the assemblies. + /// + /// 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. + /// + /// Optional custom message to prepend to the warnings. + void LogAllWarnings(string? headerMessage = null); + /// /// The list of metadata references represented as . /// 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.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/Microsoft.DotNet.ApiCompatibility.Tests.csproj b/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj index e2c0ccd07842..a5b1a26c8789 100644 --- a/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj @@ -17,7 +17,9 @@ + + - + 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.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs b/test/Microsoft.DotNet.ApiCompatibility.Tests/Rules/AttributesMustMatchTests.cs index 11d9b98aea4f..221bb16cdedb 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.CreateFromFiles(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.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 68009dbd4f0e..3f5d4a5cc3ad 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -9,13 +9,13 @@ 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 { public class CSharpFileBuilderTests { + private readonly ILog _logger = new ConsoleLog(MessageImportance.High); + class AllowAllFilter : ISymbolFilter { public bool Include(ISymbol symbol) => true; @@ -32,44 +32,26 @@ private void RunTest(string original, bool includeEffectivelyPrivateSymbols = true, bool includeExplicitInterfaceImplementationSymbols = true, bool allowUnsafe = false, - string excludedAttributeFile = null, + string[] excludedAttributeList = 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); + using StringWriter stringWriter = new(); - CompositeSymbolFilter attributeDataSymbolFilter = new(); - if (excludedAttributeFile is not null) - { - attributeDataSymbolFilter.Add(new DocIdSymbolFilter(new string[] { excludedAttributeFile })); - } - attributeDataSymbolFilter.Add(accessibilitySymbolFilter); + (IAssemblySymbolLoader loader, Dictionary assemblySymbols) = TestAssemblyLoaderFactory + .CreateFromTexts(_logger, assemblyTexts: [(assemblyName, original)], respectInternals: includeInternalSymbols, allowUnsafe); - IAssemblySymbolWriter csharpFileBuilder = new CSharpFileBuilder( + IAssemblySymbolWriter writer = new CSharpFileBuilder( new ConsoleLog(MessageImportance.Low), - symbolFilter, - attributeDataSymbolFilter, stringWriter, - null, - false, + loader, + CompositeSymbolFilter.GetSymbolFilterFromList([], includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + CompositeSymbolFilter.GetAttributeFilterFromList(excludedAttributeList, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols), + header: string.Empty, + exceptionMessage: null, + includeAssemblyAttributes: false, MetadataReferences); - 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); + writer.WriteAssembly(assemblySymbols.First().Value); 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 { } } """, @@ -2761,10 +2743,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 { @@ -2800,7 +2778,7 @@ public partial class PublicClass } """, includeInternalSymbols: false, - excludedAttributeFile: filePath); + excludedAttributeList: ["T:A.AnyTestAttribute"]); } [Fact] @@ -2828,7 +2806,7 @@ public class Foo : System.Collections.ICollection, System.Collections.Generic } } - + """, // https://github.com/dotnet/sdk/issues/32195 tracks interface expansion expected: """ @@ -2909,7 +2887,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 +2896,7 @@ public static void M(this object c, scoped System.ReadOnlySpan values) { } } } """, - expected: """ + expected: """ namespace N { public ref partial struct C @@ -2982,7 +2960,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 +2984,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 +2993,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 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 { diff --git a/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs new file mode 100644 index 000000000000..8f7fde2cfe2a --- /dev/null +++ b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs @@ -0,0 +1,37 @@ +// 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.Logging; +using Microsoft.DotNet.ApiSymbolExtensions.Tests; + +namespace Microsoft.DotNet.GenAPI.Tests; + +public class TestAssemblyLoaderFactory +{ + public static (IAssemblySymbolLoader, Dictionary) CreateFromTexts(ILog logger, (string, string)[] assemblyTexts, bool respectInternals = false, bool allowUnsafe = false) + { + if (assemblyTexts.Length == 0) + { + return (new AssemblySymbolLoader(logger, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary()); + } + + AssemblySymbolLoader loader = new(logger, 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); + } +}