|
// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues
{
internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
private sealed partial class SymbolStartAnalyzer
{
private readonly AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer _compilationAnalyzer;
private readonly INamedTypeSymbol _eventArgsTypeOpt;
private readonly ImmutableHashSet<INamedTypeSymbol> _attributeSetForMethodsToIgnore;
private readonly DeserializationConstructorCheck _deserializationConstructorCheck;
private readonly ConcurrentDictionary<IMethodSymbol, bool> _methodsUsedAsDelegates;
private readonly INamedTypeSymbol _iCustomMarshaler;
/// <summary>
/// Map from unused parameters to a boolean value indicating if the parameter has a read reference or not.
/// For example, a parameter whose initial value is overwritten before any reads
/// is an unused parameter with read reference(s).
/// </summary>
private readonly ConcurrentDictionary<IParameterSymbol, bool> _unusedParameters;
public SymbolStartAnalyzer(
AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer compilationAnalyzer,
INamedTypeSymbol eventArgsTypeOpt,
ImmutableHashSet<INamedTypeSymbol> attributeSetForMethodsToIgnore,
DeserializationConstructorCheck deserializationConstructorCheck,
INamedTypeSymbol iCustomMarshaler)
{
_compilationAnalyzer = compilationAnalyzer;
_eventArgsTypeOpt = eventArgsTypeOpt;
_attributeSetForMethodsToIgnore = attributeSetForMethodsToIgnore;
_deserializationConstructorCheck = deserializationConstructorCheck;
_unusedParameters = new ConcurrentDictionary<IParameterSymbol, bool>();
_methodsUsedAsDelegates = new ConcurrentDictionary<IMethodSymbol, bool>();
_iCustomMarshaler = iCustomMarshaler;
}
public static void CreateAndRegisterActions(
CompilationStartAnalysisContext context,
AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer analyzer)
{
var attributeSetForMethodsToIgnore = ImmutableHashSet.CreateRange(GetAttributesForMethodsToIgnore(context.Compilation).WhereNotNull());
var eventsArgType = context.Compilation.EventArgsType();
var deserializationConstructorCheck = new DeserializationConstructorCheck(context.Compilation);
var iCustomMarshaler = context.Compilation.GetTypeByMetadataName(typeof(ICustomMarshaler).FullName!);
context.RegisterSymbolStartAction(symbolStartContext =>
{
if (HasSyntaxErrors((INamedTypeSymbol)symbolStartContext.Symbol, symbolStartContext.CancellationToken))
{
// Bail out on syntax errors.
return;
}
// Create a new SymbolStartAnalyzer instance for every named type symbol
// to ensure there is no shared state (such as identified unused parameters within the type),
// as that would lead to duplicate diagnostics being reported from symbol end action callbacks
// for unrelated named types.
var symbolAnalyzer = new SymbolStartAnalyzer(analyzer, eventsArgType, attributeSetForMethodsToIgnore, deserializationConstructorCheck, iCustomMarshaler);
symbolAnalyzer.OnSymbolStart(symbolStartContext);
}, SymbolKind.NamedType);
return;
// Local functions
static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken)
{
foreach (var syntaxRef in namedTypeSymbol.DeclaringSyntaxReferences)
{
var syntax = syntaxRef.GetSyntax(cancellationToken);
if (syntax.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
{
return true;
}
}
return false;
}
}
private void OnSymbolStart(SymbolStartAnalysisContext context)
{
context.RegisterOperationBlockStartAction(OnOperationBlock);
context.RegisterSymbolEndAction(OnSymbolEnd);
}
private void OnOperationBlock(OperationBlockStartAnalysisContext context)
{
context.RegisterOperationAction(OnMethodReference, OperationKind.MethodReference);
BlockAnalyzer.Analyze(context, this);
}
private void OnMethodReference(OperationAnalysisContext context)
{
var methodBinding = (IMethodReferenceOperation)context.Operation;
_methodsUsedAsDelegates.GetOrAdd(methodBinding.Method.OriginalDefinition, true);
}
private void OnSymbolEnd(SymbolAnalysisContext context)
{
foreach (var (parameter, hasReference) in _unusedParameters)
{
ReportUnusedParameterDiagnostic(parameter, hasReference, context.ReportDiagnostic, context.Options, context.CancellationToken);
}
}
private void ReportUnusedParameterDiagnostic(
IParameterSymbol parameter,
bool hasReference,
Action<Diagnostic> reportDiagnostic,
AnalyzerOptions analyzerOptions,
CancellationToken cancellationToken)
{
if (!IsUnusedParameterCandidate(parameter, cancellationToken))
{
return;
}
var location = parameter.Locations[0];
var option = analyzerOptions.GetAnalyzerOptions(location.SourceTree).UnusedParameters;
if (option.Notification.Severity == ReportDiagnostic.Suppress ||
!ShouldReportUnusedParameters(parameter.ContainingSymbol, option.Value, option.Notification.Severity))
{
return;
}
var message = GetMessageForUnusedParameterDiagnostic(
parameter.Name,
hasReference,
isPublicApiParameter: parameter.ContainingSymbol.HasPublicResultantVisibility(),
isLocalFunctionParameter: parameter.ContainingSymbol.IsLocalFunction());
var diagnostic = DiagnosticHelper.CreateWithMessage(s_unusedParameterRule, location,
option.Notification.Severity, additionalLocations: null, properties: null, message);
reportDiagnostic(diagnostic);
}
private static LocalizableString GetMessageForUnusedParameterDiagnostic(
string parameterName,
bool hasReference,
bool isPublicApiParameter,
bool isLocalFunctionParameter)
{
LocalizableString messageFormat;
if (isPublicApiParameter &&
!isLocalFunctionParameter)
{
messageFormat = hasReference
? AnalyzersResources.Parameter_0_can_be_removed_if_it_is_not_part_of_a_shipped_public_API_its_initial_value_is_never_used
: AnalyzersResources.Remove_unused_parameter_0_if_it_is_not_part_of_a_shipped_public_API;
}
else if (hasReference)
{
messageFormat = AnalyzersResources.Parameter_0_can_be_removed_its_initial_value_is_never_used;
}
else
{
messageFormat = s_unusedParameterRule.MessageFormat;
}
return new DiagnosticHelper.LocalizableStringWithArguments(messageFormat, parameterName);
}
private static IEnumerable<INamedTypeSymbol> GetAttributesForMethodsToIgnore(Compilation compilation)
{
// Ignore conditional methods (One conditional will often call another conditional method as its only use of a parameter)
yield return compilation.ConditionalAttribute();
// Ignore methods with special serialization attributes (All serialization methods need to take 'StreamingContext')
yield return compilation.OnDeserializingAttribute();
yield return compilation.OnDeserializedAttribute();
yield return compilation.OnSerializingAttribute();
yield return compilation.OnSerializedAttribute();
// Don't flag obsolete methods.
yield return compilation.ObsoleteAttribute();
// Don't flag MEF import constructors with ImportingConstructor attribute.
yield return compilation.SystemCompositionImportingConstructorAttribute();
yield return compilation.SystemComponentModelCompositionImportingConstructorAttribute();
}
private bool IsUnusedParameterCandidate(IParameterSymbol parameter, CancellationToken cancellationToken)
{
// Ignore certain special parameters/methods.
// Note that "method.ExplicitOrImplicitInterfaceImplementations" check below is not a complete check,
// as identifying this correctly requires analyzing referencing projects, which is not
// supported for analyzers. We believe this is still a good enough check for most cases so
// we don't have to bail out on reporting unused parameters for all public methods.
if (parameter.IsImplicitlyDeclared ||
parameter.Name == DiscardVariableName ||
parameter.ContainingSymbol is not IMethodSymbol method ||
method.IsImplicitlyDeclared ||
method.IsExtern ||
method.IsAbstract ||
method.IsVirtual ||
method.IsOverride ||
method.PartialImplementationPart != null ||
method.PartialDefinitionPart != null ||
!method.ExplicitOrImplicitInterfaceImplementations().IsEmpty ||
method.IsAccessor() ||
method.IsAnonymousFunction() ||
_compilationAnalyzer.MethodHasHandlesClause(method) ||
_deserializationConstructorCheck.IsDeserializationConstructor(method))
{
return false;
}
// Ignore parameters of type primary constructors since they map to public properties
if (parameter.IsPrimaryConstructor(cancellationToken))
{
return false;
}
// Ignore event handler methods "Handler(object, MyEventArgs)"
// as event handlers are required to match this signature
// regardless of whether or not the parameters are used.
if (_eventArgsTypeOpt != null &&
method.Parameters is [{ Type.SpecialType: SpecialType.System_Object }, var secondParam] &&
secondParam.Type.InheritsFromOrEquals(_eventArgsTypeOpt))
{
return false;
}
// Ignore flagging parameters for methods with certain well-known attributes,
// which are known to have unused parameters in real world code.
if (method.GetAttributes().Any(static (a, self) => self._attributeSetForMethodsToIgnore.Contains(a.AttributeClass), this))
{
return false;
}
// Methods used as delegates likely need to have unused parameters for signature compat.
if (_methodsUsedAsDelegates.ContainsKey(method))
{
return false;
}
// Ignore special parameter names for methods that need a specific signature.
// For example, methods used as a delegate in a different type or project.
// This also serves as a convenient way to suppress instances of unused parameter diagnostic
// without disabling the diagnostic completely.
// We ignore parameter names that start with an underscore and are optionally followed by an integer,
// such as '_', '_1', '_2', etc.
if (parameter.IsSymbolWithSpecialDiscardName())
{
return false;
}
var methodSyntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken);
if (_compilationAnalyzer.ReturnsThrow(methodSyntax))
{
return false;
}
// Don't report on valid GetInstance method of ICustomMarshaler.
// See https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.icustommarshaler#implementing-the-getinstance-method
if (method is { MetadataName: "GetInstance", IsStatic: true, Parameters.Length: 1, ContainingType: { } containingType } methodSymbol &&
methodSymbol.Parameters[0].Type.SpecialType == SpecialType.System_String &&
containingType.AllInterfaces.Any((@interface, marshaler) => @interface.Equals(marshaler), _iCustomMarshaler))
{
return false;
}
return true;
}
}
}
}
|