Browse Source

Analyzer tree node bases + entity nodes

Lay the foundation for the analyzer pane. None of these classes are
wired into the live view yet — that lands when the pane view-model and
the ProDataGrid view are filled in two commits down. Background-fetch
behaviour on AnalyzerSearchTreeNode is also deferred.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
c114414883
  1. 100
      ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs
  2. 106
      ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs
  3. 111
      ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs
  4. 45
      ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs
  5. 85
      ILSpy/Analyzers/AnalyzedEventTreeNode.cs
  6. 58
      ILSpy/Analyzers/AnalyzedFieldTreeNode.cs
  7. 73
      ILSpy/Analyzers/AnalyzedMethodTreeNode.cs
  8. 81
      ILSpy/Analyzers/AnalyzedModuleTreeNode.cs
  9. 65
      ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs
  10. 68
      ILSpy/Analyzers/AnalyzedTypeTreeNode.cs
  11. 69
      ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
  12. 34
      ILSpy/Analyzers/AnalyzerMetadata.cs
  13. 49
      ILSpy/Analyzers/AnalyzerRegistry.cs
  14. 115
      ILSpy/Analyzers/AnalyzerRootNode.cs
  15. 73
      ILSpy/Analyzers/AnalyzerSearchTreeNode.cs
  16. 75
      ILSpy/Analyzers/AnalyzerTreeNode.cs

100
ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
// Copyright (c) 2026 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.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.Decompiler.TypeSystem;
using ILSpy.Analyzers;
using ILSpy.Analyzers.TreeNodes;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzedMethodTreeNodeTests
{
[AvaloniaTest]
public async Task Wrapping_A_Method_Shows_It_With_Its_Declaring_Type_Prefix()
{
// AnalyzedMethodTreeNode renders the analysed method with `ShowDeclaringType` +
// `UseFullyQualifiedEntityNames` so users see "System.Linq.Enumerable.Empty<TSource>"
// in the pane rather than the bare method name. An optional `prefix` argument lets
// accessor / event-helper nodes prepend a tag without touching the entity string.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.EnsureLazyChildren();
var emptyMethod = (IMethod)typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "Empty").Member!;
var node = new AnalyzedMethodTreeNode(emptyMethod, source: null);
node.Member.Should().BeSameAs(emptyMethod);
node.Text.ToString().Should().Contain("Empty");
node.Text.ToString().Should().Contain("Enumerable",
"AnalyzedMethodTreeNode must show the declaring type so the user can disambiguate "
+ "identically-named methods across the analyser pane");
}
[AvaloniaTest]
public async Task LoadChildren_On_A_Method_Materialises_Uses_And_Used_By_Headers()
{
// Expanding an analysed method materialises one AnalyzerSearchTreeNode per analyser
// whose Show(method) returns true. The two canonical entries for any callable method
// are "Uses" (MethodUsesAnalyzer, body scanner) and "Used By" (MethodUsedByAnalyzer,
// call-site scanner).
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.EnsureLazyChildren();
var firstMethod = (IMethod)typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "AsEnumerable").Member!;
var node = new AnalyzedMethodTreeNode(firstMethod, source: null);
node.EnsureLazyChildren();
var headers = node.Children.OfType<AnalyzerSearchTreeNode>()
.Select(c => c.AnalyzerHeader)
.ToArray();
headers.Should().Contain("Used By",
"MethodUsedByAnalyzer is the headline reverse-lookup for any concrete method");
headers.Should().Contain("Uses",
"MethodUsesAnalyzer scans the body for callees and field/type touches");
}
}

