Culture-sensitive conversion performed without an explicit CultureInfo
ID |
vbnet.portability.specify_culture |
Severity |
low |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Globalization |
Language |
VB.NET |
Description
Reports conversions whose result depends on the current thread culture but that do not say
which culture to use: ToUpper() with no argument, the single-argument Parse overloads of
the numeric and date types, and ToString(format) given a format string and no
IFormatProvider. Calls that already pass a CultureInfo, and the explicitly invariant
ToUpperInvariant, are accepted. A bare ToString() is not reported - it is far too common
to be a useful signal. Format strings that are culture-independent by definition are excluded:
the hexadecimal specifiers ("X", "X8"), the zero-padding decimal specifier ("D8") and the
round-trip and sortable date specifiers ("O", "o", "s", "u", "R", "r").
Guid.NewGuid().ToString(…) is also excluded, since every Guid format produces
hexadecimal.
Rationale
These conversions read Thread.CurrentThread.CurrentCulture, so the text they produce and the
values they accept are decided by the machine the code runs on rather than by the code. The
consequences are asymmetric and easy to miss in testing, because a developer machine and a
server are usually configured alike. Decimal.Parse("1,234") yields one thousand two hundred
and thirty-four under an English culture and one point two three four under a German one, so
the same import file loads different numbers - silently, with no exception to investigate.
A date parsed without a culture reads 03/04/2026 as March under en-US and as April under
en-GB. ToString("C2") emits the local currency symbol, which means a price written to a
report, a CSV file or a JSON payload carries whatever currency the server was configured for
and cannot be read back reliably. ToUpper() uses culture-specific casing rules: under
Turkish and Azeri, upper-casing i produces the dotted capital İ, so a value normalised for
comparison no longer matches the constant it is compared against and the comparison fails only
on that locale.
The following code illustrates the pattern detected by this rule:
Public Function NormaliseCode(ByVal productCode As String) As String
' FLAGGED: Culture-sensitive conversion performed without an explicit CultureInfo
Return productCode.ToUpper()
End Function
Remediation
State the culture at the call site, choosing it by the purpose of the value. For data that is
stored, transmitted, parsed back or compared - identifiers, keys, file content, protocol
fields - use CultureInfo.InvariantCulture, or ToUpperInvariant() for casing. For text
being shown to a user, pass CultureInfo.CurrentCulture explicitly, which produces the same
behaviour as today but records that the choice was deliberate. Prefer TryParse with an
explicit culture over Parse when the input is external, so malformed values are handled
rather than thrown.
' Before: the machine's locale decides the result
Return productCode.ToUpper()
Return Decimal.Parse(field)
Return amount.ToString("C2")
' After: the intent is stated
Return productCode.ToUpperInvariant()
Return Decimal.Parse(field, CultureInfo.InvariantCulture)
Return amount.ToString("C2", CultureInfo.CurrentCulture)
References
-
https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo
-
https://learn.microsoft.com/en-us/dotnet/standard/globalization-localization/globalization
-
https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1305
-
https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1304