unusedFluentAccessors function
Names from accessorNames (the generated manifest) that no Dart
file under sourceRoot references.
generatedFilePath is excluded from the scan — it DEFINES every
name. *AsSpans siblings are folded into their base accessor: a
message counts as used when either variant is referenced.
excludePaths skips additional trees (other generated output,
fixtures).
Typical consumer test:
test('no dead translations', () {
expect(
unusedFluentAccessors(
accessorNames: Translations.accessorNames,
generatedFilePath: 'lib/i18n/translations.g.dart',
),
isEmpty,
);
});
Implementation
Set<String> unusedFluentAccessors({
required List<String> accessorNames,
required String generatedFilePath,
String sourceRoot = 'lib',
List<String> excludePaths = const [],
}) {
// Compare through `package:path`, not raw `startsWith`: on Windows
// `File('lib/x').absolute.path` keeps the forward slashes in the
// relative part while `listSync` entities use backslashes, so a raw
// string prefix never matches. `p.equals` / `p.isWithin` are
// separator-aware (and `isWithin` respects the segment boundary, so
// `lib/foo` never swallows `lib/foobar`).
final excludedFiles = <String>[p.absolute(generatedFilePath)];
final excludedDirs = <String>[];
for (final path in excludePaths) {
if (FileSystemEntity.isDirectorySync(path)) {
excludedDirs.add(p.absolute(path));
} else {
excludedFiles.add(p.absolute(path));
}
}
final sources = StringBuffer();
final root = Directory(sourceRoot);
if (root.existsSync()) {
for (final entity in root.listSync(recursive: true)) {
if (entity is! File || !entity.path.endsWith('.dart')) continue;
final absolute = p.absolute(entity.path);
final skip =
excludedFiles.any((e) => p.equals(e, absolute)) ||
excludedDirs.any((d) => p.isWithin(d, absolute));
if (skip) continue;
sources
..write(entity.readAsStringSync())
..write('\n');
}
}
final haystack = sources.toString();
final unused = <String>{};
for (final name in accessorNames) {
// Fold the AsSpans sibling into its base accessor.
final base =
name.endsWith('AsSpans')
? name.substring(0, name.length - 'AsSpans'.length)
: name;
if (unused.contains(base)) continue;
final used =
RegExp('\\.${RegExp.escape(base)}\\b').hasMatch(haystack) ||
RegExp('\\.${RegExp.escape(base)}AsSpans\\b').hasMatch(haystack);
if (!used) unused.add(base);
}
return unused;
}