Skip to content

[ApiDiff] Add static factory method to generate AssemblySymbolLoader #46380

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
using Microsoft.Build.Framework;
using Microsoft.DotNet.ApiSymbolExtensions.Logging;
using Microsoft.NET.Build.Tasks;
Expand Down Expand Up @@ -62,17 +63,18 @@ public class GenAPITask : TaskBase
/// <inheritdoc />
protected override void ExecuteCore()
{
Debug.Assert(Assemblies != null, "Assemblies cannot be null.");

GenAPIApp.Run(new MSBuildLog(Log),
Assemblies!,
AssemblyReferences,
OutputPath,
HeaderFile,
ExceptionMessage,
ExcludeApiFiles,
ExcludeAttributesFiles,
RespectInternals,
IncludeAssemblyAttributes
);
Assemblies,
AssemblyReferences,
OutputPath,
HeaderFile,
ExceptionMessage,
ExcludeApiFiles,
ExcludeAttributesFiles,
RespectInternals,
IncludeAssemblyAttributes);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,22 @@ static int Main(string[] args)

rootCommand.SetAction((ParseResult parseResult) =>
{
GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal),
parseResult.GetValue(assembliesOption)!,
bool respectInternals = parseResult.GetValue(respectInternalsOption);

ILog log = new ConsoleLog(MessageImportance.Normal);

string[]? assemblies = parseResult.GetValue(assembliesOption);
Debug.Assert(assemblies != null, "Assemblies cannot be null.");

GenAPIApp.Run(log,
assemblies,
parseResult.GetValue(assemblyReferencesOption),
parseResult.GetValue(outputPathOption),
parseResult.GetValue(headerFileOption),
parseResult.GetValue(exceptionMessageOption),
parseResult.GetValue(excludeApiFilesOption),
parseResult.GetValue(excludeAttributesFilesOption),
parseResult.GetValue(respectInternalsOption),
respectInternals,
parseResult.GetValue(includeAssemblyAttributesOption)
);
});
Expand Down

Large diffs are not rendered by default.

354 changes: 38 additions & 316 deletions src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs

Large diffs are not rendered by default.

