pipe/test/fixtures/stuck_loading/lib/flag_cubit.dart
trvon 8d64718977
fix(loading-flag-not-reset): analyse closures once, and name handlers
The method-level check read the whole body text, closures included, for
both the `loading: true` probe and the await probe, while the try
collector skips closures. So a method whose closure set the flag and
guarded its own await was reported at high confidence as unguarded, and a
method that only registers `on<E>` closures was reported alongside each
closure. Every probe now stops at nested functions, which are analysed on
their own; the flag probe is a `field: true` named-argument visitor
instead of a regex over source.

`on<E>` handler closures were all named `<closure>`, so two handlers in one
class shared a fingerprint and encodeBaseline kept only one of them. They
are now named `on<E>`. Existing baselines that accepted a handler as
`<closure>` will see it once as new.

Fixture: scheduleGuarded (guarded closure) and registerHandlers are now
negative controls; two unguarded handlers must yield two findings with
distinct fingerprints. Before: scheduleGuarded, registerHandlers and two
colliding `<closure>` findings; after: on<FlagRefresh>, on<FlagReload>.
2026-09-22 13:30:31 -06:00

79 lines
2.3 KiB
Dart

import 'dart:async';
import 'bloc.dart';
/// A single-class state with a loading flag and no variant union — the style
/// that the variant-based rules cannot see.
class FlagEvent {}
class FlagRefresh extends FlagEvent {}
class FlagReload extends FlagEvent {}
class FlagState {
const FlagState({this.items = const <int>[], this.loading = true});
final List<int> items;
final bool loading;
FlagState copyWith({List<int>? items, bool? loading}) => FlagState(
items: items ?? this.items,
loading: loading ?? this.loading,
);
}
class FlagCubit extends Bloc<FlagEvent, FlagState> {
FlagCubit() : super(const FlagState());
/// BAD: puts `loading: true` in flight, then awaits with no catch at all.
Future<void> loadUnguarded() async {
emit(state.copyWith(loading: true));
await Future<void>.delayed(Duration.zero);
emit(state.copyWith(loading: false));
}
/// GOOD: the awaited work is inside a try whose catch resets the flag.
Future<void> loadGuarded() async {
emit(state.copyWith(loading: true));
try {
await Future<void>.delayed(Duration.zero);
emit(state.copyWith(loading: false));
} catch (error) {
emit(state.copyWith(loading: false));
}
}
/// GOOD: awaits but never puts the loading flag in flight.
Future<void> refreshQuietly() async {
await Future<void>.delayed(Duration.zero);
}
/// GOOD: the flag, the await and the guarding catch all live in a closure,
/// which is analysed on its own. The enclosing method sets nothing and awaits
/// nothing itself, so reporting it would be a false positive.
void scheduleGuarded() {
_later(() async {
emit(state.copyWith(loading: true));
try {
await Future<void>.delayed(Duration.zero);
} catch (error) {
emit(state.copyWith(loading: false));
}
});
}
void _later(Future<void> Function() work) {}
/// BAD, twice: two unguarded handlers in one class. Each is its own finding
/// with its own fingerprint, not one `<closure>` that hides the other.
void registerHandlers() {
on<FlagRefresh>((event, emit) async {
emit(state.copyWith(loading: true));
await Future<void>.delayed(Duration.zero);
});
on<FlagReload>((event, emit) async {
emit(state.copyWith(loading: true));
await Future<void>.delayed(Duration.zero);
});
}
}