Skip to content

fix(nullability): converters told the compiler Avalonia never passes null, and it does - #227

Merged
tylerkron merged 1 commit into
mainfrom
warning-zero/converter-null-contract
Sep 4, 2026
Merged

tylerkron merged 1 commit into
mainfrom
warning-zero/converter-null-contract

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

Not merging — for review.

Eleven of the twelve value converters in this app declared value and parameter as
non-nullable. Avalonia passes null for parameter on every binding that has no
ConverterParameter — which is nearly all of them — and null for value on any binding
that has not resolved yet. The annotation asserted something the framework contradicts on
the first call. Same story for the two Equals(object obj) overrides: the runtime calls
Equals with null.

This declares those parameters the way the interface declares them. Nothing else.

The class

One rule, applied everywhere it holds: a parameter's nullability must match the member
being implemented or overridden.

Code Count Where
CS8767 36 10 converters + NaturalStringComparer.Compare (implicit interface impl)
CS8769 4 EnumDescriptionConverter (explicit interface impl)
CS8765 2 AbstractChannel.Equals, LoggingSession.Equals (override)
42

NaturalSortHelper.NaturalCompare is annotated too, though it warned about nothing: the
comparer that delegates to it now accepts string?, and NaturalCompare already opens with
three null branches. Leaving it non-nullable would have traded two CS8767 for two CS8604.

Why this is behaviour-preserving

Nullable reference annotations are erased at runtime — this is metadata, not IL. And every
body was already null-safe before the change:

  • nine of the eleven converters test with is patterns, which are null-safe by construction;
  • ListToStringConverter opens with an explicit if (value == null) return string.Empty;;
  • NotNullToVisibilityConverter literally returns value != null;
  • NaturalCompare opens with three null branches;
  • both Equals overrides use is / is not patterns.

So no null check was added, removed, or moved. The interesting case is
ListToStringConverter: its null guard was unreachable-looking before this, because the
compiler had been told value could not be null. The guard was right and the signature was
wrong. That is the shape of the whole change.

No renames, no removals, no member moved — so there is no XAML-binding exposure here. Every
converter keeps its type name and its interface methods; only parameter annotations change.

Measurement

Both numbers measured in the same worktree, cold: obj/ and bin/ removed tree-wide
(including third_party/oxyplot-avalonia/obj, which only re-emits its 20 warnings when its
own obj/ is cold) and then built with the same command.

dotnet build Daqifi.Avalonia.Desktop/Daqifi.Avalonia.Desktop.csproj
merge-base (origin/main, 5414c07) this branch
Total warnings 235 193
distinct first-party 215 173
distinct third_party/ 20 20

Diffing the distinct first-party warning set on both sides, the delta is a pure
deletion
: 42 lines removed, 0 added, 0 changed, no line-number shift (the diff is
signature-only, so no line was added or removed from any file). Attribution, all 42:

  • 36 CS8767 — BoolAndToVisibilityConverter (1), BrushColorMatchConverter (1),
    BoolToActiveStatusConverter (4), BoolToStatusColorConverter (4),
    ConnectionTypeToColorConverter (4), ListToStringConverter (4),
    NotNullToVisibilityConverter (4), OxyColorToBrushConverter (4),
    BooleanToInverseBoolConverter (4), IntToVisibilityConverter (4),
    NaturalSortHelper (2)
  • 4 CS8769 — EnumDescriptionConverter
  • 2 CS8765 — AbstractChannel, LoggingSession

Every other warning code is unchanged in both count and text: CS8618 78, CS8601 15,
CS8622 13, CS8600 13, CS8625 9, CS8602 9, CS8604 8, CS0618 8, CS8603 6,
CA1416 5, MVVMTK0034 5, CS0067 2, AVLN3001 1, CS0219 1.

Diffstat

14 files changed, 24 insertions(+), 24 deletions(-)

24 for 24, no line added or removed — every changed line is a signature gaining a ?. Not
net-additive; not net-smaller either, because there is nothing to delete here. What gets
smaller is the set of false claims the code makes about itself.

Deliberately out of scope

  • EnumDescriptionConverter.cs(46): CS8603 Possible null reference return sits in a file
    this PR touches and survives it. It is a return-nullability issue, not a parameter one,
    and fixing it means choosing whether the converter may return null — a behaviour decision.
    Different class, different PR.
  • Nothing was suppressed. No #pragma, no NoWarn, no attribute. The 42 warnings are gone
    because the declarations are now true.