101 changes: 49 additions & 52 deletions src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ namespace Microsoft.DotNet.GenAPI
public static class GenAPIApp
{
/// <summary>
/// Initialize and run Roslyn-based GenAPI tool.
/// Initialize and run Roslyn-based GenAPI tool specifying the assemblies to load.
/// </summary>
public static void Run(ILog log,
string[] assemblies,
string[]? assemblyReferences,
string[] assembliesPaths,
string[]? assemblyReferencesPaths,
string? outputPath,
string? headerFile,
string? exceptionMessage,
Expand All @@ -31,43 +31,68 @@ public static void Run(ILog log,
bool respectInternals,
bool includeAssemblyAttributes)
{
bool resolveAssemblyReferences = assemblyReferences?.Length > 0;

// Create, configure and execute the assembly loader.
AssemblySymbolLoader loader = new(log, resolveAssemblyReferences, respectInternals);
if (assemblyReferences is not null)
{
loader.AddReferenceSearchPaths(assemblyReferences);
}
IReadOnlyList<IAssemblySymbol?> assemblySymbols = loader.LoadAssemblies(assemblies);

string headerFileText = ReadHeaderFile(headerFile);
(IAssemblySymbolLoader loader, Dictionary<string, IAssemblySymbol> assemblySymbols) = AssemblySymbolLoader.CreateFromFiles(
log,
assembliesPaths,
assemblyReferencesPaths,
respectInternals);

Run(log,
loader,
assemblySymbols,
outputPath,
headerFile,
exceptionMessage,
excludeApiFiles,
excludeAttributesFiles,
respectInternals,
includeAssemblyAttributes);
}

/// <summary>
/// Initialize and run Roslyn-based GenAPI tool using an assembly symbol loader that pre-loaded the assemblies separately.
/// </summary>
public static void Run(ILog log,
IAssemblySymbolLoader loader,
Dictionary<string, IAssemblySymbol> assemblySymbols,
string? outputPath,
string? headerFile,
string? exceptionMessage,
string[]? excludeApiFiles,
string[]? excludeAttributesFiles,
bool respectInternals,
bool includeAssemblyAttributes)
{
// Shared accessibility filter for the API and Attribute composite filters.
AccessibilitySymbolFilter accessibilitySymbolFilter = new(
respectInternals,
includeEffectivelyPrivateSymbols: true,
includeExplicitInterfaceImplementationSymbols: true);


// Invoke the CSharpFileBuilder for each directly loaded assembly.
foreach (IAssemblySymbol? assemblySymbol in assemblySymbols)
foreach (KeyValuePair<string, IAssemblySymbol> kvp in assemblySymbols)
{
if (assemblySymbol is null)
continue;
using TextWriter textWriter = GetTextWriter(outputPath, kvp.Key);

using TextWriter textWriter = GetTextWriter(outputPath, assemblySymbol.Name);
textWriter.Write(headerFileText);
ISymbolFilter symbolFilter = SymbolFilterFactory.GetFilterFromFiles(
excludeApiFiles, accessibilitySymbolFilter,
respectInternals: respectInternals);
ISymbolFilter attributeDataSymbolFilter = SymbolFilterFactory.GetFilterFromFiles(
excludeAttributesFiles, accessibilitySymbolFilter,
respectInternals: respectInternals);

using CSharpFileBuilder fileBuilder = new(log,
SymbolFilterFactory.GetFilterFromFiles(excludeApiFiles, respectInternals: respectInternals),
SymbolFilterFactory.GetFilterFromFiles(excludeAttributesFiles, respectInternals: respectInternals),
CSharpFileBuilder fileBuilder = new(log,
textWriter,
loader,
symbolFilter,
attributeDataSymbolFilter,
headerFile,
exceptionMessage,
includeAssemblyAttributes,
loader.MetadataReferences,
addPartialModifier: true);

fileBuilder.WriteAssembly(assemblySymbol);
fileBuilder.WriteAssembly(kvp.Value);
}
}

Expand All @@ -87,33 +112,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 = """
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

""";

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,48 @@ public class AssemblySymbolLoader : IAssemblySymbolLoader
/// </summary>
public const string AssemblyReferenceNotFoundErrorCode = "CP1002";

/// <summary>
/// Creates an assembly symbol loader and its corresponding assembly symbols from the given DLL files in the filesystem.
/// </summary>
/// <param name="log">The logger instance to use for message logging.</param>
/// <param name="assembliesPaths">A collection of paths where the assembly DLLs should be searched.</param>
/// <param name="assemblyReferencesPaths">An optional collection of paths where the assembly references should be searched.</param>
/// <param name="respectInternals">Whether to include internal symbols or not.</param>
/// <returns>A tuple containing an assembly symbol loader and its corresponding dictionary of assembly symbols.</returns>
public static (AssemblySymbolLoader, Dictionary<string, IAssemblySymbol>) CreateFromFiles(ILog log, string[] assembliesPaths, string[]? assemblyReferencesPaths, bool respectInternals = false)
{
if (assembliesPaths.Length == 0)
{
return (new AssemblySymbolLoader(log, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary<string, IAssemblySymbol>());
}

bool atLeastOneReferencePath = assemblyReferencesPaths?.Count() > 0;
AssemblySymbolLoader loader = new(log, resolveAssemblyReferences: atLeastOneReferencePath, respectInternals);
if (atLeastOneReferencePath)
{
loader.AddReferenceSearchPaths(assemblyReferencesPaths!);
}

// 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<string> fileNames = assembliesPaths.Select(path => Path.GetFileName(path)).ToImmutableHashSet();
List<MetadataReference> assembliesToReturn = loader.LoadFromPaths(assembliesPaths, fileNames);

// Create IAssemblySymbols out of the MetadataReferences.
// Doing this after resolving references to make sure that references are available.
Dictionary<string, IAssemblySymbol> dictionary = [];
foreach (MetadataReference metadataReference in assembliesToReturn)
{
if (loader._cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference) is IAssemblySymbol assemblySymbol)
{
dictionary.Add(assemblySymbol.Name, assemblySymbol);
}
}

return (loader, dictionary);
}

/// <summary>
/// Creates a new instance of the <see cref="AssemblySymbolLoader"/> class.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Tasks/Microsoft.NET.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompat.Tool\\Microsoft.DotNet.ApiCompat.Tool.csproj",
"src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompatibility\\Microsoft.DotNet.ApiCompatibility.csproj",
"src\\Compatibility\\ApiCompat\\Microsoft.DotNet.PackageValidation\\Microsoft.DotNet.PackageValidation.csproj",
"src\\Microsoft.DotNet.ApiSymbolExtensions\\Microsoft.DotNet.ApiSymbolExtensions.csproj",
"src\\Compatibility\\Microsoft.DotNet.ApiSymbolExtensions\\Microsoft.DotNet.ApiSymbolExtensions.csproj",
"src\\Microsoft.DotNet.TemplateLocator\\Microsoft.DotNet.TemplateLocator.csproj",
"src\\Microsoft.Win32.Msi\\Microsoft.Win32.Msi.csproj",
"src\\Resolvers\\Microsoft.DotNet.MSBuildSdkResolver\\Microsoft.DotNet.MSBuildSdkResolver.csproj",
Expand Down
35 changes: 14 additions & 21 deletions test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,29 @@ private void RunTest(string original,
string[] excludedAttributeList = null,
[CallerMemberName] string assemblyName = "")
{
StringWriter stringWriter = new();
using StringWriter stringWriter = new();

Mock<ILog> log = new();

(IAssemblySymbolLoader loader, Dictionary<string, IAssemblySymbol> assemblySymbols) = TestAssemblyLoaderFactory
.CreateFromTexts(log.Object, assemblyTexts: [(assemblyName, original)], respectInternals: includeInternalSymbols, allowUnsafe);

ISymbolFilter symbolFilter = SymbolFilterFactory.GetFilterFromList([], null, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols);
ISymbolFilter attributeDataSymbolFilter = SymbolFilterFactory.GetFilterFromList(excludedAttributeList, null, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols);

IAssemblySymbolWriter csharpFileBuilder = new CSharpFileBuilder(
log.Object,
SymbolFilterFactory.GetFilterFromList(
apiExclusionList: [],
accessibilitySymbolFilter: null,
includeInternalSymbols,
includeEffectivelyPrivateSymbols,
includeExplicitInterfaceImplementationSymbols),
SymbolFilterFactory.GetFilterFromList(
apiExclusionList: excludedAttributeList ?? [], accessibilitySymbolFilter: null,
includeInternalSymbols,
includeEffectivelyPrivateSymbols,
includeExplicitInterfaceImplementationSymbols),
stringWriter,
null,
false,
loader,
symbolFilter,
attributeDataSymbolFilter,
header: string.Empty,
exceptionMessage: null,
includeAssemblyAttributes: false,
MetadataReferences,
addPartialModifier: true);

using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(original, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName);
AssemblySymbolLoader assemblySymbolLoader = new(log.Object, 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);
csharpFileBuilder.WriteAssembly(assemblySymbols.First().Value);

StringBuilder stringBuilder = stringWriter.GetStringBuilder();
string resultedString = stringBuilder.ToString();
Expand Down
37 changes: 37 additions & 0 deletions test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs
Original file line number Diff line number Diff line change
@@ -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<string, IAssemblySymbol>) CreateFromTexts(ILog log, (string, string)[] assemblyTexts, bool respectInternals = false, bool allowUnsafe = false)
{
if (assemblyTexts.Length == 0)
{
return (new AssemblySymbolLoader(log, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary<string, IAssemblySymbol>());
}

AssemblySymbolLoader loader = new(log, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals);
loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!);
loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!);

Dictionary<string, IAssemblySymbol> 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);
}
}
Loading