runCliAnalysis method
Future<Iterable<UnusedCodeFileReport> >
runCliAnalysis(
- Iterable<
String> folders, - String rootFolder,
- UnusedCodeConfig config, {
- String? sdkPath,
Returns a list of unused code reports
for analyzing all files in the given folders.
The analysis is configured with the config.
Implementation
Future<Iterable<UnusedCodeFileReport>> runCliAnalysis(
Iterable<String> folders,
String rootFolder,
UnusedCodeConfig config, {
String? sdkPath,
}) async {
final collection =
createAnalysisContextCollection(folders, rootFolder, sdkPath);
final codeUsages = FileElementsUsage();
final publicCode = <String, _FileCandidates>{};
// Files any consumer of their package can import directly. Collected
// during the walk, where each unit's resolved library URI says whether it
// sits in some package's `lib/`, outside `lib/src`.
final packageImportSurface = <String>{};
for (final context in collection.contexts) {
final unusedCodeAnalysisConfig =
_getAnalysisConfig(context, rootFolder, config);
if (config.shouldPrintConfig ?? false) {
_logger?.printConfig(unusedCodeAnalysisConfig.toJson());
}
final filePaths = getFilePaths(
folders,
context,
rootFolder,
unusedCodeAnalysisConfig.globalExcludes,
);
final analyzedFiles =
filePaths.intersection(context.contextRoot.analyzedFiles().toSet());
final contextsLength = collection.contexts.length;
final filesLength = analyzedFiles.length;
final updateMessage = contextsLength == 1
? 'Checking unused code for $filesLength file(s)'
: 'Checking unused code for ${collection.contexts.indexOf(context) + 1}/$contextsLength contexts with $filesLength file(s)';
_logger?.progress.update(updateMessage);
for (final filePath in analyzedFiles) {
_logger?.infoVerbose('Analyzing $filePath');
final unit = await context.currentSession.getResolvedUnit(filePath);
final codeUsage = _analyzeFileCodeUsages(
unit,
unusedCodeAnalysisConfig,
);
if (codeUsage != null) {
codeUsages.merge(codeUsage);
}
if (!unusedCodeAnalysisConfig.analyzerExcludedPatterns
.any((pattern) => pattern.matches(filePath))) {
publicCode[filePath] = _analyzeFilePublicCode(
unit,
unusedCodeAnalysisConfig,
);
if (unusedCodeAnalysisConfig.suggestPrivateMembers &&
unit is ResolvedUnitResult &&
isOnPackageImportSurface(unit.libraryElement.uri)) {
packageImportSurface.add(filePath);
}
}
}
}
if (!(config.isMonorepo ?? false)) {
_logger?.infoVerbose(
'Removing globally exported files with code usages from the analysis: ${codeUsages.exports.length}',
);
// Only top level declarations are part of the package's exported
// surface for the *unused* verdict: a member is reachable from outside
// only through a reference to its enclosing type, which the type's own
// top level exemption already covers, so exporting a file must not
// excuse the dead members of the types it declares.
//
// The suggestions are cut whole instead, top level and member alike. A
// consumer that can import the file names the type and reaches the
// public members of that type just as directly, so a rename here breaks
// it either way. Only members of a *private* type would be safe, and
// those never reach this set: the member visitor drops them long
// before, so there is nothing left here worth keeping.
for (final exportedPath in codeUsages.exports) {
final candidates = publicCode[exportedPath];
if (candidates == null) {
continue;
}
final kept = candidates.unusedMembersOnly();
if (kept.isEmpty) {
publicCode.remove(exportedPath);
} else {
publicCode[exportedPath] = kept;
}
}
// Being re-exported is not the only way onto a package's import
// surface: a library under `lib/` outside `lib/src` is importable
// directly, with nothing exporting it, so its declarations are just as
// unsafe to suggest privatizing.
//
// Unlike the loop above, this drops the suggestions alone and leaves
// the unused candidates in place, top level ones included. The two
// verdicts want different things here: whether anything in the analyzed
// code references a declaration is a fact about that code, which is
// what the unused check has always reported for these files, while a
// suggestion to rename one is a claim about every library that could
// reach it, including the ones outside the analysis.
for (final publicPath in packageImportSurface) {
final candidates = publicCode[publicPath];
if (candidates == null) {
continue;
}
final kept = candidates.withoutSuggestions();
if (kept.isEmpty) {
publicCode.remove(publicPath);
} else {
publicCode[publicPath] = kept;
}
}
}
return _getReports(codeUsages, publicCode, rootFolder);
}