String.Format literal references a placeholder index with no matching argument

ID

vbnet.correctness.string_format_problems

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

String Handling

Language

VB.NET

Description

Reports a call to String.Format whose format string is a literal containing a placeholder index that is higher than the number of arguments supplied. A literal with {1} given one argument, {2} given two, or {3} given three is reported; the index may carry an alignment or a format specifier, so {1,10} and {2:C} count as well. Repeated use of the same index is not a mismatch and is not reported, and escaped brace pairs such as {{1}} are recognised as literal text. Both the plain overload and the one taking an IFormatProvider as the first argument are covered.

Two limits are worth stating. Calls are matched by arity, one arm per argument count, and the rule stops at six format arguments: a call with seven or more is not reported. And a call whose format string comes from a variable, a constant or a resource is left alone, because the literal is what the check reads and there is nothing to read there - which also means a mismatch built that way will not be found.

Rationale

String.Format resolves placeholder indexes against the argument array at run time, not at compile time, so a format string that asks for an argument that was never passed compiles cleanly and then throws FormatException - "Index (zero based) must be greater than or equal to zero and less than the size of the argument list" - the first time that line executes. The failure therefore surfaces in whichever branch was not covered by a test, and it is frequently in exactly the code that was meant to report a different problem: an error message, a log line, or the text of an exception. In that position the FormatException replaces the original failure, so the incident report names a formatting bug and the real fault that triggered the branch is never recorded. The usual cause is edit drift - a placeholder is added to the message, or an argument is dropped from the call, and the other side is not updated.

The following code illustrates the pattern detected by this rule:

Public Function Header(ByVal customer As String) As String
    ' FLAGGED: String.Format literal references a placeholder index with no matching argument
    Return String.Format("Invoice for {0} dated {1}", customer)
End Function

Remediation

Count the placeholders and pass one argument for each: the highest index used must be one less than the number of arguments. If the extra placeholder is no longer wanted, delete it from the literal rather than passing a filler value. For new code prefer VB.NET interpolated strings ($"…​"), which name the values inline and are checked by the compiler, so the mismatch cannot be expressed.

' Before: \{1} has no argument - FormatException when this line runs
Return String.Format("Invoice for \{0} dated \{1}", customer)

' After: one argument per placeholder
Return String.Format("Invoice for \{0} dated \{1}", customer, issued)

' Or, checked by the compiler
Return $"Invoice for \{customer} dated \{issued}"

Configuration

This detector does not need any configuration.