searchApiUsages method

Future<List<ApiUsageResult>> searchApiUsages({
  1. required String projectPath,
  2. required List<String> apis,
  3. String? packageFilter,
  4. bool resolveTypes = true,
})

Search for usages of specific apis within projectPath.

When packageFilter is supplied the scan is restricted to files that import that package, and matches are validated against the resolved element's library URI. This dramatically reduces both I/O and analysis time.

Set resolveTypes to false for a faster name-only scan that skips type resolution (useful for an initial broad pass).

Returns one ApiUsageResult per entry in apis, in the same order.

Implementation

Future<List<ApiUsageResult>> searchApiUsages({
  required String projectPath,
  required List<String> apis,
  String? packageFilter,
  bool resolveTypes = true,
}) async {
  if (apis.isEmpty) return const [];

  // 1. Determine which files to analyze.
  List<String> filesToAnalyze;
  if (packageFilter != null) {
    filesToAnalyze = await findFilesImporting(projectPath, packageFilter);
  } else {
    filesToAnalyze = await findDartFiles(projectPath);
  }

  if (filesToAnalyze.isEmpty) {
    Logger.info('No files to analyze — returning empty results.');
    return apis
        .map((api) => ApiUsageResult(
              api: api,
              packageFilter: packageFilter,
              totalMatches: 0,
              matches: const [],
            ))
        .toList();
  }

  Logger.info(
    'Analyzing ${filesToAnalyze.length} file(s) for '
    '${apis.length} API(s)${resolveTypes ? ' (resolved)' : ' (unresolved)'}…',
  );

  // 2. Build the AnalysisContextCollection.
  //    Normalize every path to absolute form — the analyzer requires it.
  final normalizedPaths =
      filesToAnalyze.map((f) => p.normalize(p.absolute(f))).toList();

  final AnalysisContextCollection collection;
  try {
    collection = AnalysisContextCollection(
      includedPaths: normalizedPaths,
      resourceProvider: PhysicalResourceProvider.INSTANCE,
    );
  } catch (e, st) {
    Logger.error('Failed to create AnalysisContextCollection', e, st);
    return apis
        .map((api) => ApiUsageResult(
              api: api,
              packageFilter: packageFilter,
              totalMatches: 0,
              matches: const [],
            ))
        .toList();
  }

  // 3. Visit each file.
  final resultsMap = <String, List<ApiMatch>>{
    for (final api in apis) api: [],
  };

  var filesAnalyzed = 0;
  var filesErrored = 0;

  for (final context in collection.contexts) {
    for (final filePath in context.contextRoot.analyzedFiles()) {
      if (!filePath.endsWith('.dart')) continue;

      // Only analyze files the caller asked for. The context root may
      // include transitive dependencies — skip those.
      if (!normalizedPaths.contains(p.normalize(filePath))) continue;

      try {
        if (resolveTypes) {
          await _analyzeResolved(
            context: context,
            filePath: filePath,
            apis: apis,
            packageFilter: packageFilter,
            resultsMap: resultsMap,
          );
        } else {
          _analyzeUnresolved(
            context: context,
            filePath: filePath,
            apis: apis,
            resultsMap: resultsMap,
          );
        }
        filesAnalyzed++;
      } catch (e) {
        filesErrored++;
        Logger.debug('Error analyzing $filePath: $e');
      }
    }
  }

  Logger.info(
    'Analysis complete: $filesAnalyzed files analyzed, '
    '$filesErrored errors, '
    '${resultsMap.values.fold<int>(0, (sum, l) => sum + l.length)} total matches.',
  );

  return apis
      .map((api) => ApiUsageResult(
            api: api,
            packageFilter: packageFilter,
            totalMatches: resultsMap[api]!.length,
            matches: resultsMap[api]!,
          ))
      .toList();
}