parseComments function
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;
}