Escape Control Char In Literal

ID

csharp.escape_control_char_in_literal

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

code_smell, readability, string

Description

Reports a regular string literal that holds a raw control character or an invisible character — an ASCII control, any Unicode whitespace other than the plain space, or a zero-width character such as the zero-width space, the word joiner or the byte order mark — where the escape sequence for that character should have been written instead.

Rationale

These characters get into source by accident: a tab copied out of a terminal, a non-breaking space pasted from a document or a web page, an ideographic space typed by an input method. Nothing marks them on screen, so the literal no longer matches what the reader sees. A comparison against the visually identical string fails, a dictionary lookup never hits, a format string splits on the wrong character, and the review that should have caught it saw only whitespace.

They are also fragile. Re-indentation, a reformat, or a round trip through a tool that trims trailing whitespace can silently change or delete the character, so the same source produces different behaviour before and after an unrelated edit. Written as an escape sequence, the same character is explicit, greppable, and survives every editor.

Raw characters are distinguished from their escaped spelling by reading the literal’s own text: an escaped tab and a raw tab decode to the same string, so only the way it was written can tell them apart. Because every escape sequence is made of printable ASCII characters, anything invisible found in the literal as written was typed raw.

Verbatim strings (@"…​") and raw string literals are excluded. They provide no escape sequences as an alternative and are meant to span lines, so raw whitespace inside them is how they are supposed to be written. Interpolated strings are not covered either, because their text is carried in interpolation tokens rather than in a string literal.

The range U+0080 to U+009F is also excluded. Those code points are control characters in Unicode, but they are not universally control data: the legacy single-byte encodings put printable characters there, so a literal that came through such an encoding would be reported for a character its author can see. U+007F is still reported.

In the example below, the placeholders <TAB>, <NBSP> and <ZWSP> stand for the character itself, typed straight into the literal — which is exactly what makes the defect invisible in a real file.

public class Parser
{
    private const string Columns = "id<TAB>name";     // FLAW — a raw tab
    private const string Amount = "1<NBSP>000";       // FLAW — a raw non-breaking space
    private const string Sku = "AB<ZWSP>12";          // FLAW — a raw zero-width space

    private const string Escaped = "id\tname";        // OK — escaped, and visible as such
    private const string Plain = "id name";           // OK — a plain space
}

The escaped form of the second literal is the code point escape for U+00A0, which reads as 1, backslash, u00A0, 000 — five visible characters where there was one invisible one.

Remediation

Replace the character with its escape sequence. A tab is \t; the other named controls are \0, \a, \b, \f and \v. Everything else takes a code point escape, written as a backslash, u, and four hex digits — u00A0 for a non-breaking space, u3000 for an ideographic space, u200B for a zero-width space. If the character got in by accident, delete it or put a plain space in its place. When the literal holds a separator that more than one place needs, lift it to a named constant so there is a single spot to inspect.