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)

Configuration

This detector does not need any configuration.