Skip to content

WHIZ161: Type Name Dissected By Hand

Verified by tests

TypeNameHandlingAnalyzerTests — library CI run #37346231411 (2026-10-05)

Severity: Warning Category: Type Naming

Description

{verified: TypeNameHandlingAnalyzerTests.Dissection_SplitOnTheAssemblyComma_ReportsWhiz161Async}

A persisted type name is not a flat string. It may carry version, culture, and public-key decoration (Version=, Culture=, PublicKeyToken=), nested-type + separators, and generic arity (`1 with the argument list in [[...]], where more commas appear). Splitting or trimming it locally reproduces one of those cases and misses the rest, and the miss is silent: the code keeps returning a string, just not the key the other side wrote.

WHIZ161 reports a string dissector applied to a type-name value (see WHIZ160 for what counts as one) when the argument is a separator the helpers own:

Method Reported when
Split the separator is ",", ", ", or "+"
IndexOf, LastIndexOf the argument is "+", ",", or the generic arity backtick
Replace the text to replace is "global::"
Substring always; a substring of a type name is a hand parse whatever the offsets

The receiver must be a type-name value. A Split(',') on a CSV field is not the rule's business. Generated code is not analyzed, and the rule is heuristic by nature, so it ships as a warning.

Diagnostic Message

'{0}' is applied to a type name. Parse it with TypeNameFormatter.Parse / GetSimpleName /
GetNamespace (runtime) or TypeNameUtilities (generators) instead; hand parsing misses version
decoration, nested '+' and generic arity.

{0} is the method that was applied: Split, Substring, IndexOf, LastIndexOf, or Replace.

Common Causes

  1. Stripping the assembly from a wire name: eventType.Split(',')[0]. The wire form of a generic type carries commas inside its [[...]] argument list, so the first segment is not the CLR form.
  2. Extracting a nested type's simple name: clrTypeName.Substring(clrTypeName.IndexOf('+') + 1).
  3. Turning a generated-source name into a key: fullyQualifiedTypeName.Replace("global::", ""). The fully qualified form renders nested types with ., so the result is not the CLR key even after the prefix is gone.

How to Fix

Parse through the helper for that side. At runtime TypeNameFormatter.GetFullName returns the bare CLR form of a wire name, GetSimpleName and GetNamespace take a name apart, Parse gives the parts at once, EventTypeMatchingHelper.NormalizeTypeName strips version decoration, and EnvelopeTypeNameHelper.ExtractInnerTypeName unwraps an envelope type name. In a generator, render the form you need from the symbol (TypeNameUtilities.BuildClrTypeName, FormatTypeNameForRuntime, GetSimpleName) instead of post-processing a display string.

Before (reported):

// WHIZ161: Split on the assembly comma; a generic wire name has commas inside [[...]]
public string Bare(string eventType) => eventType.Split(',')[0];

// WHIZ161: Substring and IndexOf('+') on a CLR name
public string Nested(string clrTypeName) => clrTypeName.Substring(clrTypeName.IndexOf('+') + 1);

After:

// Runtime: the bare CLR form of a wire name, and the simple name of a nested type
public string Bare(string eventType) => TypeNameFormatter.GetFullName(eventType);
public string Nested(string clrTypeName) => TypeNameFormatter.GetSimpleName(clrTypeName);

// Generator: render the key from the symbol instead of post-processing a display string
var clrTypeName = TypeNameUtilities.BuildClrTypeName(symbol);

When It Is Intentional

  • The helpers are exempt. Code inside a type named TypeNameFormatter, TypeNameUtilities, EventTypeMatchingHelper, EnvelopeTypeNameHelper, or TypeFormatter is not analyzed. That is where the parsing lives. {verified: TypeNameHandlingAnalyzerTests.InsideTheHelpersThemselves_EverythingIsAllowedAsync}
  • The receiver is not a type name. A Split or Substring on a value whose name carries no key marker and does not come from typeof, GetType(), or a helper is not reported.
  • A display-only site that trims a name for a log line or a trace tag may suppress the rule for that expression with a one-line reason (#pragma warning disable WHIZ161 // display only: ...).
  • Test fixtures. The framework's own test projects turn the four rules off in tests/.editorconfig, because fixtures compose and dissect names deliberately, including malformed ones that prove the parsers' tolerance. A consumer's test project can do the same.