pipe/test/fixtures/stuck_loading/lib/riverpod_gaps.dart
trvon fb055c9978
Some checks failed
Lint Commits / commitlint (pull_request) Successful in 19s
Tests / Static Analysis (pull_request) Has been cancelled
Tests / Tests (pull_request) Has been cancelled
merge: bring main into the audit and state-map work
main gained six PRs (#1-#6) while the audit fixes and the state-map seam
were in progress. Four of main's commits fixed the same bugs this branch
did, independently; where they overlap, the more complete version wins:

- Fingerprints: main's (a0844bb) - catchSite (`member#index` among all
  catches), union/owner as identity, a legacy fingerprint so version-1
  baselines still match, baseline "version": 2. This branch's `catch`
  ordinal is dropped; its fingerprint_test still guards the result.
- verify --rule: main's verifyAnalysis (cf9e0b7), which also reports
  findings in skipped files as NOT SCANNED.
- Unreadable SDK: both kept - this branch's up-front probe gives the
  early, specific error; main's catch around the analyzer (d6b6bcb) is
  the backstop.
- Riverpod: main's "a ref.listen is not a render" (f6411dc) and this
  branch's AsyncError patterns / stored-value tracking compose; the
  fixture keeps both sets of cases.

Ported onto the model-only rules (the seam), since main edited code this
branch had moved into front-end facts:
- excerpt.dart's literal-free, bounded quoting (a prompt-injection
  guard) now also covers front-end facts: SwallowedFailure.handler and
  NonSubscribingRead.excerpt, and the C++ front end elides string
  literals in handler evidence the same way.
- ScanCoverage: runChecks stays source-free; the model carries coverage
  (`scanCoverage` in the state JSON, written by both front ends), so
  `check --model` states coverage like a source check.
- main's softened wording (rendered-not-emitted "probably unreachable",
  the remove-policy dead-handler text, notifier-field-unread) and the
  backticked flag message.

Docs: main's README grew the Firestore query inventory and the output
contract; the README stays short, the inventory moves to
docs/queries.md, and the contract, exit codes, verify and baseline
details join docs/usage.md.

Checked against origin/main on resolved mirrors of the dogfood apps:
nuwayze (102), dex (0) and word_alive (0) identical; tick differs only
by the intended riverpod stored-value fix. On the fixture every
difference is one of this branch's fixes. 214 tests pass.
2026-09-24 07:33:53 -06:00

141 lines
4 KiB
Dart

/// Minimal stand-ins for Riverpod so the fixture needs no dependency. pipe
/// recognises providers by the `FutureProvider<T>(...)` / `Provider<T>(...)`
/// constructor and consumption by `ref.watch(...)`.
library;
class AsyncData<T> {
const AsyncData(this.value);
final T value;
}
class AsyncValue<T> {
const AsyncValue();
T? get value => null;
AsyncData<T>? get asData => null;
bool get hasError => false;
R when<R>({
required R Function(T value) data,
required R Function() loading,
required R Function(Object error, StackTrace stack) error,
}) =>
throw UnimplementedError();
}
class AsyncError<T> extends AsyncValue<T> {
const AsyncError();
}
class AsyncLoading<T> extends AsyncValue<T> {
const AsyncLoading();
}
class FutureProvider<T> {
const FutureProvider(this.create);
final Future<T> Function(Ref ref) create;
}
class Ref {
AsyncValue<T> watch<T>(FutureProvider<T> provider) =>
throw UnimplementedError();
void listen<T>(
FutureProvider<T> provider,
void Function(AsyncValue<T>? previous, AsyncValue<T> next) listener,
) =>
throw UnimplementedError();
}
class SubscriptionTier {}
class UnhandledDoc {}
/// Consumed without ever handling loading or error.
final unhandledProvider =
FutureProvider<UnhandledDoc>((ref) async => UnhandledDoc());
/// Consumed with `when(data:, loading:, error:)`.
final handledProvider = FutureProvider<int>((ref) async => 1);
/// Consumed only by `ref.listen`, to mirror the value into a side effect.
final listenOnlyProvider = FutureProvider<bool>((ref) async => false);
class SubscriptionView {
SubscriptionView(this.ref);
final Ref ref;
/// BAD: silently defaults, so a failure renders the wrong state.
Object? renderTier() => ref.watch(unhandledProvider).asData?.value;
/// GOOD: every AsyncValue case is handled, so no gap is reported.
int renderCount() => ref.watch(handledProvider).when(
data: (value) => value,
loading: () => -1,
error: (error, stack) => -2,
);
/// GOOD: pattern-matches AsyncError and AsyncLoading in a switch expression,
/// the form the remediation itself recommends.
int renderPatterned() => switch (ref.watch(patternedProvider)) {
AsyncError() => -2,
AsyncLoading() => -1,
_ => 0,
};
/// GOOD: the same in a switch statement.
int renderSwitchStatement() {
switch (ref.watch(switchedProvider)) {
case AsyncError():
return -2;
case AsyncLoading():
return -1;
default:
return 0;
}
}
/// GOOD: the watched value is stored in a variable and then handled.
int renderStored() {
final count = ref.watch(storedProvider);
return count.when(
data: (value) => value,
loading: () => -1,
error: (error, stack) => -2,
);
}
/// Direct value reads of the three GOOD providers above. Without a site that
/// reads the value, a provider is never reported whatever its handling, so
/// those controls would prove nothing.
int? peekPatterned() => ref.watch(patternedProvider).value;
int? peekSwitched() => ref.watch(switchedProvider).value;
int? peekStored() => ref.watch(storedProvider).value;
/// BAD: stored in a variable and then read without handling, so following
/// the variable must not silence the rule.
int? renderStoredUnhandled() {
final count = ref.watch(storedUnhandledProvider);
return count.value;
}
/// Not a render: a listener that mirrors the value into a side effect (for
/// example an analytics user property) draws nothing, so ignoring loading
/// and error here leaves no blank or stale screen.
void mirrorFlag(void Function(bool value) sink) {
ref.listen(listenOnlyProvider, (previous, next) {
final value = next.asData?.value;
if (value != null) sink(value);
});
}
}
final patternedProvider = FutureProvider<int>((ref) async => 1);
final switchedProvider = FutureProvider<int>((ref) async => 1);
final storedProvider = FutureProvider<int>((ref) async => 1);
final storedUnhandledProvider = FutureProvider<int>((ref) async => 1);