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.
63 lines
2.2 KiB
Dart
63 lines
2.2 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('models a single-class flag state that carries no variants', () {
|
|
final flag = analysis.flow.flagStates
|
|
.where((state) => state.name == 'FlagState')
|
|
.toList();
|
|
expect(flag, hasLength(1));
|
|
expect(flag.single.owner, 'FlagCubit');
|
|
expect(flag.single.fields, contains('loading'));
|
|
expect(flag.single.defaultsTrue, isTrue);
|
|
});
|
|
|
|
test('flag rule reports the unguarded await only', () {
|
|
final hits = analysis.report.findings
|
|
.where((finding) => finding.rule == ruleLoadingFlagNotReset)
|
|
.toList();
|
|
|
|
// loadGuarded resets the flag in its catch, refreshQuietly never sets it,
|
|
// and scheduleGuarded's closure guards itself, so only the unguarded method
|
|
// and the two deliberately unguarded `on<>` handlers survive.
|
|
expect(hits.map((hit) => hit.data['member']).toSet(), {
|
|
'loadUnguarded',
|
|
'on<FlagRefresh>',
|
|
'on<FlagReload>',
|
|
});
|
|
expect(hits, hasLength(3));
|
|
final unguarded = hits.singleWhere(
|
|
(hit) => hit.data['member'] == 'loadUnguarded',
|
|
);
|
|
expect(unguarded.data['stateType'], 'FlagState');
|
|
expect(unguarded.severity, PipeSeverity.warning);
|
|
expect(unguarded.confidence, 'high');
|
|
expect(unguarded.evidence, isNotEmpty);
|
|
expect(unguarded.remediation, isNotNull);
|
|
// The flag reads as one code span, `FlagState.loading`, not as
|
|
// `FlagState` immediately followed by a second span.
|
|
expect(unguarded.message, contains('`FlagState.'));
|
|
});
|
|
|
|
test('flag findings carry no false positive for guarded code', () {
|
|
final members = analysis.report.findings
|
|
.where((finding) => finding.rule == ruleLoadingFlagNotReset)
|
|
.map((finding) => finding.data['member'])
|
|
.toSet();
|
|
expect(members, isNot(contains('loadGuarded')));
|
|
expect(members, isNot(contains('refreshQuietly')));
|
|
});
|
|
}
|