Description
(From the "Save me from myself" department…)
Describe the problem you are trying to solve
C# params
arrays are awesome:
partial class Utilities {
public static void LogError(UsefulInfo info, string format, params object[] args)
{
string message = string.Format(format, args);
// Do something with `message`…
}
}
They're also a footgun, lying in wait:
class App {
static UsefulInfo info = …
public static void Whatever()
{
try {
DoSomething(…);
}
catch (Exception e) {
Utilities.LogError(info, e.ToString());
}
}
}
Quick, what's wrong with the above code? It would compile (given enough extra code), and it certainly looks reasonable, and similar code constructs have passed code review innumerable times.
What Could Possibly Go Wrong?
static void DoSomething()
{
throw new Exception("{tag-name} lol");
}
What happens is the above sketch eventually effectively calls:
string.Format("{tag-name} lol");
// -or-
string.Format("{tag-name} lol", new object[0]);
which promptly throws an exception:
System.FormatException: Input string was not in a correct format.
at System.Text.StringBuilder.AppendFormatHelper (System.IFormatProvider provider, System.String format, System.ParamsArray args)
at System.String.FormatHelper (System.IFormatProvider provider, System.String format, System.ParamsArray args)
at System.String.Format (System.String format, System.Object[] args)
There are (at least) two mitigations for this: overloading, and using the format string.
Overloading: Method overloading nicely "solves"/"avoids" the problem:
partial class Utilities {
public static void LogError(UsefulInfo info, string message) => … // doesn't call string.Format()
}
The callsite of Utilities.LogError(info, e.ToString())
is unchanged, but now avoids calling string.Format()
, and thus avoids the FormatException
.
Explicit format strings: occasionally method overloading isn't (easily) possible, in which case the format string should be explicitly specified:
Utilities.LogError(info, "{0}", e.ToString());
Describe suggestions on how to achieve the rule
When an analyzer rule encounters a method invocation:
- Does the resolved method to-be-called end in a
string, params *whatever*[]
parameter "pair"? If it does, continue to (2). - Are there any values in the
params *whatever*[]
parameter? If there are, then it's "fine". (It might not be fine; see also CA2241 - Possibility to support custom string formatting methods? roslyn-analyzers#2799! But it's not what this issue is trying to look for.). If there are no values in theparams object[]
parameter, continue to (3). - Issue a warning that the method invocation is potentially problematic.