From c1144148837c97b3e07c6fe720ac318bdc7bb889 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 13 May 2026 14:27:15 +0200 Subject: [PATCH] Analyzer tree node bases + entity nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Analyzers/AnalyzedMethodTreeNodeTests.cs | 100 +++++++++++++++ .../Analyzers/AnalyzedTypeTreeNodeTests.cs | 106 ++++++++++++++++ .../Analyzers/AnalyzerTreeNodeTests.cs | 111 +++++++++++++++++ ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs | 45 +++++++ ILSpy/Analyzers/AnalyzedEventTreeNode.cs | 85 +++++++++++++ ILSpy/Analyzers/AnalyzedFieldTreeNode.cs | 58 +++++++++ ILSpy/Analyzers/AnalyzedMethodTreeNode.cs | 73 +++++++++++ ILSpy/Analyzers/AnalyzedModuleTreeNode.cs | 81 ++++++++++++ ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs | 65 ++++++++++ ILSpy/Analyzers/AnalyzedTypeTreeNode.cs | 68 +++++++++++ ILSpy/Analyzers/AnalyzerEntityTreeNode.cs | 69 +++++++++++ ILSpy/Analyzers/AnalyzerMetadata.cs | 34 ++++++ ILSpy/Analyzers/AnalyzerRegistry.cs | 49 ++++++++ ILSpy/Analyzers/AnalyzerRootNode.cs | 115 ++++++++++++++++++ ILSpy/Analyzers/AnalyzerSearchTreeNode.cs | 73 +++++++++++ ILSpy/Analyzers/AnalyzerTreeNode.cs | 75 ++++++++++++ 16 files changed, 1207 insertions(+) create mode 100644 ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs create mode 100644 ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs create mode 100644 ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs create mode 100644 ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzedEventTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzedFieldTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzedMethodTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzedModuleTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzedTypeTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzerEntityTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzerMetadata.cs create mode 100644 ILSpy/Analyzers/AnalyzerRegistry.cs create mode 100644 ILSpy/Analyzers/AnalyzerRootNode.cs create mode 100644 ILSpy/Analyzers/AnalyzerSearchTreeNode.cs create mode 100644 ILSpy/Analyzers/AnalyzerTreeNode.cs diff --git a/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs new file mode 100644 index 000000000..445879657 --- /dev/null +++ b/ILSpy.Tests/Analyzers/AnalyzedMethodTreeNodeTests.cs @@ -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" + // 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(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.EnsureLazyChildren(); + var emptyMethod = (IMethod)typeNode.Children.OfType() + .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(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.EnsureLazyChildren(); + var firstMethod = (IMethod)typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "AsEnumerable").Member!; + + var node = new AnalyzedMethodTreeNode(firstMethod, source: null); + node.EnsureLazyChildren(); + + var headers = node.Children.OfType() + .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"); + } +} diff --git a/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs new file mode 100644 index 000000000..6bf8a4cef --- /dev/null +++ b/ILSpy.Tests/Analyzers/AnalyzedTypeTreeNodeTests.cs @@ -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(); + 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(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(); + 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(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() + .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"); + } +} diff --git a/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs b/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs new file mode 100644 index 000000000..58baebbb6 --- /dev/null +++ b/ILSpy.Tests/Analyzers/AnalyzerTreeNodeTests.cs @@ -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(); + 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(); + 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(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); + } +} diff --git a/ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs b/ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs new file mode 100644 index 000000000..882ad13c7 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedAccessorTreeNode.cs @@ -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 +{ + /// + /// 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. + /// + 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; + } +} diff --git a/ILSpy/Analyzers/AnalyzedEventTreeNode.cs b/ILSpy/Analyzers/AnalyzedEventTreeNode.cs new file mode 100644 index 000000000..51e223e3e --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedEventTreeNode.cs @@ -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; + } + } +} diff --git a/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs b/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs new file mode 100644 index 000000000..1994dd380 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedFieldTreeNode.cs @@ -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)); + } + } + } + } +} diff --git a/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs b/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs new file mode 100644 index 000000000..d5ba5e6c1 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedMethodTreeNode.cs @@ -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)); + } + } + } + } +} diff --git a/ILSpy/Analyzers/AnalyzedModuleTreeNode.cs b/ILSpy/Analyzers/AnalyzedModuleTreeNode.cs new file mode 100644 index 000000000..4afb4abf3 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedModuleTreeNode.cs @@ -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 +{ + /// + /// Wraps an entire module as an analysable entry. is + /// — module-level analyses (e.g. "Applied To" against an + /// assembly-level attribute) hang their results off this row. + /// + 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 removedAssemblies, + ICollection 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; + } + } +} diff --git a/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs b/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs new file mode 100644 index 000000000..85c7dfe35 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedPropertyTreeNode.cs @@ -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)); + } + } + } + } +} diff --git a/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs b/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs new file mode 100644 index 000000000..319bf792b --- /dev/null +++ b/ILSpy/Analyzers/AnalyzedTypeTreeNode.cs @@ -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)); + } + } + } + } +} diff --git a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs new file mode 100644 index 000000000..6134a6ce2 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs @@ -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 +{ + /// + /// Base for nodes that wrap an in the analyzer pane (the + /// per-entity root row plus every analyser result row underneath an + /// ). Concrete subclasses supply the entity, its + /// icon, and its text; this base owns the navigation hook and the assembly-change + /// pruning logic. + /// + public abstract class AnalyzerEntityTreeNode : AnalyzerTreeNode + { + /// + /// The entity this row analyses (or represents as an analyser result). Subclasses + /// return only for non-entity rows such as + /// . + /// + public abstract IEntity? Member { get; } + + /// + /// 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). + /// + public IEntity? SourceMember { get; protected set; } + + public override object? ToolTip => Member?.ParentModule?.MetadataFile?.FileName; + + public override bool HandleAssemblyListChanged( + ICollection removedAssemblies, + ICollection 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; + } + } +} diff --git a/ILSpy/Analyzers/AnalyzerMetadata.cs b/ILSpy/Analyzers/AnalyzerMetadata.cs new file mode 100644 index 000000000..4c236bc78 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzerMetadata.cs @@ -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 +{ + /// + /// Concrete metadata view consumed by . System.Composition + /// only projects metadata through a class with a parameterless constructor and public + /// settable properties — the shared IAnalyzerMetadata interface doesn't qualify. + /// Property names mirror ExportAnalyzerAttribute's so the framework copies values + /// across by name. + /// + public sealed class AnalyzerMetadata + { + public string? Header { get; set; } + + public int Order { get; set; } + } +} diff --git a/ILSpy/Analyzers/AnalyzerRegistry.cs b/ILSpy/Analyzers/AnalyzerRegistry.cs new file mode 100644 index 000000000..834d68d80 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzerRegistry.cs @@ -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 +{ + /// + /// MEF aggregator for -tagged analyzers. System.Composition + /// only resolves [ImportMany] with metadata through constructor injection, so this + /// registry is the single place that pulls the factories out of the composition host. Each + /// reads through the static + /// accessor on , which in turn resolves this registry once. + /// + [Export] + [Shared] + public sealed class AnalyzerRegistry + { + [ImportingConstructor] + public AnalyzerRegistry( + [ImportMany("Analyzer")] IEnumerable> analyzers) + { + Analyzers = analyzers + .OrderBy(a => a.Metadata?.Order ?? 0) + .ToArray(); + } + + public IReadOnlyList> Analyzers { get; } + } +} diff --git a/ILSpy/Analyzers/AnalyzerRootNode.cs b/ILSpy/Analyzers/AnalyzerRootNode.cs new file mode 100644 index 000000000..c07c64ea1 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzerRootNode.cs @@ -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 +{ + /// + /// Root of the analyzer pane's tree. Holds one + /// per analysed entity and prunes stale entries when the active + /// changes — either an assembly is closed (matching + /// children dropped) or the list itself is swapped out (entire subtree wiped). + /// + 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(); + } + 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().ToArray() ?? System.Array.Empty(); + var added = e.NewItems?.Cast().ToArray() ?? System.Array.Empty(); + HandleAssemblyListChanged(removed, added); + } + + public override bool HandleAssemblyListChanged( + ICollection removedAssemblies, + ICollection addedAssemblies) + { + this.Children.RemoveAll(node => + node is not AnalyzerTreeNode an + || !an.HandleAssemblyListChanged(removedAssemblies, addedAssemblies)); + return true; + } + } +} diff --git a/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs b/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs new file mode 100644 index 000000000..295cb52f2 --- /dev/null +++ b/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs @@ -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 +{ + /// + /// Row that runs a single 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. + /// + 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; + } + + /// The analyser whose drives this row. + public IAnalyzer Analyzer => analyzer; + + /// The symbol being analysed (a type, method, field, …). + public ISymbol AnalyzedSymbol => analyzedSymbol; + + /// + /// 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. + /// + public string AnalyzerHeader { get; protected set; } + + public override object Text => AnalyzerHeader; + + public override bool HandleAssemblyListChanged( + ICollection removedAssemblies, + ICollection addedAssemblies) + { + this.Children.RemoveAll(node => + node is not AnalyzerTreeNode an + || !an.HandleAssemblyListChanged(removedAssemblies, addedAssemblies)); + return true; + } + } +} diff --git a/ILSpy/Analyzers/AnalyzerTreeNode.cs b/ILSpy/Analyzers/AnalyzerTreeNode.cs new file mode 100644 index 000000000..5fdc431ab --- /dev/null +++ b/ILSpy/Analyzers/AnalyzerTreeNode.cs @@ -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 +{ + /// + /// Base for every node in the analyzer pane. The pane root, the per-entity wrappers + /// (), and the per-analyzer "search" headers + /// () all derive from this so the root's + /// HandleAssemblyListChanged sweep can recurse uniformly. + /// + public abstract class AnalyzerTreeNode : SharpTreeNode + { + static LanguageService? cachedLanguageService; + static AnalyzerRegistry? cachedRegistry; + + /// + /// 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. + /// + protected static Languages.Language Language + => (cachedLanguageService ??= AppComposition.Current.GetExport()).CurrentLanguage; + + /// + /// All MEF-registered exports, ordered by their declared + /// . Each + /// walks this list on LoadChildren and instantiates an + /// for every entry whose + /// returns true for the wrapped entity. + /// + public static IReadOnlyList> Analyzers + => (cachedRegistry ??= AppComposition.Current.GetExport()).Analyzers; + + public override bool CanDelete() => Parent is { IsRoot: true }; + + public override void DeleteCore() => Parent?.Children.Remove(this); + + public override void Delete() => DeleteCore(); + + /// + /// Reacts to add / remove events on the active . Each + /// node decides whether it (and its subtree) is still relevant — typically by + /// checking the removed list against its source module — and returns false + /// to ask its parent to drop it. + /// + public abstract bool HandleAssemblyListChanged( + ICollection removedAssemblies, + ICollection addedAssemblies); + } +}