// 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;
}
}
}