File: src\Analyzers\Core\Analyzers\RemoveUnusedParametersAndValues\AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
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(
        AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer compilationAnalyzer,
        INamedTypeSymbol? eventArgsType,
        ImmutableHashSet<INamedTypeSymbol> attributeSetForMethodsToIgnore,
        DeserializationConstructorCheck deserializationConstructorCheck,
        INamedTypeSymbol? iCustomMarshaler,
        SymbolStartAnalysisContext symbolStartAnalysisContext)
    {
        private readonly AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer _compilationAnalyzer = compilationAnalyzer;
 
        private readonly INamedTypeSymbol? _eventArgsType = eventArgsType;
        private readonly ImmutableHashSet<INamedTypeSymbol> _attributeSetForMethodsToIgnore = attributeSetForMethodsToIgnore;
        private readonly DeserializationConstructorCheck _deserializationConstructorCheck = deserializationConstructorCheck;
        private readonly ConcurrentDictionary<IMethodSymbol, bool> _methodsUsedAsDelegates = [];
        private readonly INamedTypeSymbol? _iCustomMarshaler = iCustomMarshaler;
        private readonly SymbolStartAnalysisContext _symbolStartAnalysisContext = symbolStartAnalysisContext;
 
        /// <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 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, symbolStartContext);
                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)
        {
            var interpolatedStringHandlerAttribute = context.Compilation.InterpolatedStringHandlerAttributeType();
 
            foreach (var (parameter, hasReference) in _unusedParameters)
            {
                ReportUnusedParameterDiagnostic(parameter, hasReference, context.ReportDiagnostic, context.Options, interpolatedStringHandlerAttribute, context.CancellationToken);
            }
        }
 
        private void ReportUnusedParameterDiagnostic(
            IParameterSymbol parameter,
            bool hasReference,
            Action<Diagnostic> reportDiagnostic,
            AnalyzerOptions analyzerOptions,
            INamedTypeSymbol? interpolatedStringHandlerAttributeType,
            CancellationToken cancellationToken)
        {
            if (!IsUnusedParameterCandidate(parameter, interpolatedStringHandlerAttributeType, 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, analyzerOptions, 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, INamedTypeSymbol? interpolatedStringHandlerAttributeType, 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 (_eventArgsType != null &&
                method.Parameters is [{ Type.SpecialType: SpecialType.System_Object }, var secondParam] &&
                secondParam.Type.InheritsFromOrEquals(_eventArgsType))
            {
                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) => a.AttributeClass is { } attributeClass && self._attributeSetForMethodsToIgnore.Contains(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;
            }
 
            if (method.ContainingType is { } containingType)
            {
                // 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: [{ Type.SpecialType: SpecialType.System_String }] } &&
                    containingType.AllInterfaces.Any((@interface, marshaler) => @interface.Equals(marshaler), _iCustomMarshaler))
                {
                    return false;
                }
 
                // 2 first `int` parameters of an interpolated string handler
                // constructor are mandatory. Therefore, do not report them as unused
                if (IsInterpolatedStringHandlerMandatoryConstructorParameter(parameter, method, containingType, interpolatedStringHandlerAttributeType))
                {
                    return false;
                }
            }
 
            return true;
 
            static bool IsInterpolatedStringHandlerMandatoryConstructorParameter(
                IParameterSymbol parameter,
                IMethodSymbol method,
                ITypeSymbol containingType,
                INamedTypeSymbol? interpolatedStringHandlerAttributeType)
            {
                if (parameter.Type.SpecialType != SpecialType.System_Int32)
                    return false;
 
                if (method.MethodKind != MethodKind.Constructor)
                    return false;
 
                if (!(method.Parameters is [var firstParameter, ..] && firstParameter == parameter) &&
                    !(method.Parameters is [_, var secondParameter, ..] && secondParameter == parameter))
                {
                    return false;
                }
 
                if (!containingType.HasAttribute(interpolatedStringHandlerAttributeType))
                    return false;
 
                return true;
            }
        }
    }
}