-
Hi, In order to keep track of the public API's of all my projects, I'm toying around generating sources for all public types and their members from the reference dll's that the C# compiler creates. The idea is to compare old versus new after a commit in the source project. I started out with ilspycmd and the output is very good, except that reference dll's also include internal stuff, which I'm not interested in. I then played with creating my own MsBuild Task (source below) which also works nicely, but I'm running into a lot of details where I get the feeling I'm duplicating existing features. And indeed, I then noticed that the IlSpy GUI actually has a checkbox "Show only public types and members". Is there a way to use that filter feature from either ilspycmd or from my own code? I will also need deterministic sorting, for which I made a first attempt in the source below. Finally I made an attempt to format the C# output to be a bit more compact and more suitable for comparing, but the decompiler options don't seem to do much. Am I doing something wrong there? using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.Metadata;
using Microsoft.Build.Framework;
namespace My.BuildTasks;
public class DecompileTask : ITask {
[Required]
public required string InputAssembly { get; set; }
[Required]
public required string OutputDirectory { get; set; }
public required bool SaveOriginals { get; set; } = true;
public required IBuildEngine BuildEngine { get; set; }
public required ITaskHost HostObject { get; set; }
bool ITask.Execute() {
List<string> generatedFilePaths = [];
try {
LogMessage($"{new { InputAssembly, OutputDirectory }}");
var stream = File.OpenRead(InputAssembly);
var peFile = new PEFile(InputAssembly, stream);
var decompiler = new CSharpDecompiler(InputAssembly, new DecompilerSettings() {
AlwaysShowEnumMemberValues = true,
AlwaysUseBraces = false,
CheckedOperators = true,
CSharpFormattingOptions = {
AutoPropertyFormatting = PropertyFormatting.SingleLine,
ConstructorBraceStyle = BraceStyle.BannerStyle,
DestructorBraceStyle = BraceStyle.BannerStyle,
MethodBraceStyle = BraceStyle.BannerStyle,
MethodDeclarationClosingParenthesesOnNewLine = NewLinePlacement.NewLine,
MethodDeclarationParameterWrapping = Wrapping.WrapIfTooLong,
PropertyBraceStyle = BraceStyle.BannerStyle,
PropertyGetBraceStyle = BraceStyle.BannerStyle,
PropertySetBraceStyle = BraceStyle.BannerStyle,
SimplePropertyFormatting = PropertyFormatting.SingleLine,
SimpleGetBlockFormatting = PropertyFormatting.SingleLine,
SimpleSetBlockFormatting = PropertyFormatting.SingleLine,
},
ShowXmlDocumentation = true,
UseExpressionBodyForCalculatedGetterOnlyProperties = true,
UseImplicitMethodGroupConversion = true,
UsePrimaryConstructorSyntax = true,
});
Directory.CreateDirectory(OutputDirectory);
foreach (TypeDefinitionHandle type in peFile.Metadata.TypeDefinitions) {
var typeDefinition = peFile.Metadata.GetTypeDefinition(type);
if (typeDefinition.IsNested) continue;
string typeName = peFile.Metadata.GetString(typeDefinition.Name);
var fullTypeName = typeDefinition.GetFullTypeName(peFile.Metadata);
var syntaxTree = decompiler.DecompileType(fullTypeName);
string? decompiledCodeOriginal = null;
if (SaveOriginals) decompiledCodeOriginal = syntaxTree.ToString();
FilterNonPublicNodes(syntaxTree, out int publicCount);
if (!SaveOriginals && publicCount == 0) continue;
var namespaceDir = peFile.Metadata.GetString(typeDefinition.Namespace).Replace(';', Path.PathSeparator);
var filePath = Path.Combine(OutputDirectory, namespaceDir, MakeValidFilename($"{fullTypeName.Name}.cs"));
Directory.CreateDirectory(Path.Combine(OutputDirectory, namespaceDir));
if (SaveOriginals) {
var origFilePath = filePath + ".orig";
generatedFilePaths.Add(origFilePath);
File.WriteAllText(origFilePath, decompiledCodeOriginal);
}
if (publicCount == 0) continue;
var decompiledCode = syntaxTree.ToString();
if (string.IsNullOrWhiteSpace(decompiledCode)) continue;
LogMessage($"{new { filePath, publicCount }}");
generatedFilePaths.Add(filePath);
// TODO: Skip write if contents hasn't changed
File.WriteAllText(filePath, decompiledCode);
}
// Delete all files that existed before but are not in the list of generated files
var existingFiles = Directory.GetFiles(OutputDirectory, "*.*", SearchOption.AllDirectories);
foreach (var existingFile in existingFiles) {
if (!generatedFilePaths.Contains(existingFile)) {
LogMessage($"Deleting old: {existingFile}");
File.Delete(existingFile);
}
}
LogMessage($"Successfully decompiled {InputAssembly} to {OutputDirectory}");
return true;
} catch (System.Exception exception) {
BuildEngine.LogErrorEvent(new("DecompileTask", "", "", 0, 0, 0, 0, $"Error during decompilation: {exception.Message}", "", ""));
return false;
}
}
private void FilterNonPublicNodes(AstNode syntaxTree, out int publicCount) {
publicCount = 0;
var nodesToRemove = new List<AstNode>();
RecursivelyFilterNonPublicNodes(syntaxTree, nodesToRemove, ref publicCount);
foreach (var node in nodesToRemove)
node.Remove();
}
private void RecursivelyFilterNonPublicNodes(AstNode node, List<AstNode> nodesToRemove, ref int publicCount) {
const Modifiers filter = Modifiers.Internal | Modifiers.Private;
// Base case: If the node is a non-public EntityDeclaration, mark it and stop recursion
if (node is EntityDeclaration entityDeclaration) {
if ((entityDeclaration.Modifiers & filter) is not Modifiers.None) {
nodesToRemove.Add(entityDeclaration);
return;
}
publicCount++;
}
foreach (var child in node.Children) {
RecursivelyFilterNonPublicNodes(child, nodesToRemove, ref publicCount);
}
if (node is TypeDeclaration typeDeclaration) {
SortEntityDeclarations(typeDeclaration);
}
}
/// <summary>Sort the members of this type declaration (methods, properties, etc.) alphabetically by name.</summary>
private void SortEntityDeclarations(TypeDeclaration typeDeclaration) {
// TODO: Members doesn't include events and possibly other things
// TODO: Same Name members should be sorted by generic type parameters and then by parameters?
var sortedMembers = typeDeclaration.Members.OrderBy(m => m.Name).ToList();
LogMessage($"{string.Join(", ", sortedMembers.Select(m => new { m.Name, m.SymbolKind, symbol = m.GetSymbol() }))}");
typeDeclaration.Members.Clear();
typeDeclaration.Members.AddRange(sortedMembers);
}
private void LogMessage(string message) {
BuildEngine.LogMessageEvent(new(message, "", "", MessageImportance.High));
}
private static string MakeValidFilename(string name)
=> Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
} Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
There are some Microsoft solutions for your problem here: |
Beta Was this translation helpful? Give feedback.
There are some Microsoft solutions for your problem here:
https://stackoverflow.com/questions/73257002/generate-text-file-of-public-api-of-net-library-for-versioning-and-compatibilit