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