parseComments function

ManualExtraction parseComments(
  1. ScannedFile file,
  2. void warn(
    1. String
    ),
  3. List<String> errors
)

Parst alle @journey:-Blöcke einer Datei. Warnungen (unbekannte Keys, unbekannte Trigger/Typen) via warn; fatale Probleme (fehlende Pflichtfelder, nicht auflösbares from) landen in errors.

Implementation

ManualExtraction parseComments(
  ScannedFile file,
  void Function(String) warn,
  List<String> errors,
) {
  final result = ManualExtraction();

  for (final block in splitBlocks(file.content)) {
    final where = '${file.relPath}:${block.line}';
    if (!_blockTypes.contains(block.type)) {
      warn('Warnung: $where: unbekannter @journey-Typ "${block.type}" — '
          'Block wird ignoriert.');
      continue;
    }

    final values = <String, String>{};
    for (final m in _pair.allMatches(block.text)) {
      final key = m.group(1)!;
      if (!_knownKeys[block.type]!.contains(key)) {
        warn('Warnung: $where: unbekannter Key "$key" in '
            '@journey:${block.type} — wird ignoriert.');
        continue;
      }
      values[key] = _unescape(m.group(2)!);
    }

    final missing = _requiredKeys[block.type]!
        .where((k) => !values.containsKey(k))
        .toList();
    if (missing.isNotEmpty) {
      errors.add('$where: @journey:${block.type} fehlen Pflichtfelder: '
          '${missing.join(', ')}.');
      continue;
    }

    final blockOffset = file.lineInfo.getOffsetOfLine(block.line - 1);
    final enclosing = _enclosingClass(file.unit, blockOffset);

    switch (block.type) {
      case 'screen':
      case 'decision':
        // Block einer Klasse zuordnen: umschließend oder direkt darüber.
        final cls = enclosing ?? _nextClassAfter(file.unit, blockOffset);
        final symbol = cls?.namePart.typeName.lexeme;
        final node = GraphNode(
          id: values['id']!,
          type: block.type,
          title: values['title'],
          flow: values['flow'],
          description: values['description'],
          tags: values.containsKey('tags') ? _splitTags(values['tags']!) : const [],
          source: SourceKind.annotation,
          sourceRef:
              SourceRef(file: file.relPath, line: block.line, symbol: symbol),
        );
        result.nodes.add(node);
        if (block.type == 'screen' && symbol != null) {
          result.screenClassNames.putIfAbsent(symbol, () => node.id);
        }
      case 'action':
        var trigger = values['trigger'] ?? 'tap';
        if (!validTriggers.contains(trigger)) {
          warn('Warnung: $where: unbekannter trigger "$trigger" — '
              'verwende "tap".');
          trigger = 'tap';
        }
        if (values['from'] == null && enclosing == null) {
          errors.add('$where: @journey:action ohne "from" und ohne '
              'umschließende Klasse — "from" nicht bestimmbar.');
          continue;
        }
        result.actions.add(ActionCandidate(
          id: values['id'],
          label: values['label']!,
          to: values['to']!,
          from: values['from'],
          trigger: trigger,
          condition: values['condition'],
          enclosingClassName: enclosing?.namePart.typeName.lexeme,
          sourceRef: SourceRef(
            file: file.relPath,
            line: block.line,
            symbol: enclosing?.namePart.typeName.lexeme,
          ),
        ));
      case 'flow':
        result.flows.add(GraphFlow(
          id: values['id']!,
          title: values['title'],
          start: values['start'],
          description: values['description'],
          source: SourceKind.annotation,
          sourceRef: SourceRef(file: file.relPath, line: block.line),
        ));
    }
  }
  return result;
}