diff --git a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb index 0e2d5bb68980..5fc5369d1ed5 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb @@ -855,6 +855,95 @@ class C Await TestAsync(workspace) End Function + + Public Async Function TestCSharpGoToOverriddenDefinition_FromOverride_LooseMatch() As Task + Dim workspace = + + + +class C +{ + public virtual void [|F|](bool x) + { + } +} + +class D : C +{ + public $$override void F(int x) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestCSharpGoToOverriddenDefinition_FromOverride_LooseMatch2() As Task + Dim workspace = + + + +class C +{ + public virtual void F() + { + } + + public virtual void [|F|](bool x) + { + } +} + +class D : C +{ + public $$override void F(int x) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestCSharpGoToOverriddenDefinition_FromOverride_LooseMatch3() As Task + Dim workspace = + + + +class B +{ + public virtual void F(bool x) + { + } +} + +class C +{ + public virtual void [|F|]() + { + } +} + +class D : C +{ + public $$override void F(int x) + { + } +} + + + + + Await TestAsync(workspace) + End Function + Public Async Function TestCSharpGoToUnmanaged_Keyword() As Task Dim workspace = diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SemanticModelExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SemanticModelExtensions.cs index 0866fcf4877c..cfc7ffee17c3 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SemanticModelExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SemanticModelExtensions.cs @@ -98,7 +98,7 @@ public static TokenSemanticInfo GetSemanticInfo( { // on an "override" token, we'll find the overridden symbol var overriddingSymbol = semanticFacts.GetDeclaredSymbol(semanticModel, overriddingIdentifier.Value, cancellationToken); - var overriddenSymbol = overriddingSymbol.GetOverriddenMember(); + var overriddenSymbol = overriddingSymbol.GetOverriddenMember(allowLooseMatch: true); allSymbols = overriddenSymbol is null ? [] : [overriddenSymbol]; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs index 6a023012023e..d4e7dbb5e49a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs @@ -72,8 +72,12 @@ public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) return visibility; } - public static ISymbol? GetOverriddenMember(this ISymbol? symbol) - => symbol switch + public static ISymbol? GetOverriddenMember(this ISymbol? symbol, bool allowLooseMatch = false) + { + if (symbol is null) + return null; + + ISymbol? exactMatch = symbol switch { IMethodSymbol method => method.OverriddenMethod, IPropertySymbol property => property.OverriddenProperty, @@ -81,6 +85,56 @@ public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) _ => null, }; + if (exactMatch != null) + return exactMatch; + + if (allowLooseMatch) + { + foreach (var baseType in symbol.ContainingType.GetBaseTypes()) + { + if (TryFindLooseMatch(symbol, baseType, out var looseMatch)) + return looseMatch; + } + } + + return null; + + bool TryFindLooseMatch(ISymbol symbol, INamedTypeSymbol baseType, [NotNullWhen(true)] out ISymbol? looseMatch) + { + IMethodSymbol? bestMethod = null; + var parameterCount = symbol.GetParameters().Length; + + foreach (var member in baseType.GetMembers(symbol.Name)) + { + if (member.Kind != symbol.Kind) + continue; + + if (member.IsSealed) + continue; + + if (!member.IsVirtual && !member.IsOverride && !member.IsAbstract) + continue; + + if (symbol.Kind is SymbolKind.Event or SymbolKind.Property) + { + // We've found a matching event/property in the base type (perhaps differing by return type). This + // is a good enough match to return as a loose match for the starting symbol. + looseMatch = member; + return true; + } + else if (member is IMethodSymbol method) + { + // Prefer methods that are closed in parameter count to the original method we started with. + if (bestMethod is null || Math.Abs(method.Parameters.Length - parameterCount) < Math.Abs(bestMethod.Parameters.Length - parameterCount)) + bestMethod = method; + } + } + + looseMatch = bestMethod; + return looseMatch != null; + } + } + public static ImmutableArray ExplicitInterfaceImplementations(this ISymbol symbol) => symbol switch { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ITypeSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ITypeSymbolExtensions.cs index c3db3f7a0677..1de694e2114d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ITypeSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ITypeSymbolExtensions.cs @@ -121,9 +121,9 @@ public static IEnumerable GetBaseTypesAndThis(this ITypeSymbol? typ } } - public static IEnumerable GetBaseTypes(this ITypeSymbol type) + public static IEnumerable GetBaseTypes(this ITypeSymbol? type) { - var current = type.BaseType; + var current = type?.BaseType; while (current != null) { yield return current;