runCliAnalysis method

Future<Iterable<UnusedCodeFileReport>> runCliAnalysis(
  1. Iterable<String> folders,
  2. String rootFolder,
  3. UnusedCodeConfig config, {
  4. 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);
}