Files
FlClash/test/lint/dead_file_test.dart
T
chen08209 96fcb9ab5e chore(agents,tool): rebuild agent config, lint gates, CI, and release tooling
Rework .agents/, Claude/Codex config, CI workflows, lint gates and
lint-rule tests, and the release/changelog tooling. The 5% comment
density gate stays diff-scoped inside git hooks, where GIT_DIR outranks
the -C the gate uses to locate the repository. The tray icon generator
writes multi-size ICO files from the SVG sources into the per-platform
tray asset directories. The custom code-review subagents are gone: the
built-in /code-review never dispatched them, and the subagent model now
lives in a user-level setting outside the repository.

The Makefile, build_config.yaml and CI hand the Core and Helper builds to
the setup build hook, so the buildkit launchers go, and setup.dart
packages AppImage and rpm on arm64 with flutter_distributor pinned to the
fork tag that normalizes packaging permissions.

Changelog: skip
2026-09-10 12:42:52 +08:00

95 lines
3.2 KiB
Dart

import 'dart:io';
import 'package:test/test.dart';
const _entryPoints = ['lib/main.dart'];
/// Types, and the top level names a file publishes alongside them — the
/// `final appPath = AppPath()` singleton pattern is how most of `lib/common`
/// is actually consumed.
final _declaration = RegExp(
r'^(?:abstract |sealed |final |base |mixin )*'
r'(?:(?:class|enum|mixin)\s+([A-Za-z]\w*)'
r'|(?:final|const)\s+(?:[\w<>,\s\[\]?]+\s+)?([a-z]\w*)\s*=)',
multiLine: true,
);
Iterable<File> _dartFiles({required bool includeGenerated}) sync* {
for (final root in ['lib', 'test', 'tool', 'plugins']) {
final directory = Directory(root);
if (!directory.existsSync()) {
fail('$root no longer exists; update this test.');
}
for (final entity in directory.listSync(recursive: true)) {
if (entity is! File || !entity.path.endsWith('.dart')) continue;
final generated =
entity.path.endsWith('.g.dart') ||
entity.path.endsWith('.freezed.dart') ||
entity.path.contains('/generated/');
if (generated && !includeGenerated) continue;
yield entity;
}
}
}
bool _isBarrel(String source) {
final lines = source
.split('\n')
.map((line) => line.trim())
.where((line) => line.isNotEmpty && !line.startsWith('//'));
return lines.isNotEmpty &&
lines.every(
(line) => line.startsWith('export ') || line.startsWith('import '),
);
}
void main() {
test('every file under lib declares something used outside itself', () {
// Generated code counts as a consumer but never as a declarer: a riverpod
// notifier is reached through the provider its annotation generates.
final consumers = {
for (final file in _dartFiles(includeGenerated: true))
file.path: file.readAsStringSync(),
};
final sources = {
for (final file in _dartFiles(includeGenerated: false))
file.path: consumers[file.path]!,
};
final barrels = {
for (final MapEntry(key: path, value: source) in sources.entries)
if (_isBarrel(source)) path,
};
final orphans = <String>[];
for (final MapEntry(key: path, value: source) in sources.entries) {
if (!path.startsWith('lib/')) continue;
if (barrels.contains(path) || _entryPoints.contains(path)) continue;
if (path.startsWith('lib/l10n/')) continue;
// Extensions and typedefs are reached through the types they attach to,
// never by name, so a file that publishes only those cannot be measured
// this way.
final names = _declaration
.allMatches(source)
.map((match) => match.group(1) ?? match.group(2)!)
.toSet();
if (names.isEmpty) continue;
final used = consumers.entries.any((entry) {
if (entry.key == path || barrels.contains(entry.key)) return false;
return names.any((name) => RegExp('\\b$name\\b').hasMatch(entry.value));
});
if (!used) {
orphans.add(
'$path — publishes ${names.join(', ')}, none of which is referenced '
'anywhere else. A barrel export keeps it compiling; it is still dead.',
);
}
}
expect(orphans, isEmpty, reason: orphans.join('\n'));
});
}