diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.Collector.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.Collector.cs index c4f9fe1710..c004413d91 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.Collector.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.Collector.cs @@ -445,13 +445,6 @@ private void RecordAssignment(IOperation op, ITypeSymbol valueType) break; } - case OperationKind.ParameterReference: - { - var paramRef = (IParameterReferenceOperation)op; - RecordAssignment(paramRef.Parameter, valueType); - break; - } - case OperationKind.DeclarationExpression: { var declEx = (IDeclarationExpressionOperation)op; diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.cs index 34beb7915f..fafcc19ad8 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeAnalyzer.cs @@ -115,6 +115,7 @@ public override void Initialize(AnalysisContext context) { var coll = Collector.GetInstance(voidType); + // we accumulate a bunch of info in the collector object context.RegisterOperationAction(context => coll.HandleInvocation((IInvocationOperation)context.Operation), OperationKind.Invocation); context.RegisterOperationAction(context => coll.HandleSimpleAssignment((ISimpleAssignmentOperation)context.Operation), OperationKind.SimpleAssignment); context.RegisterOperationAction(context => coll.HandleCoalesceAssignment((ICoalesceAssignmentOperation)context.Operation), OperationKind.CoalesceAssignment); @@ -126,6 +127,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolEndAction(context => { + // based on what we've collected, spit out relevant diagnostics Report(context, coll); Collector.ReturnInstance(coll, context.CancellationToken); }); @@ -138,6 +140,7 @@ public override void Initialize(AnalysisContext context) /// private static void Report(SymbolAnalysisContext context, Collector coll) { + // for all eligible private fields that are used as the receiver for a virtual call foreach (var field in coll.VirtualDispatchFields.Keys) { if (coll.FieldAssignments.TryGetValue(field, out var assignments)) @@ -146,6 +149,7 @@ private static void Report(SymbolAnalysisContext context, Collector coll) } } + // for all eligible local variables that are used as the receiver for a virtual call foreach (var local in coll.VirtualDispatchLocals.Keys) { if (coll.LocalAssignments.TryGetValue(local, out var assignments)) @@ -154,6 +158,7 @@ private static void Report(SymbolAnalysisContext context, Collector coll) } } + // for all eligible parameters that are used as the receiver for a virtual call foreach (var parameter in coll.VirtualDispatchParameters.Keys) { if (coll.ParameterAssignments.TryGetValue(parameter, out var assignments)) @@ -168,11 +173,13 @@ private static void Report(SymbolAnalysisContext context, Collector coll) } } + // for eligible all return types of private methods foreach (var pair in coll.MethodReturns) { var method = pair.Key; var returns = pair.Value; + // only report the method if it never assigned to a delegate if (CanUpgrade(method)) { Report(method, method.ReturnType, returns, UseConcreteTypeForMethodReturn); @@ -181,10 +188,15 @@ private static void Report(SymbolAnalysisContext context, Collector coll) void Report(ISymbol sym, ITypeSymbol fromType, PooledConcurrentSet assignments, DiagnosticDescriptor desc) { + // a set of the values assigned to the given symbol using var types = PooledHashSet.GetInstance(assignments, SymbolEqualityComparer.Default); + // 'void' is the magic value we use to represent null assignment var assignedNull = types.Remove(coll.Void!); + // We currently only handle the case where there is only a single consistent type of value assigned to the + // symbol. If there are multiple different types, we could try to find the common base for these, but it doesn't + // seem worth the complication. if (types.Count == 1) { var toType = types.Single(); @@ -195,6 +207,7 @@ void Report(ISymbol sym, ITypeSymbol fromType, PooledConcurrentSet if (!toType.DerivesFrom(fromType.OriginalDefinition)) { + // can readily replace fromType by toType return; } diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeTests.cs index 9f089ec666..3072076001 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Performance/UseConcreteTypeTests.cs @@ -45,6 +45,48 @@ public class Derived2 : BaseType await TestCSAsync(Source); } + [Fact] + public static async Task ShouldNotTrigger2() + { + const string Source = @" + #nullable enable + + using System.Collections.Generic; + + namespace System + { + public static partial class MemoryExtensions + { + public static unsafe bool SequenceEqual(this ReadOnlySpan span, ReadOnlySpan other, IEqualityComparer? comparer = null) + { + comparer = EqualityComparer.Default; + return comparer!.Equals(span[0], other[0]); + } + + public static int CommonPrefixLength(this Span span, ReadOnlySpan other, IEqualityComparer? comparer) + { + return comparer!.Equals(span[0], other[0]) ? 0 : 1; + } + + public static bool Foo() + { + Span s1 = stackalloc byte[2]; + Span s2 = stackalloc byte[2]; + return SequenceEqual(s1, s2, EqualityComparer.Default); + } + + public static int Bar() + { + Span s1 = stackalloc byte[2]; + Span s2 = stackalloc byte[2]; + return CommonPrefixLength(s1, s2, EqualityComparer.Default); + } + } + }"; + + await TestCSAsync(Source); + } + [Fact] public static async Task ShouldTrigger1() {