analyze method

Future<List<FutureCallDefinition>> analyze({
  1. required CodeAnalysisCollector collector,
  2. List<SerializableModelDefinition>? analyzedModels,
  3. Set<String>? changedFiles,
})

Analyze files in the AnalysisContextCollection.

On the first call, analyzes every Dart file. On subsequent calls, only re-analyzes files listed in changedFiles, reusing cached results for unchanged files.

analyzedModels are the validated models from StatefulAnalyzer.validateAll. When provided, they are cached for subsequent calls. When omitted, the cached models from a previous call are used.

changedFiles is the set of files that have changed since the last call.

Implementation

Future<List<FutureCallDefinition>> analyze({
  required CodeAnalysisCollector collector,
  List<SerializableModelDefinition>? analyzedModels,
  Set<String>? changedFiles,
}) async {
  if (analyzedModels != null) {
    _cachedAnalyzedModels = analyzedModels;
  }

  changedFiles ??= {};
  await _refreshContextForFiles(changedFiles);

  // On the first run, mark every Dart file as changed so the single
  // code path handles both first and subsequent runs.
  if (_fileCache.isEmpty) {
    changedFiles.addAll(_allAnalyzedDartFiles);
  }

  // Analyze changed files + previously errored future call files
  // (fixing a dependency elsewhere might unblock them).
  final filesToAnalyze = <String>{
    ...changedFiles,
    ..._fileCache.keys,
  };

  // Remove deleted files from cache.
  for (var path in filesToAnalyze) {
    if (!File(path).existsSync()) {
      _fileCache.remove(path);
    }
  }

  // Resolve only the files that need re-analysis.
  List<(ResolvedLibraryResult, String)> validLibraries = [];
  List<String> erroredFiles = [];

  for (var path in filesToAnalyze) {
    if (!path.endsWith('.dart') || path.endsWith('_test.dart')) continue;
    if (!File(path).existsSync()) continue;

    var library = await _resolveLibrary(path);
    if (library == null) continue;

    var futureCallClasses = _getFutureCallClasses(library);
    if (futureCallClasses.isEmpty) {
      _fileCache.remove(path);
      continue;
    }

    var maybeDartErrors = await _getErrorsForFile(library.session, path);
    if (maybeDartErrors.isNotEmpty) {
      erroredFiles.add(path);
      collector.addError(
        SourceSpanSeverityException(
          'FutureCall analysis skipped due to invalid Dart syntax. Please '
          'review and correct the syntax errors.'
          '\nFile: $path',
          null,
          severity: SourceSpanSeverity.error,
        ),
      );

      _fileCache[path] = _CachedFutureCallFileResult(
        definitions: [],
        hadErrors: true,
      );
      continue;
    }

    validLibraries.add((library, path));
  }

  // Build future call class map from ALL files for duplicate detection.
  // Errored files have empty definitions so they naturally don't contribute,
  // matching the original behavior.
  Map<String, int> futureCallClassMap = {};
  for (var entry in _fileCache.entries) {
    if (validLibraries.any((lib) => lib.$2 == entry.key)) continue;
    for (var def in entry.value.definitions) {
      futureCallClassMap.update(
        def.className,
        (v) => v + 1,
        ifAbsent: () => 1,
      );
    }
  }
  for (var (library, _) in validLibraries) {
    for (var cls in _getFutureCallClasses(library)) {
      futureCallClassMap.update(
        cls.name!,
        (v) => v + 1,
        ifAbsent: () => 1,
      );
    }
  }

  var duplicateFutureCallClasses = futureCallClassMap.entries
      .where((entry) => entry.value > 1)
      .map((entry) => entry.key)
      .toSet();

  // Validate and parse re-analyzed files, update cache.
  //
  // Skip parameter validation when models aren't available yet (e.g. when
  // called from updateFileContexts before performGenerate provides models).
  // Validation will run on the next analyze() call with models.
  final hasModels = _cachedAnalyzedModels != null;
  final templateRegistry = DartDocTemplateRegistry();

  for (var (library, filePath) in validLibraries) {
    var failingExceptions = <String, List<SourceSpanSeverityException>>{};

    if (hasModels) {
      var severityExceptions = _validateLibrary(
        library,
        filePath,
        duplicateFutureCallClasses,
        _cachedAnalyzedModels!,
      );
      collector.addErrors(
        severityExceptions.values.expand((e) => e).toList(),
      );
      failingExceptions = _filterNoFailExceptions(severityExceptions);
    }

    var defs = _parseLibrary(
      library,
      filePath,
      failingExceptions,
      templateRegistry: templateRegistry,
    );

    _fileCache[filePath] = _CachedFutureCallFileResult(
      definitions: defs,
      hadErrors: !hasModels,
    );
  }

  // Phase 4: Collect all future call definitions from cache.
  // _fileCache is a SplayTreeMap so iteration is in sorted key order.
  var futureCallDefs = <FutureCallDefinition>[];
  for (var result in _fileCache.values) {
    futureCallDefs.addAll(result.definitions);
  }
  futureCallDefs.removeWhere((e) => e.filePath.startsWith('package:'));

  return futureCallDefs;
}