Skip to content

Commit f60398a

Browse files
author
Mike Hadlow
committed
Added ConsoleTreeVisualizer. Further refactoring.
1 parent e4136a5 commit f60398a

9 files changed

Lines changed: 363 additions & 116 deletions

AsmSpy.CommandLine/AsmSpy.CommandLine.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
</Compile>
6565
<Compile Include="BindingRedirectExport.cs" />
6666
<Compile Include="ConsoleLogger.cs" />
67+
<Compile Include="ConsoleTreeVisualizer.cs" />
6768
<Compile Include="ConsoleVisualizer.cs" />
6869
<Compile Include="DgmlExport.cs" />
6970
<Compile Include="DotExport.cs" />
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using AsmSpy.Core;
2+
using Microsoft.Extensions.CommandLineUtils;
3+
using System;
4+
using System.Linq;
5+
using static System.Console;
6+
7+
namespace AsmSpy.CommandLine
8+
{
9+
public class ConsoleTreeVisualizer : IDependencyVisualizer
10+
{
11+
CommandOption noConsole;
12+
13+
public void CreateOption(CommandLineApplication commandLineApplication)
14+
{
15+
noConsole = commandLineApplication.Option("-tr|--tree", "Output a console tree view of dependencies.", CommandOptionType.NoValue);
16+
}
17+
18+
public bool IsConfigured() => noConsole.HasValue();
19+
20+
public void Visualize(DependencyAnalyzerResult result, ILogger logger, VisualizerOptions visualizerOptions)
21+
{
22+
23+
foreach(var root in result.Roots)
24+
{
25+
WalkDependencies(root);
26+
}
27+
28+
29+
void WalkDependencies(IAssemblyReferenceInfo assembly, string tab = "", bool lastParent = true)
30+
{
31+
var label = lastParent ? endNodeLabel : nodeLabel;
32+
var currentForgroundColor = ForegroundColor;
33+
Write($"{tab}{label}");
34+
ForegroundColor = SelectConsoleColor(assembly.AssemblySource);
35+
36+
var alternativeVersion = assembly.AlternativeFoundVersion == null
37+
? ""
38+
: $" -> {assembly.AlternativeFoundVersion.AssemblyName.Version.ToString()}";
39+
40+
WriteLine($"{assembly.AssemblyName.Name} {assembly.AssemblyName.Version.ToString()}{alternativeVersion}");
41+
ForegroundColor = currentForgroundColor;
42+
43+
assembly = assembly.AlternativeFoundVersion ?? assembly;
44+
45+
var count = 1;
46+
var totalChildren = assembly.References.Count();
47+
foreach(var dependency in assembly.References)
48+
{
49+
if(dependency.AssemblySource == AssemblySource.GlobalAssemblyCache && visualizerOptions.SkipSystem)
50+
{
51+
continue;
52+
}
53+
var parentLast = count++ == totalChildren;
54+
var parentLabel = lastParent ? tabLabel : continuationLabel;
55+
WalkDependencies(dependency, tab + parentLabel, parentLast);
56+
}
57+
}
58+
}
59+
60+
private ConsoleColor SelectConsoleColor(AssemblySource assemblySource)
61+
{
62+
switch (assemblySource)
63+
{
64+
case AssemblySource.NotFound:
65+
return AssemblyNotFoundColor;
66+
case AssemblySource.Local:
67+
return AssemblyLocalColor;
68+
case AssemblySource.GlobalAssemblyCache:
69+
return AssemblyGlobalAssemblyCacheColor;
70+
case AssemblySource.Unknown:
71+
return AssemblyUnknownColor;
72+
default:
73+
throw new InvalidOperationException("Unknown AssemblySource value.");
74+
}
75+
}
76+
77+
private const ConsoleColor AssemblyNotFoundColor = ConsoleColor.Red;
78+
private const ConsoleColor AssemblyLocalColor = ConsoleColor.Green;
79+
private const ConsoleColor AssemblyGlobalAssemblyCacheColor = ConsoleColor.Yellow;
80+
private const ConsoleColor AssemblyUnknownColor = ConsoleColor.Magenta;
81+
82+
private const string tabLabel = " ";
83+
private const string nodeLabel = "├──";
84+
private const string endNodeLabel = "└──";
85+
private const string continuationLabel = "│  ";
86+
}
87+
}

AsmSpy.CommandLine/Program.cs

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,46 +35,57 @@ public static int Main(string[] args)
3535

