Summary
OutOfProcTaskHostNode.ShouldTreatWarningAsError() checks WarningsAsMessages instead of WarningsAsErrors when matching specific warning codes. This means <MSBuildWarningsAsErrors>CS0618</MSBuildWarningsAsErrors> silently fails to promote warnings to errors when the task runs out-of-process via TaskHostFactory.
Introduced in: v17.10.0 (PR #7309)
Affected file: src/MSBuild/OutOfProcTaskHostNode.cs
Semantics (confusing but intentional)
WarningsAsErrors = null → feature disabled
WarningsAsErrors = {} (empty set) → ALL warnings become errors (universal)
WarningsAsErrors = {"CS0618"} → only CS0618 becomes error (specific)
Empty set = "all" is the established convention — see LoggingService.cs:2010:
"If the list is empty or contains the code, the warning should be treated as an error"
Bug
// OutOfProcTaskHostNode.cs — BUGGY (main)
public bool ShouldTreatWarningAsError(string warningCode)
{
if (WarningsAsErrors == null || WarningsAsMessages?.Contains(warningCode) == true)
return false;
return (WarningsAsErrors.Count == 0 && WarningAsErrorNotOverriden(warningCode))
|| WarningsAsMessages.Contains(warningCode);
// ^^^^^^^^^^^^^^^^^ should be WarningsAsErrors
}
Compare the canonical correct implementation in LoggingService.cs:2011:
(WarningsAsErrors.Count == 0 && WarningAsErrorNotOverriden(warningEvent))
|| WarningsAsErrors.Contains(warningEvent.Code)
// ^^^^^^^^^^^^^^^^ correct collection
Trace: specific codes (e.g. WarningsAsErrors = {"CS0618"}, task emits CS0618)
| Step |
Expression |
Value |
Note |
| 1 |
WarningsAsErrors == null |
false |
set has 1 item |
| 2 |
WarningsAsMessages?.Contains("CS0618") |
false or null |
CS0618 not in messages |
| 3 |
→ don't return false, continue |
|
|
| 4 |
WarningsAsErrors.Count == 0 |
false |
Count is 1, not 0 |
| 5 |
→ first operand of || is false |
|
|
| 6 |
WarningsAsMessages.Contains("CS0618") |
NRE or false |
Wrong collection! |
| 7 |
→ returns false |
|
BUG: should return true |
Step 6 hits the wrong collection. If WarningsAsMessages is null → NRE crash. If non-null → always false here (step 2 already verified the code isn't in messages). Either way, specific-code matching never works.
Trace: treat-all (e.g. WarningsAsErrors = {}, task emits MY0001)
The empty-set path was also confirmed broken in testing (warning not promoted). The WarningsAsMessages.Contains() on the return line likely crashes or returns false before the Count == 0 short-circuit can help, depending on null state of WarningsAsMessages. Even if it doesn't NRE, the overall method is unreliable for OOP TaskHost scenarios.
Impact
Verified on .NET SDK 10.0.101 (public release):
| Scenario |
Expected |
Actual (main) |
| In-proc + specific codes |
✅ Error |
✅ Error |
OOP + specific codes (MY0001) |
✅ Error |
❌ NRE crash (MSB4018) |
OOP + treat-all (-warnaserror) |
✅ Error |
❌ Warning (not promoted) |
Both OOP paths are broken. The NRE crash (not a silent pass) is the more common scenario since most users don't set <MSBuildWarningsAsMessages>.
NRE stack trace (shipped MSBuild)
error MSB4018: The "EmitWarningTask" task failed unexpectedly.
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.Build.CommandLine.OutOfProcTaskHostNode.ShouldTreatWarningAsError(String warningCode)
Repro
See attached repro.zip. Steps:
cd WarnTask && dotnet build && cd ..
# Control — in-proc promotes correctly (build FAILS):
dotnet msbuild TestInProc.proj -t:Build
# Bug — OOP TaskHost does NOT promote (build SUCCEEDS):
dotnet msbuild TestOOP.proj -t:Build
Fix
- return (WarningsAsErrors.Count == 0 && WarningAsErrorNotOverriden(warningCode))
- || WarningsAsMessages.Contains(warningCode);
+ return (warningsAsErrors.Count == 0 && WarningAsErrorNotOverriden(warningCode))
+ || warningsAsErrors.Contains(warningCode);
repro.zip
Summary
OutOfProcTaskHostNode.ShouldTreatWarningAsError()checksWarningsAsMessagesinstead ofWarningsAsErrorswhen matching specific warning codes. This means<MSBuildWarningsAsErrors>CS0618</MSBuildWarningsAsErrors>silently fails to promote warnings to errors when the task runs out-of-process viaTaskHostFactory.Introduced in: v17.10.0 (PR #7309)
Affected file:
src/MSBuild/OutOfProcTaskHostNode.csSemantics (confusing but intentional)
Empty set = "all" is the established convention — see
LoggingService.cs:2010:Bug
Compare the canonical correct implementation in
LoggingService.cs:2011:Trace: specific codes (e.g.
WarningsAsErrors = {"CS0618"}, task emits CS0618)WarningsAsErrors == nullWarningsAsMessages?.Contains("CS0618")WarningsAsErrors.Count == 0||is falseWarningsAsMessages.Contains("CS0618")Step 6 hits the wrong collection. If
WarningsAsMessagesis null → NRE crash. If non-null → always false here (step 2 already verified the code isn't in messages). Either way, specific-code matching never works.Trace: treat-all (e.g.
WarningsAsErrors = {}, task emits MY0001)The empty-set path was also confirmed broken in testing (warning not promoted). The
WarningsAsMessages.Contains()on the return line likely crashes or returns false before theCount == 0short-circuit can help, depending on null state ofWarningsAsMessages. Even if it doesn't NRE, the overall method is unreliable for OOP TaskHost scenarios.Impact
Verified on .NET SDK 10.0.101 (public release):
MY0001)-warnaserror)Both OOP paths are broken. The NRE crash (not a silent pass) is the more common scenario since most users don't set
<MSBuildWarningsAsMessages>.NRE stack trace (shipped MSBuild)
Repro
See attached
repro.zip. Steps:Fix
repro.zip