Verification

  • dotnet build Daqifi.Avalonia.Desktop — green, 193 warnings, 0 errors
  • dotnet restore Daqifi.Avalonia.iOS then
    dotnet build Daqifi.Avalonia.iOS -c Release -r iossimulator-arm64 --no-restore — green,
    194 warnings, 0 errors (no merge-base pair measured for iOS; reported as an absolute only)
  • dotnet test Daqifi.Avalonia.Tests — 462 passed, 0 failed
  • packages.lock.json unchanged in all six lock-file projects (restore was run without -r)
  • The Android head is not built here — the workload is absent on this machine. CI covers it.

Prior art in this repo

The correct shape was already present, in every file that was written by hand rather than
scaffolded: BoolOrConverter.Convert already takes object? parameter, and
FirmwareOption.Equals / DaqifiStreamingDevice.Equals already take object? obj. All
fourteen files changed here carry the // Generated by portomatic scaffold header. This
brings the scaffolded stragglers into line with the hand-written ones.

…null, and it does

Avalonia calls IValueConverter.Convert with a null `value` for any unset binding and a null `parameter` on every binding without a ConverterParameter — which is most of them. Eleven of the twelve converters here declared those parameters non-nullable anyway, so the annotation asserted something the framework contradicts on the first call. Same for the two `Equals(object obj)` overrides, which the runtime calls with null.

The bodies were already null-safe (`is` patterns, an explicit `value == null` guard in ListToStringConverter, three null branches at the top of NaturalCompare), so nothing about behaviour changes — only the declaration stops lying. ListToStringConverter's null guard in particular read as dead code before this: the compiler had been told value could not be null.

Signature-only: 24 insertions, 24 deletions, no line added or removed. Desktop warnings 235 -> 193 on cold builds of merge-base and branch; the distinct first-party warning set differs by exactly the 42 removed lines (36 CS8767, 4 CS8769, 2 CS8765) with no additions and no line-number shift.

The correct shape was already in the tree — BoolOrConverter, FirmwareOption.Equals and DaqifiStreamingDevice.Equals are all hand-written and all get it right. This brings the portomatic-scaffolded stragglers into line with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Align converter and override signatures with nullable contracts

🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Aligns Avalonia converter signatures with nullable framework contracts.
• Corrects comparer and equality overrides to accept runtime-null inputs.
• Removes 42 nullability warnings without changing runtime behavior.
Diagram

classDiagram
class AvaloniaBindings
class IValueConverter {
  <<interface>>
}
class IMultiValueConverter {
  <<interface>>
}
class ConverterClasses
class IComparerString {
  <<interface>>
}
class NaturalComparer
class ObjectBase
class DomainModels
AvaloniaBindings --> IValueConverter : invokes
AvaloniaBindings --> IMultiValueConverter : invokes
ConverterClasses ..|> IValueConverter : nullable signatures
ConverterClasses ..|> IMultiValueConverter : nullable parameter
NaturalComparer ..|> IComparerString : nullable strings
DomainModels --|> ObjectBase : nullable Equals
Loading
High-Level Assessment

The signature-only approach is optimal because it directly matches the implemented framework and runtime contracts without changing behavior. Warning suppression or added null guards would either hide incorrect metadata or duplicate null handling already present in the method bodies.

Files changed (14) +24 / -24

Bug fix (14) +24 / -24
AbstractChannel.csAllow null in AbstractChannel equality checks +1/-1

Allow null in AbstractChannel equality checks

• Changes the Equals override argument to object? so its declaration matches object.Equals. The existing pattern match already rejects null safely.

Daqifi.Avalonia/Daqifi.Desktop/Channel/AbstractChannel.cs

BoolAndToVisibilityConverter.csCorrect multi-value converter parameter nullability +1/-1

Correct multi-value converter parameter nullability

• Marks the optional converter parameter nullable to match IMultiValueConverter. Boolean aggregation behavior remains unchanged.

Daqifi.Avalonia/Daqifi.Desktop/Converters/BoolAndToVisibilityConverter.cs

BoolToActiveStatusConverter.csCorrect active-status converter nullability +2/-2

Correct active-status converter nullability

• Marks value and parameter inputs nullable for both conversion directions. Existing type-pattern handling continues to provide the fallback behavior for null values.

Daqifi.Avalonia/Daqifi.Desktop/Converters/BoolToActiveStatusConverter.cs

BoolToStatusColorConverter.csCorrect status-color converter nullability +2/-2

Correct status-color converter nullability

• Aligns Convert and ConvertBack input annotations with IValueConverter by accepting nullable values and parameters. Conversion behavior is unchanged.

