Skip to content

BRO1523: Each call of a split call chain starts its own line

When a member access chain is split over several lines (a . or ?. of the chain starts a line), every call after the chain's first line starts its own line. Not a StyleCop rule; Roslynator's RCS0054 checks the same.

Example

Before

using System.Collections.Generic;
using System.Linq;

public class Directory
{
    public List<string> ActiveNames(IEnumerable<string> people) =>
        people
            .Where(p => p.Length > 0).Select(p => p.Trim())
            .Distinct().OrderBy(p => p).ToList();
}

After

using System.Collections.Generic;
using System.Linq;

public class Directory
{
    public List<string> ActiveNames(IEnumerable<string> people) =>
        people
            .Where(p => p.Length > 0)
            .Select(p => p.Trim())
            .Distinct()
            .OrderBy(p => p)
            .ToList();
}

Why

A chain someone has already split reads as a list of steps, one per line; a step tacked onto the end of another line is easy to miss when scanning the left edge. In the eight reference repositories, 99.6% of the split chains (20,880 of 20,959) already follow the rule (see decisions.md).

What counts

  • A chain is split when one of its . or ?. starts a line. A chain on one line is fine, and so is one where only the arguments span lines (Task.Run(() => { ... }).ConfigureAwait(false)).
  • The chain's first line may hold the receiver and one call: services.AddLogging() + newline + .AddOptions() is checked from the second line on. Only calls below the first line are checked.
  • A chain whose first line holds two or more calls wraps by filling lines and isn't checked: sb.Append(id).Append(name) + newline + .Append(text).Append(end) stays as it is. The receiver's own call counts (RuleFor(x => x.Name).NotNull() + newline + .WithMessage(...)).
  • A call counts from the member right after the previous call: .WriteTo.Sink(x) and .Items[0].Trim() are one step each and stay together, .Result or .Length after the last call stays where it is, and the first member after the receiver (new Config { ... }.Apply()) isn't checked.
  • ?. counts like ., x! and x[0] after a call belong to that call.

How the fix works

Only the gap before the . (or ?.) changes: it becomes a line break plus the indentation of the chain's lines that already start with ., the nearest one above (or else below). The rule doesn't check indentation, so a chain aligned under its first . stays aligned. Arguments and lambda bodies aren't touched; a multi-line step moves only when its other lines are already at least as deep as its new line (a lambda body under the step), otherwise it's skipped.

Not reported

  • A comment in the gap (.Where(x) /* why */ .Select(y)) or a directive anywhere in the chain (#if between steps or inside an argument). #region/#endregion lines don't count (also not for a multi-line step's indentation): the chain is judged the same as after BRO1112/BRO1113 remove them, so one run is enough.
  • A multi-line step whose other lines are less indented than its new line would be: the fix would leave its lambda body to the left of the call.
  • Chains whose first line holds two or more calls (see What counts).
  • Chains inside an interpolated string's hole (a line break there needs C# 11) and code with syntax errors.

Compared with StyleCop

StyleCop has no such rule. Roslynator's RCS0054 (off by default) checks the same breaks with four differences: it checks chains whose first line holds several calls (below that line), it also splits a property step (.WriteTo + newline + .Sink(x)) and a trailing property, it reports chains where only the arguments span lines, and it enforces one indentation unit deeper than the chain's first line (StyleBro keeps the chain's own indentation). stylebro-migrate turns this rule off, so migrating doesn't change a StyleCop-clean repository.