106
ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
// Copyright (c) 2026 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.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ILSpy.Analyzers;
using ILSpy.Analyzers.TreeNodes;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzedTypeTreeNodeTests
{
[AvaloniaTest]
public async Task Wrapping_A_TypeDefinition_Produces_The_Languages_Stringification()
{
// AnalyzedTypeTreeNode renders the analysed type using the same Language.TypeToString
// the rest of the assembly tree uses — so users see "System.Object" exactly the way
// it appears under the assembly node, not a raw ReflectionName.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull();
module.Should().NotBeNull();
var typeSystem = module!.GetTypeSystemOrNull();
typeSystem.Should().NotBeNull();
var objectType = typeSystem!.MainModule.TopLevelTypeDefinitions
.Single(t => t.FullName == "System.Object");
var node = new AnalyzedTypeTreeNode(objectType, source: null);
node.Text.ToString().Should().Contain("Object");
node.Member.Should().BeSameAs(objectType);
node.Icon.Should().NotBeNull("every entity node must surface a non-null icon");
}
[AvaloniaTest]
public async Task LoadChildren_On_A_Type_Materialises_All_Applicable_Analyzer_Headers()
{
// Expanding an analysed type lazily fills its Children with one AnalyzerSearchTreeNode
// per applicable analyzer (the analyzers whose Show(symbol) returns true). For a public
// non-static reference type like System.Object, "Used By", "Instantiated By", "Exposed
// By", and "Extension Methods" all match.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull();
module.Should().NotBeNull();
var typeSystem = module!.GetTypeSystemOrNull();
typeSystem.Should().NotBeNull();
var objectType = typeSystem!.MainModule.TopLevelTypeDefinitions
.Single(t => t.FullName == "System.Object");
var node = new AnalyzedTypeTreeNode(objectType, source: null);
node.EnsureLazyChildren();
var headers = node.Children.OfType<AnalyzerSearchTreeNode>()
.Select(c => c.AnalyzerHeader)
.ToArray();
headers.Should().Contain("Used By", "TypeUsedByAnalyzer applies to every TypeDefinition");
headers.Should().Contain("Exposed By", "TypeExposedByAnalyzer applies to public types");
headers.Should().Contain("Extension Methods",
"TypeExtensionMethodsAnalyzer applies to every TypeDefinition that could be a target");
}
}

111
ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs

@ -0,0 +1,111 @@ @@ -0,0 +1,111 @@
// Copyright (c) 2026 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.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.Analyzers;
using ILSpy.Analyzers;
using ILSpy.Analyzers.TreeNodes;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzerTreeNodeTests
{
[AvaloniaTest]
public async Task Analyzers_Property_Surfaces_All_MEF_Registered_Analyzers_Sorted_By_Order()
{
// AnalyzerTreeNode.Analyzers exposes the MEF-discovered IAnalyzer exports — the
// list every Analyzed*TreeNode walks when populating its child analyzer headers.
// Two contracts: (a) every [ExportAnalyzer] in ICSharpCode.ILSpyX.Analyzers.Builtin
// reaches the list, (b) the order is monotonically non-decreasing by Metadata.Order
// so users see headers in the same sequence the WPF host produces.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var analyzers = AnalyzerTreeNode.Analyzers.ToArray();
analyzers.Should().HaveCountGreaterThanOrEqualTo(15,
"the shared analyzer library defines 17 built-in analyzers — all reachable through MEF");
var orders = analyzers.Select(a => a.Metadata?.Order ?? 0).ToArray();
orders.Should().BeInAscendingOrder(
"AnalyzerTreeNode.Analyzers must hand back exports sorted by metadata Order");
var headers = analyzers.Select(a => a.Metadata?.Header).ToArray();
headers.Should().Contain("Used By",
"TypeUsedByAnalyzer / MethodUsedByAnalyzer is the canonical first analyzer");
}
[AvaloniaTest]
public async Task AnalyzerRootNode_Clears_Children_When_The_Active_AssemblyList_Resets()
{
// AnalyzerRootNode is the pane's tree root. When the active AssemblyList raises
// a Reset CollectionChanged event (typically when a different list is loaded
// from the dropdown), the analyzer pane's results are stale by definition —
// every Analyzed*TreeNode's symbol references a module the new list may not
// contain. The root must wipe its children rather than risk a stale navigation.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var root = new AnalyzerRootNode();
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
var module = assemblyNode.LoadedAssembly.GetMetadataFileOrNull();
module.Should().NotBeNull("the test needs a loaded CoreLib module to seed a sample analyzed entry");
var typeSystem = module!.GetTypeSystemOrNull();
typeSystem.Should().NotBeNull();
var sampleType = typeSystem!.MainModule.TopLevelTypeDefinitions.First();
root.Children.Add(new AnalyzedTypeTreeNode(sampleType, source: null));
root.Children.Should().HaveCount(1);
// Simulate a Reset by firing the collection-changed pathway the root subscribes to.
// We do this through the live AssemblyList rather than reaching into private subscribers
// — the active list firing CollectionChanged with Reset is what triggers the same flow
// at app-runtime.
var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
var snapshot = assemblyList.GetAssemblies();
// Clearing then re-adding the assemblies emits a Reset on the underlying ObservableCollection.
assemblyList.Clear();
root.Children.Should().BeEmpty(
"AnalyzerRootNode.CurrentAssemblyList_Changed must wipe children on Reset");
// Restore the assemblies so subsequent tests don't see an empty list.
foreach (var asm in snapshot)
assemblyList.OpenAssembly(asm.FileName);
}
}

