Skip to content

refactor: make the unreachable-surface guard see every observable property - #2115

Merged
laurentiu021 merged 1 commit into
mainfrom
refactor/unreachable-surface-guard-and-dead-properties
Sep 4, 2026
Merged

refactor: make the unreachable-surface guard see every observable property#2115
laurentiu021 merged 1 commit into
mainfrom
refactor/unreachable-surface-guard-and-dead-properties

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Part of #2100. refactor: — no behaviour change, no release.

The guard for unreachable model surface could not see 12% of it

EveryModelProperty_IsEitherWrittenOrShown exists to catch a property that is never written and never shown. Its detection pattern was:

\[ObservableProperty\][^\n]*\n?\s*(?:private|internal)\s+[\w\?<>,\[\]\. ]+?\s+_(\w+)\s*[;=]

[^\n]*\n? is wrong in both directions.

It skips forward. For the single-line form followed by a plain field, the prefix consumes the attribute's own line, crosses the newline, and captures the next field:

[ObservableProperty] private string _quickActionNavigateLabel = "";
private string? _quickActionNavigateTarget;      // <- this is what the pattern captured

Four such mis-captures exist in ViewModels/ today (_quickActionNavigateTarget, _suppressAllSelected, _blocker, _disposed) — private fields reported as observable properties, while the real property beside each went unchecked.

And one newline is not enough. The multi-attribute form never matched at all:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HealthPercent))]
private double? _temperatureC;

That is 23 of the 187 [ObservableProperty] declarations in Models/, six of them on DiskHealthReport alone.

The pattern now allows whitespace and any number of further attributes between the marker and the field, and nothing on the attribute's own line can be walked past.

The floor was the part that made it invisible

The old pattern saw 164 of 187 and the assertion was checkedProperties >= 40, so a 12% blind spot reported perfect health for as long as it shipped. Floor re-measured and raised to 180.

Mutation proof, restored byte-for-byte:

Mutation Result
the prefix goes back to the greedy form REDonly 164 model properties were inspected, out of 187 measured
greedy prefix and the old floor of 40 GREEN

The second row is the finding, not a footnote: with the old floor the old pattern is undetectable. The regex was the defect, the floor value is what let it live.

Re-running the corrected guard over all 187 properties finds no new dead model surface — so this is purely a coverage fix, not a bug hunt with a result.

Three view-model properties removed

From the hand-measured list in #2100, the three that are redundant rather than a missing capability:

Property State
SpeedTestViewModel.SpeedStatus declared, never assigned, never read; the literal SpeedStatus appeared exactly once in the whole app
DiskAnalyzerViewModel.TotalFolders assigned from a full recursive Entries.Sum(e => e.FolderCount) on every scan, read nowhere
DiskAnalyzerViewModel.CurrentFolder assigned and cleared on the scan path, read nowhere — and the next line already puts the same value into StatusMessage, which is bound

Constructor_InitialState_IsCorrect asserted TotalFolders == 0, which is exactly what made it look exercised; the assertion went with the property and the reason is recorded where it was.

The other four in #2100 stay open, because each is a missing capability rather than dead weight — the active Menu Style preset, clickable release-history entries, free space on the chkdsk picker, release notes on About — and they need a mockup first.

Not done here

Widening the guard to ViewModels/ is still open on #2100. It needs a different rule: a model property's mere appearance in its own file proves use, but a view-model property is normally assigned there, so every write-only property looks alive. The rule has to distinguish a read from a write, and a first pass at it produced two false positives from same-name collisions across types — which is why it is not in this PR.

Verification

Builds 0 errors / 0 warnings (app + tests), dotnet format --verify-no-changes clean on both, 143 cases green across ArchitectureTests, DiskAnalyzerViewModelTests and SpeedTestViewModelTests. No version bump and no CHANGELOG entry: nothing user-visible changed and refactor: does not release.

…perty

EveryModelProperty_IsEitherWrittenOrShown detects [ObservableProperty] fields with
the prefix [^\n]*\n?, which is wrong in both directions.

It skips forward: for the single-line form followed by a plain private field, the
prefix eats the attribute's own line, crosses the newline and captures the NEXT
field. Four such mis-captures exist in ViewModels/ today, each hiding the real
property beside it.

And one newline is not enough for the multi-attribute form, so every property with a
[NotifyPropertyChangedFor] beside it was invisible -- 23 of the 187 declarations in
Models/, six on DiskHealthReport alone.

The floor is what made that survivable: 164 of 187 matched, and the assertion was
>= 40. Re-measured and raised to 180. A mutation proof shows the greedy prefix now
reddens, and that with the old floor of 40 it stays GREEN -- the regex was the
defect, the floor value is what let it live. The corrected guard finds no new dead
model surface, so this is a coverage fix rather than a bug hunt with a result.

Also removes three view-model properties from the hand-measured list in #2100:
SpeedStatus (declared, never assigned, never read), TotalFolders (a full recursive
Sum per scan, read nowhere) and CurrentFolder (read nowhere, and the next line
already puts the same value into the bound StatusMessage). The assertion on
TotalFolders' default -- which is what made it look exercised -- went with it.
@laurentiu021
laurentiu021 merged commit 1cb67be into main Sep 4, 2026
5 checks passed
@laurentiu021
laurentiu021 deleted the refactor/unreachable-surface-guard-and-dead-properties branch September 4, 2026 14:38
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