Exemplo n.º 1
0
        private static Task <Solution> ReOrderPlaceholdersAsync(Document document, SyntaxNode root,
                                                                InvocationExpressionSyntax stringFormatInvocation)
        {
            var firstArgumentIsLiteral =
                stringFormatInvocation.ArgumentList.Arguments[0].Expression is LiteralExpressionSyntax;
            var formatString =
                ((LiteralExpressionSyntax)
                 stringFormatInvocation.ArgumentList.Arguments[firstArgumentIsLiteral ? 0 : 1].Expression).GetText()
                .ToString();
            var elements = PlaceholderHelpers.GetPlaceholdersSplit(formatString);
            var matches  = PlaceholderHelpers.GetPlaceholders(formatString);

            // Here we will store a key-value pair of the old placeholder value and the new value that we associate with it
            var placeholderMapping = new Dictionary <int, int>();

            // This contains the order in which the placeholders appeared in the original format string.
            // For example if it had the string "{1} x {0} y {2} {2}" then this collection would contain the values 1-0-2.
            // You'll notice that we omitted the duplicate: we don't want to add an argument twice.
            // Typically we'd do this by using a HashSet<T> but since we can't easily retrieve an item from the HashSet<T>,
            // we'll just check for duplicates upon inserting in the list.
            // Based on this we can then reconstruct the argument list by reordering the existing arguments.
            var placeholderIndexOrder = new List <int>();

            var amountOfPlaceholders = 0;
            var newPlaceholderValue  = 0;

            var sb = new StringBuilder(elements.Length);

            for (var index = 0; index < elements.Length; index++)
            {
                // If it's a numerical value, it means we're dealing with a placeholder
                // Use GetPlaceholderIndex() to account for formatted placeholders
                if (int.TryParse(PlaceholderHelpers.GetPlaceholderIndex(elements[index]), out var placeholderValue))
                {
                    // If we already have a new value associated with this placeholder, retrieve it and add it to our result
                    if (placeholderMapping.ContainsKey(placeholderValue))
                    {
                        sb.Append(GetNewElement(matches, amountOfPlaceholders, placeholderMapping[placeholderValue]));
                    }
                    else // Otherwise use the new placeholder value and store the mapping
                    {
                        sb.Append(GetNewElement(matches, amountOfPlaceholders, newPlaceholderValue));
                        placeholderMapping.Add(placeholderValue, newPlaceholderValue);
                        newPlaceholderValue++;
                    }

                    if (!placeholderIndexOrder.Contains(placeholderValue))
                    {
                        placeholderIndexOrder.Add(placeholderValue);
                    }

                    amountOfPlaceholders++;
                }
                else
                {
                    sb.Append(elements[index]);
                }
            }
            var newFormat = sb.ToString();

            // Create a new argument for the formatting string
            var newArgument =
                stringFormatInvocation.ArgumentList.Arguments[firstArgumentIsLiteral ? 0 : 1].WithExpression(
                    SyntaxFactory.ParseExpression(newFormat));

            // Create a new list for the arguments which are injected in the formatting string
            // In order to do this we iterate over the mapping which is in essence a guideline that tells us which index
            IEnumerable <ArgumentSyntax> args = firstArgumentIsLiteral
                ? new[] { newArgument }
                : new[] { stringFormatInvocation.ArgumentList.Arguments[0], newArgument };

            // Skip the formatting literal and, if applicable, the formatprovider
            var argumentsToSkip = firstArgumentIsLiteral ? 1 : 2;

            for (var index = 0; index < placeholderIndexOrder.Count; index++)
            {
                args =
                    args.Concat(new[]
                                { stringFormatInvocation.ArgumentList.Arguments[placeholderIndexOrder[index] + argumentsToSkip] });
            }

            // If there are less arguments in the new list compared to the old one, it means there was an unused argument
            // In that case we will loop over all the old arguments, see if they're contained in the new list and if not: add them
            // Since the variables weren't used in the first place, we can add them in whatever order we want
            // However because we are traversing from the front, they will be added in the same order as they were anyway
            if (stringFormatInvocation.ArgumentList.Arguments.Count != args.Count())
            {
                foreach (var arg in stringFormatInvocation.ArgumentList.Arguments.Skip(argumentsToSkip))
                {
                    if (!args.Contains(arg))
                    {
                        args = args.Concat(new[] { arg });
                    }
                }
            }

            var newArguments  = stringFormatInvocation.ArgumentList.WithArguments(SyntaxFactory.SeparatedList(args));
            var newInvocation = stringFormatInvocation.WithArgumentList(newArguments);
            var newRoot       = root.ReplaceNode(stringFormatInvocation, newInvocation);
            var newDocument   = document.WithSyntaxRoot(newRoot);

            return(Task.FromResult(newDocument.Project.Solution));
        }
