A writable shared field is exposed outside its type
ID |
vbnet.maintainability.no_visible_mutable_static_field |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Api Design |
Language |
VB.NET |
Description
Reports a Shared field declared Public, Protected or Protected Friend that is neither
ReadOnly nor Const, with or without an initialiser. Private and Friend shared fields are
not reported, because they are not reachable from outside the assembly’s own code, and neither
are instance fields.
Rationale
A writable shared field is a single value with no owner. Every assembly that can see the type can assign to it, so the set of writers is the set of callers rather than a list you can enumerate, and when the value turns out to be wrong there is no method to breakpoint and no property setter to log - the write that caused it may be in code you do not ship. That alone makes the field expensive to reason about, and it gets worse in two directions. Concurrency: shared state written without coordination is a data race by construction, and since the field is public the type cannot impose any locking discipline on the writers, so the usual symptoms - a setting that reverts, a counter that drifts, a cache that is briefly inconsistent - appear under load and not in tests. Evolution: the field is part of the public contract, so adding validation, lazy initialisation, change notification or a per-tenant lookup later means changing a field into a property, which is a source and binary breaking change for every consumer. Initialisation order compounds it, because a caller can read the field before the code that was supposed to populate it has run and will see the type default with nothing to indicate the value was never set.
The following code illustrates the pattern detected by this rule:
Public Class RuntimeSettings
' FLAGGED: A writable shared field is exposed outside its type
Public Shared ConnectionString As String = "Server=.;Database=acme"
' FLAGGED: A writable shared field is exposed outside its type
Public Shared RetryCount As Integer
' FLAGGED: A writable shared field is exposed outside its type
Public Shared Handlers As New List(Of String)()
' FLAGGED: A writable shared field is exposed outside its type
Protected Shared Culture As String = "en-US"
' FLAGGED: A writable shared field is exposed outside its type
Protected Friend Shared LastReloadUtc As Date
Remediation
If the value never changes after start-up, mark it ReadOnly (or Const for a compile-time
literal) - that is usually the whole fix, and it is source-compatible. If it does change, take the
field out of the public surface: make it Private Shared and expose a Shared property, so
validation, locking and logging have somewhere to live and the accessor can evolve without
breaking callers. Better still, drop the shared state: pass the value in through a constructor or
an options object so each consumer gets its own instance and the lifetime is explicit. Where a
writable shared collection is used as a registry, expose it as an immutable snapshot and mutate it
through methods that take a lock.
' Before: any assembly can assign this, from any thread
Public Class RuntimeSettings
Public Shared ConnectionString As String = "Server=.;Database=acme"
End Class
' After: the field is owned by its type, and the accessor can evolve
Public Class RuntimeSettings
Private Shared _connectionString As String = "Server=.;Database=acme"
Public Shared Property ConnectionString As String
Get
Return _connectionString
End Get
Set(value As String)
If String.IsNullOrWhiteSpace(value) Then Throw New ArgumentException(NameOf(value))
_connectionString = value
End Set
End Property
End Class