pipe/test/unresolved_test.dart
trvon 17ff523a11
fix(check): lower confidence where unresolved code can hide evidence
check never looked at analyzer errors, so a project that resolved only
partly was analysed as if it were whole. pipe reads names off the syntax
tree, and an unresolved `Foo()` is a method call rather than a
construction, so a dispatch, emission or construction in such a file
simply disappears - and every finding is an absence claim that the
missing evidence could refute.

Found live during this work: with the Flutter SDK unreadable, nuwayze
resolved with 40,079 analyzer errors (package:flutter unresolvable) and
check still reported 102 findings at full confidence, with no warning.

Now, when any analysed file has an unresolved-reference diagnostic
(uri_does_not_exist, undefined_*, creation_with_non_type, ...):
- check warns on stderr and adds a report note naming the first one;
- a finding is lowered one confidence step, with
  data.confidenceLowered = unresolved-references, when an unresolved file
  mentions one of its identity names (event, variant, union, owner,
  provider, member). pipe's evidence is name-based, so a file that never
  names X cannot hide evidence about X; lowering everything for one stray
  error would bury the signal (a first cut did exactly that, taking all
  102 nuwayze findings down for a single unresolved file).

Validated on mirrors of the dogfood repos with a readable SDK (0 analyzer
errors): nuwayze 102 findings identical to the original main, dex 0,
word_alive 1 and tick 1 (the intended riverpod change). The fixture
test adds an unresolved file that constructs NeverDispatched: its
dead-handler finding drops to medium while UnhandledTap, which that file
never names, stays high.
2026-09-22 14:04:32 -06:00

95 lines
3.2 KiB
Dart

import 'dart:io';
import 'package:pipe/pipe.dart';
import 'package:test/test.dart';
/// The fixture plus one file whose references do not resolve: it constructs
/// `NeverDispatched` without importing it, and imports a file that is missing.
String _corpusWithUnresolvedFile() {
final from = Directory('test/fixtures/stuck_loading');
final to = Directory.systemTemp.createTempSync('pipe-unresolved');
for (final entity in from.listSync(recursive: true)) {
final relative = entity.path.substring(from.path.length + 1);
final target = '${to.path}/$relative';
if (entity is Directory) {
Directory(target).createSync(recursive: true);
} else if (entity is File) {
File(target)
..createSync(recursive: true)
..writeAsBytesSync(entity.readAsBytesSync());
}
}
File('${to.path}/lib/unresolved_ui.dart').writeAsStringSync('''
import 'widgets_that_do_not_exist.dart';
// Unresolved, `NeverDispatched()` parses as a method call rather than a
// construction, so the dispatch below is invisible to the analysis.
void tapNever(dynamic bloc) => bloc.add(NeverDispatched());
''');
return to.path;
}
void main() {
test(
'a finding an unresolved file could refute is not high confidence',
() async {
// Every pipe finding is a whole-program absence claim, and an unresolved
// reference anywhere can hide the evidence that refutes it. On a real app
// whose package:flutter could not be read (40k analyzer errors), check
// reported findings at high confidence with no warning at all.
final analysis = await analyzeProject(
await loadProject(_corpusWithUnresolvedFile()),
concurrency: 1,
);
try {
final findings = analysis.report.findings;
final dead = findings.singleWhere(
(f) =>
f.rule == ruleDeadHandler && f.data['event'] == 'NeverDispatched',
);
expect(dead.confidence, 'medium');
expect(dead.data['confidenceLowered'], 'unresolved-references');
// The unresolved file never mentions UnhandledTap, so it cannot hide
// evidence about it: one stray error must not bury every finding.
final untouched = findings.singleWhere(
(f) =>
f.rule == ruleEventWithoutHandler &&
f.data['event'] == 'UnhandledTap',
);
expect(untouched.confidence, 'high');
expect(untouched.data.containsKey('confidenceLowered'), isFalse);
expect(
analysis.report.notes.join('\n'),
allOf(
contains('unresolved references'),
contains('unresolved_ui.dart'),
contains('1 finding(s)'),
),
);
} finally {
await analysis.dispose();
}
},
);
test('a fully resolved project keeps its confidence', () async {
final analysis = await analyzeProject(
await loadProject('test/fixtures/stuck_loading'),
concurrency: 1,
);
try {
expect(
analysis.report.findings.where((f) => f.confidence == 'high'),
isNotEmpty,
);
expect(
analysis.report.findings.where(
(f) => f.data.containsKey('confidenceLowered'),
),
isEmpty,
);
} finally {
await analysis.dispose();
}
});
}