Daqifi.Avalonia/Daqifi.Desktop/Converters/BoolToStatusColorConverter.cs

BrushColorMatchConverter.csAllow a null brush-match converter parameter +1/-1

Allow a null brush-match converter parameter

• Marks the unused multi-value converter parameter nullable, matching Avalonia calls without a ConverterParameter. Brush comparison logic is unchanged.

Daqifi.Avalonia/Daqifi.Desktop/Converters/BrushColorMatchConverter.cs

ConnectionTypeToColorConverter.csCorrect connection-color converter nullability +2/-2

Correct connection-color converter nullability

• Allows nullable value and parameter inputs in Convert and ConvertBack. The existing connection-type pattern check remains responsible for fallback handling.

Daqifi.Avalonia/Daqifi.Desktop/Converters/ConnectionTypeToColorConverter.cs

ListToStringConverter.csExpose nullable list-converter inputs accurately +2/-2

Expose nullable list-converter inputs accurately

• Marks conversion values and parameters nullable, making the existing null-to-empty-string guard consistent with the declared contract. ConvertBack remains unsupported.

Daqifi.Avalonia/Daqifi.Desktop/Converters/ListToStringConverter.cs

NotNullToVisibilityConverter.csCorrect null-check converter signatures +2/-2

Correct null-check converter signatures

• Allows nullable inputs and parameters in both conversion methods. The converter still maps value presence directly to a boolean result.

Daqifi.Avalonia/Daqifi.Desktop/Converters/NotNullToVisibilityConverter.cs

OxyColorToBrushConverter.csCorrect OxyColor converter nullability +2/-2

Correct OxyColor converter nullability

• Marks Convert and ConvertBack inputs nullable to match IValueConverter. Existing pattern matching safely handles unresolved or incompatible values.

Daqifi.Avalonia/Daqifi.Desktop/Converters/OxyColorToBrushConverter.cs

BooleanToInverseBoolConverter.csCorrect inverse-boolean converter nullability +2/-2

Correct inverse-boolean converter nullability

• Allows nullable values and parameters in both conversion directions. Non-boolean and null values continue to produce false.

Daqifi.Avalonia/Daqifi.Desktop/Helpers/BooleanToInverseBoolConverter.cs

EnumDescriptionConverter.csCorrect explicit converter contract nullability +2/-2

Correct explicit converter contract nullability

• Updates both explicit IValueConverter implementations to accept nullable values and parameters. Existing null handling and enum-description behavior remain intact.

Daqifi.Avalonia/Daqifi.Desktop/Helpers/EnumDescriptionConverter.cs

IntToVisibilityConverter.csCorrect integer converter nullability +2/-2

Correct integer converter nullability

• Aligns Convert and ConvertBack inputs with the nullable IValueConverter contract. Existing integer pattern matching continues to handle null safely.

Daqifi.Avalonia/Daqifi.Desktop/Helpers/IntToVisibilityConverter.cs

NaturalSortHelper.csPropagate nullable strings through natural sorting +2/-2

Propagate nullable strings through natural sorting

• Allows NaturalCompare and NaturalStringComparer.Compare to receive nullable strings, matching IComparer<string>. Existing null-ordering branches preserve current comparison behavior.

Daqifi.Avalonia/Daqifi.Desktop/Helpers/NaturalSortHelper.cs

LoggingSession.csAllow null in LoggingSession equality checks +1/-1

Allow null in LoggingSession equality checks

• Changes the Equals override argument to object? to match the base object contract. The existing type pattern continues to reject null safely.

Daqifi.Avalonia/Daqifi.Desktop/Loggers/LoggingSession.cs

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review. Verified on head 839cabb: Qodo clean on both surfaces (comment marker commit/839cabb, reviewThreads 0 unresolved) with a settled re-check; CI run 33810629358 green on all six heads, its own logs reading 193 Warning(s) / 0 Error(s) and Failed: 0, Passed: 462. The 42 removed warnings reconcile line-by-line against the diff (36 CS8767 + 4 CS8769 + 2 CS8765 across 24 changed signatures), and the CI log contains zero CS8767/CS8769/CS8765. No suppression anywhere and no build-config change; EnumDescriptionConverter.cs(46) CS8603 correctly left untouched. XAML exposure checked and nil — no type, member or x:Key renamed, and NRT annotations are erased at runtime. main is still at 5414c07, and no test overrides Equals, so the #189-shaped hazard does not apply. Not merging.

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.

1 participant