verifyTaskInputsAndOutputsConsistency function

Future<DeletionTasksByTask> verifyTaskInputsAndOutputsConsistency(
  1. Map<String, TaskWithDeps> taskMap
)

Verify that all tasks in taskMap have inputs/outputs that are mutually consistent.

If a task uses the outputs of another task as its inputs, then it must have an explicit dependency on the other task.

If a deletion task affects inputs or outputs of another task, then it must execute on a phase that precedes those other tasks.

Any inconsistencies will cause a DartleException to be thrown by this method.

Returns the tasks affected by deletion tasks, so that the engine can detect when it must run a task due to deletions.

Implementation

Future<DeletionTasksByTask> verifyTaskInputsAndOutputsConsistency(
    Map<String, TaskWithDeps> taskMap) async {
  final inputsByTask = <TaskWithDeps, FileCollection>{};
  final outputsByTask = <TaskWithDeps, FileCollection>{};
  final deletionsByTask = <TaskWithDeps, FileCollection>{};

  // will return relationship between deletion tasks and others
  final tasksAffectedByDeletion = <String, Set<String>>{};

  for (var task in taskMap.values) {
    final rc = task.runCondition;
    if (rc is FilesCondition) {
      inputsByTask[task] = rc.inputs;
      outputsByTask[task] = rc.outputs;
      deletionsByTask[task] = rc.deletions;
    }
  }

  final dependencyErrors = <String>{};
  final phaseErrors = <String>{};

  // 1. a task's inputs may only include another's outputs if it depends on it
  inputsByTask.forEach((task, ins) {
    outputsByTask.forEach((otherTask, otherOuts) {
      if (task.name == otherTask.name ||
          task.dependencySet.contains(otherTask.name)) return;
      final intersectInsOuts = ins.intersection(otherOuts);
      if (intersectInsOuts.isNotEmpty) {
        dependencyErrors
            .add("Task '${task.name}' must dependOn '${otherTask.name}' "
                '(clashing outputs: $intersectInsOuts)');
      }
    });
  });

  // 2. deletion tasks must be in a phase that precedes tasks whose
  //    inputs/outputs are deleted by them
  inputsByTask.forEach((task, ins) {
    deletionsByTask.forEach((deletionTask, deletedFiles) {
      final outs = outputsByTask[task]!;

      bool addErrorIfNotEmpty(Set<String> intersection, String io) {
        if (intersection.isNotEmpty) {
          tasksAffectedByDeletion.accumulate(task.name, deletionTask.name);
          if (!task.phase.isAfter(deletionTask.phase)) {
            phaseErrors.add("Task '${deletionTask.name}' "
                "(phase '${deletionTask.phase.name}') deletes $io of "
                "'${task.name}' (phase '${task.phase.name}'): $intersection");
            return true;
          }
        }
        return false;
      }

      if (!addErrorIfNotEmpty(ins.intersection(deletedFiles), 'inputs')) {
        addErrorIfNotEmpty(outs.intersection(deletedFiles), 'outputs');
      }
    });
  });

  if (dependencyErrors.isNotEmpty || phaseErrors.isNotEmpty) {
    final error = StringBuffer();
    if (dependencyErrors.isNotEmpty) {
      error.writeln("The following tasks have implicit dependencies due to "
          "their inputs depending on other tasks' outputs:");
      error.writeln(dependencyErrors.map((e) => '  * $e.').join('\n'));
      error.writeln('\nPlease add the dependencies explicitly.');
    }
    if (phaseErrors.isNotEmpty) {
      if (error.isNotEmpty) {
        error.writeln();
      }
      error.writeln("The following tasks delete inputs or outputs of another "
          "task that does not run on a later phase, hence could corrupt those "
          "tasks execution:");
      error.writeln(phaseErrors.map((e) => '  * $e.').join('\n'));
      error.writeln('\nPlease change the task phases so that deletion tasks '
          "run on earlier phases (typically 'setup') than other tasks.");
    }
    throw DartleException(message: error.toString());
  }
  return tasksAffectedByDeletion;
}