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.
95 lines
3.2 KiB
Dart
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();
|
|
}
|
|
});
|
|
}
|