Skip to content

Commit a945fa6

Browse files
authored
[ApiDiff] Add static factory method to generate AssemblySymbolLoader (#46380)
1 parent 714b823 commit a945fa6

File tree

9 files changed

+573
-403
lines changed

9 files changed

+573
-403
lines changed

src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Task/GenAPITask.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using Microsoft.Build.Framework;
56
using Microsoft.DotNet.ApiSymbolExtensions.Logging;
67
using Microsoft.NET.Build.Tasks;
@@ -62,17 +63,18 @@ public class GenAPITask : TaskBase
6263
/// <inheritdoc />
6364
protected override void ExecuteCore()
6465
{
66+
Debug.Assert(Assemblies != null, "Assemblies cannot be null.");
67+
6568
GenAPIApp.Run(new MSBuildLog(Log),
66-
Assemblies!,
67-
AssemblyReferences,
68-
OutputPath,
69-
HeaderFile,
70-
ExceptionMessage,
71-
ExcludeApiFiles,
72-
ExcludeAttributesFiles,
73-
RespectInternals,
74-
IncludeAssemblyAttributes
75-
);
69+
Assemblies,
70+
AssemblyReferences,
71+
OutputPath,
72+
HeaderFile,
73+
ExceptionMessage,
74+
ExcludeApiFiles,
75+
ExcludeAttributesFiles,
76+
RespectInternals,
77+
IncludeAssemblyAttributes);
7678
}
7779
}
7880
}

src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI.Tool/Program.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,22 @@ static int Main(string[] args)
9797