45
ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
// Copyright (c) 2026 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;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.Analyzers.TreeNodes
{
/// <summary>
/// Renders a property getter / setter or event add / remove / invoke as a short
/// "get" / "set" / "add" / "remove" label under its containing entity. Inherits the
/// method-node analyser plumbing so the accessor still surfaces "Used By" / "Uses"
/// children at its own level.
/// </summary>
internal sealed class AnalyzedAccessorTreeNode : AnalyzedMethodTreeNode
{
readonly string name;
public AnalyzedAccessorTreeNode(IMethod analyzedMethod, IEntity? source, string name)
: base(analyzedMethod, source)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("name must be a non-empty string", nameof(name));
this.name = name;
}
public override object Text => name;
}
}

85
ILSpy/Analyzers/AnalyzedEventTreeNode.cs

@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
// Copyright (c) 2026 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;
using System.Diagnostics.CodeAnalysis;
using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.Analyzers.TreeNodes
{
internal sealed class AnalyzedEventTreeNode : AnalyzerEntityTreeNode
{
readonly IEvent analyzedEvent;
readonly string prefix;
public AnalyzedEventTreeNode(IEvent analyzedEvent, IEntity? source, string prefix = "")
{
this.analyzedEvent = analyzedEvent ?? throw new ArgumentNullException(nameof(analyzedEvent));
this.SourceMember = source;
this.prefix = prefix;
LazyLoading = true;
}
public override IEntity Member => analyzedEvent;
public override object Text => prefix + Language.EntityToString(analyzedEvent,
ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Icon => Images.Images.GetIcon(Images.Images.Event,
Images.Images.GetOverlay(analyzedEvent.Accessibility), analyzedEvent.IsStatic);
protected override void LoadChildren()
{
if (analyzedEvent.CanAdd)
this.Children.Add(new AnalyzedAccessorTreeNode(analyzedEvent.AddAccessor!, this.SourceMember, "add"));
if (analyzedEvent.CanRemove)
this.Children.Add(new AnalyzedAccessorTreeNode(analyzedEvent.RemoveAccessor!, this.SourceMember, "remove"));
if (TryFindBackingField(analyzedEvent, out var backingField))
this.Children.Add(new AnalyzedFieldTreeNode(backingField, this.SourceMember));
foreach (var factory in Analyzers)
{
var analyzer = factory.CreateExport().Value;
if (analyzer.Show(analyzedEvent))
{
this.Children.Add(
new AnalyzerSearchTreeNode(analyzedEvent, analyzer, factory.Metadata?.Header));
}
}
}
static bool TryFindBackingField(IEvent analyzedEvent, [NotNullWhen(true)] out IField? backingField)
{
backingField = null;
var declaringType = analyzedEvent.DeclaringTypeDefinition;
if (declaringType == null)
return false;
foreach (var field in declaringType.GetFields(options: GetMemberOptions.IgnoreInheritedMembers))
{
if (field.Name == analyzedEvent.Name && field.Accessibility == Accessibility.Private)
{
backingField = field;
return true;
}
}
return false;
}
}
}

58
ILSpy/Analyzers/AnalyzedFieldTreeNode.cs

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
// Copyright (c) 2026 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;
using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.Analyzers.TreeNodes
{
internal sealed class AnalyzedFieldTreeNode : AnalyzerEntityTreeNode
{
readonly IField analyzedField;
public AnalyzedFieldTreeNode(IField analyzedField, IEntity? source)
{
this.analyzedField = analyzedField ?? throw new ArgumentNullException(nameof(analyzedField));
this.SourceMember = source;
LazyLoading = true;
}
public override IEntity Member => analyzedField;
public override object Text => Language.EntityToString(analyzedField,
ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Icon => Images.Images.GetIcon(Images.Images.Field,
Images.Images.GetOverlay(analyzedField.Accessibility), analyzedField.IsStatic);
protected override void LoadChildren()
{
foreach (var factory in Analyzers)
{
var analyzer = factory.CreateExport().Value;
if (analyzer.Show(analyzedField))
{
this.Children.Add(
new AnalyzerSearchTreeNode(analyzedField, analyzer, factory.Metadata?.Header));
}
}
}
}
}

73
ILSpy/Analyzers/AnalyzedMethodTreeNode.cs

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
// Copyright (c) 2026 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;
using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.Analyzers.TreeNodes
{
internal class AnalyzedMethodTreeNode : AnalyzerEntityTreeNode
{
readonly IMethod analyzedMethod;
readonly string prefix;
public AnalyzedMethodTreeNode(IMethod analyzedMethod, IEntity? source, string prefix = "")
{
this.analyzedMethod = analyzedMethod ?? throw new ArgumentNullException(nameof(analyzedMethod));
this.SourceMember = source;
this.prefix = prefix;
LazyLoading = true;
}
public override IEntity Member => analyzedMethod;
public override object Text
=> prefix + Language.EntityToString(analyzedMethod,
ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Icon => ResolveIcon(analyzedMethod);
internal static object ResolveIcon(IMethod method)
{
var baseImage = method.IsConstructor
? Images.Images.Constructor
: method.IsOperator
? Images.Images.Operator
: Images.Images.Method;
return Images.Images.GetIcon(baseImage,
Images.Images.GetOverlay(method.Accessibility),
method.IsStatic,
method.IsExtensionMethod);
}
protected override void LoadChildren()
{
foreach (var factory in Analyzers)
{
var analyzer = factory.CreateExport().Value;
if (analyzer.Show(analyzedMethod))
{
this.Children.Add(
new AnalyzerSearchTreeNode(analyzedMethod, analyzer, factory.Metadata?.Header));
}
}
}
}
}

81
ILSpy/Analyzers/AnalyzedModuleTreeNode.cs

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
// Copyright (c) 2026 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;
using System.Collections.Generic;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
namespace ILSpy.Analyzers.TreeNodes
{
/// <summary>
/// Wraps an entire module as an analysable entry. <see cref="Member"/> is
/// <see langword="null"/> — module-level analyses (e.g. "Applied To" against an
/// assembly-level attribute) hang their results off this row.
/// </summary>
internal sealed class AnalyzedModuleTreeNode : AnalyzerEntityTreeNode
{
readonly IModule analyzedModule;
public AnalyzedModuleTreeNode(IModule analyzedModule, IEntity? source)
{
this.analyzedModule = analyzedModule ?? throw new ArgumentNullException(nameof(analyzedModule));
this.SourceMember = source;
LazyLoading = true;
}
public override IEntity? Member => null;
public override object Text => analyzedModule.AssemblyName;
public override object Icon => Images.Images.Assembly;
public override object? ToolTip => analyzedModule.MetadataFile?.FileName;
public IModule Module => analyzedModule;
protected override void LoadChildren()
{
foreach (var factory in Analyzers)
{
var analyzer = factory.CreateExport().Value;
if (analyzer.Show(analyzedModule))
{
this.Children.Add(
new AnalyzerSearchTreeNode(analyzedModule, analyzer, factory.Metadata?.Header));
}
}
}
public override bool HandleAssemblyListChanged(
ICollection<LoadedAssembly> removedAssemblies,
ICollection<LoadedAssembly> addedAssemblies)
{
foreach (var asm in removedAssemblies)
{
if (analyzedModule.MetadataFile == asm.GetMetadataFileOrNull())
return false; // module is gone — drop me
}
this.Children.RemoveAll(node =>
node is not AnalyzerTreeNode an
|| !an.HandleAssemblyListChanged(removedAssemblies, addedAssemblies));
return true;
}
}
}

65
ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
// Copyright (c) 2026 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;
using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.Analyzers.TreeNodes
{
internal sealed class AnalyzedPropertyTreeNode : AnalyzerEntityTreeNode
{
readonly IProperty analyzedProperty;
readonly string prefix;
public AnalyzedPropertyTreeNode(IProperty analyzedProperty, IEntity? source, string prefix = "")
{
this.analyzedProperty = analyzedProperty ?? throw new ArgumentNullException(nameof(analyzedProperty));
this.SourceMember = source;
this.prefix = prefix;
LazyLoading = true;
}
public override IEntity Member => analyzedProperty;
public override object Text => prefix + Language.EntityToString(analyzedProperty,
ConversionFlags.ShowDeclaringType | ConversionFlags.UseFullyQualifiedEntityNames);
public override object Icon => Images.Images.GetIcon(Images.Images.Property,
Images.Images.GetOverlay(analyzedProperty.Accessibility), analyzedProperty.IsStatic);
protected override void LoadChildren()
{
if (analyzedProperty.CanGet)
this.Children.Add(new AnalyzedAccessorTreeNode(analyzedProperty.Getter!, this.SourceMember, "get"));
if (analyzedProperty.CanSet)
this.Children.Add(new AnalyzedAccessorTreeNode(analyzedProperty.Setter!, this.SourceMember, "set"));
foreach (var factory in Analyzers)
{
var analyzer = factory.CreateExport().Value;
if (analyzer.Show(analyzedProperty))
{
this.Children.Add(
new AnalyzerSearchTreeNode(analyzedProperty, analyzer, factory.Metadata?.Header));
}
}
}
}
}

68
ILSpy/Analyzers/AnalyzedTypeTreeNode.cs

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
// Copyright (c) 2026 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;
using ICSharpCode.Decompiler.TypeSystem;
namespace ILSpy.Analyzers.TreeNodes
{
internal sealed class AnalyzedTypeTreeNode : AnalyzerEntityTreeNode
{
readonly ITypeDefinition analyzedType;
public AnalyzedTypeTreeNode(ITypeDefinition analyzedType, IEntity? source)
{
this.analyzedType = analyzedType ?? throw new ArgumentNullException(nameof(analyzedType));
this.SourceMember = source;
LazyLoading = true;
}
public override IEntity Member => analyzedType;
public override object Text => Language.TypeToString(analyzedType);
public override object Icon => ResolveIcon(analyzedType);
static object ResolveIcon(ITypeDefinition type)
{
var baseImage = type.Kind switch {
TypeKind.Interface => Images.Images.Interface,
TypeKind.Struct or TypeKind.Void => Images.Images.Struct,
TypeKind.Delegate => Images.Images.Delegate,
TypeKind.Enum => Images.Images.Enum,
_ => Images.Images.Class,
};
return Images.Images.GetIcon(baseImage,
Images.Images.GetOverlay(type.Accessibility), type.IsStatic);
}
protected override void LoadChildren()
{
foreach (var factory in Analyzers)
{
var analyzer = factory.CreateExport().Value;
if (analyzer.Show(analyzedType))
{
this.Children.Add(
new AnalyzerSearchTreeNode(analyzedType, analyzer, factory.Metadata?.Header));
}
}
}
}
}

69
ILSpy/Analyzers/AnalyzerEntityTreeNode.cs

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
// Copyright (c) 2026 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.Collections.Generic;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView;
namespace ILSpy.Analyzers
{
/// <summary>
/// Base for nodes that wrap an <see cref="IEntity"/> in the analyzer pane (the
/// per-entity root row plus every analyser result row underneath an
/// <see cref="AnalyzerSearchTreeNode"/>). Concrete subclasses supply the entity, its
/// icon, and its text; this base owns the navigation hook and the assembly-change
/// pruning logic.
/// </summary>
public abstract class AnalyzerEntityTreeNode : AnalyzerTreeNode
{
/// <summary>
/// The entity this row analyses (or represents as an analyser result). Subclasses
/// return <see langword="null"/> only for non-entity rows such as
/// <see cref="ILSpy.Analyzers.TreeNodes.AnalyzedModuleTreeNode"/>.
/// </summary>
public abstract IEntity? Member { get; }
/// <summary>
/// The entity that the user originally clicked "Analyze" on. Used by analyser
/// result rows to reverse-look-up the analysis they belong to (e.g. for the
/// "Remove" context menu entry).
/// </summary>
public IEntity? SourceMember { get; protected set; }
public override object? ToolTip => Member?.ParentModule?.MetadataFile?.FileName;
public override bool HandleAssemblyListChanged(
ICollection<LoadedAssembly> removedAssemblies,
ICollection<LoadedAssembly> addedAssemblies)
{
if (Member == null)
return true;
foreach (var asm in removedAssemblies)
{
if (this.Member.ParentModule?.MetadataFile == asm.GetMetadataFileOrNull())
return false; // ask parent to drop me — my module is gone
}
this.Children.RemoveAll(node =>
node is not AnalyzerTreeNode an
|| !an.HandleAssemblyListChanged(removedAssemblies, addedAssemblies));
return true;
}
}
}

34
ILSpy/Analyzers/AnalyzerMetadata.cs

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
// Copyright (c) 2026 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.
namespace ILSpy.Analyzers
{
/// <summary>
/// Concrete metadata view consumed by <see cref="AnalyzerRegistry"/>. System.Composition
/// only projects metadata through a class with a parameterless constructor and public
/// settable properties — the shared <c>IAnalyzerMetadata</c> interface doesn't qualify.
/// Property names mirror <c>ExportAnalyzerAttribute</c>'s so the framework copies values
/// across by name.
/// </summary>
public sealed class AnalyzerMetadata
{
public string? Header { get; set; }
public int Order { get; set; }
}
}

49
ILSpy/Analyzers/AnalyzerRegistry.cs

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
// Copyright (c) 2026 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.Collections.Generic;
using System.Composition;
using System.Linq;
using ICSharpCode.ILSpyX.Analyzers;
namespace ILSpy.Analyzers
{
/// <summary>
/// MEF aggregator for <see cref="ExportAnalyzerAttribute"/>-tagged analyzers. System.Composition
/// only resolves <c>[ImportMany]</c> with metadata through constructor injection, so this
/// registry is the single place that pulls the factories out of the composition host. Each
/// <see cref="AnalyzerEntityTreeNode"/> reads <see cref="Analyzers"/> through the static
/// accessor on <see cref="AnalyzerTreeNode"/>, which in turn resolves this registry once.
/// </summary>
[Export]
[Shared]
public sealed class AnalyzerRegistry
{
[ImportingConstructor]
public AnalyzerRegistry(
[ImportMany("Analyzer")] IEnumerable<ExportFactory<IAnalyzer, AnalyzerMetadata>> analyzers)
{
Analyzers = analyzers
.OrderBy(a => a.Metadata?.Order ?? 0)
.ToArray();
}
public IReadOnlyList<ExportFactory<IAnalyzer, AnalyzerMetadata>> Analyzers { get; }
}
}

115
ILSpy/Analyzers/AnalyzerRootNode.cs

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
// Copyright (c) 2026 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.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView;
using ILSpy.AppEnv;
using ILSpy.AssemblyTree;
namespace ILSpy.Analyzers
{
/// <summary>
/// Root of the analyzer pane's tree. Holds one <see cref="AnalyzerEntityTreeNode"/>
/// per analysed entity and prunes stale entries when the active
/// <see cref="AssemblyList"/> changes — either an assembly is closed (matching
/// children dropped) or the list itself is swapped out (entire subtree wiped).
/// </summary>
public sealed class AnalyzerRootNode : AnalyzerTreeNode
{
readonly AssemblyTreeModel? assemblyTreeModel;
AssemblyList? activeList;
public AnalyzerRootNode()
: this(TryGetAssemblyTreeModel())
{
}
internal AnalyzerRootNode(AssemblyTreeModel? assemblyTreeModel)
{
this.assemblyTreeModel = assemblyTreeModel;
if (assemblyTreeModel == null)
return;
assemblyTreeModel.PropertyChanged += OnModelPropertyChanged;
AttachToActiveList(assemblyTreeModel.AssemblyList);
}
static AssemblyTreeModel? TryGetAssemblyTreeModel()
{
try
{
return AppComposition.Current.GetExport<AssemblyTreeModel>();
}
catch
{
// Design-time previews / minimal tests boot without the full composition.
// The pane still works as a passive container in that case.
return null;
}
}
void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(AssemblyTreeModel.AssemblyList))
return;
AttachToActiveList(assemblyTreeModel?.AssemblyList);
// A new active list means every analysed entity references a module that may
// not be reachable any more — wipe the tree the same way a Reset on the old
// list would have.
this.Children.Clear();
}
void AttachToActiveList(AssemblyList? list)
{
if (ReferenceEquals(activeList, list))
return;
if (activeList != null)
activeList.CollectionChanged -= OnAssemblyListCollectionChanged;
activeList = list;
if (activeList != null)
activeList.CollectionChanged += OnAssemblyListCollectionChanged;
}
void OnAssemblyListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
this.Children.Clear();
return;
}
var removed = e.OldItems?.Cast<LoadedAssembly>().ToArray() ?? System.Array.Empty<LoadedAssembly>();
var added = e.NewItems?.Cast<LoadedAssembly>().ToArray() ?? System.Array.Empty<LoadedAssembly>();
HandleAssemblyListChanged(removed, added);
}
public override bool HandleAssemblyListChanged(
ICollection<LoadedAssembly> removedAssemblies,
ICollection<LoadedAssembly> addedAssemblies)
{
this.Children.RemoveAll(node =>
node is not AnalyzerTreeNode an
|| !an.HandleAssemblyListChanged(removedAssemblies, addedAssemblies));
return true;
}
}
}

