import 'dart:io'; import 'package:pipe/pipe.dart'; import 'package:test/test.dart'; /// notifier-field-unread discounts reads inside the notifier's own class body, /// and nothing else. It used to discount the notifier's whole file, so a /// private controller rendered by the widget declared next to it was reported /// as unread (nuwayze's `_AgentHubController`, eight getters, all rendered). /// /// Each case uses member names no other case reads, because a read is matched /// by name anywhere in the scan. const Map _files = { 'lib/support.dart': ''' class ChangeNotifier { void notifyListeners() {} } class BuildContext { T watch() => throw UnimplementedError(); } ''', // A private controller and the widget that renders it, in one file. 'lib/same_file.dart': ''' import 'support.dart'; class _SameFileController extends ChangeNotifier { int _sameCount = 0; String? _sameLoadError; int get sameCount => _sameCount; String? get sameLoadError => _sameLoadError; void refresh() { _sameCount++; notifyListeners(); } } class SameFileView { String build(BuildContext context) { final controller = context.watch<_SameFileController>(); return '\${controller.sameCount} \${controller.sameLoadError}'; } } ''', // A controller whose only reader is a part of its library. 'lib/part_host.dart': ''' import 'support.dart'; part 'part_host_view.dart'; class PartController extends ChangeNotifier { int _partTotal = 0; int get partTotal => _partTotal; String get partStatus => 'ok'; void bump() { _partTotal++; notifyListeners(); } } ''', 'lib/part_host_view.dart': ''' part of 'part_host.dart'; class PartView { String build(BuildContext context) { final controller = context.watch(); return '\${controller.partTotal} \${controller.partStatus}'; } } ''', // `unreadHiddenCount` is read nowhere, though its sibling is rendered next // door: the true positive the same-file fix must keep. 'lib/unread.dart': ''' import 'support.dart'; class UnreadController extends ChangeNotifier { int _unreadShown = 0; int _unreadHiddenCount = 0; int get unreadShown => _unreadShown; int get unreadHiddenCount => _unreadHiddenCount; void bump() { _unreadShown++; _unreadHiddenCount++; notifyListeners(); } } class UnreadView { String build(BuildContext context) => '\${context.watch().unreadShown}'; } ''', // `selfErrorMessage` is read only by the class itself, which is not a // render. 'lib/self_read.dart': ''' import 'support.dart'; class SelfReadController extends ChangeNotifier { String? _selfErrorMessage; String get selfLabel => 'label'; String? get selfErrorMessage => _selfErrorMessage; // ignore: unnecessary_this String describe() => 'error: \${this.selfErrorMessage}'; bool sameErrorAs(SelfReadController other) => other.selfErrorMessage == _selfErrorMessage; void fail(String message) { _selfErrorMessage = message; notifyListeners(); } } class SelfReadView { String build(BuildContext context) => context.watch().selfLabel; } ''', }; String _project() { final root = Directory.systemTemp.createTempSync('pipe-field-unread'); File('${root.path}/pubspec.yaml').writeAsStringSync(''' name: field_unread publish_to: none environment: sdk: ^3.0.0 '''); File('${root.path}/.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' { "configVersion": 2, "packages": [ { "name": "field_unread", "rootUri": "../", "packageUri": "lib/", "languageVersion": "3.0" } ] } '''); for (final entry in _files.entries) { File('${root.path}/${entry.key}') ..createSync(recursive: true) ..writeAsStringSync(entry.value); } return root.path; } void main() { late String root; late ProjectAnalysis analysis; late Map> unreadByOwner; setUpAll(() async { root = _project(); analysis = await analyzeProject( await loadProject(root), concurrency: 1, enabledRules: {ruleNotifierFieldUnread}, ); unreadByOwner = { for (final finding in analysis.report.findings) if (finding.rule == ruleNotifierFieldUnread) finding.data['owner']! as String: finding.data['members']! as List, }; }); tearDownAll(() async { await analysis.dispose(); Directory(root).deleteSync(recursive: true); }); test('the scan resolved every file', () { // Otherwise a missing finding below could be an analysis failure. expect(analysis.report.coverage!.resolved, _files.length); expect(analysis.report.notes.join('\n'), isNot(contains('unresolved'))); }); test('reads by a widget in the notifier\'s own file count', () { expect(unreadByOwner, isNot(contains('_SameFileController'))); }); test('reads in a part of the notifier\'s library count', () { expect(unreadByOwner, isNot(contains('PartController'))); }); test('a member read nowhere is still reported', () { expect(unreadByOwner['UnreadController'], ['unreadHiddenCount']); }); test('a member read only inside its own class is still reported', () { expect(unreadByOwner['SelfReadController'], ['selfErrorMessage']); }); }