3636
commandLineApplication.OnExecute(() =>
3737
{
38-
var visualizerOptions = new VisualizerOptions(
39-
skipSystem: nonsystem.HasValue(),
40-
onlyConflicts: !all.HasValue(),
41-
referencedStartsWith: referencedStartsWith.HasValue() ? referencedStartsWith.Value() : string.Empty
42-
);
43-
44-
var consoleLogger = new ConsoleLogger(!silent.HasValue());
45-
46-
var finalResult = GetFileList(directoryOrFile, includeSubDirectories, consoleLogger)
47-
.Bind(fileList => GetAppDomainWithBindingRedirects(configurationFile)
48-
.Map(appDomain => DependencyAnalyzer.Analyze(
49-
fileList,
50-
appDomain,
51-
consoleLogger,
52-
visualizerOptions)))
53-
.Map(result => RunVisualizers(result, consoleLogger, visualizerOptions))
54-
.Bind(FailOnMissingAssemblies);
55-
56-
switch(finalResult)
38+
try
5739
{
58-
case Failure<bool> fail:
59-
consoleLogger.LogError(fail.Message);
60-
return -1;
61-
case Success<bool> succeed:
62-
return 0;
63-
default:
64-
throw new InvalidOperationException("Unexpected result type");
65-
}
40+
var visualizerOptions = new VisualizerOptions(
41+
skipSystem: nonsystem.HasValue(),
42+
onlyConflicts: !all.HasValue(),
43+
referencedStartsWith: referencedStartsWith.HasValue() ? referencedStartsWith.Value() : string.Empty
44+
);
45+
46+
var consoleLogger = new ConsoleLogger(!silent.HasValue());
47+
48+
var finalResult = GetFileList(directoryOrFile, includeSubDirectories, consoleLogger)
49+
.Bind(x => GetAppDomainWithBindingRedirects(configurationFile)
50+
.Map(appDomain => DependencyAnalyzer.Analyze(
51+
x.FileList,
52+
appDomain,
53+
consoleLogger,
54+
visualizerOptions,
55+
x.RootFileName)))
56+
.Map(result => RunVisualizers(result, consoleLogger, visualizerOptions))
57+
.Bind(FailOnMissingAssemblies);
58+
59+
switch (finalResult)
60+
{
61+
case Failure<bool> fail:
62+
consoleLogger.LogError(fail.Message);
63+
return -1;
64+
case Success<bool> succeed:
65+
return 0;
66+
default:
67+
throw new InvalidOperationException("Unexpected result type");
68+
}
6669

67-
DependencyAnalyzerResult RunVisualizers(DependencyAnalyzerResult dependencyAnalyzerResult, ILogger logger, VisualizerOptions options)
68-
{
69-
foreach(var visualizer in dependencyVisualizers.Where(x => x.IsConfigured()))
70+
DependencyAnalyzerResult RunVisualizers(DependencyAnalyzerResult dependencyAnalyzerResult, ILogger logger, VisualizerOptions options)
7071
{
71-
visualizer.Visualize(dependencyAnalyzerResult, logger, options);
72+
foreach (var visualizer in dependencyVisualizers.Where(x => x.IsConfigured()))
73+
{
74+
visualizer.Visualize(dependencyAnalyzerResult, logger, options);
75+
}
76+
return dependencyAnalyzerResult;
7277
}
73-
return dependencyAnalyzerResult;
74-
}
7578

76-
Result<bool> FailOnMissingAssemblies(DependencyAnalyzerResult dependencyAnalyzerResult)
77-
=> failOnMissing.HasValue() && dependencyAnalyzerResult.MissingAssemblies.Any() ? "Missing Assemblies" : Result<bool>.Succeed(true);
79+
Result<bool> FailOnMissingAssemblies(DependencyAnalyzerResult dependencyAnalyzerResult)
80+
=> failOnMissing.HasValue() && dependencyAnalyzerResult.MissingAssemblies.Any()
81+
? "Missing Assemblies"
82+
: Result<bool>.Succeed(true);
83+
}
84+
catch(Exception exception)
85+
{
86+
Console.WriteLine(exception.ToString());
87+
return -1;
88+
}
7889
});
7990

8091
try
@@ -99,27 +110,32 @@ Result<bool> FailOnMissingAssemblies(DependencyAnalyzerResult dependencyAnalyzer
99110
}
100111
}
101112

102-
private static Result<List<FileInfo>> GetFileList(CommandArgument directoryOrFile, CommandOption includeSubDirectories, ILogger logger)
113+
private static Result<(List<FileInfo> FileList, string RootFileName)> GetFileList(CommandArgument directoryOrFile, CommandOption includeSubDirectories, ILogger logger)
103114
{
104115
var searchPattern = includeSubDirectories.HasValue() ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
105116
var directoryOrFilePath = directoryOrFile.Value;
106-
var directoryPath = Path.GetDirectoryName(directoryOrFilePath);
117+
var directoryPath = directoryOrFilePath;
107118

108119
if (!File.Exists(directoryOrFilePath) && !Directory.Exists(directoryOrFilePath))
109120
{
110121
return (string.Format(CultureInfo.InvariantCulture, "Directory or file: '{0}' does not exist.", directoryOrFilePath));
111122
}
112123

124+
var rootFileName = "";
113125
if (File.Exists(directoryOrFilePath))
114126
{
115-
var fileName = Path.GetFileName(directoryOrFilePath);
116-
logger.LogMessage($"Root assembly specified: '{fileName}'");
127+
rootFileName = Path.GetFileName(directoryOrFilePath);
128+
logger.LogMessage($"Root assembly specified: '{rootFileName}'");
129+
directoryPath = Path.GetDirectoryName(directoryOrFilePath);
117130
}
118131

119132
var directoryInfo = new DirectoryInfo(directoryPath);
120133

121134
logger.LogMessage($"Checking for local assemblies in: '{directoryInfo}', {searchPattern}");
122-
return directoryInfo.GetFiles("*.dll", searchPattern).Concat(directoryInfo.GetFiles("*.exe", searchPattern)).ToList();
135+
136+
var fileList = directoryInfo.GetFiles("*.dll", searchPattern).Concat(directoryInfo.GetFiles("*.exe", searchPattern)).ToList();
137+
138+
return (fileList, rootFileName);
123139
}
124140

