Skip to content

BRO1151: Pass the elements, not an array, to a params parameter

An array created in the call for a params parameter is noise: Sum(1, new[] { 2, 3 }) becomes Sum(1, 2, 3), and the compiler creates the array. Not a StyleCop rule: Sonar's S3878 reports the same pattern, and StyleBro also fixes it.

Example

Before

public static class Paths
{
    public static string Log(string root) => Join(new[] { root, "logs", "app.log" });

    public static int Total() => Sum(1, new int[] { 2, 3 });

    private static string Join(params string[] parts) => string.Join("/", parts);

    private static int Sum(int first, params int[] rest) => first + rest.Length;
}

After

public static class Paths
{
    public static string Log(string root) => Join(root, "logs", "app.log");

    public static int Total() => Sum(1, 2, 3);

    private static string Join(params string[] parts) => string.Join("/", parts);

    private static int Sum(int first, params int[] rest) => first + rest.Length;
}

Why

params exists so callers don't have to build the array. Writing it out anyway hides the arguments behind new[] { }.

How the fix works

The text before the first element and after the last goes (new[] { and }, a trailing comma too); an empty array goes with the comma before it (Sum(1, new int[] { }) becomes Sum(1)). Arrays (new T[] { }, new[] { }) and collection expressions ([a, b]) are fixed; an array in one of the elements is fixed in the same run.

Not reported

  • A call that would bind to another method with the elements: another overload (Sum(int, int)), other type arguments (M<T>(params T[]) with new object[] { "a" } infers string from the elements), a params ReadOnlySpan<T> overload on newer frameworks.
  • A params collection (params ReadOnlySpan<T>, params IEnumerable<T>, ...) in a project with a target framework before .NET 9, which has none: the framework that has it reports the call, but the fix goes into the file every framework compiles (AngleSharp: SearchValues.Create(['<', '&']) in a net8.0;net10.0 project). Needs the package's build targets, which pass the project's target frameworks to the analyzers.
  • An array of another type than the parameter (new string[] { } for params object[]): the callee could rely on the array's type.
  • One element that converts to the array itself (new object[] { null }, new object[] { otherArray }, new Item[] { default! }): the call would pass it as the array. The compiler decides (the call without the array must bind each element as a params element), so parentheses, ! or casts around the element don't hide it.
  • An element whose meaning comes from the type it converts to: new(), default, null, lambdas, anonymous methods, method groups, collection expressions, switch expressions, conditionals without a type of their own. Out of the array it converts to whatever the call wants there: M([new() { Name = "a" }]) would become M(new() { ... }), a new() of the array type (CS8752).
  • Named arguments, arrays with a size (new int[2]), spread elements ([.. items]), constructor initializers and attribute arguments.
  • An array spread over several lines, or one with two or more elements that doesn't start on the first argument's line (Sum(1, + new line + new[] { 2, 3 })): the elements would share a line in a list that BRO1108 (SA1117) wants one per line.
  • A comment or directive in the text that goes (comments between the elements stay).

Configuration

[*.cs]
# all (default): every params array
# except_object_and_jagged: not for 'params object[]', 'params Array[]' or a jagged 'params T[][]', like Sonar's S3878
stylebro_params_arrays = all

With except_object_and_jagged, Log(new object[] { id, name }) stays: Sonar's S3878 accepts arrays for these parameters, and stylebro-migrate writes the setting for a repository that enforces S3878.

Compared with other analyzers

Sonar's S3878 reports the same pattern and has no fix.

Also fixes Sonar's S3878 when the SonarAnalyzer.CSharp package is in the project: the fix is registered for S3878 too (the array, where Sonar reports it) and applies at the places this rule would fix, so dotnet format clears Sonar's warnings there even with this rule off. Where this rule skips the code (see Not reported), no fix is offered and Sonar's warning stays: for example a collection expression spread over several lines (Create(path, null, [ + one element per line + ]), Buildalyzer's tests). Sonar also reports an array of a derived type (new string[] { ... } for params object[]), which stays.

Compared with StyleCop

StyleCop has no such rule. stylebro-migrate leaves it off.