Two real defects found by an audit of nuwayze, both confirmed against the code. 1. Recall: the switch 'case ChatListLoaded value => value' binds the type on a DeclaredVariablePattern, not the object pattern ChatListLoaded() this matcher looked for. An exhaustive switch was therefore invisible and two variants were wrongly reported as never rendered. Both the mention collector and the branch collector now handle declared variable patterns. 2. Precision: the UI often reads a union's payload off the BASE class (state.currentBooking, state.isRefreshing), so the data reaches the user even though the variant is never matched. StateUnion now records uiReadsBaseFields and those findings drop to info with an explanation instead of warning. That removed 5 false-positive warnings on nuwayze. 3. recall/manifest.json gains status=open|fixed. A fixed case that is reported again is a REGRESSION, so repaired bugs become permanent guards against the real code instead of deleted expectations. The three bugs fixed earlier today (dex x2, word_alive) are now fixed-status guards: recall --strict -> 5/5 accounted for, 0 regressions. nuwayze: 111 findings, 11 warnings (was 16). 75 tests, fixture regression guard patternView covers the typed-pattern case.
139 lines
4 KiB
Dart
139 lines
4 KiB
Dart
import 'notifier_gaps.dart';
|
|
import 'provider_setup.dart';
|
|
|
|
/// Matched only through typed variable patterns. An object-pattern-only matcher
|
|
/// reads `PatternA()` but not `PatternA value`, so it would wrongly call both
|
|
/// variants unrendered - this function is the regression guard for that.
|
|
///
|
|
/// The wildcard is needed because PatternState is a plain abstract union, not a
|
|
/// sealed one: the compiler cannot know it is closed. That mirrors the real
|
|
/// nuwayze case this rule was fixed against.
|
|
Object? patternView(PatternState state) => switch (state) {
|
|
PatternA value => value,
|
|
PatternB value => value,
|
|
_ => null,
|
|
};
|
|
|
|
/// A provider declaration, so the collector can see which types are Listenable.
|
|
final liveProvider = ChangeNotifierProvider<LiveNotifier>(
|
|
create: (_) => LiveNotifier(),
|
|
);
|
|
|
|
/// A provider for an immutable type, created once outside any build. Together
|
|
/// with [PassesConfigView] below this is the negative control for
|
|
/// `provider-created-in-build`: a `Provider<AppConfig>` exists, but both uses go
|
|
/// through `.value`, so neither may be reported.
|
|
final appConfigProvider = Provider<AppConfig>.value(value: const AppConfig());
|
|
|
|
/// BAD: reads a ChangeNotifier without subscribing inside build, so this widget
|
|
/// will not rebuild when the notifier changes.
|
|
class ReadInBuildView {
|
|
const ReadInBuildView();
|
|
|
|
Object build(BuildContext context) {
|
|
final live = context.read<LiveNotifier>();
|
|
return live.value;
|
|
}
|
|
}
|
|
|
|
/// BAD: the same mistake through Provider.of(..., listen: false).
|
|
class ProviderOfView {
|
|
const ProviderOfView();
|
|
|
|
Object build(BuildContext context) {
|
|
return Provider.of<LiveNotifier>(context, listen: false).value;
|
|
}
|
|
}
|
|
|
|
/// GOOD: subscribes, so it rebuilds.
|
|
class WatchInBuildView {
|
|
const WatchInBuildView();
|
|
|
|
Object build(BuildContext context) {
|
|
return context.watch<LiveNotifier>();
|
|
}
|
|
}
|
|
|
|
/// GOOD: an immutable service is not a Listenable, so not subscribing is
|
|
/// correct and a rule that flagged every context.read would be wrong here.
|
|
class ServiceInBuildView {
|
|
const ServiceInBuildView();
|
|
|
|
Object build(BuildContext context) {
|
|
return context.read<AppConfig>();
|
|
}
|
|
}
|
|
|
|
/// GOOD: read in a callback, where not rebuilding is exactly what you want.
|
|
class CallbackReadView {
|
|
const CallbackReadView();
|
|
|
|
Object build(BuildContext context) => const Object();
|
|
|
|
void onTap(BuildContext context) {
|
|
context.read<LiveNotifier>().setValue(1);
|
|
}
|
|
}
|
|
|
|
class Button {
|
|
const Button({required this.onPressed});
|
|
|
|
final void Function() onPressed;
|
|
}
|
|
|
|
/// GOOD: the read is inside a closure passed to a widget, so it runs later, not
|
|
/// while this build method runs.
|
|
class ClosureReadView {
|
|
const ClosureReadView();
|
|
|
|
Object build(BuildContext context) {
|
|
return Button(
|
|
onPressed: () => context.read<LiveNotifier>().setValue(1),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Consumed by the UI, but only `value` is displayed: `unusedFlag` is dead state.
|
|
class PartiallyReadView {
|
|
const PartiallyReadView();
|
|
|
|
Object build(BuildContext context) {
|
|
return context.watch<PartiallyReadNotifier>().value;
|
|
}
|
|
}
|
|
|
|
/// BAD: a provider constructed inside build without `.value`, so a new notifier
|
|
/// is created on every rebuild.
|
|
class CreatesProviderView {
|
|
const CreatesProviderView();
|
|
|
|
Object build(BuildContext context) {
|
|
return ChangeNotifierProvider<LiveNotifier>(create: (_) => LiveNotifier());
|
|
}
|
|
}
|
|
|
|
/// GOOD: `.value` passes an instance created once.
|
|
class PassesValueView {
|
|
const PassesValueView(this.notifier);
|
|
|
|
final LiveNotifier notifier;
|
|
|
|
Object build(BuildContext context) {
|
|
return ChangeNotifierProvider<LiveNotifier>.value(value: notifier);
|
|
}
|
|
}
|
|
|
|
/// GOOD: an immutable value passed with `.value` inside build.
|
|
///
|
|
/// This and [appConfigProvider] are the negative controls for
|
|
/// `provider-created-in-build`: the type `AppConfig` is provided, but only via
|
|
/// `.value`, which passes an instance created once.
|
|
class PassesConfigView {
|
|
const PassesConfigView(this.config);
|
|
|
|
final AppConfig config;
|
|
|
|
Object build(BuildContext context) {
|
|
return Provider<AppConfig>.value(value: config);
|
|
}
|
|
}
|