Skip to content

DirectiveScope writes a bare #if (CS1517) for an empty condition, and DirectiveScope/PragmaScope push text after a line break out of the directive into code #154

Open

Description

@matt-edmondson

What's wrong

DirectiveScope and PragmaScope write their argument verbatim after the directive (CodeBlocker/Scopes.cs). Nothing checks it first, so two kinds of input produce generated C# that is wrong or doesn't compile:

  1. An empty or null DirectiveScope condition writes a bare #if . That is CS1517 Invalid preprocessor expression, so the generated file fails to compile. (RegionScope handles an empty name, and PragmaScope treats empty warnings as a no-op since PragmaScope with an empty warning list emits a bare disable/restore, silently suppressing all warnings and clobbering an enclosing scope #97/Make PragmaScope a no-op for empty warning lists to prevent global warning suppression leakage #98.)
  2. A line terminator in the condition or warning list (LF, CR, NEL, U+2028, U+2029) ends the directive early, and the rest of the text is written as ordinary code. WriteAutoGeneratedHeader with a multi-line copyright writes the second and later lines as code, so the generated file doesn't compile #151/Comment every line of a multi-line copyright, and refuse a multi-line region name [patch] #152 fixed exactly this for RegionScope names and copyright headers, using the new LineTerminators helper. The other two directive scopes didn't get the same guard.

private static void Begin(CodeBlocker codeBlocker, string condition)
{
Ensure.NotNull(codeBlocker);
codeBlocker.WriteLine($"#if {condition}");
}
private static void End(CodeBlocker codeBlocker)
{
Ensure.NotNull(codeBlocker);
codeBlocker.WriteLine("#endif");
}

private static void Begin(CodeBlocker codeBlocker, string warnings)
{
Ensure.NotNull(codeBlocker);
if (string.IsNullOrWhiteSpace(warnings))
{
return;
}
codeBlocker.WriteLine($"#pragma warning disable {warnings}");
}
private static void End(CodeBlocker codeBlocker, string warnings)
{
Ensure.NotNull(codeBlocker);
if (string.IsNullOrWhiteSpace(warnings))
{
return;
}
codeBlocker.WriteLine($"#pragma warning restore {warnings}");
}

Reproduction

Built against main at 21ac04c, and parsed with Roslyn:

using (new DirectiveScope(cb, "")) cb.WriteLine("class C {}");
// #if
// class C {}
// #endif
// -> (1,5): error CS1517: Invalid preprocessor expression

using (new DirectiveScope(cb, "DEBUG\n|| TRACE")) cb.WriteLine("class C {}");
// #if DEBUG
// || TRACE            <- now code, a syntax error whenever DEBUG is defined
// class C {}
// #endif

using (new PragmaScope(cb, "CS1591\nCA1707")) cb.WriteLine("class C {}");
// #pragma warning disable CS1591
// CA1707              <- code: CS1001 / CS1002, and CA1707 is never suppressed
// class C {}
// #pragma warning restore CS1591
// CA1707

The PragmaScope(CodeBlocker, IEnumerable<string>) overload joins its entries with ", ", so a single entry with a stray line break, e.g. one read from a file line by line with its CR kept, hits the same path.

Why it matters

These scopes exist so a generator can't produce unbalanced or malformed directives. When a computed condition comes out empty, or a warning list has a stray line break, the result is a generated file that doesn't compile, and the error points at generated code instead of the bad argument.

Suggested fix

Validate before writing anything, as RegionScope now does:

  • DirectiveScope: throw ArgumentException when condition is null, empty or whitespace, or when LineTerminators.Contains(condition) is true.
  • PragmaScope: throw ArgumentException when warnings contains a line terminator. Keep the existing no-op for empty or whitespace input. Joining the IEnumerable overload's entries means one check covers both constructors.

Acceptance criteria

  • new DirectiveScope(cb, ""), (cb, " ") and (cb, null!) throw ArgumentException and write nothing.
  • DirectiveScope and PragmaScope throw ArgumentException and write nothing for an argument containing \n, \r, U+0085, U+2028 or U+2029.
  • Existing ScopesTests still pass, including PragmaScopeWithAnEmptyStringIsANoOp.

Generated by Claude Code

Activity

  1. matt-edmondson commented on Nov 1, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions