Skip to content

Fix #1300 - nullable loop conditions - #1312

Merged
GrahamTheCoder merged 21 commits into
icsharpcode:masterfrom
GrahamTheCoder:claude/fix-1300-nullable-loop-conditions
Nov 11, 2026
Merged

GrahamTheCoder merged 21 commits into
icsharpcode:masterfrom
GrahamTheCoder:claude/fix-1300-nullable-loop-conditions

Conversation

@GrahamTheCoder

@GrahamTheCoder GrahamTheCoder commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Fixes #1300

Root cause

VbSyntaxNodeExtensions.AlwaysHasBooleanTypeInCSharp is the shared, syntax-based list of places where VB binds a condition with BindBooleanExpression. There, a nullable value that is Nothing becomes False. The nullable converter and TypeConversionAnalyzer (TypeConversionKind.NullableBool) use it, and statement visitors call AddExplicitConversion(..., forceTargetType: Boolean). There were four problems:

  1. The list only had If/ElseIf/single-line If, the ternary If(), Where and Take/Skip While. It was missing While, Do While/Until, Loop While/Until and Catch ... When, and those visitors didn't add the Boolean conversion. That gave CS0266 (the issue).
  2. It didn't look through parentheses, but VB's binder does (isOperandOfConditionalBranch is passed through ParenthesizedExpression in Binder_Expressions.vb). So If (a < b) Then with nullables gave (bool)(...), which throws InvalidOperationException at runtime when a value is null. This also affected If statements on master, and the issue reports the parenthesized While form.
  3. With Option Strict Off, a nullable non-Boolean condition (If x Then / While x with x As Integer?) converted to Conversions.ToBoolean(x.Value), which throws when x is Nothing. VB lifts the conversion and treats Nothing as False. This also affected If on master.
  4. A nullable structure with a user-defined Widening CType to Boolean, used as a condition, converted to (bool)s, which throws when s is Nothing. VB lifts the user-defined conversion too. This also affected If on master. Once loops are treated as conditions, it would otherwise have turned a loop's compile error into a runtime throw.

