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.
93 lines
3.1 KiB
Dart
93 lines
3.1 KiB
Dart
import 'package:pipe/pipe.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
const String fixturePath = 'test/fixtures/stuck_loading';
|
|
|
|
void main() {
|
|
late ProjectAnalysis analysis;
|
|
|
|
setUpAll(() async {
|
|
final project = await loadProject(fixturePath);
|
|
analysis = await analyzeProject(project, concurrency: 1);
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
await analysis.dispose();
|
|
});
|
|
|
|
test('discovers Riverpod providers and their value types', () {
|
|
final names = analysis.flow.riverpod.providers.map((p) => p.name).toSet();
|
|
expect(
|
|
names,
|
|
containsAll(<String>['unhandledProvider', 'handledProvider']),
|
|
);
|
|
|
|
final unhandled = analysis.flow.riverpod.providers.firstWhere(
|
|
(p) => p.name == 'unhandledProvider',
|
|
);
|
|
expect(unhandled.kind, 'FutureProvider');
|
|
expect(unhandled.isAsync, isTrue);
|
|
expect(unhandled.valueType, 'UnhandledDoc');
|
|
});
|
|
|
|
test('records watch sites with their AsyncValue handling', () {
|
|
final sites = analysis.flow.riverpod.sitesFor('unhandledProvider');
|
|
expect(sites, hasLength(1));
|
|
expect(sites.single.watches, isTrue);
|
|
expect(sites.single.readsValue, isTrue);
|
|
expect(sites.single.handlesError, isFalse);
|
|
expect(sites.single.handlesLoading, isFalse);
|
|
});
|
|
|
|
test('reports the unhandled provider and not the handled one', () {
|
|
final hits = analysis.report.findings
|
|
.where((f) => f.rule == ruleRiverpodAsyncGap)
|
|
.toList();
|
|
|
|
// The two deliberate gaps: a direct read and a read through a variable.
|
|
expect(hits.map((f) => f.data['provider']).toSet(), {
|
|
'unhandledProvider',
|
|
'storedUnhandledProvider',
|
|
});
|
|
expect(hits, hasLength(2));
|
|
final unhandled = hits.singleWhere(
|
|
(f) => f.data['provider'] == 'unhandledProvider',
|
|
);
|
|
expect(
|
|
unhandled.data['missing'],
|
|
containsAll(<String>['error', 'loading']),
|
|
);
|
|
// A non-bool value type keeps this at high confidence and warning severity.
|
|
expect(unhandled.confidence, 'high');
|
|
expect(unhandled.severity, PipeSeverity.warning);
|
|
expect(unhandled.evidence, isNotEmpty);
|
|
expect(unhandled.remediation, isNotNull);
|
|
});
|
|
|
|
test('the handled provider produces no gap', () {
|
|
final providers = analysis.report.findings
|
|
.where((f) => f.rule == ruleRiverpodAsyncGap)
|
|
.map((f) => f.data['provider'])
|
|
.toSet();
|
|
expect(providers, isNot(contains('handledProvider')));
|
|
expect(
|
|
analysis.flow.riverpod.gaps.map((g) => g.provider.name),
|
|
isNot(contains('handledProvider')),
|
|
);
|
|
});
|
|
|
|
test('a provider consumed only by ref.listen is not a render gap', () {
|
|
// A listener mirrors the value into a side effect and draws nothing, so
|
|
// it cannot leave a blank or stale screen. Seen when dogfooding on
|
|
// word_alive: an analytics user-property listener was reported as a UI
|
|
// read with unhandled loading and error.
|
|
final sites = analysis.flow.riverpod.sitesFor('listenOnlyProvider');
|
|
expect(sites, hasLength(1));
|
|
expect(sites.single.watches, isFalse);
|
|
expect(sites.single.readsValue, isTrue);
|
|
expect(
|
|
analysis.flow.riverpod.gaps.map((g) => g.provider.name),
|
|
isNot(contains('listenOnlyProvider')),
|
|
);
|
|
});
|
|
}
|