73
ILSpy/Analyzers/AnalyzerSearchTreeNode.cs

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
// Copyright (c) 2026 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;
using System.Collections.Generic;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.Analyzers;
namespace ILSpy.Analyzers
{
/// <summary>
/// Row that runs a single <see cref="IAnalyzer"/> against an analysed symbol and
/// shows its results as its lazy-loaded children. The shell defined here holds the
/// analyser + symbol + header triple; the background-fetch wiring (Task.Run,
/// "Loading…" placeholder, count + elapsed-time text update, cancellation) lands in
/// a follow-up commit.
/// </summary>
public class AnalyzerSearchTreeNode : AnalyzerTreeNode
{
readonly ISymbol analyzedSymbol;
readonly IAnalyzer analyzer;
public AnalyzerSearchTreeNode(ISymbol analyzedSymbol, IAnalyzer analyzer, string? analyzerHeader)
{
this.analyzedSymbol = analyzedSymbol ?? throw new ArgumentNullException(nameof(analyzedSymbol));
this.analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
AnalyzerHeader = analyzerHeader ?? string.Empty;
LazyLoading = true;
}
/// <summary>The analyser whose <see cref="IAnalyzer.Analyze"/> drives this row.</summary>
public IAnalyzer Analyzer => analyzer;
/// <summary>The symbol being analysed (a type, method, field, …).</summary>
public ISymbol AnalyzedSymbol => analyzedSymbol;
/// <summary>
/// The header string this row started with (e.g. "Used By"). Updated in-place by
/// the background-fetch pipeline to include the result count and elapsed time
/// once the analyser completes.
/// </summary>
public string AnalyzerHeader { get; protected set; }
public override object Text => AnalyzerHeader;
public override bool HandleAssemblyListChanged(
ICollection<LoadedAssembly> removedAssemblies,
ICollection<LoadedAssembly> addedAssemblies)
{
this.Children.RemoveAll(node =>
node is not AnalyzerTreeNode an
|| !an.HandleAssemblyListChanged(removedAssemblies, addedAssemblies));
return true;
}
}
}

