File: Microsoft.NetCore.Analyzers\Runtime\DoNotPassLiteralsAsLocalizedParameters.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.ValueContentAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.PointsToAnalysis;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA1303: Do not pass literals as localized parameters
    /// A method passes a string literal as a parameter to a constructor or method in the .NET Framework class library and that string should be localizable.
    /// This warning is raised when a literal string is passed as a value to a parameter or property and one or more of the following cases is true:
    ///   1. The LocalizableAttribute attribute of the parameter or property is set to true.
    ///   2. The parameter or property name contains "Text", "Message", or "Caption".
    ///   3. The name of the string parameter that is passed to a Console.Write or Console.WriteLine method is either "value" or "format".
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class DoNotPassLiteralsAsLocalizedParameters : AbstractGlobalizationDiagnosticAnalyzer
    {
        internal const string RuleId = "CA1303";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(DoNotPassLiteralsAsLocalizedParametersTitle)),
            CreateLocalizableResourceString(nameof(DoNotPassLiteralsAsLocalizedParametersMessage)),
            DiagnosticCategory.Globalization,
            RuleLevel.Disabled,
            description: CreateLocalizableResourceString(nameof(DoNotPassLiteralsAsLocalizedParametersDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: true);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        protected override void InitializeWorker(CompilationStartAnalysisContext context)
        {
            INamedTypeSymbol? localizableStateAttributeSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemComponentModelLocalizableAttribute);
            INamedTypeSymbol? conditionalAttributeSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsConditionalAttribute);
            INamedTypeSymbol? systemConsoleSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemConsole);
            ImmutableHashSet<INamedTypeSymbol> typesToIgnore = GetTypesToIgnore(context.Compilation);

            context.RegisterOperationBlockStartAction(operationBlockStartContext =>
            {
                if (operationBlockStartContext.OwningSymbol is not IMethodSymbol containingMethod ||
                    operationBlockStartContext.Options.IsConfiguredToSkipAnalysis(Rule, containingMethod, operationBlockStartContext.Compilation))
                {
                    return;
                }

                Lazy<DataFlowAnalysisResult<ValueContentBlockAnalysisResult, ValueContentAbstractValue>?> lazyValueContentResult = new Lazy<DataFlowAnalysisResult<ValueContentBlockAnalysisResult, ValueContentAbstractValue>?>(
                    valueFactory: ComputeValueContentAnalysisResult, isThreadSafe: false);

                operationBlockStartContext.RegisterOperationAction(operationContext =>
                {
                    var argument = (IArgumentOperation)operationContext.Operation;
                    if (argument.Parameter == null)
                        return;

                    IMethodSymbol? targetMethod = null;
                    switch (argument.Parent)
                    {
                        case IInvocationOperation invocation:
                            targetMethod = invocation.TargetMethod;
                            break;

                        case IObjectCreationOperation objectCreation:
                            targetMethod = objectCreation.Constructor;
                            break;
                    }

                    if (ShouldAnalyze(targetMethod))
                    {
                        AnalyzeArgument(argument.Parameter, containingPropertySymbol: null, operation: argument, reportDiagnostic: operationContext.ReportDiagnostic, GetUseNamingHeuristicOption(operationContext));
                    }
                }, OperationKind.Argument);

                operationBlockStartContext.RegisterOperationAction(operationContext =>
                {
                    var propertyReference = (IPropertyReferenceOperation)operationContext.Operation;
                    if (propertyReference.Parent is IAssignmentOperation assignment &&
                        assignment.Target == propertyReference &&
                        !propertyReference.Property.IsIndexer &&
                        propertyReference.Property.SetMethod?.Parameters.Length == 1 &&
                        ShouldAnalyze(propertyReference.Property))
                    {
                        IParameterSymbol valueSetterParam = propertyReference.Property.SetMethod.Parameters[0];
                        AnalyzeArgument(valueSetterParam, propertyReference.Property, assignment, operationContext.ReportDiagnostic, GetUseNamingHeuristicOption(operationContext));
                    }
                }, OperationKind.PropertyReference);

                return;

                // Local functions
                bool ShouldAnalyze(ISymbol? symbol)
                    => symbol != null && !operationBlockStartContext.Options.IsConfiguredToSkipAnalysis(Rule, symbol, operationBlockStartContext.OwningSymbol, operationBlockStartContext.Compilation);

                static bool GetUseNamingHeuristicOption(OperationAnalysisContext operationContext)
                    => operationContext.Options.GetBoolOptionValue(EditorConfigOptionNames.UseNamingHeuristic, Rule,
                        operationContext.Operation.Syntax.SyntaxTree, operationContext.Compilation, defaultValue: false);

                void AnalyzeArgument(IParameterSymbol parameter, IPropertySymbol? containingPropertySymbol, IOperation operation, Action<Diagnostic> reportDiagnostic, bool useNamingHeuristic)
                {
                    if (ShouldBeLocalized(parameter.OriginalDefinition, containingPropertySymbol?.OriginalDefinition, localizableStateAttributeSymbol, conditionalAttributeSymbol, systemConsoleSymbol, typesToIgnore, useNamingHeuristic) &&
                        lazyValueContentResult.Value != null)
                    {
                        ValueContentAbstractValue stringContentValue = lazyValueContentResult.Value[operation.Kind, operation.Syntax];
                        if (stringContentValue.IsLiteralState)
                        {
                            Debug.Assert(!stringContentValue.LiteralValues.IsEmpty);

                            if (stringContentValue.LiteralValues.Any(l => l is not string))
                            {
                                return;
                            }

                            var stringLiteralValues = stringContentValue.LiteralValues.Cast<string?>();

                            // FxCop compat: Do not fire if the literal value came from a default parameter value
                            if (stringContentValue.LiteralValues.Count == 1 &&
                                parameter.IsOptional &&
                                parameter.ExplicitDefaultValue is string defaultValue &&
                                defaultValue == stringLiteralValues.Single())
                            {
                                return;
                            }

                            // FxCop compat: Do not fire if none of the string literals have any non-control character.
                            if (!LiteralValuesHaveNonControlCharacters(stringLiteralValues))
                            {
                                return;
                            }

                            // FxCop compat: Filter out xml string literals.
                            IEnumerable<string> filteredStrings = stringLiteralValues.Where(literal => literal != null && !LooksLikeXmlTag(literal))!;
                            if (filteredStrings.Any())
                            {
                                // Method '{0}' passes a literal string as parameter '{1}' of a call to '{2}'. Retrieve the following string(s) from a resource table instead: "{3}".
                                var arg1 = containingMethod.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
                                var arg2 = parameter.Name;
                                var arg3 = parameter.ContainingSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
                                var arg4 = FormatLiteralValues(filteredStrings);
                                var diagnostic = operation.CreateDiagnostic(Rule, arg1, arg2, arg3, arg4);
                                reportDiagnostic(diagnostic);
                            }
                        }
                    }
                }

                DataFlowAnalysisResult<ValueContentBlockAnalysisResult, ValueContentAbstractValue>? ComputeValueContentAnalysisResult()
                {
                    var cfg = operationBlockStartContext.OperationBlocks.GetControlFlowGraph();
                    if (cfg != null)
                    {
                        var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(operationBlockStartContext.Compilation);
                        return ValueContentAnalysis.TryGetOrComputeResult(cfg, containingMethod, wellKnownTypeProvider,
                            operationBlockStartContext.Options, Rule, PointsToAnalysisKind.PartialWithoutTrackingFieldsAndProperties);
                    }

                    return null;
                }
            });
        }

        private static ImmutableHashSet<INamedTypeSymbol> GetTypesToIgnore(Compilation compilation)
        {
            var builder = PooledHashSet<INamedTypeSymbol>.GetInstance();

            var xmlWriter = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemXmlXmlWriter);
            if (xmlWriter != null)
            {
                builder.Add(xmlWriter);
            }

            var webUILiteralControl = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemWebUILiteralControl);
            if (webUILiteralControl != null)
            {
                builder.Add(webUILiteralControl);
            }

            var unitTestingAssert = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingAssert);
            if (unitTestingAssert != null)
            {
                builder.Add(unitTestingAssert);
            }

            var unitTestingCollectionAssert = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCollectionAssert);
            if (unitTestingCollectionAssert != null)
            {
                builder.Add(unitTestingCollectionAssert);
            }

            var unitTestingCollectionStringAssert = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingStringAssert);
            if (unitTestingCollectionStringAssert != null)
            {
                builder.Add(unitTestingCollectionStringAssert);
            }

            return builder.ToImmutableAndFree();
        }

        private static bool ShouldBeLocalized(
            IParameterSymbol parameterSymbol,
            IPropertySymbol? containingPropertySymbol,
            INamedTypeSymbol? localizableStateAttributeSymbol,
            INamedTypeSymbol? conditionalAttributeSymbol,
            INamedTypeSymbol? systemConsoleSymbol,
            ImmutableHashSet<INamedTypeSymbol> typesToIgnore,
            bool useNamingHeuristic)
        {
            Debug.Assert(parameterSymbol.ContainingSymbol.Kind == SymbolKind.Method);

            if (parameterSymbol.Type.SpecialType != SpecialType.System_String)
            {
                return false;
            }

            // Verify LocalizableAttributeState
            if (localizableStateAttributeSymbol != null)
            {
                LocalizableAttributeState localizableAttributeState = GetLocalizableAttributeState(parameterSymbol, localizableStateAttributeSymbol);
                switch (localizableAttributeState)
                {
                    case LocalizableAttributeState.False:
                        return false;

                    case LocalizableAttributeState.True:
                        return true;

                    default:
                        break;
                }
            }

            // FxCop compat checks.
            if (typesToIgnore.Contains(parameterSymbol.ContainingType) ||
                parameterSymbol.ContainingSymbol.HasAnyAttribute(conditionalAttributeSymbol))
            {
                return false;
            }

            // FxCop compat: For overrides, check for localizability of the corresponding parameter in the overridden method.
            var method = (IMethodSymbol)parameterSymbol.ContainingSymbol;
            if (method.IsOverride &&
                method.OverriddenMethod?.Parameters.Length == method.Parameters.Length)
            {
                int parameterIndex = method.GetParameterIndex(parameterSymbol);
                IParameterSymbol overridenParameter = method.OverriddenMethod.Parameters[parameterIndex];
                if (Equals(overridenParameter.Type, parameterSymbol.Type))
                {
                    return ShouldBeLocalized(overridenParameter, containingPropertySymbol, localizableStateAttributeSymbol, conditionalAttributeSymbol, systemConsoleSymbol, typesToIgnore, useNamingHeuristic);
                }
            }

            if (useNamingHeuristic)
            {
                if (IsLocalizableByNameHeuristic(parameterSymbol) ||
                    containingPropertySymbol != null && IsLocalizableByNameHeuristic(containingPropertySymbol))
                {
                    return true;
                }
            }

            if (method.ContainingType.Equals(systemConsoleSymbol) &&
                (method.Name.Equals("Write", StringComparison.Ordinal) ||
                 method.Name.Equals("WriteLine", StringComparison.Ordinal)) &&
                (parameterSymbol.Name.Equals("format", StringComparison.OrdinalIgnoreCase) ||
                 parameterSymbol.Name.Equals("value", StringComparison.OrdinalIgnoreCase)))
            {
                return true;
            }

            return false;

            // FxCop compat: If a localizable attribute isn't defined then fall back to name heuristics.
            static bool IsLocalizableByNameHeuristic(ISymbol symbol) =>
                symbol.Name.Equals("message", StringComparison.OrdinalIgnoreCase) ||
                symbol.Name.Equals("text", StringComparison.OrdinalIgnoreCase) ||
                symbol.Name.Equals("caption", StringComparison.OrdinalIgnoreCase);
        }

        private static LocalizableAttributeState GetLocalizableAttributeState(ISymbol symbol, INamedTypeSymbol localizableAttributeTypeSymbol)
        {
            if (symbol == null)
            {
                return LocalizableAttributeState.Undefined;
            }

            LocalizableAttributeState localizedState = GetLocalizableAttributeStateCore(symbol.GetAttributes(), localizableAttributeTypeSymbol);
            if (localizedState != LocalizableAttributeState.Undefined)
            {
                return localizedState;
            }

            ISymbol containingSymbol = symbol is IMethodSymbol { AssociatedSymbol: IPropertySymbol propertySymbol } ? propertySymbol : symbol.ContainingSymbol;
            return GetLocalizableAttributeState(containingSymbol, localizableAttributeTypeSymbol);
        }

        private static LocalizableAttributeState GetLocalizableAttributeStateCore(ImmutableArray<AttributeData> attributeList, INamedTypeSymbol localizableAttributeTypeSymbol)
        {
            var localizableAttribute = attributeList.FirstOrDefault(attr => localizableAttributeTypeSymbol.Equals(attr.AttributeClass));
            if (localizableAttribute != null &&
                localizableAttribute.AttributeConstructor?.Parameters.Length == 1 &&
                localizableAttribute.AttributeConstructor.Parameters[0].Type.SpecialType == SpecialType.System_Boolean &&
                localizableAttribute.ConstructorArguments.Length == 1 &&
                localizableAttribute.ConstructorArguments[0].Kind == TypedConstantKind.Primitive &&
                localizableAttribute.ConstructorArguments[0].Value is bool isLocalizable)
            {
                return isLocalizable ? LocalizableAttributeState.True : LocalizableAttributeState.False;
            }

            return LocalizableAttributeState.Undefined;
        }

        private static string FormatLiteralValues(IEnumerable<string> literalValues)
        {
            var literals = new StringBuilder();
            foreach (string literal in literalValues.Order())
            {
                // sanitize the literal to ensure it's not multi-line
                // replace any newline characters with a space
                var sanitizedLiteral = literal.Replace((char)13, ' ');
                sanitizedLiteral = sanitizedLiteral.Replace((char)10, ' ');

                if (literals.Length > 0)
                {
                    literals.Append(", ");
                }

                literals.Append(sanitizedLiteral);
            }

            return literals.ToString();
        }

        /// <summary>
        /// Returns true if the given string looks like an XML/HTML tag
        /// </summary>
        private static bool LooksLikeXmlTag(string literal)
        {
            // Call the trim function to remove any spaces around the beginning and end of the string so we can more accurately detect
            // XML strings
            string trimmedLiteral = literal.Trim();
            return trimmedLiteral.Length > 2 && trimmedLiteral[0] == '<' && trimmedLiteral[^1] == '>';
        }

        /// <summary>
        /// Returns true if any character in literalValues is not a control character
        /// </summary>
        private static bool LiteralValuesHaveNonControlCharacters(IEnumerable<string?> literalValues)
        {
            foreach (string? literal in literalValues)
            {
                if (literal == null)
                {
                    continue;
                }

                foreach (char ch in literal)
                {
                    if (!char.IsControl(ch))
                    {
                        return true;
                    }
                }
            }

            return false;
        }
    }
}