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>.
79 lines
2.3 KiB
Dart
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);
|
|
});
|
|
}
|
|
}
|