traverse static method

Future<ProcessingResult> traverse({
  1. required BaseTraversalInfo info,
  2. required Future<bool> run(
    1. CommandContext ctx
    ),
  3. Set<Type>? requiredNatures,
  4. Set<Type> worksWithNatures = const {},
  5. bool verbose = false,
})

Traverse folders and execute callback on each.

info - ProjectTraversalInfo or GitTraversalInfo configuration. run - Callback for each matching folder. requiredNatures - Nature filter (see below). worksWithNatures - Supported natures (see below).

Nature Filtering Logic

At least one of the two nature parameters must be configured. If neither is set, an ArgumentError is thrown. To traverse all folders, set requiredNatures: {FsFolder} or worksWithNatures: {FsFolder}.

  1. requiredNatures is non-empty → Folder MUST have ALL required natures. worksWithNatures is ignored.
  2. worksWithNatures is non-empty → Folder must have at least ONE of the supported natures.
  3. Both unset/emptyArgumentError is thrown.

Special types:

  • FsFolder in either set always matches (every folder is an FsFolder).
  • DartProjectFolder matches any Dart project subtype (hierarchy check).

Implementation

static Future<ProcessingResult> traverse({
  required BaseTraversalInfo info,
  required Future<bool> Function(CommandContext) run,
  Set<Type>? requiredNatures,
  Set<Type> worksWithNatures = const {},
  bool verbose = false,
}) async {
  final detector = NatureDetector();
  final filter = FilterPipeline();
  final sorter = FolderSorter();
  final result = ProcessingResult();

  // Get folders based on traversal type
  List<FsFolder> folders;
  switch (info) {
    case ProjectTraversalInfo pi:
      folders = await _scanProjects(pi, verbose: verbose);
    case GitTraversalInfo gi:
      folders = await _findGitRepos(gi);
    default:
      folders = [];
  }

  // Detect natures FIRST (needed for project ID/name filtering)
  for (final folder in folders) {
    final natures = detector.detectNatures(folder);
    // Store natures in folder for filter pipeline access
    folder.natures.addAll(natures);
  }

  // Preserve unfiltered list for build order computation.
  // Build order must be computed from ALL scanned folders so that
  // dependency ordering is correct even when filters are applied.
  final allScannedFolders = List<FsFolder>.of(folders);

  // Apply filters AFTER nature detection (so ID/name matching works)
  switch (info) {
    case ProjectTraversalInfo pi:
      folders = filter.applyProjectFilters(folders, pi);
    case GitTraversalInfo gi:
      folders = filter.applyGitFilters(folders, gi);
    default:
      break;
  }

  // Create contexts from filtered folders
  final contexts = <CommandContext>[];
  for (final folder in folders) {
    contexts.add(
      CommandContext(
        fsFolder: folder,
        natures: folder.natures.whereType<RunFolder>().toList(),
        executionRoot: info.executionRoot,
        traversal: info,
      ),
    );
  }

  // Apply ordering based on traversal type
  List<CommandContext> ordered;
  switch (info) {
    case GitTraversalInfo gi:
      ordered = gi.gitMode == GitTraversalMode.innerFirst
          ? sorter.sortByInnerFirst(contexts, (c) => c.path)
          : sorter.sortByOuterFirst(contexts, (c) => c.path);
    case ProjectTraversalInfo pi when pi.buildOrder:
      // Build order: compute order from ALL scanned folders (pre-filter),
      // then sort the filtered contexts by that global order.
      final allDartPaths = allScannedFolders
          .where((f) => File('${f.path}/pubspec.yaml').existsSync())
          .map((f) => f.path)
          .toList();
      final globalOrder =
          BuildOrderComputer.computeBuildOrder(allDartPaths) ?? [];
      ordered = sorter.sortByBuildOrder(contexts, (c) => c.path, globalOrder);
    default:
      ordered = contexts;
  }

  // Validate nature configuration — at least one must be set.
  // Tools that want all folders must explicitly use FsFolder.
  final hasRequired = requiredNatures != null && requiredNatures.isNotEmpty;
  final hasWorksWith = worksWithNatures.isNotEmpty;
  if (!hasRequired && !hasWorksWith) {
    throw ArgumentError(
      'Neither requiredNatures nor worksWithNatures is configured. '
      'Set requiredNatures or worksWithNatures (use FsFolder for all folders).',
    );
  }

  // Execute on each context
  for (final ctx in ordered) {
    // Nature filtering:
    // 1. non-empty requiredNatures → must have ALL required
    // 2. non-empty worksWithNatures → must have at least ONE
    if (!_shouldInvokeForNatures(
      ctx.natures,
      requiredNatures,
      worksWithNatures,
    )) {
      continue;
    }

    try {
      final success = await run(ctx);
      success
          ? result.recordSuccess(ctx.path)
          : result.recordFailure(ctx.path);
    } catch (e) {
      result.recordError(ctx.path, e);
    }
  }

  return result;
}