public function collect($job_list) {
    $container_builder = $this->containerBuilderGetter->getContainerBuilder();

    $collectors_info = $this->getCollectorServiceIds($container_builder);

    $data = [];

    // Declare event subscriber services. These don't have collectors in Drupal
    // as they are native to Symfony, so we need to declare explicitly.
    $data['event_subscriber'] = [
      'label' => 'Event subscriber',
      'interface' => 'Symfony\Component\EventDispatcher\EventSubscriberInterface',
      'methods' => $this->methodCollector->collectMethods('Symfony\Component\EventDispatcher\EventSubscriberInterface'),
      // TODO: services of this type should go in the EventSubscriber namespace.
    ];

    foreach ($collectors_info as $service_name => $tag_infos) {
      // Sloppy array structure...
      $collector_type = $tag_infos['collector_type'];
      unset($tag_infos['collector_type']);

      $service_definition = $container_builder->getDefinition($service_name);
      $service_class = $service_definition->getClass();

      // A single service collector service can collect on more than one tag.
      foreach ($tag_infos as $tag_info) {
        $tag = $tag_info['tag'];

        if ($collector_type == 'service_id_collector') {
          // Service ID collectors don't give us anything but the service ID,
          // so nothing can be detected about the interface collected services
          // should use.
          // The only example of a service collector using this tag
          // in core is 'theme.negotiator', which is self-consuming: it expects
          // its tagged services to implement its own interface. So all we can
          // do is assume other implementations will do the same.
          $service_class_reflection = new \ReflectionClass($service_class);

          $label = CaseString::pascal($service_class_reflection->getShortName())->title();

          // Hope there's only one interface...
          $service_interfaces = $service_class_reflection->getInterfaceNames();
          $collected_services_interface = array_shift($service_interfaces);

          $interface_methods = $this->methodCollector->collectMethods($collected_services_interface);
        }
        else {
          // Service collectors tell us what the container should call on the
          // collector to add a tagged service, and so from that we can deduce
          // the interface that tagged services must implement.
          if (!isset($tag_info['call'])) {
            // Shouldn't normally happen, but protected against badly-declated
            // services.
            continue;
          }

          $collecting_method = $tag_info['call'];

          $collecting_methodR = new \ReflectionMethod($service_class, $collecting_method);
          $collecting_method_paramR = $collecting_methodR->getParameters();

          // TODO: skip if more than 1 param.
          // getNumberOfParameters

          $collected_services_interface = (string) $collecting_method_paramR[0]->getType();

          if (!interface_exists($collected_services_interface)) {
            // Shouldn't happen, as the typehint will be an interface the
            // collected services must implement.
            continue;
          }

          // Make a label from the interface name: take the short interface name,
          // and remove an 'Interface' suffix, convert to title case.
          $interface_pieces = explode('\\', $collected_services_interface);
          $label = array_pop($interface_pieces);
          $label = preg_replace('@Interface$@', '', $label);
          $label = CaseString::pascal($label)->title();

          $interface_methods = $this->methodCollector->collectMethods($collected_services_interface);
        }

        $data[$tag] = [
          'label' => $label,
          'collector_type' => $collector_type,
          'interface' => $collected_services_interface,
          'methods' => $interface_methods,
        ];
      }
    }

    // Sort by ID.
    ksort($data);

    return $data;
  }