Changes (commits)

  1. Pure refactor: extract ConvertConditionAsync (If/ElseIf/single-line If).
  2. AlwaysHasBooleanTypeInCSharp now walks out of the parentheses around a condition. TypeConversionAnalyzer looks inside parentheses when it excludes binary/unary nodes from NullableBool.
  3. Self-verifying tests for loop conditions. This commit was red on purpose (CS0266) and turns green with commit 5.
  4. Pure refactor: extract ConvertLoopContinueConditionAsync, so the top and bottom While/Until clauses share one conversion.
  5. Fix: AlwaysHasBooleanTypeInCSharp now covers WhileStatementSyntax, WhileOrUntilClauseSyntax and CatchFilterClauseSyntax. VisitWhileBlock, ConvertLoopContinueConditionAsync and the Catch filter use ConvertConditionAsync. An Until condition is converted to bool before it's inverted.
  6. Fix (review round 1): in a condition, a nullable non-Boolean value converted by a Conversions method uses Conversions.ToBoolean(x.GetValueOrDefault()) instead of .Value. The zero default of every type that Conversions.ToBoolean accepts also converts to False, so this matches VB and evaluates x once. This commit also pins Option Strict Off Object conditions in loops, which already worked.
  7. Pure refactor (review nit): compute SkipIntoParens() once in the NullableBool analysis.
  8. Fix (review round 2): in a condition, a cast from a nullable non-Boolean value (e.g. a user-defined operator) becomes s.HasValue && (bool)s.Value, or GetS() is { } arg1 && (bool)arg1 when the expression isn't safely reusable. This is the new VisualBasicNullableExpressionsConverter.ConvertWhenNotNullElseFalse. It can't use GetValueOrDefault, because the operator may convert the default to True. The commit also pins that conversions outside conditions keep .Value and throw on Nothing, as VB does (Dim b As Boolean = x, If IsTrue(x), If CBool(x)).
  9. Fix (review round 3): the generated is { } argN / is not { } argN / is var argN patterns now parenthesize a non-primary operand (AddParensUnlessPrimary). Before, If(a, b, c) or If(a, b) as a nullable structure condition gave text that reparsed wrongly (CS0173/CS0165), even though the syntax tree was right. This was a latent bug in the shared helpers, which InvokeConversionWhenNotNull and the relational/logical operator conversions also use. No existing test output changed.
  10. Fix (review round 4): a user-defined CType that returns Boolean? is excluded from commit 8's lifting, because VB doesn't lift it the same way. Commit 13 replaces the fallback to master's output.
  11. Pure refactor (review round 5 nits): extract the IsMadeBoolByNullableOperatorConversion predicate from the NullableBool condition.
  12. Pure refactor: extract CreateCastForContext (renamed CreateCastOrConditionCast in commit 21).
  13. Fix (decision 3): a condition converted by a user-defined CType returning Boolean? gives (bool?)s.Value == true (or (bool?)ns == true for a non-nullable operand) instead of (bool)s, which threw when the operator returned Nothing. .Value keeps VB's throw for a Nothing operand, which C# would otherwise lift to null.
  14. Decision 2: InvertCondition takes negateOrderingComparisons. An Until condition containing a nullable <, <=, > or >= explicitly isn't flipped (a doc comment gives the reason). Before, a redundant (bool) cast stopped the flip by coincidence. No output changed. Commit 19 replaces the mechanism.
  15. Don't add the redundant (bool) cast where the nullable operator logic already gives bool. Now Until conditions simplify through &&/||: Loop Until Not b gives b != false and Do Until b = True gives b != true.
  16. Fix (decision 4): in a condition, Not (x < y) (and <=, >, >=) with nullable operands of a totally ordered type (integral, Decimal, Date, Char, enum) flips to the lifted opposite operator. Both are false when an operand is Nothing. The fix doesn't apply to floating point (NaN), Boolean (C# has no ordering operators for bool), structures (user-defined operators), or =/<> (both operands Nothing), and a test pins that.
  17. Fix (review round 6): TypeConversionKind.NullableBool parenthesizes the condition before == true (EqualsTrue()). A Boolean? ternary condition (If If(f, x < y, x > y), Do Until If(f, b, Not b)) gave f ? A : B == true, which reparses as f ? A : (B == true) (CS0173/CS0266). The If form was also broken on master.
  18. Fix (review round 6, existing bug): InvertCondition inverted only a ternary's condition, which swaps its arms rather than negating it. Do Until If(f, a, b) gave while (!f ? a : b), and now gives while (!(f ? a : b)).
  19. Review round 6: the nullable converter marks a lifted comparison whose operand may be null (CSharpUtil.MayCompareNullAnnotation), and InvertCondition refuses to flip only those </<=/>/>= nodes. This replaces commit 14's negateOrderingComparisons parameter and its rescan of the VB condition. It's precise per node: Do Until x > y AndAlso i < 3 gives !(x > y) || i >= 3 instead of !(x > y) || !(i < 3).
  20. Pure refactor: share a VB IsOrderingComparison extension.
  21. Pure refactor: rename CreateCastForContext to CreateCastOrConditionCast, and say why in the NegatedForParentNotAnnotation doc comment.

Decisions

Principle: use the simplest output form we can cheaply create that keeps VB's behaviour, even where the issue proposed a more complex one.

  1. Lifted <= stays: While currentDate <= endDate gives while (currentDate <= endDate), not the issue's currentDate.HasValue && endDate.HasValue && currentDate.Value <= endDate.Value. C#'s lifted <= is false when an operand is null, which matches VB's Nothing→False. This matches the existing If output.
  2. Nullable Until keeps !(x < y), never x >= y (wrong when an operand is null). This is now explicit: InvertCondition doesn't flip a comparison the nullable converter marked as lifted over a possibly-null operand, and no longer depends on cast/simplifier ordering. Other comparisons in the same condition still flip. DoUntilLessThan/LoopUntilGreaterThan in NullableLoopConditionTests.vb guard it.
  3. A user-defined CType returning Boolean? in a condition compares with True: (bool?)s.Value == true, with Until giving != true through the normal inversion. Self-verifying tests check that VB and C# both throw when s is Nothing and both give False when the operator returns Nothing, for both S? and non-nullable S. The proposed form was (bool?)s == true. .Value was added because without it C# lifts a null s to false instead of throwing as VB does.
  4. Not in conditions:
    • (a) Not of a nullable ordering comparison on a totally ordered type flips the lifted operator.
    • (b) !(b == false) / !(b == true) on bool? become b != false / b != true.
    • (c) Floating point and =/<> keep the existing form, and a test pins that.

Review