9898
rootCommand.SetAction((ParseResult parseResult) =>
9999
{
100-
GenAPIApp.Run(new ConsoleLog(MessageImportance.Normal),
101-
parseResult.GetValue(assembliesOption)!,
100+
bool respectInternals = parseResult.GetValue(respectInternalsOption);
101+
102+
ILog log = new ConsoleLog(MessageImportance.Normal);
103+
104+
string[]? assemblies = parseResult.GetValue(assembliesOption);
105+
Debug.Assert(assemblies != null, "Assemblies cannot be null.");
106+
107+
GenAPIApp.Run(log,
108+
assemblies,
102109
parseResult.GetValue(assemblyReferencesOption),
103110
parseResult.GetValue(outputPathOption),
104111
parseResult.GetValue(headerFileOption),
105112
parseResult.GetValue(exceptionMessageOption),
106113
parseResult.GetValue(excludeApiFilesOption),
107114
parseResult.GetValue(excludeAttributesFilesOption),
108-
parseResult.GetValue(respectInternalsOption),
115+
respectInternals,
109116
parseResult.GetValue(includeAssemblyAttributesOption)
110117
);
111118
});

src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs

Lines changed: 370 additions & 0 deletions
Large diffs are not rendered by default.

src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs

Lines changed: 38 additions & 316 deletions
Large diffs are not rendered by default.

src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs

Lines changed: 49 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ namespace Microsoft.DotNet.GenAPI
1818
public static class GenAPIApp
1919
{
2020
/// <summary>
21-
/// Initialize and run Roslyn-based GenAPI tool.
21+
/// Initialize and run Roslyn-based GenAPI tool specifying the assemblies to load.
2222
/// </summary>
2323
public static void Run(ILog log,
24-
string[] assemblies,
25-
string[]? assemblyReferences,
24+
string[] assembliesPaths,
25+
string[]? assemblyReferencesPaths,
2626
string? outputPath,
2727
string? headerFile,
2828
string? exceptionMessage,
@@ -31,43 +31,68 @@ public static void Run(ILog log,
3131
bool respectInternals,
3232
bool includeAssemblyAttributes)
3333
{
34-
bool resolveAssemblyReferences = assemblyReferences?.Length > 0;
35-
36-
// Create, configure and execute the assembly loader.
37-
AssemblySymbolLoader loader = new(log, resolveAssemblyReferences, respectInternals);
38-
if (assemblyReferences is not null)
39-
{
40-
loader.AddReferenceSearchPaths(assemblyReferences);
41-
}
42-
IReadOnlyList<IAssemblySymbol?> assemblySymbols = loader.LoadAssemblies(assemblies);
43-
44-
string headerFileText = ReadHeaderFile(headerFile);
34+
(IAssemblySymbolLoader loader, Dictionary<string, IAssemblySymbol> assemblySymbols) = AssemblySymbolLoader.CreateFromFiles(
35+
log,
36+
assembliesPaths,
37+
assemblyReferencesPaths,
38+
respectInternals);
39+
40+
Run(log,
41+
loader,
42+
assemblySymbols,
43+
outputPath,
44+
headerFile,
45+
exceptionMessage,
46+
excludeApiFiles,
47+
excludeAttributesFiles,
48+
respectInternals,
49+
includeAssemblyAttributes);
50+
}
4551

52+
/// <summary>
53+
/// Initialize and run Roslyn-based GenAPI tool using an assembly symbol loader that pre-loaded the assemblies separately.
54+
/// </summary>
55+
public static void Run(ILog log,
56+
IAssemblySymbolLoader loader,
57+
Dictionary<string, IAssemblySymbol> assemblySymbols,
58+
string? outputPath,
59+
string? headerFile,
60+
string? exceptionMessage,
61+
string[]? excludeApiFiles,
62+
string[]? excludeAttributesFiles,
63+
bool respectInternals,
64+
bool includeAssemblyAttributes)
65+
{
66+
// Shared accessibility filter for the API and Attribute composite filters.
4667
AccessibilitySymbolFilter accessibilitySymbolFilter = new(
4768
respectInternals,
4869
includeEffectivelyPrivateSymbols: true,
4970
includeExplicitInterfaceImplementationSymbols: true);
5071

51-
5272
// Invoke the CSharpFileBuilder for each directly loaded assembly.
53-
foreach (IAssemblySymbol? assemblySymbol in assemblySymbols)
73+
foreach (KeyValuePair<string, IAssemblySymbol> kvp in assemblySymbols)
5474
{
55-
if (assemblySymbol is null)
56-
continue;
75+
using TextWriter textWriter = GetTextWriter(outputPath, kvp.Key);
5776

58-
using TextWriter textWriter = GetTextWriter(outputPath, assemblySymbol.Name);
59-
textWriter.Write(headerFileText);
77+
ISymbolFilter symbolFilter = SymbolFilterFactory.GetFilterFromFiles(
78+
excludeApiFiles, accessibilitySymbolFilter,
79+
respectInternals: respectInternals);
80+
ISymbolFilter attributeDataSymbolFilter = SymbolFilterFactory.GetFilterFromFiles(
81+
excludeAttributesFiles, accessibilitySymbolFilter,
82+
respectInternals: respectInternals);
6083

61-
using CSharpFileBuilder fileBuilder = new(log,
62-
SymbolFilterFactory.GetFilterFromFiles(excludeApiFiles, respectInternals: respectInternals),
63-
SymbolFilterFactory.GetFilterFromFiles(excludeAttributesFiles, respectInternals: respectInternals),
84+
CSharpFileBuilder fileBuilder = new(log,
6485
textWriter,
86+
loader,
87+
symbolFilter,
88+
attributeDataSymbolFilter,
89+
headerFile,
6590
exceptionMessage,
6691
includeAssemblyAttributes,
6792
loader.MetadataReferences,
6893
addPartialModifier: true);
6994

70-
fileBuilder.WriteAssembly(assemblySymbol);
95+
fileBuilder.WriteAssembly(kvp.Value);
7196
}
7297
}
7398

@@ -87,33 +112,5 @@ private static TextWriter GetTextWriter(string? outputDirPath, string assemblyNa
87112

88113
return File.CreateText(outputDirPath);
89114
}
90-
91-
// Read the header file if specified, or use default one.
92-
private static string ReadHeaderFile(string? headerFile)
93-
{
94-
const string defaultFileHeader = """
95-
//------------------------------------------------------------------------------
96-
// <auto-generated>
97-
// This code was generated by a tool.
98-
//
99-
// Changes to this file may cause incorrect behavior and will be lost if
100-
// the code is regenerated.
101-
// </auto-generated>
102-
//------------------------------------------------------------------------------
103-
104-
""";
105-
106-
string header = !string.IsNullOrEmpty(headerFile) ?
107-
File.ReadAllText(headerFile) :
108-
defaultFileHeader;
109-
110-
#if NET
111-
header = header.ReplaceLineEndings();
112-
#else
113-
header = Regex.Replace(header, @"\r\n|\n\r|\n|\r", Environment.NewLine);
114-
#endif
115-
116-
return header;
117-
}
118115
}
119116
}

src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,48 @@ public class AssemblySymbolLoader : IAssemblySymbolLoader
5151
/// </summary>
5252
public const string AssemblyReferenceNotFoundErrorCode = "CP1002";
5353

54+
/// <summary>
55+
/// Creates an assembly symbol loader and its corresponding assembly symbols from the given DLL files in the filesystem.
56+
/// </summary>
57+
/// <param name="log">The logger instance to use for message logging.</param>
58+
/// <param name="assembliesPaths">A collection of paths where the assembly DLLs should be searched.</param>
59+
/// <param name="assemblyReferencesPaths">An optional collection of paths where the assembly references should be searched.</param>
60+
/// <param name="respectInternals">Whether to include internal symbols or not.</param>
61+
/// <returns>A tuple containing an assembly symbol loader and its corresponding dictionary of assembly symbols.</returns>
62+
public static (AssemblySymbolLoader, Dictionary<string, IAssemblySymbol>) CreateFromFiles(ILog log, string[] assembliesPaths, string[]? assemblyReferencesPaths, bool respectInternals = false)
63+
{
64+
if (assembliesPaths.Length == 0)
65+
{
66+
return (new AssemblySymbolLoader(log, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary<string, IAssemblySymbol>());
67+
}
68+
69+
bool atLeastOneReferencePath = assemblyReferencesPaths?.Count() > 0;
70+
AssemblySymbolLoader loader = new(log, resolveAssemblyReferences: atLeastOneReferencePath, respectInternals);
71+
if (atLeastOneReferencePath)
72+
{
73+
loader.AddReferenceSearchPaths(assemblyReferencesPaths!);
74+
}
75+
76+
// First resolve all assemblies that are passed in and create metadata references out of them.
77+
// Reference assemblies of the passed in assemblies that themselves are passed in, will be skipped to be resolved,
78+
// as they are resolved as part of the loop below.
79+
ImmutableHashSet<string> fileNames = assembliesPaths.Select(path => Path.GetFileName(path)).ToImmutableHashSet();
80+
List<MetadataReference> assembliesToReturn = loader.LoadFromPaths(assembliesPaths, fileNames);
81+
82+
// Create IAssemblySymbols out of the MetadataReferences.
83+
// Doing this after resolving references to make sure that references are available.
84+
Dictionary<string, IAssemblySymbol> dictionary = [];
85+
foreach (MetadataReference metadataReference in assembliesToReturn)
86+
{
87+
if (loader._cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference) is IAssemblySymbol assemblySymbol)
88+
{
89+
dictionary.Add(assemblySymbol.Name, assemblySymbol);
90+
}
91+
}
92+
93+
return (loader, dictionary);
94+
}
95+
5496
/// <summary>
5597
/// Creates a new instance of the <see cref="AssemblySymbolLoader"/> class.
5698
/// </summary>

src/Tasks/Microsoft.NET.slnf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompat.Tool\\Microsoft.DotNet.ApiCompat.Tool.csproj",
1313
"src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompatibility\\Microsoft.DotNet.ApiCompatibility.csproj",
1414
"src\\Compatibility\\ApiCompat\\Microsoft.DotNet.PackageValidation\\Microsoft.DotNet.PackageValidation.csproj",
15-
"src\\Microsoft.DotNet.ApiSymbolExtensions\\Microsoft.DotNet.ApiSymbolExtensions.csproj",
15+
"src\\Compatibility\\Microsoft.DotNet.ApiSymbolExtensions\\Microsoft.DotNet.ApiSymbolExtensions.csproj",
1616
"src\\Microsoft.DotNet.TemplateLocator\\Microsoft.DotNet.TemplateLocator.csproj",
1717
"src\\Microsoft.Win32.Msi\\Microsoft.Win32.Msi.csproj",
1818
"src\\Resolvers\\Microsoft.DotNet.MSBuildSdkResolver\\Microsoft.DotNet.MSBuildSdkResolver.csproj",

test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,29 @@ private void RunTest(string original,
3535
string[] excludedAttributeList = null,
3636
[CallerMemberName] string assemblyName = "")
3737
{
38-
StringWriter stringWriter = new();
38+
using StringWriter stringWriter = new();
3939

4040
Mock<ILog> log = new();
4141

42+
(IAssemblySymbolLoader loader, Dictionary<string, IAssemblySymbol> assemblySymbols) = TestAssemblyLoaderFactory
43+
.CreateFromTexts(log.Object, assemblyTexts: [(assemblyName, original)], respectInternals: includeInternalSymbols, allowUnsafe);
44+
45+
ISymbolFilter symbolFilter = SymbolFilterFactory.GetFilterFromList([], null, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols);
46+
ISymbolFilter attributeDataSymbolFilter = SymbolFilterFactory.GetFilterFromList(excludedAttributeList, null, includeInternalSymbols, includeEffectivelyPrivateSymbols, includeExplicitInterfaceImplementationSymbols);
47+
4248
IAssemblySymbolWriter csharpFileBuilder = new CSharpFileBuilder(
4349
log.Object,
44-
SymbolFilterFactory.GetFilterFromList(
45-
apiExclusionList: [],
46-
accessibilitySymbolFilter: null,
47-
includeInternalSymbols,
48-
includeEffectivelyPrivateSymbols,
49-
includeExplicitInterfaceImplementationSymbols),
50-
SymbolFilterFactory.GetFilterFromList(
51-
apiExclusionList: excludedAttributeList ?? [], accessibilitySymbolFilter: null,
52-
includeInternalSymbols,
53-
includeEffectivelyPrivateSymbols,
54-
includeExplicitInterfaceImplementationSymbols),
5550
stringWriter,
56-
null,
57-
false,
51+
loader,
52+
symbolFilter,
53+
attributeDataSymbolFilter,
54+
header: string.Empty,
55+
exceptionMessage: null,
56+
includeAssemblyAttributes: false,
5857
MetadataReferences,
5958
addPartialModifier: true);
6059

61-
using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(original, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName);
62-
AssemblySymbolLoader assemblySymbolLoader = new(log.Object, resolveAssemblyReferences: true, includeInternalSymbols: includeInternalSymbols);
63-
assemblySymbolLoader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!);
64-
assemblySymbolLoader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!);
65-
IAssemblySymbol assemblySymbol = assemblySymbolLoader.LoadAssembly(assemblyName, assemblyStream);
66-
67-
csharpFileBuilder.WriteAssembly(assemblySymbol);
60+
csharpFileBuilder.WriteAssembly(assemblySymbols.First().Value);
6861

6962
StringBuilder stringBuilder = stringWriter.GetStringBuilder();
7063
string resultedString = stringBuilder.ToString();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.CompilerServices;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.DotNet.ApiSymbolExtensions;
7+
using Microsoft.DotNet.ApiSymbolExtensions.Logging;
8+
using Microsoft.DotNet.ApiSymbolExtensions.Tests;
9+
10+
namespace Microsoft.DotNet.GenAPI.Tests;
11+
12+
public class TestAssemblyLoaderFactory
13+
{
14+
public static (IAssemblySymbolLoader, Dictionary<string, IAssemblySymbol>) CreateFromTexts(ILog log, (string, string)[] assemblyTexts, bool respectInternals = false, bool allowUnsafe = false)
15+
{
16+
if (assemblyTexts.Length == 0)
17+
{
18+
return (new AssemblySymbolLoader(log, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals), new Dictionary<string, IAssemblySymbol>());
19+
}
20+
21+
AssemblySymbolLoader loader = new(log, resolveAssemblyReferences: true, includeInternalSymbols: respectInternals);
22+
loader.AddReferenceSearchPaths(typeof(object).Assembly!.Location!);
23+
loader.AddReferenceSearchPaths(typeof(DynamicAttribute).Assembly!.Location!);
24+
25+
Dictionary<string, IAssemblySymbol> assemblySymbols = new();
26+
foreach ((string assemblyName, string assemblyText) in assemblyTexts)
27+
{
28+
using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(assemblyText, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName);
29+
if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol)
30+
{
31+
assemblySymbols.Add(assemblyName, assemblySymbol);
32+
}
33+
}
34+
35+
return (loader, assemblySymbols);
36+
}
37+
}

0 commit comments

Comments
 (0)