Zero-valued member of a Flags enumeration is not named None
ID |
vbnet.maintainability.flags_enum_zero_member_named_none |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
medium |
Remediation Effort |
low |
Resource |
Type Design |
Language |
VB.NET |
Description
Reports a member with the value zero, declared in an enumeration marked <Flags>, whose name is
anything other than None. Both the decimal literal 0 and the hexadecimal &H0 forms are
reported, and the attribute is recognised as <Flags>, <Flags()>, <FlagsAttribute()> or a
qualified equivalent, alone or alongside other attributes on the same declaration. Enumerations
that are not marked <Flags> are not examined - a zero member named Unknown or Unspecified
is entirely conventional there - and neither is a <Flags> enumeration that declares no zero
member at all.
Rationale
A <Flags> enumeration is a set, and its members are the bits that can be present in that set.
Zero is the only value that names no bit, so it does not mean "this particular flag" - it means
"no flags", the empty set. None is the name the framework, the base class library and every
reader of the type expect for that value, and the expectation is load-bearing rather than
cosmetic: Enum.HasFlag and the equivalent (value And member) = member test are always true
against a zero member, so code written as if scope.HasFlag(AuditScope.NotAudited) reads as a
membership test but is a tautology, and the branch it guards runs for every value including a
fully populated one. Naming the member None makes the mistake visible at the call site,
because HasFlag(None) is obviously not a question worth asking, whereas HasFlag(Empty) or
HasFlag(NotAudited) looks like a legitimate check. The name also drives the framework’s own
behaviour: ToString on a zero value emits the member’s name, and Enum.Parse accepts it, so
the chosen name leaks into logs, serialized payloads and configuration files, and renaming it
after those exist is a breaking change for anything that round-trips the text.
The following code illustrates the pattern detected by this rule:
Namespace Acme.Storage
<Flags>
Enum FileAccessRights
' FLAGGED: Zero-valued member of a Flags enumeration is not named None
Empty = 0
Read = 1
Write = 2
Delete = 4
End Enum
<Flags>
Public Enum NotificationChannels
Remediation
Rename the zero-valued member to None. Keep the numeric value at zero so existing persisted
values continue to deserialize, and treat the rename as a source-compatible but text-breaking
change: update any configuration, serialized payload or query that stores the member by name. If
the enumeration genuinely needs a distinct "not yet determined" state as well as an empty set,
that state does not belong in a flags enumeration - model it separately, since it is not a bit
that can be combined with the others.
' Before: HasFlag(FileAccessRights.Empty) is always True
<Flags>
Public Enum FileAccessRights
Empty = 0
Read = 1
Write = 2
End Enum
' After
<Flags>
Public Enum FileAccessRights
None = 0
Read = 1
Write = 2
End Enum