Repository navigation
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
Description
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:
- An empty or null
DirectiveScopecondition writes a bare#if. That is CS1517 Invalid preprocessor expression, so the generated file fails to compile. (RegionScopehandles an empty name, andPragmaScopetreats 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/MakePragmaScopea no-op for empty warning lists to prevent global warning suppression leakage #98.) - 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
RegionScopenames and copyright headers, using the newLineTerminatorshelper. The other two directive scopes didn't get the same guard.
CodeBlocker/CodeBlocker/Scopes.cs
Lines 146 to 156 in 21ac04c
| 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"); | |
| } |
CodeBlocker/CodeBlocker/Scopes.cs
Lines 187 to 207 in 21ac04c
| 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
// CA1707The 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: throwArgumentExceptionwhenconditionis null, empty or whitespace, or whenLineTerminators.Contains(condition)is true.PragmaScope: throwArgumentExceptionwhenwarningscontains a line terminator. Keep the existing no-op for empty or whitespace input. Joining theIEnumerableoverload's entries means one check covers both constructors.
Acceptance criteria
new DirectiveScope(cb, ""),(cb, " ")and(cb, null!)throwArgumentExceptionand write nothing.DirectiveScopeandPragmaScopethrowArgumentExceptionand write nothing for an argument containing\n,\r, U+0085, U+2028 or U+2029.- Existing
ScopesTestsstill pass, includingPragmaScopeWithAnEmptyStringIsANoOp.
Generated by Claude Code
Activity
matt-edmondson commentedon Nov 1, 2026
Triage
LineTerminatorsguard already exists and the fix is small.CodeBlocker/Scopes.cs, inDirectiveScopeandPragmaScope(directive scopes)RegionScopeand copyright headers) and PragmaScope with an empty warning list emits a bare disable/restore, silently suppressing all warnings and clobbering an enclosing scope #97/MakePragmaScopea no-op for empty warning lists to prevent global warning suppression leakage #98 (PragmaScopetreating empty input as a no-op). Open Multi-line DefaultValue, comments and attributes lose their indent after the first line: a nested field's[\n\t1,\n]initialiser drops to column 1 #132 and Template bodies whose line endings differ from the writer's come out with mixed line endings, or as one run-on line with no indentation (CRLF writer + LF body becomes an expression body) #123 are about multi-line content too, but they cover indentation and line endings, not directives.Generated by Claude Code