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.
141 lines
4 KiB
Dart
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);
|