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}"