Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
760 changes: 453 additions & 307 deletions src/EditorFeatures/Test/InheritanceMargin/InheritanceMarginTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToImplementation
<[UseExportProvider]>
<Trait(Traits.Feature, Traits.Features.GoToImplementation)>
Public Class GoToImplementationTests

Private Shared Async Function TestAsync(workspaceDefinition As XElement, host As TestHost, Optional shouldSucceed As Boolean = True, Optional metadataDefinitions As String() = Nothing) As Task
Await GoToHelpers.TestAsync(
workspaceDefinition,
Expand Down Expand Up @@ -787,5 +786,30 @@ class D : C { public override void [|M|]() { } }}

Await TestAsync(workspace, host)
End Function

<Theory, CombinatorialData>
<WorkItem("https://github.com/dotnet/roslyn/issues/26167")>
Public Async Function FindLooseMatch1(host As TestHost) As Task
Dim workspace =
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class C
{
public abstract void $$Foo() { }
}

class D : C
{
public override void [|Foo|](int i)
{
base.Foo();
}
} </Document>
</Project>
</Workspace>

Await TestAsync(workspace, host)
End Function
End Class
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,17 @@ public async Task FindImplementationsAsync(
IFindUsagesContext context, Document document, int position, OptionsProvider<ClassificationOptions> classificationOptions, CancellationToken cancellationToken)
{
// If this is a symbol from a metadata-as-source project, then map that symbol back to a symbol in the primary workspace.
var symbolAndProjectOpt = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync(
var symbolAndProject = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync(
document, position, cancellationToken).ConfigureAwait(false);
if (symbolAndProjectOpt == null)
if (symbolAndProject is not var (symbol, project))
{
await context.ReportNoResultsAsync(
FeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret, cancellationToken).ConfigureAwait(false);
return;
}

var symbolAndProject = symbolAndProjectOpt.Value;
await FindImplementationsAsync(
context, symbolAndProject.symbol, symbolAndProject.project, classificationOptions, cancellationToken).ConfigureAwait(false);
context, symbol, project, classificationOptions, cancellationToken).ConfigureAwait(false);
}