75
ILSpy/Analyzers/AnalyzerTreeNode.cs

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
// Copyright (c) 2026 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.Collections.Generic;
using System.Composition;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.Analyzers;
using ICSharpCode.ILSpyX.TreeView;
using ILSpy.AppEnv;
using ILSpy.Languages;
namespace ILSpy.Analyzers
{
/// <summary>
/// Base for every node in the analyzer pane. The pane root, the per-entity wrappers
/// (<see cref="AnalyzerEntityTreeNode"/>), and the per-analyzer "search" headers
/// (<see cref="AnalyzerSearchTreeNode"/>) all derive from this so the root's
/// <c>HandleAssemblyListChanged</c> sweep can recurse uniformly.
/// </summary>
public abstract class AnalyzerTreeNode : SharpTreeNode
{
static LanguageService? cachedLanguageService;
static AnalyzerRegistry? cachedRegistry;
/// <summary>
/// The active language used to format entity text. Resolved lazily through the
/// composition host so design-time previews (no MEF) don't NRE during XAML reload.
/// </summary>
protected static Languages.Language Language
=> (cachedLanguageService ??= AppComposition.Current.GetExport<LanguageService>()).CurrentLanguage;
/// <summary>
/// All MEF-registered <see cref="IAnalyzer"/> exports, ordered by their declared
/// <see cref="AnalyzerMetadata.Order"/>. Each <see cref="AnalyzerEntityTreeNode"/>
/// walks this list on <c>LoadChildren</c> and instantiates an
/// <see cref="AnalyzerSearchTreeNode"/> for every entry whose
/// <see cref="IAnalyzer.Show"/> returns true for the wrapped entity.
/// </summary>
public static IReadOnlyList<ExportFactory<IAnalyzer, AnalyzerMetadata>> Analyzers
=> (cachedRegistry ??= AppComposition.Current.GetExport<AnalyzerRegistry>()).Analyzers;
public override bool CanDelete() => Parent is { IsRoot: true };
public override void DeleteCore() => Parent?.Children.Remove(this);
public override void Delete() => DeleteCore();
/// <summary>
/// Reacts to add / remove events on the active <see cref="AssemblyList"/>. Each
/// node decides whether it (and its subtree) is still relevant — typically by
/// checking the removed list against its source module — and returns <c>false</c>
/// to ask its parent to drop it.
/// </summary>
public abstract bool HandleAssemblyListChanged(
ICollection<LoadedAssembly> removedAssemblies,
ICollection<LoadedAssembly> addedAssemblies);
}
}
Loading…
Cancel
Save