Exemplo n.º 2
0
        private void AnalyzeNode(SyntaxNodeAnalysisContext context)
        {
            var invocation = (InvocationExpressionSyntax)context.Node;

            if (invocation.ArgumentList == null)
            {
                return;
            }

            // Get the format string
            // This corresponds to the argument passed to the parameter with name 'format'
            var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;

            if (methodSymbol == null)
            {
                return;
            }

            // Verify we're dealing with a call to a method that accepts a variable named 'format' and a object, params object[] or a plain object[]
            // params object[] and object[] can both be verified by looking for the latter
            // This allows us to support similar calls like Console.WriteLine("{0}", "test") as well which carry an implicit string.Format
            IParameterSymbol formatParam = null;

            foreach (var parameter in methodSymbol.Parameters)
            {
                if (parameter.Name == "format")
                {
                    formatParam = parameter;
                    break;
                }
            }

            if (formatParam == null)
            {
                return;
            }

            var formatIndex      = formatParam.Ordinal;
            var formatParameters = new List <IParameterSymbol>();

            for (var i = formatIndex + 1; i < methodSymbol.Parameters.Length; i++)
            {
                formatParameters.Add(methodSymbol.Parameters[i]);
            }

            // If the method definition doesn't contain any parameter to pass format arguments, we ignore it
            if (!formatParameters.Any())
            {
                return;
            }

            var symbolsAreNotArraysOrObjects = true;

            foreach (var symbol in formatParameters)
            {
                if (symbol.Type.Kind != SymbolKind.ArrayType || ((IArrayTypeSymbol)symbol.Type).ElementType.SpecialType != SpecialType.System_Object)
                {
                    symbolsAreNotArraysOrObjects = false;
                    break;
                }
            }
            var hasObjectArray = formatParameters.Count == 1 && symbolsAreNotArraysOrObjects;

            var hasObject = true;

            foreach (var symbol in formatParameters)
            {
                if (symbol.Type.SpecialType != SpecialType.System_Object)
                {
                    hasObject = false;
                    break;
                }
            }

            if (!(hasObject || hasObjectArray))
            {
                return;
            }

            // In case less arguments are passed in than the format definition, we escape
            // This can occur when dealing with optional arguments
            // Definition: string MyMethod(string format = null, object[] args = null) { }
            // Invocation: MyMethod();
            // Result: ArgumentOutOfRangeException when trying to access the format argument based on the format parameter's index
            if (invocation.ArgumentList.Arguments.Count <= formatIndex)
            {
                return;
            }

            var formatExpression = invocation.ArgumentList.Arguments[formatIndex].Expression;
            var formatString     = context.SemanticModel.GetConstantValue(formatExpression);

            if (!formatString.HasValue)
            {
                return;
            }

            // Get the total amount of arguments passed in for the format
            // If the first one is the literal (aka: the format specified) then every other argument is an argument to the format
            // If not, it means the first one is the CultureInfo, the second is the format and all others are format arguments
            // We also have to check whether or not the arguments are passed in through an explicit array or whether they use the params syntax
            var formatArguments = new List <ArgumentSyntax>();

            for (var i = formatIndex + 1; i < invocation.ArgumentList.Arguments.Count; i++)
            {
                formatArguments.Add(invocation.ArgumentList.Arguments[i]);
            }

            var amountOfFormatArguments = formatArguments.Count;

            if (amountOfFormatArguments == 1)
            {
                var argumentType = context.SemanticModel.GetTypeInfo(formatArguments[0].Expression);
                if (argumentType.Type == null)
                {
                    return;
                }

                // Inline array creation à la string.Format("{0}", new object[] { "test" })
                if (argumentType.Type.TypeKind == TypeKind.Array)
                {
                    // We check for an invocation first to account for the scenario where you have both an invocation and an array initializer
                    // Think about something like this: string.Format(""{0}{1}{2}"", new[] { 1 }.Concat(new[] {2}).ToArray());
                    var methodInvocation = formatArguments[0].DescendantNodes().OfType <InvocationExpressionSyntax>(SyntaxKind.InvocationExpression).FirstOrDefault();
                    if (methodInvocation != null)
                    {
                        // We don't handle method calls that return an array in the case of a single argument
                        return;
                    }

                    InitializerExpressionSyntax inlineArrayCreation = null;
                    foreach (var argument in formatArguments[0].DescendantNodes())
                    {
                        if (argument is InitializerExpressionSyntax)
                        {
                            inlineArrayCreation = (InitializerExpressionSyntax)argument;
                        }
                    }

                    if (inlineArrayCreation != null)
                    {
                        amountOfFormatArguments = inlineArrayCreation.Expressions.Count;
                        goto placeholderVerification;
                    }

                    // If we got here it means the arguments are passed in through an identifier which resolves to an array
                    // aka: referencing a variable/field that is of type T[]
                    // We cannot reliably get the amount of arguments if it's a method
                    // We could get them when it's a field/variable/property but that takes some more work and thinking about it
                    // This is tracked in workitem https://github.com/Vannevelj/VSDiagnostics/issues/330
                    if (hasObjectArray)
                    {
                        return;
                    }
                }
            }

placeholderVerification:
            // Get the placeholders we use stripped off their format specifier, get the highest value
            // and verify that this value + 1 (to account for 0-based indexing) is not greater than the amount of placeholder arguments
            var placeholders = new List <int>();

            foreach (Match placeholder in PlaceholderHelpers.GetPlaceholders((string)formatString.Value))
            {
                placeholders.Add(int.Parse(PlaceholderHelpers.GetPlaceholderIndex(placeholder.Value)));
            }

            if (!placeholders.Any())
            {
                return;
            }

            var highestPlaceholder = placeholders.Max();

            if (highestPlaceholder + 1 > amountOfFormatArguments)
            {
                context.ReportDiagnostic(Diagnostic.Create(Rule, formatExpression.GetLocation()));
            }
        }
        private void AnalyzeNode(SyntaxNodeAnalysisContext context)
        {
            var invocation = context.Node as InvocationExpressionSyntax;

            if (invocation == null)
            {
                return;
            }

            // Verify we're dealing with a string.Format() call
            if (!invocation.IsAnInvocationOf(typeof(string), nameof(string.Format), context.SemanticModel))
            {
                return;
            }

            // Verify the format is a literal expression and not a method invocation or an identifier
            // The overloads are in the form string.Format(string, object[]) or string.Format(CultureInfo, string, object[])
            if (invocation.ArgumentList == null || invocation.ArgumentList.Arguments.Count < 2)
            {
                return;
            }

            var firstArgument  = invocation.ArgumentList.Arguments[0];
            var secondArgument = invocation.ArgumentList.Arguments[1];

            var firstArgumentSymbol = context.SemanticModel.GetSymbolInfo(firstArgument.Expression);

            if (!(firstArgument.Expression is LiteralExpressionSyntax) &&
                (firstArgumentSymbol.Symbol?.MetadataName == typeof(CultureInfo).Name &&
                 !(secondArgument?.Expression is LiteralExpressionSyntax)))
            {
                return;
            }

            if (!(firstArgument.Expression is LiteralExpressionSyntax) &&
                !(secondArgument.Expression is LiteralExpressionSyntax))
            {
                return;
            }

            // Get the formatted string from the correct position
            var firstArgumentIsLiteral = firstArgument.Expression is LiteralExpressionSyntax;
            var formatString           = firstArgumentIsLiteral
                ? ((LiteralExpressionSyntax)firstArgument.Expression).GetText().ToString()
                : ((LiteralExpressionSyntax)secondArgument.Expression).GetText().ToString();

            // Verify that all placeholders are counting from low to high.
            // Not all placeholders have to be used necessarily, we only re-order the ones that are actually used in the format string.
            //
            // Display a warning when the integers in question are not in ascending or equal order.
            var placeholders = PlaceholderHelpers.GetPlaceholders(formatString);

            // If there's no placeholder used or there's only one, there's nothing to re-order
            if (placeholders.Count <= 1)
            {
                return;
            }

            for (var index = 1; index < placeholders.Count; index++)
            {
                int firstValue, secondValue;
                if (!int.TryParse(placeholders[index - 1].Groups["index"].Value, out firstValue) ||
                    !int.TryParse(placeholders[index].Groups["index"].Value, out secondValue))
                {
                    // Parsing failed
                    return;
                }

                // Given a scenario like this:
                //     string.Format("{0} {1} {4} {3}", a, b, c, d)
                // it would otherwise crash because it's trying to access index 4, which we obviously don't have.
                var argumentsToSkip = firstArgumentIsLiteral ? 1 : 2;
                if (firstValue >= invocation.ArgumentList.Arguments.Count - argumentsToSkip ||
                    secondValue >= invocation.ArgumentList.Arguments.Count - argumentsToSkip)
                {
                    return;
                }

                // Given a scenario {0} {1} {0} we have to make sure that this doesn't trigger a warning when we're simply re-using an index.
                // Those are exempt from the "always be ascending or equal" rule.
                Func <int, int, bool> hasBeenUsedBefore = (value, currentIndex) =>
                {
                    for (var counter = 0; counter < currentIndex; counter++)
                    {
                        int intValue;
                        if (int.TryParse(placeholders[counter].Groups["index"].Value, out intValue) && intValue == value)
                        {
                            return(true);
                        }
                    }

                    return(false);
                };

                // They should be in ascending or equal order
                if (firstValue > secondValue && !hasBeenUsedBefore(secondValue, index))
                {
                    context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation()));
                    return;
                }
            }
        }