// 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.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.NetCore.Analyzers.Runtime { using static MicrosoftNetCoreAnalyzersResources; /// <summary> /// CA2241: <inheritdoc cref="ProvideCorrectArgumentsToFormattingMethodsTitle"/> /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public class ProvideCorrectArgumentsToFormattingMethodsAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA2241"; internal static readonly DiagnosticDescriptor ArgumentCountRule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(ProvideCorrectArgumentsToFormattingMethodsTitle)), CreateLocalizableResourceString(nameof(ProvideCorrectArgumentsToFormattingMethodsMessage)), DiagnosticCategory.Usage, RuleLevel.BuildWarningCandidate, description: CreateLocalizableResourceString(nameof(ProvideCorrectArgumentsToFormattingMethodsDescription)), isPortedFxCopRule: true, isDataflowRule: false); internal static readonly DiagnosticDescriptor InvalidFormatRule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(ProvideCorrectArgumentsToFormattingMethodsTitle)), CreateLocalizableResourceString(nameof(ProvideCorrectArgumentsToFormattingMethodsInvalidFormatMessage)), DiagnosticCategory.Usage, RuleLevel.BuildWarningCandidate, description: CreateLocalizableResourceString(nameof(ProvideCorrectArgumentsToFormattingMethodsDescription)), isPortedFxCopRule: true, isDataflowRule: false); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(ArgumentCountRule, InvalidFormatRule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterCompilationStartAction(compilationContext => { var formatInfo = new StringFormatInfo(compilationContext.Compilation); compilationContext.RegisterOperationAction(operationContext => { var invocation = (IInvocationOperation)operationContext.Operation; StringFormatInfo.Info? info = formatInfo.TryGet(invocation.TargetMethod, operationContext); if (info == null || invocation.Arguments.Length <= info.FormatStringIndex) { // not a target method return; } IArgumentOperation formatStringArgument = invocation.Arguments[info.FormatStringIndex]; if (!Equals(formatStringArgument.Value.Type, formatInfo.String) || !(formatStringArgument.Value.ConstantValue.Value is string stringFormat)) { // wrong argument or not a constant return; } int expectedStringFormatArgumentCount = GetFormattingArguments(stringFormat); if (expectedStringFormatArgumentCount < 0) { // Malformed format specification operationContext.ReportDiagnostic(operationContext.Operation.Syntax.CreateDiagnostic(InvalidFormatRule)); return; } // explicit parameter case if (info.ExpectedStringFormatArgumentCount >= 0) { // __arglist is not supported here if (invocation.TargetMethod.IsVararg) { // can't deal with this for now. return; } if (info.ExpectedStringFormatArgumentCount != expectedStringFormatArgumentCount) { operationContext.ReportDiagnostic(operationContext.Operation.Syntax.CreateDiagnostic(ArgumentCountRule)); } return; } else if (info.ExpectedStringFormatArgumentCount == -2) { // Not a formatting method, we only checked the format specification. return; } // ensure argument is an array IArgumentOperation paramsArgument = invocation.Arguments[info.FormatStringIndex + 1]; if (paramsArgument.ArgumentKind is not ArgumentKind.ParamArray and not ArgumentKind.Explicit) { // wrong format return; } if (paramsArgument.Value is not IArrayCreationOperation arrayCreation || arrayCreation.GetElementType() is not ITypeSymbol elementType || !Equals(elementType, formatInfo.Object) || arrayCreation.DimensionSizes.Length != 1) { // wrong format return; } // compiler generating object array for params case if (arrayCreation.Initializer is not { } initializer) { // unsupported format return; } // REVIEW: "ElementValues" is a bit confusing where I need to double dot those to get number of elements int actualArgumentCount = initializer.ElementValues.Length; if (actualArgumentCount != expectedStringFormatArgumentCount) { operationContext.ReportDiagnostic(operationContext.Operation.Syntax.CreateDiagnostic(ArgumentCountRule)); } }, OperationKind.Invocation); }); } private static int GetFormattingArguments(string format) { // code is from mscorlib // https://github.com/dotnet/coreclr/blob/bc146608854d1db9cdbcc0b08029a87754e12b49/src/mscorlib/src/System/Text/StringBuilder.cs#L1312 // return count of this format - {index[,alignment][:formatString]} var pos = 0; int len = format.Length; var uniqueNumbers = new HashSet<int>(); // main loop while (true) { // loop to find starting "{" char ch; while (pos < len) { ch = format[pos]; pos++; if (ch == '}') { if (pos < len && format[pos] == '}') // Treat as escape character for }} { pos++; } else { return -1; } } if (ch == '{') { if (pos < len && format[pos] == '{') // Treat as escape character for {{ { pos++; } else { pos--; break; } } } // finished with "{" if (pos == len) { break; } pos++; if (pos == len || (ch = format[pos]) < '0' || ch > '9') { // finished with "{x" return -1; } // searching for index var index = 0; do { index = index * 10 + ch - '0'; pos++; if (pos == len) { // wrong index format return -1; } ch = format[pos]; } while (ch >= '0' && ch <= '9' && index < 1000000); // eat up whitespace while (pos < len && (ch = format[pos]) == ' ') { pos++; } // searching for alignment var width = 0; if (ch == ',') { pos++; // eat up whitespace while (pos < len && format[pos] == ' ') { pos++; } if (pos == len) { // wrong format, reached end without "}" return -1; } ch = format[pos]; if (ch == '-') { pos++; if (pos == len) { // wrong format. reached end without "}" return -1; } ch = format[pos]; } if (ch is < '0' or > '9') { // wrong format after "-" return -1; } do { width = width * 10 + ch - '0'; pos++; if (pos == len) { // wrong width format return -1; } ch = format[pos]; } while (ch >= '0' && ch <= '9' && width < 1000000); } // eat up whitespace while (pos < len && (ch = format[pos]) == ' ') { pos++; } // searching for embedded format string if (ch == ':') { pos++; while (true) { if (pos == len) { // reached end without "}" return -1; } ch = format[pos]; pos++; if (ch == '{') { if (pos < len && format[pos] == '{') // Treat as escape character for {{ pos++; else return -1; } else if (ch == '}') { if (pos < len && format[pos] == '}') // Treat as escape character for }} { pos++; } else { pos--; break; } } } } if (ch != '}') { // "}" is expected return -1; } pos++; uniqueNumbers.Add(index); } // end of main loop return uniqueNumbers.Count == 0 ? 0 : uniqueNumbers.Max() + 1; } private class StringFormatInfo { private const string Format = "format"; private const string CompositeFormat = "CompositeFormat"; private readonly ConcurrentDictionary<IMethodSymbol, Info?> _map; public StringFormatInfo(Compilation compilation) { _map = new ConcurrentDictionary<IMethodSymbol, Info?>(); INamedTypeSymbol? console = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemConsole); AddStringFormatMap(console, "Write"); AddStringFormatMap(console, "WriteLine"); INamedTypeSymbol @string = compilation.GetSpecialType(SpecialType.System_String); AddStringFormatMap(@string, "Format"); String = @string; Object = compilation.GetSpecialType(SpecialType.System_Object); StringSyntaxAttributes = compilation.GetTypesByMetadataName(WellKnownTypeNames.SystemDiagnosticsCodeAnalysisStringSyntaxAttributeName); } public INamedTypeSymbol String { get; } public INamedTypeSymbol Object { get; } public ImmutableArray<INamedTypeSymbol> StringSyntaxAttributes { get; } public Info? TryGet(IMethodSymbol method, OperationAnalysisContext context) { if (_map.TryGetValue(method, out Info? info)) { return info; } if (!StringSyntaxAttributes.IsEmpty && TryGetFormatInfoByCompositeFormatStringSyntaxAttribute(method, out info)) { return info; } // Check if this the underlying method is user configured string formatting method. var additionalStringFormatMethodsOption = context.Options.GetAdditionalStringFormattingMethodsOption(ArgumentCountRule, context.Operation.Syntax.SyntaxTree, context.Compilation); if (additionalStringFormatMethodsOption.Contains(method.OriginalDefinition) && TryGetFormatInfoByParameterName(method, out info)) { return info; } // Check if the user configured automatic determination of formatting methods. // If so, check if the method called has a 'string format' parameter followed by an params array. var determineAdditionalStringFormattingMethodsAutomatically = context.Options.GetBoolOptionValue(EditorConfigOptionNames.TryDetermineAdditionalStringFormattingMethodsAutomatically, ArgumentCountRule, context.Operation.Syntax.SyntaxTree, context.Compilation, defaultValue: false); if (determineAdditionalStringFormattingMethodsAutomatically && TryGetFormatInfoByParameterName(method, out info)) { if (info.ExpectedStringFormatArgumentCount == -1) { return info; } else { // TryGetFormatInfoByParameterName always adds info to _map // We only support heuristically found format method that use 'params'. _map.TryRemove(method, out _); } } _map.TryAdd(method, null); return null; } private void AddStringFormatMap(INamedTypeSymbol? type, string methodName) { if (type == null) { return; } foreach (IMethodSymbol method in type.GetMembers(methodName).OfType<IMethodSymbol>()) { TryGetFormatInfoByParameterName(method, out var _); } } private bool TryGetFormatInfoByCompositeFormatStringSyntaxAttribute(IMethodSymbol method, [NotNullWhen(returnValue: true)] out Info? formatInfo) { int formatIndex = FindParameterIndexOfCompositeFormatStringSyntaxAttribute(method.Parameters); return TryGetFormatInfo(method, formatIndex, out formatInfo); } private bool TryGetFormatInfoByParameterName(IMethodSymbol method, [NotNullWhen(returnValue: true)] out Info? formatInfo) { int formatIndex = FindParameterIndexByParameterName(method.Parameters, Format); return TryGetFormatInfo(method, formatIndex, out formatInfo); } private bool TryGetFormatInfo(IMethodSymbol method, int formatIndex, [NotNullWhen(returnValue: true)] out Info? formatInfo) { formatInfo = default; if (formatIndex < 0) { // no valid format string return false; } if (method.Parameters[formatIndex].Type.SpecialType != SpecialType.System_String) { // no valid format string return false; } int expectedArguments = GetExpectedNumberOfArguments(method.Parameters, formatIndex); formatInfo = new Info(formatIndex, expectedArguments); _map.TryAdd(method, formatInfo); return true; } private static int GetExpectedNumberOfArguments(ImmutableArray<IParameterSymbol> parameters, int formatIndex) { if (formatIndex == parameters.Length - 1) { // format specification is the last parameter (e.g. CompositeFormat.Parse) // this is therefore not a formatting method. return -2; } // check params IParameterSymbol nextParameter = parameters[formatIndex + 1]; if (nextParameter.IsParams) { return -1; } return parameters.Length - formatIndex - 1; } private static int FindParameterIndexByParameterName(ImmutableArray<IParameterSymbol> parameters, string name) { for (var i = 0; i < parameters.Length; i++) { if (string.Equals(parameters[i].Name, name, StringComparison.Ordinal)) { return i; } } return -1; } private int FindParameterIndexOfCompositeFormatStringSyntaxAttribute(ImmutableArray<IParameterSymbol> parameters) { for (var i = 0; i < parameters.Length; i++) { if (String.Equals(parameters[i].Type, SymbolEqualityComparer.Default)) { foreach (AttributeData attribute in parameters[i].GetAttributes()) { if (StringSyntaxAttributes.Contains(attribute.AttributeClass, SymbolEqualityComparer.Default)) { ImmutableArray<TypedConstant> arguments = attribute.ConstructorArguments; if (arguments.Length == 1 && CompositeFormat.Equals(arguments[0].Value)) { return i; } } } } } return -1; } public class Info { public Info(int formatIndex, int expectedArguments) { FormatStringIndex = formatIndex; ExpectedStringFormatArgumentCount = expectedArguments; } public int FormatStringIndex { get; } public int ExpectedStringFormatArgumentCount { get; } } } } }