// 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.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues
{
    // Map from different combinations of diagnostic properties to a properties map that gets added to each diagnostic instance.
    using PropertiesMap = ImmutableDictionary<(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment),
                                              ImmutableDictionary<string, string>>;

    /// <summary>
    /// Analyzer to report unused expression values and parameters:
    /// It flags the following cases:
    ///     1. Expression statements that drop computed value, for example, "Computation();".
    ///        These should either be removed (redundant computation) or should be replaced
    ///        with explicit assignment to discard variable OR an unused local variable,
    ///        i.e. "_ = Computation();" or "var unused = Computation();"
    ///        This diagnostic configuration is controlled by language specific code style option "UnusedValueExpressionStatement".
    ///     2. Value assignments to locals/parameters that are never used on any control flow path,
    ///        For example, value assigned to 'x' in first statement below is unused and will be flagged:
    ///             x = Computation();
    ///             if (...)
    ///                 x = Computation2();
    ///             else
    ///                 Computation3(out x);
    ///             ... = x;
    ///        Just as for case 1., these should either be removed (redundant computation) or
    ///        should be replaced with explicit assignment to discard variable OR an unused local variable,
    ///        i.e. "_ = Computation();" or "var unused = Computation();"
    ///        This diagnostic configuration is controlled by language specific code style option "UnusedValueAssignment".
    ///     3. Redundant parameters that fall into one of the following two categories:
    ///         a. Have no references in the executable code block(s) for its containing method symbol.
    ///         b. Have one or more references but its initial value at start of code block is never used.
    ///            For example, if 'x' in the example for case 2. above was a parameter symbol with RefKind.None
    ///            and "x = Computation();" is the first statement in the method body, then its initial value
    ///            is never used. Such a parameter should be removed and 'x' should be converted into a local.
    ///        We provide additional information in the diagnostic message to clarify the above two categories
    ///        and also detect and mention about potential breaking change if the containing method is a public API.
    ///        Currently, we do not provide any code fix for removing unused parameters as it needs fixing the
    ///        call sites and any automated fix can lead to subtle overload resolution differences,
    ///        though this may change in future.
    ///        This diagnostic configuration is controlled by <see cref="CodeStyleOptions2.UnusedParameters"/> option.
    /// </summary>
    internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
    {
        public const string DiscardVariableName = "_";

        private const string UnusedValuePreferenceKey = nameof(UnusedValuePreferenceKey);
        private const string IsUnusedLocalAssignmentKey = nameof(IsUnusedLocalAssignmentKey);
        private const string IsRemovableAssignmentKey = nameof(IsRemovableAssignmentKey);

        // Diagnostic reported for expression statements that drop computed value, for example, "Computation();".
        // This is **not** an unnecessary (fading) diagnostic as the expression being flagged is not unnecessary, but the dropped value is.
        private static readonly DiagnosticDescriptor s_expressionValueIsUnusedRule = CreateDescriptorWithId(
            IDEDiagnosticIds.ExpressionValueIsUnusedDiagnosticId,
            EnforceOnBuildValues.ExpressionValueIsUnused,
            new LocalizableResourceString(nameof(AnalyzersResources.Expression_value_is_never_used), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            new LocalizableResourceString(nameof(AnalyzersResources.Expression_value_is_never_used), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            isUnnecessary: false);

        // Diagnostic reported for value assignments to locals/parameters that are never used on any control flow path.
        private static readonly DiagnosticDescriptor s_valueAssignedIsUnusedRule = CreateDescriptorWithId(
            IDEDiagnosticIds.ValueAssignedIsUnusedDiagnosticId,
            EnforceOnBuildValues.ValueAssignedIsUnused,
            new LocalizableResourceString(nameof(AnalyzersResources.Unnecessary_assignment_of_a_value), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            new LocalizableResourceString(nameof(AnalyzersResources.Unnecessary_assignment_of_a_value_to_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            description: new LocalizableResourceString(nameof(AnalyzersResources.Avoid_unnecessary_value_assignments_in_your_code_as_these_likely_indicate_redundant_value_computations_If_the_value_computation_is_not_redundant_and_you_intend_to_retain_the_assignmentcomma_then_change_the_assignment_target_to_a_local_variable_whose_name_starts_with_an_underscore_and_is_optionally_followed_by_an_integercomma_such_as___comma__1_comma__2_comma_etc), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            isUnnecessary: true);

        // Diagnostic reported for unnecessary parameters that can be removed.
        private static readonly DiagnosticDescriptor s_unusedParameterRule = CreateDescriptorWithId(
            IDEDiagnosticIds.UnusedParameterDiagnosticId,
            EnforceOnBuildValues.UnusedParameter,
            new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_parameter), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            new LocalizableResourceString(nameof(AnalyzersResources.Remove_unused_parameter_0), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            description: new LocalizableResourceString(nameof(AnalyzersResources.Avoid_unused_parameters_in_your_code_If_the_parameter_cannot_be_removed_then_change_its_name_so_it_starts_with_an_underscore_and_is_optionally_followed_by_an_integer_such_as__comma__1_comma__2_etc_These_are_treated_as_special_discard_symbol_names), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
            isUnnecessary: true);

        private static readonly PropertiesMap s_propertiesMap = CreatePropertiesMap();

        protected AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer(
            Option2<CodeStyleOption2<UnusedValuePreference>> unusedValueExpressionStatementOption,
            Option2<CodeStyleOption2<UnusedValuePreference>> unusedValueAssignmentOption)
            : base(ImmutableDictionary<DiagnosticDescriptor, IOption2>.Empty
                        .Add(s_expressionValueIsUnusedRule, unusedValueExpressionStatementOption)
                        .Add(s_valueAssignedIsUnusedRule, unusedValueAssignmentOption)
                        .Add(s_unusedParameterRule, CodeStyleOptions2.UnusedParameters),
                   fadingOption: null)
        {
        }

        protected abstract ISyntaxFacts SyntaxFacts { get; }
        protected abstract Location GetDefinitionLocationToFade(IOperation unusedDefinition);
        protected abstract bool SupportsDiscard(SyntaxTree tree);
        protected abstract bool MethodHasHandlesClause(IMethodSymbol method);
        protected abstract bool IsIfConditionalDirective(SyntaxNode node);
        protected abstract bool ReturnsThrow(SyntaxNode node);
        protected abstract CodeStyleOption2<UnusedValuePreference> GetUnusedValueExpressionStatementOption(AnalyzerOptionsProvider provider);
        protected abstract CodeStyleOption2<UnusedValuePreference> GetUnusedValueAssignmentOption(AnalyzerOptionsProvider provider);

        /// <summary>
        /// Indicates if we should bail from removable assignment analysis for the given
        /// symbol write operation.
        /// Removable assignment analysis determines if the assigned value for the symbol write
        /// has no side effects and can be removed without changing the semantics.
        /// </summary>
        protected virtual bool ShouldBailOutFromRemovableAssignmentAnalysis(IOperation unusedSymbolWriteOperation)
            => false;

        /// <summary>
        /// Indicates if the given expression statement operation has an explicit "Call" statement syntax indicating explicit discard.
        /// For example, VB "Call" statement.
        /// </summary>
        /// <returns></returns>
        protected abstract bool IsCallStatement(IExpressionStatementOperation expressionStatement);

        /// <summary>
        /// Indicates if the given operation is an expression of an expression body.
        /// </summary>
        protected abstract bool IsExpressionOfExpressionBody(IExpressionStatementOperation expressionStatement);

        /// <summary>
        /// Method to compute well-known diagnostic property maps for different combinations of diagnostic properties.
        /// The property map is added to each instance of the reported diagnostic and is used by the code fixer to
        /// compute the correct code fix.
        /// It currently maps to three different properties of the diagnostic:
        ///     1. The underlying <see cref="UnusedValuePreference"/> for the reported diagnostic
        ///     2. "isUnusedLocalAssignment": Flag indicating if the flagged local variable has no reads/uses.
        ///     3. "isRemovableAssignment": Flag indicating if the assigned value is from an expression that has no side effects
        ///             and hence can be removed completely. For example, if the assigned value is a constant or a reference
        ///             to a local/parameter, then it has no side effects, but if it is method invocation, it may have side effects.
        /// </summary>
        /// <returns></returns>
        private static PropertiesMap CreatePropertiesMap()
        {
            var builder = ImmutableDictionary.CreateBuilder<(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment),
                                                            ImmutableDictionary<string, string>>();
            AddEntries(UnusedValuePreference.DiscardVariable);
            AddEntries(UnusedValuePreference.UnusedLocalVariable);
            return builder.ToImmutable();

            void AddEntries(UnusedValuePreference preference)
            {
                AddEntries2(preference, isUnusedLocalAssignment: true);
                AddEntries2(preference, isUnusedLocalAssignment: false);
            }

            void AddEntries2(UnusedValuePreference preference, bool isUnusedLocalAssignment)
            {
                AddEntryCore(preference, isUnusedLocalAssignment, isRemovableAssignment: true);
                AddEntryCore(preference, isUnusedLocalAssignment, isRemovableAssignment: false);
            }

            void AddEntryCore(UnusedValuePreference preference, bool isUnusedLocalAssignment, bool isRemovableAssignment)
            {
                var propertiesBuilder = ImmutableDictionary.CreateBuilder<string, string>();

                propertiesBuilder.Add(UnusedValuePreferenceKey, preference.ToString());
                if (isUnusedLocalAssignment)
                {
                    propertiesBuilder.Add(IsUnusedLocalAssignmentKey, string.Empty);
                }

                if (isRemovableAssignment)
                {
                    propertiesBuilder.Add(IsRemovableAssignmentKey, string.Empty);
                }

                builder.Add((preference, isUnusedLocalAssignment, isRemovableAssignment), propertiesBuilder.ToImmutable());
            }
        }

        // Our analysis is limited to unused expressions in a code block, hence is unaffected by changes outside the code block.
        // Hence, we can support incremental span based method body analysis.
        public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

        protected sealed override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze;

        protected sealed override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterCompilationStartAction(
                compilationContext => SymbolStartAnalyzer.CreateAndRegisterActions(compilationContext, this));
        }

        private bool TryGetOptions(SyntaxTree syntaxTree, AnalyzerOptions analyzerOptions, out Options options)
        {
            options = null;

            var optionsProvider = analyzerOptions.GetAnalyzerOptions(syntaxTree);

            var unusedParametersOption = optionsProvider.UnusedParameters;
            var (unusedValueExpressionStatementPreference, unusedValueExpressionStatementSeverity) = GetPreferenceAndSeverity(GetUnusedValueExpressionStatementOption(optionsProvider));
            var (unusedValueAssignmentPreference, unusedValueAssignmentSeverity) = GetPreferenceAndSeverity(GetUnusedValueAssignmentOption(optionsProvider));

            if (unusedParametersOption.Notification.Severity == ReportDiagnostic.Suppress &&
                unusedValueExpressionStatementSeverity == ReportDiagnostic.Suppress &&
                unusedValueAssignmentSeverity == ReportDiagnostic.Suppress)
            {
                return false;
            }

            options = new Options(unusedValueExpressionStatementPreference, unusedValueExpressionStatementSeverity,
                                  unusedValueAssignmentPreference, unusedValueAssignmentSeverity,
                                  unusedParametersOption.Value, unusedParametersOption.Notification.Severity);
            return true;

            // Local functions.
            (UnusedValuePreference preference, ReportDiagnostic severity) GetPreferenceAndSeverity(CodeStyleOption2<UnusedValuePreference> option)
            {
                var preferenceOpt = option?.Value;
                if (preferenceOpt == null ||
                    option.Notification.Severity == ReportDiagnostic.Suppress)
                {
                    // Prefer does not matter as the severity is suppressed - we will never report this diagnostic.
                    return (default(UnusedValuePreference), ReportDiagnostic.Suppress);
                }

                // If language or language version does not support discard, fall back to prefer unused local variable.
                if (preferenceOpt.Value == UnusedValuePreference.DiscardVariable &&
                    !SupportsDiscard(syntaxTree))
                {
                    preferenceOpt = UnusedValuePreference.UnusedLocalVariable;
                }

                return (preferenceOpt.Value, option.Notification.Severity);
            }
        }

        private sealed class Options
        {
            private readonly UnusedParametersPreference _unusedParametersPreference;
            private readonly ReportDiagnostic _unusedParametersSeverity;

            public Options(
                UnusedValuePreference unusedValueExpressionStatementPreference,
                ReportDiagnostic unusedValueExpressionStatementSeverity,
                UnusedValuePreference unusedValueAssignmentPreference,
                ReportDiagnostic unusedValueAssignmentSeverity,
                UnusedParametersPreference unusedParametersPreference,
                ReportDiagnostic unusedParametersSeverity)
            {
                Debug.Assert(unusedValueExpressionStatementSeverity != ReportDiagnostic.Suppress ||
                             unusedValueAssignmentSeverity != ReportDiagnostic.Suppress ||
                             unusedParametersSeverity != ReportDiagnostic.Suppress);

                UnusedValueExpressionStatementPreference = unusedValueExpressionStatementPreference;
                UnusedValueExpressionStatementSeverity = unusedValueExpressionStatementSeverity;
                UnusedValueAssignmentPreference = unusedValueAssignmentPreference;
                UnusedValueAssignmentSeverity = unusedValueAssignmentSeverity;
                _unusedParametersPreference = unusedParametersPreference;
                _unusedParametersSeverity = unusedParametersSeverity;
            }

            public UnusedValuePreference UnusedValueExpressionStatementPreference { get; }
            public ReportDiagnostic UnusedValueExpressionStatementSeverity { get; }
            public UnusedValuePreference UnusedValueAssignmentPreference { get; }
            public ReportDiagnostic UnusedValueAssignmentSeverity { get; }
            public bool IsComputingUnusedParams(ISymbol symbol)
                => ShouldReportUnusedParameters(symbol, _unusedParametersPreference, _unusedParametersSeverity);
        }

        public static bool ShouldReportUnusedParameters(
            ISymbol symbol,
            UnusedParametersPreference unusedParametersPreference,
            ReportDiagnostic unusedParametersSeverity)
        {
            if (unusedParametersSeverity == ReportDiagnostic.Suppress)
            {
                return false;
            }

            if (unusedParametersPreference == UnusedParametersPreference.NonPublicMethods)
            {
                return !symbol.HasPublicResultantVisibility();
            }

            return true;
        }

        public static bool TryGetUnusedValuePreference(Diagnostic diagnostic, out UnusedValuePreference preference)
        {
            if (diagnostic.Properties != null &&
                diagnostic.Properties.TryGetValue(UnusedValuePreferenceKey, out var preferenceString))
            {
                switch (preferenceString)
                {
                    case nameof(UnusedValuePreference.DiscardVariable):
                        preference = UnusedValuePreference.DiscardVariable;
                        return true;

                    case nameof(UnusedValuePreference.UnusedLocalVariable):
                        preference = UnusedValuePreference.UnusedLocalVariable;
                        return true;
                }
            }

            preference = default;
            return false;
        }

        public static bool GetIsUnusedLocalDiagnostic(Diagnostic diagnostic)
        {
            Debug.Assert(TryGetUnusedValuePreference(diagnostic, out _));
            return diagnostic.Properties.ContainsKey(IsUnusedLocalAssignmentKey);
        }

        public static bool GetIsRemovableAssignmentDiagnostic(Diagnostic diagnostic)
        {
            Debug.Assert(TryGetUnusedValuePreference(diagnostic, out _));
            return diagnostic.Properties.ContainsKey(IsRemovableAssignmentKey);
        }
    }
}
