|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeQuality;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
namespace Microsoft.CodeAnalysis.RemoveUnusedMembers
{
internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer<TDocumentationCommentTriviaSyntax, TIdentifierNameSyntax>
: AbstractCodeQualityDiagnosticAnalyzer
where TDocumentationCommentTriviaSyntax : SyntaxNode
where TIdentifierNameSyntax : SyntaxNode
{
/// <summary>
/// Produces names like TypeName.MemberName
/// </summary>
private static readonly SymbolDisplayFormat ContainingTypeAndNameOnlyFormat = new(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);
// IDE0051: "Remove unused members" (Symbol is declared but never referenced)
private static readonly DiagnosticDescriptor s_removeUnusedMembersRule = CreateDescriptor(
IDEDiagnosticIds.RemoveUnusedMembersDiagnosticId,
EnforceOnBuildValues.RemoveUnusedMembers,
new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_is_unused), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
isUnnecessary: true);
// IDE0052: "Remove unread members" (Value is written and/or symbol is referenced, but the assigned value is never read)
// Internal for testing
internal static readonly DiagnosticDescriptor s_removeUnreadMembersRule = CreateDescriptor(
IDEDiagnosticIds.RemoveUnreadMembersDiagnosticId,
EnforceOnBuildValues.RemoveUnreadMembers,
new LocalizableResourceString(nameof(AnalyzersResources.Remove_unread_private_members), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_can_be_removed_as_the_value_assigned_to_it_is_never_read), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
isUnnecessary: true);
protected AbstractRemoveUnusedMembersDiagnosticAnalyzer()
: base(ImmutableArray.Create(s_removeUnusedMembersRule, s_removeUnreadMembersRule),
GeneratedCodeAnalysisFlags.Analyze) // We want to analyze references in generated code, but not report unused members in generated code.
{
}
// We need to analyze the whole document even for edits within a method body,
// because we might add or remove references to members in executable code.
// For example, if we had an unused field with no references, then editing any single method body
// to reference this field should clear the unused field diagnostic.
// Hence, we need to re-analyze the declarations in the whole file for any edits within the document.
public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;
protected sealed override void InitializeWorker(AnalysisContext context)
=> context.RegisterCompilationStartAction(compilationStartContext
=> CompilationAnalyzer.CreateAndRegisterActions(compilationStartContext, this));
/// <summary>
/// Override this method to register custom language specific actions to find symbol usages.
/// </summary>
protected virtual void HandleNamedTypeSymbolStart(SymbolStartAnalysisContext context, Action<ISymbol, ValueUsageInfo> onSymbolUsageFound)
{
}
private sealed class CompilationAnalyzer
{
private readonly object _gate;
/// <summary>
/// State map for candidate member symbols, with the value indicating how each symbol is used in executable code.
/// </summary>
private readonly Dictionary<ISymbol, ValueUsageInfo> _symbolValueUsageStateMap = new();
/// <summary>
/// List of properties that have a 'get' accessor usage, while the value itself is not used, e.g.:
/// <code>
/// class C
/// {
/// private int P { get; set; }
/// public void M() { P++; }
/// }
/// </code>
/// Here, 'get' accessor is used in an increment operation, but the result of the increment operation isn't used and 'P' itself is not used anywhere else, so it can be safely removed
/// </summary>
private readonly HashSet<IPropertySymbol> _propertiesWithShadowGetAccessorUsages = new();
private readonly INamedTypeSymbol _taskType, _genericTaskType, _debuggerDisplayAttributeType, _structLayoutAttributeType;
private readonly INamedTypeSymbol _eventArgsType;
private readonly DeserializationConstructorCheck _deserializationConstructorCheck;
private readonly ImmutableHashSet<INamedTypeSymbol> _attributeSetForMethodsToIgnore;
private readonly AbstractRemoveUnusedMembersDiagnosticAnalyzer<TDocumentationCommentTriviaSyntax, TIdentifierNameSyntax> _analyzer;
private CompilationAnalyzer(
Compilation compilation,
AbstractRemoveUnusedMembersDiagnosticAnalyzer<TDocumentationCommentTriviaSyntax, TIdentifierNameSyntax> analyzer)
{
_gate = new object();
_analyzer = analyzer;
_taskType = compilation.TaskType();
_genericTaskType = compilation.TaskOfTType();
_debuggerDisplayAttributeType = compilation.DebuggerDisplayAttributeType();
_structLayoutAttributeType = compilation.StructLayoutAttributeType();
_eventArgsType = compilation.EventArgsType();
_deserializationConstructorCheck = new DeserializationConstructorCheck(compilation);
_attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(compilation));
}
private static IEnumerable<INamedTypeSymbol> GetAttributesForMethodsToIgnore(Compilation compilation)
{
// Ignore methods with special serialization attributes, which are invoked by the runtime
// for deserialization.
var onDeserializingAttribute = compilation.OnDeserializingAttribute();
if (onDeserializingAttribute != null)
{
yield return onDeserializingAttribute;
}
var onDeserializedAttribute = compilation.OnDeserializedAttribute();
if (onDeserializedAttribute != null)
{
yield return onDeserializedAttribute;
}
var onSerializingAttribute = compilation.OnSerializingAttribute();
if (onSerializingAttribute != null)
{
yield return onSerializingAttribute;
}
var onSerializedAttribute = compilation.OnSerializedAttribute();
if (onSerializedAttribute != null)
{
yield return onSerializedAttribute;
}
var comRegisterFunctionAttribute = compilation.ComRegisterFunctionAttribute();
if (comRegisterFunctionAttribute != null)
{
yield return comRegisterFunctionAttribute;
}
var comUnregisterFunctionAttribute = compilation.ComUnregisterFunctionAttribute();
if (comUnregisterFunctionAttribute != null)
{
yield return comUnregisterFunctionAttribute;
}
}
public static void CreateAndRegisterActions(
CompilationStartAnalysisContext compilationStartContext,
AbstractRemoveUnusedMembersDiagnosticAnalyzer<TDocumentationCommentTriviaSyntax, TIdentifierNameSyntax> analyzer)
{
var compilationAnalyzer = new CompilationAnalyzer(compilationStartContext.Compilation, analyzer);
compilationAnalyzer.RegisterActions(compilationStartContext);
}
private void RegisterActions(CompilationStartAnalysisContext compilationStartContext)
{
// We register following actions in the compilation:
// 1. A symbol action for member symbols to ensure the member's unused state is initialized to true for every private member symbol.
// 2. Operation actions for member references, invocations and object creations to detect member usages, i.e. read or read reference taken.
// 3. Operation action for field initializers to detect non-constant initialization.
// 4. Operation action for invalid operations to bail out on erroneous code.
// 5. A symbol start/end action for named types to report diagnostics for candidate members that have no usage in executable code.
//
// Note that we need to register separately for OperationKind.Invocation and OperationKind.ObjectCreation due to https://github.com/dotnet/roslyn/issues/26206
compilationStartContext.RegisterSymbolAction(AnalyzeSymbolDeclaration, SymbolKind.Method, SymbolKind.Field, SymbolKind.Property, SymbolKind.Event);
Action<ISymbol, ValueUsageInfo> onSymbolUsageFound = OnSymbolUsage;
compilationStartContext.RegisterSymbolStartAction(symbolStartContext =>
{
var hasUnsupportedOperation = false;
symbolStartContext.RegisterOperationAction(AnalyzeMemberReferenceOperation, OperationKind.FieldReference, OperationKind.MethodReference, OperationKind.PropertyReference, OperationKind.EventReference);
symbolStartContext.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer);
symbolStartContext.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation);
symbolStartContext.RegisterOperationAction(AnalyzeNameOfOperation, OperationKind.NameOf);
symbolStartContext.RegisterOperationAction(AnalyzeObjectCreationOperation, OperationKind.ObjectCreation);
// We bail out reporting diagnostics for named types if it contains following kind of operations:
// 1. Invalid operations, i.e. erroneous code:
// We do so to ensure that we don't report false positives during editing scenarios in the IDE, where the user
// is still editing code and fixing unresolved references to symbols, such as overload resolution errors.
// 2. Dynamic operations, where we do not know the exact member being referenced at compile time.
// 3. Operations with OperationKind.None.
symbolStartContext.RegisterOperationAction(_ => hasUnsupportedOperation = true, OperationKind.Invalid, OperationKind.None,
OperationKind.DynamicIndexerAccess, OperationKind.DynamicInvocation, OperationKind.DynamicMemberReference, OperationKind.DynamicObjectCreation);
symbolStartContext.RegisterSymbolEndAction(symbolEndContext => OnSymbolEnd(symbolEndContext, hasUnsupportedOperation));
// Register custom language-specific actions, if any.
_analyzer.HandleNamedTypeSymbolStart(symbolStartContext, onSymbolUsageFound);
}, SymbolKind.NamedType);
}
private void AnalyzeSymbolDeclaration(SymbolAnalysisContext symbolContext)
{
var symbol = symbolContext.Symbol.OriginalDefinition;
if (IsCandidateSymbol(symbol))
{
lock (_gate)
{
// Initialize unused state to 'ValueUsageInfo.None' to indicate that
// no read/write references have been encountered yet for this symbol.
// Note that we might receive a symbol reference (AnalyzeMemberOperation) callback before
// this symbol declaration callback, so even though we cannot receive duplicate callbacks for a symbol,
// an entry might already be present of the declared symbol here.
if (!_symbolValueUsageStateMap.ContainsKey(symbol))
{
_symbolValueUsageStateMap.Add(symbol, ValueUsageInfo.None);
}
}
}
}
private void AnalyzeFieldInitializer(OperationAnalysisContext operationContext)
{
// Check if the initialized fields are being initialized a non-constant value.
// If so, we want to consider these fields as being written to,
// so that we conservatively report an "Unread member" diagnostic instead of an "Unused member" diagnostic.
// This ensures that we do not offer a code fix for these fields that silently removes the initializer,
// as a non-constant initializer might have side-effects, which need to be preserved.
// On the other hand, initialization with a constant value can have no side-effects, and is safe to be removed.
var initializer = (IFieldInitializerOperation)operationContext.Operation;
if (!initializer.Value.ConstantValue.HasValue)
{
foreach (var field in initializer.InitializedFields)
{
OnSymbolUsage(field, ValueUsageInfo.Write);
}
}
}
private void OnSymbolUsage(ISymbol memberSymbol, ValueUsageInfo usageInfo)
{
if (!IsCandidateSymbol(memberSymbol))
{
return;
}
lock (_gate)
{
// Update the usage info for the memberSymbol
if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out var currentUsageInfo))
{
usageInfo = currentUsageInfo | usageInfo;
}
_symbolValueUsageStateMap[memberSymbol] = usageInfo;
}
}
private bool TryRemove(ISymbol memberSymbol, out ValueUsageInfo valueUsageInfo)
{
lock (_gate)
{
if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out valueUsageInfo))
{
_symbolValueUsageStateMap.Remove(memberSymbol);
return true;
}
return false;
}
}
private void AnalyzeMemberReferenceOperation(OperationAnalysisContext operationContext)
{
var memberReference = (IMemberReferenceOperation)operationContext.Operation;
var memberSymbol = memberReference.Member.OriginalDefinition;
if (IsCandidateSymbol(memberSymbol))
{
// Get the value usage info.
var valueUsageInfo = memberReference.GetValueUsageInfo(operationContext.ContainingSymbol);
if (valueUsageInfo == ValueUsageInfo.ReadWrite)
{
Debug.Assert(memberReference.Parent is ICompoundAssignmentOperation compoundAssignment &&
compoundAssignment.Target == memberReference ||
memberReference.Parent is ICoalesceAssignmentOperation coalesceAssignment &&
coalesceAssignment.Target == memberReference ||
memberReference.Parent is IIncrementOrDecrementOperation ||
memberReference.Parent is IReDimClauseOperation reDimClause && reDimClause.Operand == memberReference);
// Compound assignment or increment whose value is being dropped (parent is an expression statement)
// is treated as a Write as the value was never actually 'read' in a way that is observable.
//
// Consider the following example:
// class C
// {
// private int _f1 = 0, _f2 = 0;
// public void M1() { _f1++; }
// public int M2() { return _f2++; }
// }
//
// Note that the increment operation '_f1++' is child of an expression statement, which drops the result of the increment.
// while the increment operation '_f2++' is child of a return statement, which uses the result of the increment.
// For the above test, '_f1' can be safely removed without affecting the semantics of the program, while '_f2' cannot be removed.
if (memberReference.Parent.Parent is IExpressionStatementOperation)
{
valueUsageInfo = ValueUsageInfo.Write;
// If the symbol is a property, than mark it as having shadow 'get' accessor usages.
// Later we will produce message "Private member X can be removed as the value assigned to it is never read"
// rather than "Private property X can be converted to a method as its get accessor is never invoked" depending on this information.
if (memberSymbol is IPropertySymbol propertySymbol)
{
lock (_gate)
{
_propertiesWithShadowGetAccessorUsages.Add(propertySymbol);
}
}
}
}
OnSymbolUsage(memberSymbol, valueUsageInfo);
}
}
private void AnalyzeInvocationOperation(OperationAnalysisContext operationContext)
{
var targetMethod = ((IInvocationOperation)operationContext.Operation).TargetMethod.OriginalDefinition;
// A method invocation is considered as a read reference to the symbol
// to ensure that we consider the method as "used".
OnSymbolUsage(targetMethod, ValueUsageInfo.Read);
// If the invoked method is a reduced extension method, also mark the original
// method from which it was reduced as "used".
if (targetMethod.ReducedFrom != null)
{
OnSymbolUsage(targetMethod.ReducedFrom, ValueUsageInfo.Read);
}
}
private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext)
{
// 'nameof(argument)' is very commonly used for reading/writing to 'argument' in following ways:
// 1. Reflection based usage: See https://github.com/dotnet/roslyn/issues/32488
// 2. Custom/Test frameworks: See https://github.com/dotnet/roslyn/issues/32008 and https://github.com/dotnet/roslyn/issues/31581
// We treat 'nameof(argument)' as ValueUsageInfo.ReadWrite instead of ValueUsageInfo.NameOnly to avoid such false positives.
var nameofArgument = ((INameOfOperation)operationContext.Operation).Argument;
if (nameofArgument is IMemberReferenceOperation memberReference)
{
OnSymbolUsage(memberReference.Member.OriginalDefinition, ValueUsageInfo.ReadWrite);
return;
}
// Workaround for https://github.com/dotnet/roslyn/issues/19965
// IOperation API does not expose potential references to methods/properties within
// a bound method group/property group.
var symbolInfo = nameofArgument.SemanticModel.GetSymbolInfo(nameofArgument.Syntax, operationContext.CancellationToken);
foreach (var symbol in symbolInfo.GetAllSymbols())
{
switch (symbol.Kind)
{
// Handle potential references to methods/properties from missing IOperation
// for method group/property group.
case SymbolKind.Method:
case SymbolKind.Property:
OnSymbolUsage(symbol.OriginalDefinition, ValueUsageInfo.ReadWrite);
break;
}
}
}
private void AnalyzeObjectCreationOperation(OperationAnalysisContext operationContext)
{
var constructor = ((IObjectCreationOperation)operationContext.Operation).Constructor.OriginalDefinition;
// An object creation is considered as a read reference to the constructor
// to ensure that we consider the constructor as "used".
OnSymbolUsage(constructor, ValueUsageInfo.Read);
}
private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsupportedOperation)
{
if (hasUnsupportedOperation)
{
return;
}
if (symbolEndContext.Symbol.GetAttributes().Any(static (a, self) => a.AttributeClass == self._structLayoutAttributeType, this))
{
// Bail out for types with 'StructLayoutAttribute' as the ordering of the members is critical,
// and removal of unused members might break semantics.
return;
}
// Report diagnostics for unused candidate members.
var first = true;
PooledHashSet<ISymbol> symbolsReferencedInDocComments = null;
ArrayBuilder<string> debuggerDisplayAttributeArguments = null;
try
{
var entryPoint = symbolEndContext.Compilation.GetEntryPoint(symbolEndContext.CancellationToken);
var namedType = (INamedTypeSymbol)symbolEndContext.Symbol;
foreach (var member in namedType.GetMembers())
{
if (SymbolEqualityComparer.Default.Equals(entryPoint, member))
{
continue;
}
// Check if the underlying member is neither read nor a readable reference to the member is taken.
// If so, we flag the member as either unused (never written) or unread (written but not read).
if (TryRemove(member, out var valueUsageInfo) &&
!valueUsageInfo.IsReadFrom())
{
Debug.Assert(IsCandidateSymbol(member));
Debug.Assert(!member.IsImplicitlyDeclared);
if (first)
{
// Bail out if there are syntax errors in any of the declarations of the containing type.
// Note that we check this only for the first time that we report an unused or unread member for the containing type.
if (HasSyntaxErrors(namedType, symbolEndContext.CancellationToken))
{
return;
}
// Compute the set of candidate symbols referenced in all the documentation comments within the named type declarations.
// This set is computed once and used for all the iterations of the loop.
symbolsReferencedInDocComments = GetCandidateSymbolsReferencedInDocComments(namedType, symbolEndContext.Compilation, symbolEndContext.CancellationToken);
// Compute the set of string arguments to DebuggerDisplay attributes applied to any symbol within the named type declaration.
// These strings may have an embedded reference to the symbol.
// This set is computed once and used for all the iterations of the loop.
debuggerDisplayAttributeArguments = GetDebuggerDisplayAttributeArguments(namedType);
first = false;
}
// Simple heuristic for members referenced in DebuggerDisplayAttribute's string argument:
// bail out if any of the DebuggerDisplay string arguments contains the member name.
// In future, we can consider improving this heuristic to parse the embedded expression
// and resolve symbol references.
if (debuggerDisplayAttributeArguments.Any(arg => arg.Contains(member.Name)))
{
continue;
}
// Report IDE0051 or IDE0052 based on whether the underlying member has any Write/WritableRef/NonReadWriteRef references or not.
var rule = !valueUsageInfo.IsWrittenTo() && !valueUsageInfo.IsNameOnly() && !symbolsReferencedInDocComments.Contains(member)
? s_removeUnusedMembersRule
: s_removeUnreadMembersRule;
// Do not flag write-only properties that are not read.
// Write-only properties are assumed to have side effects
// visible through other means than a property getter.
if (rule == s_removeUnreadMembersRule &&
member is IPropertySymbol property &&
property.IsWriteOnly)
{
continue;
}
// Most of the members should have a single location, except for partial methods.
// We report the diagnostic on the first location of the member.
var diagnostic = DiagnosticHelper.CreateWithMessage(
rule,
member.Locations[0],
rule.GetEffectiveSeverity(symbolEndContext.Compilation.Options),
additionalLocations: null,
properties: null,
GetMessage(rule, member));
symbolEndContext.ReportDiagnostic(diagnostic);
}
}
}
finally
{
symbolsReferencedInDocComments?.Free();
debuggerDisplayAttributeArguments?.Free();
}
return;
}
private LocalizableString GetMessage(
DiagnosticDescriptor rule,
ISymbol member)
{
var messageFormat = rule.MessageFormat;
if (rule == s_removeUnreadMembersRule)
{
// IDE0052 has a different message for method and property symbols.
switch (member)
{
case IMethodSymbol _:
messageFormat = AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked;
break;
case IPropertySymbol property:
// We change the message only if both 'get' and 'set' accessors are present and
// there are no shadow 'get' accessor usages. Otherwise the message will be confusing
if (property.GetMethod != null && property.SetMethod != null &&
!_propertiesWithShadowGetAccessorUsages.Contains(property))
{
messageFormat = AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked;
}
break;
}
}
return new DiagnosticHelper.LocalizableStringWithArguments(
messageFormat, member.ToDisplayString(ContainingTypeAndNameOnlyFormat));
}
private static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken)
{
foreach (var tree in namedTypeSymbol.Locations.Select(l => l.SourceTree))
{
if (tree.GetDiagnostics(cancellationToken).Any(d => d.Severity == DiagnosticSeverity.Error))
{
return true;
}
}
return false;
}
private PooledHashSet<ISymbol> GetCandidateSymbolsReferencedInDocComments(INamedTypeSymbol namedTypeSymbol, Compilation compilation, CancellationToken cancellationToken)
{
var builder = PooledHashSet<ISymbol>.GetInstance();
foreach (var root in namedTypeSymbol.Locations.Select(l => l.SourceTree.GetRoot(cancellationToken)))
{
SemanticModel lazyModel = null;
foreach (var node in root.DescendantNodes(descendIntoTrivia: true)
.OfType<TDocumentationCommentTriviaSyntax>()
.SelectMany(n => n.DescendantNodes().OfType<TIdentifierNameSyntax>()))
{
lazyModel ??= compilation.GetSemanticModel(root.SyntaxTree);
var symbol = lazyModel.GetSymbolInfo(node, cancellationToken).Symbol?.OriginalDefinition;
if (symbol != null && IsCandidateSymbol(symbol))
{
builder.Add(symbol);
}
}
}
return builder;
}
private ArrayBuilder<string> GetDebuggerDisplayAttributeArguments(INamedTypeSymbol namedTypeSymbol)
{
var builder = ArrayBuilder<string>.GetInstance();
AddDebuggerDisplayAttributeArguments(namedTypeSymbol, builder);
return builder;
}
private void AddDebuggerDisplayAttributeArguments(INamedTypeSymbol namedTypeSymbol, ArrayBuilder<string> builder)
{
AddDebuggerDisplayAttributeArgumentsCore(namedTypeSymbol, builder);
foreach (var member in namedTypeSymbol.GetMembers())
{
switch (member)
{
case INamedTypeSymbol nestedType:
AddDebuggerDisplayAttributeArguments(nestedType, builder);
break;
case IPropertySymbol _:
case IFieldSymbol _:
AddDebuggerDisplayAttributeArgumentsCore(member, builder);
break;
}
}
}
private void AddDebuggerDisplayAttributeArgumentsCore(ISymbol symbol, ArrayBuilder<string> builder)
{
foreach (var attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass == _debuggerDisplayAttributeType &&
attribute.ConstructorArguments is [{ Kind: TypedConstantKind.Primitive, Type.SpecialType: SpecialType.System_String, Value: string value }])
{
builder.Add(value);
}
}
}
/// <summary>
/// Returns true if the given symbol meets the following criteria to be
/// a candidate for dead code analysis:
/// 1. It is marked as "private".
/// 2. It is not an implicitly declared symbol.
/// 3. It is either a method, field, property or an event.
/// 4. If method, then it is a constructor OR a method with <see cref="MethodKind.Ordinary"/>,
/// such that is meets a few criteria (see implementation details below).
/// 5. If field, then it must not be a backing field for an auto property.
/// Backing fields have a non-null <see cref="IFieldSymbol.AssociatedSymbol"/>.
/// 6. If property, then it must not be an explicit interface property implementation.
/// 7. If event, then it must not be an explicit interface event implementation.
/// </summary>
private bool IsCandidateSymbol(ISymbol memberSymbol)
{
Debug.Assert(memberSymbol == memberSymbol.OriginalDefinition);
if (memberSymbol.DeclaredAccessibility == Accessibility.Private &&
!memberSymbol.IsImplicitlyDeclared)
{
switch (memberSymbol.Kind)
{
case SymbolKind.Method:
var methodSymbol = (IMethodSymbol)memberSymbol;
switch (methodSymbol.MethodKind)
{
case MethodKind.Constructor:
// It is fine to have an unused private constructor
// without parameters.
// This is commonly used for static holder types
// that want to block instantiation of the type.
if (methodSymbol.Parameters.Length == 0)
{
return false;
}
// ISerializable constructor is invoked by the runtime for deserialization
// and it is a common pattern to have a private serialization constructor
// that is not explicitly referenced in code.
if (_deserializationConstructorCheck.IsDeserializationConstructor(methodSymbol))
{
return false;
}
return true;
case MethodKind.Ordinary:
// Do not track accessors, as we will track/flag the associated symbol.
if (methodSymbol.AssociatedSymbol != null)
{
return false;
}
// Do not flag unused entry point (Main) method.
if (methodSymbol.IsEntryPoint(_taskType, _genericTaskType))
{
return false;
}
// It is fine to have unused virtual/abstract/overrides/extern
// methods as they might be used in another type in the containing
// type's type hierarchy.
if (methodSymbol.IsAbstract ||
methodSymbol.IsVirtual ||
methodSymbol.IsOverride ||
methodSymbol.IsExtern)
{
return false;
}
// Explicit interface implementations are not referenced explicitly,
// but are still used.
if (!methodSymbol.ExplicitInterfaceImplementations.IsEmpty)
{
return false;
}
// Ignore methods with special attributes that indicate special/reflection
// based access.
if (IsMethodWithSpecialAttribute(methodSymbol))
{
return false;
}
// ShouldSerializeXXX and ResetXXX are ok if there is a matching
// property XXX as they are used by the windows designer property grid
if (IsShouldSerializeOrResetPropertyMethod(methodSymbol))
{
return false;
}
// Ignore methods with event handler signature
// as lot of ASP.NET types have many special event handlers
// that are invoked with reflection (e.g. Application_XXX, Page_XXX,
// OnTransactionXXX, etc).
if (methodSymbol.HasEventHandlerSignature(_eventArgsType))
{
return false;
}
return true;
default:
return false;
}
case SymbolKind.Field:
return ((IFieldSymbol)memberSymbol).AssociatedSymbol == null;
case SymbolKind.Property:
return ((IPropertySymbol)memberSymbol).ExplicitInterfaceImplementations.IsEmpty;
case SymbolKind.Event:
return ((IEventSymbol)memberSymbol).ExplicitInterfaceImplementations.IsEmpty;
}
}
return false;
}
private bool IsMethodWithSpecialAttribute(IMethodSymbol methodSymbol)
=> methodSymbol.GetAttributes().Any(static (a, self) => self._attributeSetForMethodsToIgnore.Contains(a.AttributeClass), this);
private static bool IsShouldSerializeOrResetPropertyMethod(IMethodSymbol methodSymbol)
{
// "bool ShouldSerializeXXX()" and "void ResetXXX()" are ok if there is a matching
// property XXX as they are used by the windows designer property grid
// Note that we do a case sensitive compare for compatibility with legacy FxCop
// implementation of this rule.
return methodSymbol.Parameters.IsEmpty &&
(IsSpecialMethodWithMatchingProperty("ShouldSerialize") && methodSymbol.ReturnType.SpecialType == SpecialType.System_Boolean ||
IsSpecialMethodWithMatchingProperty("Reset") && methodSymbol.ReturnsVoid);
// Local functions.
bool IsSpecialMethodWithMatchingProperty(string prefix)
{
if (methodSymbol.Name.StartsWith(prefix))
{
var suffix = methodSymbol.Name[prefix.Length..];
return suffix.Length > 0 &&
methodSymbol.ContainingType.GetMembers(suffix).Any(static m => m is IPropertySymbol);
}
return false;
}
}
}
}
}
|