public static async Task FindImplementationsAsync(
Expand Down Expand Up @@ -145,7 +144,9 @@ private static async Task<ImmutableArray<ISymbol>> FindImplementationsWorkerAsyn
var overrides = result.Where(s => s.IsOverride).ToImmutableArray();
foreach (var ov in overrides)
{
for (var overridden = ov.GetOverriddenMember(); overridden != null; overridden = overridden.GetOverriddenMember())
for (var overridden = ov.GetOverriddenMember(allowLooseMatch: true);
overridden != null;
overridden = overridden.GetOverriddenMember(allowLooseMatch: true))
{
if (overridden.IsAbstract)
result.Remove(overridden.OriginalDefinition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,16 @@ internal abstract class AbstractGoToBaseService : IGoToBaseService

public async Task FindBasesAsync(IFindUsagesContext context, Document document, int position, OptionsProvider<ClassificationOptions> classificationOptions, CancellationToken cancellationToken)
{
var symbolAndProjectOpt = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync(
var symbolAndProject = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync(
document, position, cancellationToken).ConfigureAwait(false);

if (symbolAndProjectOpt == null)
if (symbolAndProject is not var (symbol, project))
{
await context.ReportNoResultsAsync(
FeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret, cancellationToken).ConfigureAwait(false);
return;
}

var (symbol, project) = symbolAndProjectOpt.Value;

var solution = project.Solution;
var bases = FindBaseHelpers.FindBases(symbol, solution, cancellationToken);
if (bases.Length == 0 && symbol is IMethodSymbol { MethodKind: MethodKind.Constructor } constructor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ private static async ValueTask AddInheritanceMemberItemsForMembersAsync(
var allOverridingSymbols = await SymbolFinder.FindOverridesArrayAsync(memberSymbol, solution, cancellationToken: cancellationToken).ConfigureAwait(false);

// Go up the inheritance chain to find all overridden targets
var overriddenSymbols = GetOverriddenSymbols(memberSymbol);
var overriddenSymbols = GetOverriddenSymbols(memberSymbol, allowLooseMatch: true);

// Go up the inheritance chain to find all the implemented targets.
var implementedSymbols = GetImplementedSymbolsForTypeMember(memberSymbol, overriddenSymbols);
Expand Down Expand Up @@ -650,24 +650,20 @@ private static async Task<ImmutableArray<ISymbol>> GetImplementingSymbolsForType
/// <summary>
/// Get overridden members the <param name="memberSymbol"/>.
/// </summary>
private static ImmutableArray<ISymbol> GetOverriddenSymbols(ISymbol memberSymbol)
private static ImmutableArray<ISymbol> GetOverriddenSymbols(ISymbol memberSymbol, bool allowLooseMatch)
{
if (memberSymbol is INamedTypeSymbol)
{
return [];
}
else
{
using var _ = ArrayBuilder<ISymbol>.GetInstance(out var builder);
for (var overriddenMember = memberSymbol.GetOverriddenMember();
overriddenMember != null;
overriddenMember = overriddenMember.GetOverriddenMember())
{
builder.Add(overriddenMember.OriginalDefinition);
}

return builder.ToImmutableArray();
using var _ = ArrayBuilder<ISymbol>.GetInstance(out var builder);
for (var overriddenMember = memberSymbol.GetOverriddenMember(allowLooseMatch);
overriddenMember != null;
overriddenMember = overriddenMember.GetOverriddenMember(allowLooseMatch))
{
builder.Add(overriddenMember.OriginalDefinition);
}

return builder.ToImmutableAndClear();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public override int GetHashCode()
public override bool Equals(object? obj)
=> obj is InheritanceMarginItem item && Equals(item);

public override string ToString()
=> string.Join("", DisplayTexts.Select(d => d.Text));

public bool Equals(InheritanceMarginItem other)
=> this.LineNumber == other.LineNumber &&
this.TopLevelDisplayText == other.TopLevelDisplayText &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,59 @@ public static ImmutableArray<ISymbol> FindOverriddenAndImplementedMembers(
// This is called for all: class, struct or interface member.
results.AddRange(symbol.ExplicitOrImplicitInterfaceImplementations());

// The type scenario. Iterate over all base classes to find overridden and hidden (new/Shadows) methods.
foreach (var type in FindBaseTypes(symbol.ContainingType))
AddOverrides(allowLooseMatch: false);

// If we've found nothing at all (either interface impls or exact override matches), then attempt a loose match
// to see if we can find something in an error condition.
if (results.Count == 0)
AddOverrides(allowLooseMatch: true);

// Remove duplicates from interface implementations before adding their projects.
results.RemoveDuplicates();
return results.ToImmutableAndClear();

void AddOverrides(bool allowLooseMatch)
{
foreach (var member in type.GetMembers(symbol.Name))
// The type scenario. Iterate over all base classes to find overridden and hidden (new/Shadows) methods.
foreach (var type in FindBaseTypes(symbol.ContainingType))
{
cancellationToken.ThrowIfCancellationRequested();

// Add to results overridden members only. Do not add hidden members.
if (SymbolFinder.IsOverride(solution, symbol, member))
foreach (var member in type.GetMembers(symbol.Name))
{
results.Add(member);
cancellationToken.ThrowIfCancellationRequested();

// Add to results overridden members only. Do not add hidden members.
if (SymbolFinder.IsOverride(solution, symbol, member, allowLooseMatch))
{
results.Add(member);

// We should add implementations only for overridden members but not for hidden ones.
// In the following example:
//
// interface I { void M(); }
// class A : I { public void M(); }
// class B : A { public new void M(); }
//
// we should not find anything for B.M() because it does not implement the interface:
//
// I i = new B(); i.M();
//
// will call the method from A.
// However, if we change the code to
//
// class B : A, I { public new void M(); }
//
// then
//
// I i = new B(); i.M();
//
// will call the method from B. We should find the base for B.M in this case.
// And if we change 'new' to 'override' in the original code and add 'virtual' where needed,
// we should find I.M as a base for B.M(). And the next line helps with this scenario.
results.AddRange(member.ExplicitOrImplicitInterfaceImplementations());
// We should add implementations only for overridden members but not for hidden ones.
// In the following example:
//
// interface I { void M(); }
// class A : I { public void M(); }
// class B : A { public new void M(); }
//
// we should not find anything for B.M() because it does not implement the interface:
//
// I i = new B(); i.M();
//
// will call the method from A.
// However, if we change the code to
//
// class B : A, I { public new void M(); }
//
// then
//
// I i = new B(); i.M();
//
// will call the method from B. We should find the base for B.M in this case.
// And if we change 'new' to 'override' in the original code and add 'virtual' where needed,
// we should find I.M as a base for B.M(). And the next line helps with this scenario.
results.AddRange(member.ExplicitOrImplicitInterfaceImplementations());
}
}
}
}

// Remove duplicates from interface implementations before adding their projects.
results.RemoveDuplicates();
return results.ToImmutableAndClear();
}

private static ImmutableArray<INamedTypeSymbol> FindBaseTypes(INamedTypeSymbol type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,38 +36,50 @@ public static async Task<IEnumerable<ISymbol>> FindOverridesAsync(
internal static async Task<ImmutableArray<ISymbol>> FindOverridesArrayAsync(
ISymbol symbol, Solution solution, IImmutableSet<Project>? projects = null, CancellationToken cancellationToken = default)
{
var results = ArrayBuilder<ISymbol>.GetInstance();

symbol = symbol.OriginalDefinition;
if (symbol.IsOverridable())
{
// To find the overrides, we need to walk down the type hierarchy and check all
// derived types.
var containingType = symbol.ContainingType;
var derivedTypes = await FindDerivedClassesAsync(
containingType, solution, projects, cancellationToken).ConfigureAwait(false);
if (!symbol.IsOverridable())
return [];

using var _ = ArrayBuilder<ISymbol>.GetInstance(out var results);

// To find the overrides, we need to walk down the type hierarchy and check all
// derived types.
var containingType = symbol.ContainingType;
var derivedTypes = await FindDerivedClassesAsync(
containingType, solution, projects, cancellationToken).ConfigureAwait(false);

// First try finding exact overrides. If that fails to find anything, look for overrides that loosely
// match due to errors.
await FindOverridesAsync(allowLooseMatch: false).ConfigureAwait(false);
if (results.Count == 0)
await FindOverridesAsync(allowLooseMatch: true).ConfigureAwait(false);

return results.ToImmutableAndClear();

async Task FindOverridesAsync(bool allowLooseMatch)
{
foreach (var type in derivedTypes)
{
foreach (var m in type.GetMembers(symbol.Name))
{
var sourceMember = await FindSourceDefinitionAsync(m, solution, cancellationToken).ConfigureAwait(false);
var bestMember = sourceMember ?? m;

if (IsOverride(solution, bestMember, symbol))
if (IsOverride(solution, bestMember, symbol, allowLooseMatch))
results.Add(bestMember);
}
}
}

return results.ToImmutableAndFree();
}

internal static bool IsOverride(Solution solution, ISymbol member, ISymbol symbol)
internal static bool IsOverride(Solution solution, ISymbol member, ISymbol symbol, bool allowLooseMatch)
{
for (var current = member; current != null; current = current.GetOverriddenMember())
for (var current = member;
current != null;
current = current.GetOverriddenMember(allowLooseMatch))
{
if (OriginalSymbolsMatch(solution, current.GetOverriddenMember(), symbol.OriginalDefinition))
if (OriginalSymbolsMatch(solution, current.GetOverriddenMember(allowLooseMatch), symbol.OriginalDefinition))
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public static SymbolVisibility GetResultantVisibility(this ISymbol symbol)
if (exactMatch != null)
return exactMatch;

if (allowLooseMatch)
if (allowLooseMatch &&
(symbol.IsVirtual || symbol.IsAbstract || symbol.IsOverride))
{
foreach (var baseType in symbol.ContainingType.GetBaseTypes())
{
Expand All @@ -109,10 +110,7 @@ bool TryFindLooseMatch(ISymbol symbol, INamedTypeSymbol baseType, [NotNullWhen(t
if (member.Kind != symbol.Kind)
continue;

if (member.IsSealed)
continue;

if (!member.IsVirtual && !member.IsOverride && !member.IsAbstract)
if (!member.IsOverridable())
continue;

if (symbol.Kind is SymbolKind.Event or SymbolKind.Property)
Expand Down Expand Up @@ -163,11 +161,9 @@ public static ImmutableArray<ISymbol> ImplicitInterfaceImplementations(this ISym

public static bool IsOverridable([NotNullWhen(true)] this ISymbol? symbol)
{
// Members can only have overrides if they are virtual, abstract or override and is not
// sealed.
return symbol?.ContainingType?.TypeKind == TypeKind.Class &&
(symbol.IsVirtual || symbol.IsAbstract || symbol.IsOverride) &&
!symbol.IsSealed;
// Members can only have overrides if they are virtual, abstract or override and is not sealed.
return symbol is { ContainingType.TypeKind: TypeKind.Class, IsSealed: false } &&
(symbol.IsVirtual || symbol.IsAbstract || symbol.IsOverride);
}

public static bool IsImplementableMember([NotNullWhen(true)] this ISymbol? symbol)
Expand Down
Loading