mirror of https://github.com/icsharpcode/ILSpy.git
11 changed files with 626 additions and 25 deletions
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2025 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Composition; |
||||
|
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpy.Docking; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
using ICSharpCode.ILSpy.ViewModels; |
||||
using ICSharpCode.ILSpy.Views; |
||||
|
||||
namespace ICSharpCode.ILSpy |
||||
{ |
||||
[ExportContextMenuEntry(Header = "Compare...", Order = 9999)] |
||||
[Shared] |
||||
internal sealed class CompareContextMenuEntry(AssemblyTreeModel assemblyTreeModel, DockWorkspace dockWorkspace) : IContextMenuEntry |
||||
{ |
||||
public void Execute(TextViewContext context) |
||||
{ |
||||
var left = ((AssemblyTreeNode)context.SelectedTreeNodes[0]).LoadedAssembly; |
||||
var right = ((AssemblyTreeNode)context.SelectedTreeNodes[1]).LoadedAssembly; |
||||
|
||||
var tabPage = dockWorkspace.AddTabPage(); |
||||
|
||||
tabPage.Title = $"Compare {left.Text} - {right.Text}"; |
||||
tabPage.SupportsLanguageSwitching = false; |
||||
tabPage.FrozenContent = true; |
||||
var compareView = new CompareView(); |
||||
compareView.DataContext = new CompareViewModel(assemblyTreeModel, left, right); |
||||
tabPage.Content = compareView; |
||||
} |
||||
|
||||
public bool IsEnabled(TextViewContext context) |
||||
{ |
||||
return true; |
||||
} |
||||
|
||||
public bool IsVisible(TextViewContext context) |
||||
{ |
||||
return context.SelectedTreeNodes is [AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }, AssemblyTreeNode { LoadedAssembly.IsLoadedAsValidAssembly: true }]; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,434 @@
@@ -0,0 +1,434 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Reflection.Metadata; |
||||
|
||||
using ICSharpCode.Decompiler.CSharp.OutputVisitor; |
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
using TomsToolbox.Wpf; |
||||
|
||||
namespace ICSharpCode.ILSpy.ViewModels |
||||
{ |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
|
||||
class CompareViewModel : ObservableObject |
||||
{ |
||||
private readonly AssemblyTreeModel assemblyTreeModel; |
||||
private LoadedAssembly leftAssembly; |
||||
private LoadedAssembly rightAssembly; |
||||
private LoadedAssembly[] assemblies; |
||||
private ComparisonEntryTreeNode root; |
||||
|
||||
public CompareViewModel(AssemblyTreeModel assemblyTreeModel, LoadedAssembly left, LoadedAssembly right) |
||||
{ |
||||
MessageBus<CurrentAssemblyListChangedEventArgs>.Subscribers += (sender, e) => this.Assemblies = assemblyTreeModel.AssemblyList.GetAssemblies(); |
||||
|
||||
leftAssembly = left; |
||||
rightAssembly = right; |
||||
assemblies = assemblyTreeModel.AssemblyList.GetAssemblies(); |
||||
|
||||
var leftTree = CreateEntityTree(new DecompilerTypeSystem(leftAssembly.GetMetadataFileOrNull(), leftAssembly.GetAssemblyResolver())); |
||||
var rightTree = CreateEntityTree(new DecompilerTypeSystem(rightAssembly.GetMetadataFileOrNull(), rightAssembly.GetAssemblyResolver())); |
||||
|
||||
this.root = new ComparisonEntryTreeNode(MergeTrees(leftTree.Item2, rightTree.Item2)); |
||||
} |
||||
|
||||
public LoadedAssembly[] Assemblies { |
||||
get => assemblies; |
||||
set { |
||||
if (assemblies != value) |
||||
{ |
||||
assemblies = value; |
||||
OnPropertyChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public LoadedAssembly LeftAssembly { |
||||
get => leftAssembly; |
||||
set { |
||||
if (leftAssembly != value) |
||||
{ |
||||
leftAssembly = value; |
||||
OnPropertyChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public LoadedAssembly RightAssembly { |
||||
get => rightAssembly; |
||||
set { |
||||
if (rightAssembly != value) |
||||
{ |
||||
rightAssembly = value; |
||||
OnPropertyChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public ComparisonEntryTreeNode RootEntry { |
||||
get => root; |
||||
set { |
||||
if (root != value) |
||||
{ |
||||
root = value; |
||||
OnPropertyChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Entry MergeTrees(Entry a, Entry b) |
||||
{ |
||||
var m = new Entry() { |
||||
Entity = a.Entity, |
||||
Signature = a.Signature, |
||||
}; |
||||
|
||||
if (a.Children?.Count > 0 && b.Children?.Count > 0) |
||||
{ |
||||
var diff = CalculateDiff(a.Children, b.Children); |
||||
m.Children ??= new(); |
||||
|
||||
foreach (var (left, right) in diff) |
||||
{ |
||||
if (left != null && right != null) |
||||
m.Children.Add(MergeTrees(left, right)); |
||||
else if (left != null) |
||||
m.Children.Add(left); |
||||
else if (right != null) |
||||
m.Children.Add(right); |
||||
else |
||||
Debug.Fail("wh00t?"); |
||||
m.Children[^1].Parent = m; |
||||
} |
||||
} |
||||
else if (a.Children?.Count > 0) |
||||
{ |
||||
m.Children ??= new(); |
||||
foreach (var child in a.Children) |
||||
{ |
||||
child.Parent = m; |
||||
m.Children.Add(child); |
||||
} |
||||
} |
||||
else if (b.Children?.Count > 0) |
||||
{ |
||||
m.Children ??= new(); |
||||
foreach (var child in b.Children) |
||||
{ |
||||
child.Parent = m; |
||||
m.Children.Add(child); |
||||
} |
||||
} |
||||
|
||||
return m; |
||||
} |
||||
|
||||
(List<Entry>, Entry) CreateEntityTree(DecompilerTypeSystem typeSystem) |
||||
{ |
||||
var module = typeSystem.MainModule; |
||||
var metadata = module.MetadataFile.Metadata; |
||||
var ambience = new CSharpAmbience(); |
||||
ambience.ConversionFlags = ICSharpCode.Decompiler.Output.ConversionFlags.All & ~ICSharpCode.Decompiler.Output.ConversionFlags.ShowDeclaringType; |
||||
|
||||
List<Entry> results = new(); |
||||
Dictionary<TypeDefinitionHandle, Entry> typeEntries = new(); |
||||
Dictionary<string, Entry> namespaceEntries = new(StringComparer.Ordinal); |
||||
|
||||
Entry root = new Entry { Entity = null!, Signature = module.FullAssemblyName }; |
||||
|
||||
// typeEntries need a different signature: must include list of base types
|
||||
|
||||
Entry? TryCreateEntry(IEntity entity) |
||||
{ |
||||
if (entity.EffectiveAccessibility() != Accessibility.Public) |
||||
return null; |
||||
|
||||
Entry? parent = null; |
||||
|
||||
if (entity.DeclaringTypeDefinition != null |
||||
&& !typeEntries.TryGetValue((TypeDefinitionHandle)entity.DeclaringTypeDefinition.MetadataToken, out parent)) |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
var entry = new Entry { |
||||
Signature = ambience.ConvertSymbol(entity), |
||||
Entity = entity, |
||||
Parent = parent, |
||||
}; |
||||
|
||||
if (parent != null) |
||||
{ |
||||
parent.Children ??= new(); |
||||
parent.Children.Add(entry); |
||||
} |
||||
|
||||
return entry; |
||||
} |
||||
|
||||
foreach (var typeDefHandle in metadata.TypeDefinitions) |
||||
{ |
||||
var typeDef = module.GetDefinition(typeDefHandle); |
||||
|
||||
if (typeDef.EffectiveAccessibility() != Accessibility.Public) |
||||
continue; |
||||
|
||||
var entry = typeEntries[typeDefHandle] = new Entry { |
||||
Signature = ambience.ConvertSymbol(typeDef), |
||||
Entity = typeDef |
||||
}; |
||||
|
||||
if (typeDef.DeclaringType == null) |
||||
{ |
||||
if (!namespaceEntries.TryGetValue(typeDef.Namespace, out var nsEntry)) |
||||
{ |
||||
namespaceEntries[typeDef.Namespace] = nsEntry = new Entry { Parent = root, Signature = typeDef.Namespace, Entity = null! }; |
||||
root.Children ??= new(); |
||||
root.Children.Add(nsEntry); |
||||
} |
||||
|
||||
entry.Parent = nsEntry; |
||||
nsEntry.Children ??= new(); |
||||
nsEntry.Children.Add(entry); |
||||
} |
||||
} |
||||
|
||||
foreach (var fieldHandle in metadata.FieldDefinitions) |
||||
{ |
||||
var fieldDef = module.GetDefinition(fieldHandle); |
||||
var entry = TryCreateEntry(fieldDef); |
||||
|
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
|
||||
foreach (var eventHandle in metadata.EventDefinitions) |
||||
{ |
||||
var eventDef = module.GetDefinition(eventHandle); |
||||
var entry = TryCreateEntry(eventDef); |
||||
|
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
|
||||
foreach (var propertyHandle in metadata.PropertyDefinitions) |
||||
{ |
||||
var propertyDef = module.GetDefinition(propertyHandle); |
||||
var entry = TryCreateEntry(propertyDef); |
||||
|
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
|
||||
foreach (var methodHandle in metadata.MethodDefinitions) |
||||
{ |
||||
var methodDef = module.GetDefinition(methodHandle); |
||||
|
||||
if (methodDef.AccessorOwner != null) |
||||
continue; |
||||
|
||||
var entry = TryCreateEntry(methodDef); |
||||
|
||||
if (entry != null) |
||||
results.Add(entry); |
||||
} |
||||
|
||||
return (results, root); |
||||
} |
||||
|
||||
List<(Entry? Left, Entry? Right)> CalculateDiff(List<Entry> left, List<Entry> right) |
||||
{ |
||||
Dictionary<string, List<Entry>> leftMap = new(); |
||||
Dictionary<string, List<Entry>> rightMap = new(); |
||||
|
||||
foreach (var item in left) |
||||
{ |
||||
string key = item.Signature; |
||||
if (leftMap.ContainsKey(key)) |
||||
leftMap[key].Add(item); |
||||
else |
||||
leftMap[key] = [item]; |
||||
} |
||||
|
||||
foreach (var item in right) |
||||
{ |
||||
string key = item.Signature; |
||||
if (rightMap.ContainsKey(key)) |
||||
rightMap[key].Add(item); |
||||
else |
||||
rightMap[key] = [item]; |
||||
} |
||||
|
||||
List<(Entry? Left, Entry? Right)> results = new(); |
||||
|
||||
foreach (var (key, items) in leftMap) |
||||
{ |
||||
if (rightMap.TryGetValue(key, out var rightEntries)) |
||||
{ |
||||
foreach (var item in items) |
||||
{ |
||||
var other = rightEntries.Find(_ => EntryComparer.Instance.Equals(_, item)); |
||||
results.Add((item, other)); |
||||
if (other == null) |
||||
{ |
||||
item.Kind = DiffKind.Remove; |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
foreach (var item in items) |
||||
{ |
||||
item.Kind = DiffKind.Remove; |
||||
results.Add((item, null)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach (var (key, items) in rightMap) |
||||
{ |
||||
if (leftMap.TryGetValue(key, out var leftEntries)) |
||||
{ |
||||
foreach (var item in items) |
||||
{ |
||||
if (!leftEntries.Any(_ => EntryComparer.Instance.Equals(_, item))) |
||||
{ |
||||
results.Add((null, item)); |
||||
item.Kind = DiffKind.Add; |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
foreach (var item in items) |
||||
{ |
||||
item.Kind = DiffKind.Add; |
||||
results.Add((null, item)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return results; |
||||
} |
||||
} |
||||
|
||||
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] |
||||
public class Entry |
||||
{ |
||||
private DiffKind kind = DiffKind.None; |
||||
|
||||
public DiffKind Kind { |
||||
get { |
||||
if (Children == null || Children.Count == 0) |
||||
{ |
||||
return kind; |
||||
} |
||||
|
||||
int addCount = 0, removeCount = 0, updateCount = 0; |
||||
|
||||
foreach (var item in Children) |
||||
{ |
||||
switch (item.Kind) |
||||
{ |
||||
case DiffKind.Add: |
||||
addCount++; |
||||
break; |
||||
case DiffKind.Remove: |
||||
removeCount++; |
||||
break; |
||||
case DiffKind.Update: |
||||
updateCount++; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (addCount == Children.Count) |
||||
return DiffKind.Add; |
||||
if (removeCount == Children.Count) |
||||
return DiffKind.Remove; |
||||
if (addCount > 0 || removeCount > 0 || updateCount > 0) |
||||
return DiffKind.Update; |
||||
return DiffKind.None; |
||||
} |
||||
set { |
||||
if (Children == null || Children.Count == 0) |
||||
{ |
||||
kind = value; |
||||
} |
||||
} |
||||
} |
||||
public required string Signature { get; init; } |
||||
public required IEntity Entity { get; init; } |
||||
|
||||
public Entry? Parent { get; set; } |
||||
public List<Entry>? Children { get; set; } |
||||
|
||||
private string GetDebuggerDisplay() |
||||
{ |
||||
return $"Entry{Kind}{Entity?.ToString() ?? Signature}"; |
||||
} |
||||
} |
||||
|
||||
public class EntryComparer : IEqualityComparer<Entry> |
||||
{ |
||||
public static EntryComparer Instance = new(); |
||||
|
||||
public bool Equals(Entry? x, Entry? y) |
||||
{ |
||||
return x?.Signature == y?.Signature; |
||||
} |
||||
|
||||
public int GetHashCode([DisallowNull] Entry obj) |
||||
{ |
||||
return obj.Signature.GetHashCode(); |
||||
} |
||||
} |
||||
|
||||
public enum DiffKind |
||||
{ |
||||
None = ' ', |
||||
Add = '+', |
||||
Remove = '-', |
||||
Update = '~' |
||||
} |
||||
|
||||
class ComparisonEntryTreeNode : ILSpyTreeNode |
||||
{ |
||||
private readonly Entry entry; |
||||
|
||||
public ComparisonEntryTreeNode(Entry entry) |
||||
{ |
||||
this.entry = entry; |
||||
this.LazyLoading = entry.Children != null; |
||||
} |
||||
|
||||
protected override void LoadChildren() |
||||
{ |
||||
if (entry.Children == null) |
||||
return; |
||||
|
||||
foreach (var item in entry.Children) |
||||
{ |
||||
this.Children.Add(new ComparisonEntryTreeNode(item)); |
||||
} |
||||
} |
||||
|
||||
public override object Text => entry.Signature; |
||||
|
||||
public override object Icon => Images.GetIcon(; |
||||
|
||||
public DiffKind Difference => entry.Kind; |
||||
|
||||
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
<UserControl x:Class="ICSharpCode.ILSpy.Views.CompareView" |
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:local="clr-namespace:ICSharpCode.ILSpy.Views" |
||||
xmlns:stv="clr-namespace:ICSharpCode.ILSpy.Controls.TreeView" |
||||
mc:Ignorable="d" |
||||
d:DesignHeight="450" d:DesignWidth="800"> |
||||
<Grid> |
||||
<Grid.RowDefinitions> |
||||
<RowDefinition Height="Auto" /> |
||||
<RowDefinition Height="*" /> |
||||
</Grid.RowDefinitions> |
||||
<StackPanel Orientation="Horizontal" Margin="5"> |
||||
<Label Content="Left:" /> |
||||
<ComboBox SelectedValue="{Binding LeftAssembly}" ItemsSource="{Binding Assemblies}" DisplayMemberPath="Text" /> |
||||
<Label Content="Right:" /> |
||||
<ComboBox SelectedValue="{Binding RightAssembly}" ItemsSource="{Binding Assemblies}" DisplayMemberPath="Text" /> |
||||
</StackPanel> |
||||
<stv:SharpTreeView Grid.Row="1" Root="{Binding RootEntry}" ShowRoot="True"> |
||||
<stv:SharpTreeView.ItemContainerStyle> |
||||
<Style TargetType="stv:SharpTreeViewItem"> |
||||
<Setter Property="Template"> |
||||
<Setter.Value> |
||||
<ControlTemplate TargetType="{x:Type stv:SharpTreeViewItem}"> |
||||
<Border Background="Transparent"> |
||||
<Border Background="{TemplateBinding Background}"> |
||||
<stv:SharpTreeNodeView x:Name="nodeView" HorizontalAlignment="Left" /> |
||||
</Border> |
||||
</Border> |
||||
<ControlTemplate.Triggers> |
||||
<DataTrigger Binding="{Binding Difference}" Value="Update"> |
||||
<Setter Property="Background" Value="LightBlue" /> |
||||
</DataTrigger> |
||||
<DataTrigger Binding="{Binding Difference}" Value="Add"> |
||||
<Setter Property="Background" Value="LightGreen" /> |
||||
</DataTrigger> |
||||
<DataTrigger Binding="{Binding Difference}" Value="Remove"> |
||||
<Setter Property="Background" Value="LightPink" /> |
||||
</DataTrigger> |
||||
<DataTrigger Binding="{Binding IsPublicAPI}" Value="False"> |
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" /> |
||||
</DataTrigger> |
||||
<Trigger Property="IsSelected" Value="True"> |
||||
<Setter TargetName="nodeView" Property="TextBackground" |
||||
Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" /> |
||||
<Setter TargetName="nodeView" Property="Foreground" |
||||
Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" /> |
||||
</Trigger> |
||||
<Trigger Property="IsEnabled" Value="False"> |
||||
<Setter TargetName="nodeView" Property="Foreground" |
||||
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" /> |
||||
</Trigger> |
||||
</ControlTemplate.Triggers> |
||||
</ControlTemplate> |
||||
</Setter.Value> |
||||
</Setter> |
||||
</Style> |
||||
</stv:SharpTreeView.ItemContainerStyle> |
||||
</stv:SharpTreeView> |
||||
</Grid> |
||||
</UserControl> |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Text; |
||||
using System.Windows; |
||||
using System.Windows.Controls; |
||||
using System.Windows.Data; |
||||
using System.Windows.Documents; |
||||
using System.Windows.Input; |
||||
using System.Windows.Media; |
||||
using System.Windows.Media.Imaging; |
||||
using System.Windows.Navigation; |
||||
using System.Windows.Shapes; |
||||
|
||||
namespace ICSharpCode.ILSpy.Views |
||||
{ |
||||
/// <summary>
|
||||
/// Interaction logic for CompareView.xaml
|
||||
/// </summary>
|
||||
public partial class CompareView : UserControl |
||||
{ |
||||
public CompareView() |
||||
{ |
||||
InitializeComponent(); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue