Skip to content

BRO1311: Tuple element names should use correct casing

Element names in tuple types are PascalCase: (int Count, string Name), not (int count, string name). Replaces StyleCop SA1316.

Example

Before

internal class Inventory
{
    public (int count, string name) Largest() => (count: 3, name: "bolts");

    public string Describe()
    {
        var item = Largest();
        return item.name + ": " + item.count;
    }
}

After

internal class Inventory
{
    public (int Count, string Name) Largest() => (Count: 3, Name: "bolts");

    public string Describe()
    {
        var item = Largest();
        return item.Name + ": " + item.Count;
    }
}

Why

Tuple elements are public fields of the tuple, so they read like members: item.Name, as for a property. Consistent casing also keeps a tuple-returning method and a record or class returning the same data interchangeable.

Configuration

[*.cs]
# PascalCase (default) or camelCase, like StyleCop's tupleElementNameCasing.
stylebro_tuple_element_name_casing = camelCase

stylebro-migrate writes it from stylecop.json.

stylebro_rename_public_api = true also renames names other assemblies see (default false, see Not reported; the owner's decision in decisions.md).

How the fix works

Tuple element names belong to the tuple type where they're written, and a use like item.count binds to the element of whichever tuple type item has. So the fix renames the name everywhere at once, in the whole solution:

  • every tuple type element with that name (a method's return type, a field, a local's type, a type argument);
  • every name in a tuple literal converted to one of them ((count: 3, ...) returned from the method above), so the compiler doesn't warn that the literal's name is ignored (CS8123);
  • every use of those elements: item.count, item?.count, nameof(item.count), item is (count: 1, _);
  • overrides and implementations together with what they override: their names have to match (CS8139).

A tuple or anonymous object that takes its element name from a renamed use ((item.count, 1), new { item.count }) gets the old name written out ((count: item.Count, 1)), so it and its own uses keep working unchanged; not when the literal is converted to a tuple type (return (true, item.Count); from a method returning (bool Found, int Total)), whose names win anyway.

The names of one tuple type are renamed all or none: when one of them has to keep its name (see below), the others in the same tuple types keep theirs too, rather than leaving (int Total, int max). stylebro-migrate format lists the element that keeps its name with its reason, and each of the others as kept with it. Deconstruction (var (count, name) = item;) declares variables, not element names, and isn't touched.

Not reported

  • Where IDE1006 is suppressed: after #pragma warning disable IDE1006 (up to its restore), or with [SuppressMessage("Style", "IDE1006")] on the declaration, its member or type, or a global one targeting them. Code that opts out of the SDK's naming rules (a file copied from another project) keeps its names.
  • The name appears in code excluded by #if anywhere in the project: disabled code isn't compiled, so the rename can't follow it.
  • Public API (unless stylebro_rename_public_api = true): Tuple element names in the types of a member's signature (return, parameter, property, field or event types) that other assemblies see, that is public, protected or protected internal in types that are visible too. Renaming them breaks code outside the solution. Internal code is renamed, also with InternalsVisibleTo.
  • _ and names that don't start with a letter.
  • Names a member has to take from a library member it overrides or implements, like StyleCop: the library can't be renamed. An override or implementation of a member in the solution is reported, and renamed together with it.
  • Names in tuple literals and names a literal infers ((count, name) with variables of those names). StyleCop checks inferred names only with includeInferredTupleElementNames, off by default; StyleBro doesn't check them.
  • A new name that would be a keyword (Var in camelCase).

Reported but not fixed

If dotnet format leaves a finding, StyleBro kept it on purpose. The fix looks at the whole solution, other projects included, which the analyzer can't see, and leaves a change that would break something there: the name in a string or a nameof (reflection, serialization), a member that would hide it, a use in generated code. It offers no code action for such a finding, and stylebro-migrate format lists each one with its reason (Findings kept on purpose); its --verify-no-changes doesn't fail on them. Change it by hand only after checking those uses, or suppress it or record it in the baseline.

A rename that would add compile errors (or CS8123 warnings) anywhere in the solution is skipped, and the warning stays: for example a tuple that already has the new name ((int count, int Count)), or an override of a library member whose own signature names the elements. When such an element is in another project, each project's elements with that name are judged on their own: one project's tuple that can't be renamed doesn't keep an unrelated one elsewhere. So is a rename that would let IDE0042 (deconstruct variable declaration) report a variable where your settings make IDE0042 a warning or an error: IDE0042 leaves var result = Get(); alone while an element shares its name with something in scope (a local pointer next to result.pointer), and the new name would end that.

Compared with StyleCop

Same reports as StyleCop 1.2.0-beta.556 (checked with the parity set tuple-element-casing). StyleCop's master (unreleased, d1b6183d) no longer reports names in the signature of an override or interface implementation; StyleBro still does and renames them together with the base (master's fix renames only the base, which then fails with CS8139 on the override). StyleCop's fix renames only the declaration, so every use (item.count) and every override breaks the build; StyleBro renames them together.