125141
public static Result<AppDomain> GetAppDomainWithBindingRedirects(CommandOption configurationFile)
@@ -147,6 +163,7 @@ public static Result<AppDomain> GetAppDomainWithBindingRedirects(CommandOption c
147163
private static IDependencyVisualizer[] GetDependencyVisualizers() => new IDependencyVisualizer[]
148164
{
149165
new ConsoleVisualizer(),
166+
new ConsoleTreeVisualizer(),
150167
new DgmlExport(),
151168
new XmlExport(),
152169
new DotExport(),

AsmSpy.Core.Tests/DependencyAnalyzerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public class DependencyAnalyzerTests
1414
private readonly ITestOutputHelper output;
1515
private readonly TestLogger logger;
1616

17-
private IEnumerable<FileInfo> filesToAnalyse;
18-
private VisualizerOptions options = new VisualizerOptions(false, false, "");
17+
private readonly IEnumerable<FileInfo> filesToAnalyse;
18+
private readonly VisualizerOptions options = new VisualizerOptions(false, false, "");
1919

2020
public DependencyAnalyzerTests(ITestOutputHelper output)
2121
{

AsmSpy.Core/AssemblyReferenceInfo.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Reflection;
45

@@ -16,11 +17,18 @@ public class AssemblyReferenceInfo : IAssemblyReferenceInfo
1617
public virtual ICollection<IAssemblyReferenceInfo> ReferencedBy => _referencedBy.ToArray();
1718
public virtual ICollection<IAssemblyReferenceInfo> References => _references.ToArray();
1819
public bool IsSystem => AssemblyInformationProvider.IsSystemAssembly(AssemblyName);
20+
public string FileName { get; }
21+
public bool ReferencedByRoot { get; set; } = false;
22+
public AssemblyReferenceInfo AlternativeFoundVersion { get; private set; }
1923

20-
public AssemblyReferenceInfo(AssemblyName assemblyName, AssemblyName redirectedAssemblyName)
24+
public AssemblyReferenceInfo(AssemblyName assemblyName, AssemblyName redirectedAssemblyName, string fileName = "")
2125
{
22-
AssemblyName = assemblyName;
23-
RedirectedAssemblyName = redirectedAssemblyName;
26+
AssemblyName = assemblyName
27+
?? throw new ArgumentNullException(nameof(assemblyName));
28+
RedirectedAssemblyName = redirectedAssemblyName
29+
?? throw new ArgumentNullException(nameof(redirectedAssemblyName));
30+
FileName = fileName
31+
?? throw new ArgumentNullException(nameof(fileName));
2432
}
2533

2634
public virtual void AddReference(IAssemblyReferenceInfo info)
@@ -39,6 +47,26 @@ public virtual void AddReferencedBy(IAssemblyReferenceInfo info)
3947
}
4048
}
4149

50+
public void SetAlternativeFoundVersion(AssemblyReferenceInfo alternativeVersion)
51+
{
52+
if(alternativeVersion.AssemblyName.Name != AssemblyName.Name)
53+
{
54+
throw new InvalidOperationException(
55+
$"Alternative version to {AssemblyName.Name}, must have the same Name, but is {alternativeVersion.AssemblyName.Name}");
56+
}
57+
if(ReflectionOnlyAssembly != null)
58+
{
59+
throw new InvalidOperationException(
60+
$"AssemblyReferenceInfo for {AssemblyName.Name} has a ReflectionOnlyAssembly, so an alternative should not be set.");
61+
}
62+
if(AssemblySource != AssemblySource.NotFound)
63+
{
64+
throw new InvalidOperationException(
65+
$"AssemblyReferenceInfo.AssemblySource for {AssemblyName.Name} is not 'NotFound', so an alternative should not be set.");
66+
}
67+
AlternativeFoundVersion = alternativeVersion;
68+
}
69+
4270
public override int GetHashCode()
4371
{
4472
return AssemblyName.FullName.GetHashCode();

0 commit comments

Comments
 (0)