Browse Source

Fix InvalidCastException analysing a constructor's Uses

Language.GetCodeMappingInfo was unconditionally casting the incoming
EntityHandle to TypeDefinitionHandle. MethodUsesAnalyzer calls it
with the method's own handle and expects the language to walk to
the declaring type — the WPF impl does, the Avalonia port didn't.
Right-clicking a constructor → Analyze → expanding "Uses" therefore
threw at runtime; the error surfaced only as a single-line message
in the analyzer pane because AnalyzerErrorNode dropped the stack.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
1008d2b754
  1. 144
      ILSpy.Tests/Analyzers/AnalyzerConstructorUsesTests.cs
  2. 16
      ILSpy/Languages/Language.cs

144
ILSpy.Tests/Analyzers/AnalyzerConstructorUsesTests.cs

@ -0,0 +1,144 @@
// 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;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.Analyzers;
using ILSpy.Analyzers;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
/// <summary>
/// Regression: analysing the "Used By" of a constructor must not throw. The reported
/// user-visible failure was an InvalidCastException in the live app; this test exercises
/// the same path (analyzer enumeration + the search-node WrapResult switch) but bypasses
/// the search node's catch so any exception bubbles up to NUnit with its stack trace.
/// </summary>
[TestFixture]
public class AnalyzerConstructorUsesTests
{
[AvaloniaTest]
public async Task Used_By_Analyzer_Runs_Over_A_Constructor_Without_Throwing()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
// Pick any concrete type with at least one public instance ctor; the type itself
// doesn't matter — what matters is that the symbol fed to the analyzer is a method
// whose IsConstructor is true.
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Lookup`2");
typeNode.EnsureLazyChildren();
var ctorTreeNode = typeNode.Children.OfType<MethodTreeNode>()
.FirstOrDefault(m => m.MethodDefinition.IsConstructor);
(ctorTreeNode is null).Should().BeFalse(
"Lookup<TKey,TElement> defines at least one constructor — the test depends on it");
var ctor = (IMethod)ctorTreeNode!.Member!;
ctor.IsConstructor.Should().BeTrue();
// Resolve the "Used By" analyzer that actually applies to this method.
var analyzer = AnalyzerTreeNode.Analyzers
.Where(a => a.Metadata?.Header == "Used By")
.Select(a => a.CreateExport().Value)
.First(a => a.Show(ctor));
// Drive Analyze directly (no AnalyzerSearchTreeNode → no swallowing catch). Take
// a small slice so we don't pay for a full scan; the bug repros on the first call
// site if it's going to throw at all.
var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
var context = new AnalyzerContext {
CancellationToken = CancellationToken.None,
Language = AppComposition.Current.GetExport<global::ILSpy.Languages.LanguageService>().CurrentLanguage,
AssemblyList = assemblyList,
};
var results = analyzer.Analyze(ctor, context).Take(20).ToList();
results.Should().NotBeEmpty("constructors of public LINQ types must have at least one in-tree caller");
}
[AvaloniaTest]
public async Task Uses_Analyzer_Runs_Over_A_Constructor_Without_Throwing()
{
// "Uses" is the forward direction (what does the body of this method touch?).
// MethodUsesAnalyzer can return type/method/field/property/event ISymbols — the
// path most likely to surface a switch-arm hole for some result shape we missed.
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.Lookup`2");
typeNode.EnsureLazyChildren();
var ctor = (IMethod)typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.IsConstructor).Member!;
var analyzer = AnalyzerTreeNode.Analyzers
.Where(a => a.Metadata?.Header == "Uses")
.Select(a => a.CreateExport().Value)
.First(a => a.Show(ctor));
var assemblyList = vm.AssemblyTreeModel.AssemblyList!;
var context = new AnalyzerContext {
CancellationToken = CancellationToken.None,
Language = AppComposition.Current.GetExport<global::ILSpy.Languages.LanguageService>().CurrentLanguage,
AssemblyList = assemblyList,
};
var results = analyzer.Analyze(ctor, context).Take(20).ToList();
results.Should().NotBeEmpty();
}
[AvaloniaTest]
public async Task Analyze_Pushes_A_Constructor_Onto_The_Pane_Without_Throwing()
{
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.Lookup`2");
typeNode.EnsureLazyChildren();
var ctor = (IMethod)typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.IsConstructor).Member!;
var analyzerVm = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var beforeCount = analyzerVm.Root.Children.Count;
// This was the user-reported throw point.
analyzerVm.Analyze(ctor);
analyzerVm.Root.Children.Count.Should().Be(beforeCount + 1);
}
}

16
ILSpy/Languages/Language.cs

@ -56,11 +56,21 @@ namespace ILSpy.Languages
public bool HasLanguageVersions => LanguageVersions.Count > 0; public bool HasLanguageVersions => LanguageVersions.Count > 0;
/// <summary> /// <summary>
/// Token-to-source-line mapping for navigation. Default returns a no-op CodeMappingInfo — /// Token-to-source-line mapping for navigation. Default walks <paramref name="member"/>
/// language subclasses with full decompilation override and produce a real one. /// up to its declaring type and hands back a <see cref="CodeMappingInfo"/> keyed on
/// that type. The <see cref="EntityHandle"/> the caller passes may name any member
/// kind (method, field, …) — analyzers like <c>MethodUsesAnalyzer</c> pass the
/// method's own token and expect the language to walk to the type itself, not crash
/// trying to cast the method handle. Subclasses with full decompilation override and
/// build the mapping from the decompiler pipeline.
/// </summary> /// </summary>
public virtual CodeMappingInfo GetCodeMappingInfo(MetadataFile module, EntityHandle member) public virtual CodeMappingInfo GetCodeMappingInfo(MetadataFile module, EntityHandle member)
=> new(module, (TypeDefinitionHandle)member); {
var declaringType = (TypeDefinitionHandle)member.GetDeclaringType(module.Metadata);
if (declaringType.IsNil && member.Kind == HandleKind.TypeDefinition)
declaringType = (TypeDefinitionHandle)member;
return new CodeMappingInfo(module, declaringType);
}
/// <summary> /// <summary>
/// Stable name string for an entity reachable only by metadata token. Falls back to the /// Stable name string for an entity reachable only by metadata token. Falls back to the

Loading…
Cancel
Save