Five blind review rounds on commits 1-11; round 5 found only nitpicks. Commits 12-16 implement the decisions above. Round 6 reviewed commits 12-16 and found no bugs in them. It found two bugs in the area (commits 17, 18) and two quality items (commits 19-21); all are fixed. Commits 17-21 have not had a further blind review round. Declined nitpicks:

  • Redundant parentheses in (c?.S) is { } arg1. AddParensUnlessPrimary is shared with member-access operands, where wrapping ?. is needed ((c?.S).X isn't the same as c?.S.X).
  • Optional refactor of InvokeConversionWhenNotNull in terms of ConvertWhenNotNullElseFalse. The only shared part is the single HasValue(vbExpr, ref csExpr) call; the two then combine differently (? (T?)conv : null vs && conv).
  • The BOM on TypeConversionAnalyzer.cs line 1. It matches the other files in the directory.
  • Some overlap between NullableConditionTests.vb and NullableLoopConditionTests.vb, and a repeated Do Until s pin.

Semantics (verified by running VB in the self-verifying tests)

Roslyn Binder_Statements.vb: BindWhileBlock/BindDoLoop bind the condition with BindBooleanExpression, and Until is a flag applied after the conversion. So Do Until <Nothing> is not done and the loop continues. The VB side of NullableLoopConditionTests.vb passes with these expectations. In a condition, VB lifts intrinsic conversions and user-defined conversions that return Boolean, so Nothing is False (NullableConditionOptionStrictOffTests.vb, NullableConditionTests.NullableStructureWithWideningToBooleanConditions). Outside conditions, the conversion throws. A user-defined conversion returning Boolean? isn't lifted that way: a Nothing operand throws, and a Nothing result is False.

Output

VB C#
While currentDate <= endDate (issue repro, also parenthesized) while (currentDate <= endDate)
While x = y while (x.HasValue && y.HasValue && x.Value == y.Value)
While b / While Not b while (b == true) / while (b == false)
Do Until x < y while (!(x < y))
Do Until x = y while (!x.HasValue || !y.HasValue || x.Value != y.Value)
Do Until b / Do Until b = True while (b != true)
Loop Until Not b while (b != false);
Do Until x > y AndAlso i < 3 (x, y nullable; i Integer) while (!(x > y) || i >= 3)
If If(f, x < y, x > y) / Do Until If(f, b, Not b) (Boolean? ternary) if ((f ? A : B) == true) (A, B: the expanded nullable comparisons) / while ((f ? b : !b) != true)
Do Until If(f, a, b) (Boolean) while (!(f ? a : b)) (was while (!f ? a : b))
While Not (x < y) / Do Until Not (x < y) / If Not (x <= y) (Integer?, Decimal?, enum?…) while (x >= y) / while (!(x >= y)) / if (x > y)
While Not (a < 1.0) (Double?/Single?) / If Not (x = y) unchanged: while ((a.HasValue ? a.Value < 1.0d : null) == false) / if ((x.HasValue && y.HasValue ? x.Value == y.Value : null) == false)
Catch e When x = y / When (b) when (x.HasValue && y.HasValue && x.Value == y.Value) / when (b == true)
Option Strict Off, x As Integer?: If x / Do Until x if (Conversions.ToBoolean(x.GetValueOrDefault())) / while (!Conversions.ToBoolean(x.GetValueOrDefault()))
Option Strict Off, o As Object: While o / Do Until o > 1 while (Conversions.ToBoolean(o)) / while (!Conversions.ToBoolean(Operators.ConditionalCompareObjectGreater(o, 1, false)))
s As S? with Widening CType to Boolean: If s / Do Until s / Loop While GetS() if (s.HasValue && (bool)s.Value) / while (!s.HasValue || !(bool)s.Value) / while (GetS() is { } arg1 && (bool)arg1);
If If(s, GetS()) / Do Until If(s.HasValue, s, GetS()) if ((s ?? GetS()) is { } arg2 && (bool)arg2) / while (!((s.HasValue ? s : GetS()) is { } arg3) || !(bool)arg3)
Widening CType to Boolean?: If s / Do Until s (s As S?) / While ns (ns As S) if ((bool?)s.Value == true) / while ((bool?)s.Value != true) / while ((bool?)ns == true)

Non-nullable output is unchanged, e.g. Do Until i < 1 still gives while (i >= 1). Changed existing expectations: only the Until outputs above, which are now simplified through &&/||/!=, and the Boolean? operator pin (decision 3).

For reviewer attention

  • IsNonBooleanNullable excludes Boolean? conditions, because the nullable Boolean logic already gives them bool.
  • Decision 3 uses .Value on a nullable operand, which differs from the proposed (bool?)s == true (see Decisions).
  • Decision 4(a) treats enums as totally ordered. It doesn't apply to structures with user-defined operators or Object operands; those keep the existing form.

Out of scope (found while investigating; possible separate issues)

  • Select Case False / Select Case someBool with Case <expr> converts to case object _ when <expr>, which ignores the selector value. With Select Case True, a Boolean? case value gives a bool? when-clause (CS0266).
  • A lambda body of type Boolean? converted to Func(Of T, Boolean) (e.g. xs.Where(Function(x) x > 2)) is a narrowing conversion that throws on Nothing in VB. It isn't a condition context.
  • Evaluation order (existing nullable converter bug, If is affected too): If f() = g() where f() returns Integer? and g() has side effects converts to g() is var a && f() is {} b ..., which calls g() first. VB calls f() first. Found with a self-verifying test, in ForRelationalOperators (binds the RHS first when the LHS is nullable).
  • Existing bug unrelated to nullables: Do Until d < 1.0 (Double) becomes while (d >= 1.0d), which is wrong for NaN (VB keeps looping, C# stops).
  • Option Strict Off, While x AndAlso y with Integer? operands gives a correct but ~300-character expression.
  • Option Strict Off, If Not x with x As Integer? gives CS0023 (! on int?; it should be ~x). Master does the same.
  • Existing, now reachable from more conditions: the argN temporary names come from a counter that doesn't check scope, so a local Dim arg1 = 1 in the same method plus a nullable-struct condition that needs a temporary gives CS0128.
  • Existing: a ternary used as the condition of another ternary isn't parenthesized: If(If(f, a, b), 0, 3) gives f ? a : b ? 0 : 3, which C# parses as f ? a : (b ? 0 : 3) (CS1503 in an argument). Found while writing commit 18's test.
  • Existing: structures with Operator IsTrue/IsFalse aren't supported at all. The operator declarations fail to convert, and If t gives CS0029.

Tests

  • Conversion tests:
    • LoopStatementTests.NullableComparisonInWhileConditionIssue1300Async: the exact repro plus the parenthesized form.
    • NullableLoopConditionsTreatNothingAsFalseAsync: every loop form.
    • OptionStrictOffNonBooleanConditionsTreatNothingAsFalseAsync: If/While/Do Until on Integer? and Object.
    • NullableStructureWithWideningToBooleanConditionsTreatNothingAsFalseAsync: If/While/Do Until/Loop While on a struct with a user-defined operator, including a non-reusable call and coalesce/ternary conditions.
    • StructureWithWideningToNullableBooleanConditionsCompareResultWithTrueAsync: decision 3.
    • NotOfNullableOrderingComparisonFlipsLiftedOperatorAsync / NotOfFloatingPointOrEqualityNullableComparisonIsNotFlippedAsync: decision 4.
    • OptionStrictOffNullableToBooleanOutsideConditionsKeepsValueAsync: the boundary, where .Value is kept outside conditions.
    • NonNullableUntilConditionsAreInvertedAsync: pins the unchanged simple output.
    • MethodStatementTests.TryCatchWithNullableFilterTreatsNothingAsFalseAsync.
    • BinaryExpressionTests.ParenthesizedNullableConditionsTreatNothingAsFalseAsync.
  • Self-verifying (VB and C# both run):
    • NullableConditionTests.vb:
      • Parenthesized If, Catch When filters, and query Where/Take While/Skip While with Nothing.
      • A nullable struct whose operator maps the default to True, in If/While/Do Until, with a call evaluated once.
      • Ternary/coalesce struct conditions in If and Do Until.
      • Boolean? ternary conditions in If, Do Until and Loop Until (commit 17).
      • A Boolean? CType, for a Nothing result and a Nothing operand (throws).
    • NullableLoopConditionTests.vb:
      • While, Do While/Until, Loop While/Until, Not, AndAlso/OrElse, Until Not, the condition evaluated once per iteration, the issue's Date repro, ternary.
      • Not of </<=/>/>= with Nothing on each side.
      • The non-flippable Double (NaN) and = cases.
      • A nullable and a non-nullable comparison in one Until condition (commit 19).
    • BooleanTests.vb: Do Until If(f, a, b) and Do Until If(f, True, b) (commit 18).
    • NullableConditionOptionStrictOffTests.vb:
      • Integer?/Double? conditions in If/While/Do Until/Loop While.
      • Object conditions with Nothing, "True", "0" and 2.
      • The nullable struct in If/Do Until.
      • Throws-on-Nothing outside conditions: Dim b As Boolean = x, If IsTrue(x), If CBool(x).
  • Each fix's tests failed first for the expected reason. Decision 4(a)'s conversion test failed on the old == false form, and its self-verifying test passed before and after, because that output was already correct. Commit 17's tests failed with CS0173/CS0266, commit 18's at runtime (the loop ran when VB's didn't), and commit 19's conversion test on !(i < 3).
  • Full suite (at commit 21): 1548 passed, 0 failed, 5 skipped (1553). Master baseline: 1425 passed, 0 failed, 5 skipped (1430). No new failures; +123 tests. Commits 13-15 each build and pass the loop/nullable condition tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg

Pure refactor so loop conditions can share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
VB's binder passes the condition context through parentheses, so Nothing
means False. Previously a (bool) cast was emitted, which throws on null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Pure refactor: top and bottom While/Until clauses share one conversion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
VB converts these conditions to Boolean (Nothing to False) like If conditions,
but they weren't treated as Boolean contexts, so a Boolean? condition gave CS0266.
Until conditions are converted before being inverted, matching VB, so an Until
loop continues while its condition is Nothing.

Fixes icsharpcode#1300

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
With Option Strict Off, a nullable non-Boolean condition (e.g. If x Then, While x
with x As Integer?) converted to Conversions.ToBoolean(x.Value), which throws when
x is Nothing. VB lifts the conversion and treats Nothing as False, so use
GetValueOrDefault: its zero default also converts to False.
Also pins Option Strict Off Object conditions in loops, which already worked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Pure refactor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
…nversion

A nullable structure with a Widening CType to Boolean used as a condition
converted to (bool)s, which throws when s is Nothing. VB lifts the conversion
and treats Nothing as False. Since the operator may convert the default value
to True, check HasValue (or pattern-match a non-reusable expression) first.
Also pins that conversions outside conditions keep .Value and throw like VB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
@GrahamTheCoder
GrahamTheCoder marked this pull request as draft November 10, 2026 19:21
@GrahamTheCoder GrahamTheCoder changed the title Claude/fix 1300 nullable loop conditions Fix #1300 nullable loop conditions Nov 10, 2026
@GrahamTheCoder GrahamTheCoder changed the title Fix #1300 nullable loop conditions Fix #1300 - nullable loop conditions Nov 10, 2026
A conditional or coalesce expression as the operand of a generated
'is { } argN' / 'is var argN' pattern was emitted without parentheses, so the
C# text parsed differently (e.g. If(a, b, c) as a nullable structure condition
gave CS0173). The syntax tree was right, so only reparsing showed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
VB doesn't lift a CType returning Boolean? the same way: it throws for a
Nothing operand and treats a Nothing result as False. Leave such conditions
with the previous cast output rather than emitting HasValue checks that hide
the throw and still throw on a Nothing result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Pure refactor of the NullableBool condition, plus a comment explaining why a
test pins master's output for CType returning Boolean?.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Pure refactor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
A condition converted by a user-defined CType returning Boolean? gave (bool)s,
which throws when the operator returns Nothing; VB treats that as False. Emit
(bool?)s.Value == true: .Value keeps VB's throw for a Nothing operand, which C#
would otherwise lift to null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
…tions

A lifted <, <=, > or >= is false when an operand is null, so its negated
operator would be false too rather than true. The output is unchanged: the
inversion previously stopped at a redundant (bool) cast by coincidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
The (bool) cast was redundant (the simplifier removed it), but it stopped an
Until loop's inversion from simplifying, e.g. Loop Until Not b gave
!(b == false). Now it gives b != false, and Do Until b = True gives b != true.
Other Until conditions are now inverted through && and ||.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
… conditions

Where Not (x < y) is only tested for True, x >= y is equivalent: both are false
when an operand is Nothing. So While Not (x < y) gives while (x >= y), and
Do Until Not (x < y) gives while (!(x >= y)). Floating point (NaN is unordered),
Boolean, structures, and = and <> keep the existing form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
A ternary Boolean? condition such as If If(f, x < y, x > y) or Do Until
If(f, b, Not b) gave f ? A : B == true, which reparses as f ? A : (B == true)
(CS0173/CS0266).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Inverting only the condition of a ternary swaps its arms rather than negating
it: Do Until If(f, a, b) gave while (!f ? a : b). Now it gives
while (!(f ? a : b)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
…ions

InvertCondition now refuses to flip a <, <=, > or >= the nullable converter
marked as a lifted comparison whose operand may be null, rather than Until
loops rescanning the VB condition and refusing every ordering comparison in
it. So Do Until x > y AndAlso i < 3 (x, y nullable) gives
!(x > y) || i >= 3 instead of !(x > y) || !(i < 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
Pure refactor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
…NegatedForParentNotAnnotation

Pure refactor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg
@GrahamTheCoder
GrahamTheCoder marked this pull request as ready for review November 11, 2026 14:23
@GrahamTheCoder
GrahamTheCoder merged commit 900c916 into icsharpcode:master Nov 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VB -> C#: Nullable comparisons in While conditions produce invalid C#

2 participants