From 911f368362fff745fda65e81d3c59256a309c2e1 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 4 Jun 2026 02:24:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(editor):=20match=20real=20F=E2=88=86I=20YAM?= =?UTF-8?q?L=20type=20shape=20+=20warn=20on=20bad=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.15.0 type-token coloring regex hunted for `type: `, but the F∆I YAML actually shapes types as `: ` under `inputs:` / `outputs:` blocks. Real-world flows (`hello.yaml`, `extract.yaml`) and module manifests (echo, text-summarize, …) never produce the `type:` keyword unless the operator hand-authors the long form. Fix the regex to match an indented `KEY: VALUE` line where the value is one of `text|json|bytes|file|number|integer`. Leading whitespace is required so top-level keys (`name: foo`, `version: 0.1.0`) can't false-match. This handles both shapes — the implicit `pdf: bytes` and the explicit `type: bytes` (used by module schema v2 inputs lists) — because either way the pattern boils down to "key colon known type token". Analyzer also grows a type-token check: any inputs/outputs field whose value isn't a known type lights up as a warning ("Unknown input type 'byes' …"), modulo `$ref` expressions (flow outputs) and empty values (operator is mid-keystroke). Adds `test/flow_analyzer_test.dart` with seven cases covering empty, valid hello, unknown capability, unknown type, parse error, version-bare matching, and the empty-installed-list silence path. Bumps the package to 0.15.1. Signed-off-by: flemming-it --- lib/src/flow_analyzer.dart | 36 ++++++++++ lib/src/flow_yaml_controller.dart | 24 +++++-- pubspec.yaml | 2 +- test/flow_analyzer_test.dart | 107 ++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 test/flow_analyzer_test.dart diff --git a/lib/src/flow_analyzer.dart b/lib/src/flow_analyzer.dart index bb22e2c..4fa5c9f 100644 --- a/lib/src/flow_analyzer.dart +++ b/lib/src/flow_analyzer.dart @@ -19,6 +19,8 @@ import 'package:flutter_code_editor/flutter_code_editor.dart'; import 'package:yaml/yaml.dart'; +import 'wire_colors.dart'; + class FlowAnalyzer extends AbstractAnalyzer { /// Returns the names of capabilities the operator has installed. /// A closure so the analyzer always sees the current list @@ -84,10 +86,44 @@ class FlowAnalyzer extends AbstractAnalyzer { } } } + + // Flag any inputs/outputs declaration whose value isn't a + // recognised type token. Flow YAML keeps each field as a + // `: ` pair under `inputs:` / `outputs:` — a + // typo like `pdf: byes` should light up rather than fail + // silently at run time. + _checkFieldTypes(doc['inputs'], 'input', issues); + _checkFieldTypes(doc['outputs'], 'output', issues); } return AnalysisResult(issues: issues); } + + void _checkFieldTypes(YamlNode? block, String kind, List out) { + if (block is! YamlMap) return; + for (final entry in block.nodes.entries) { + final value = entry.value.value; + if (value is! String) continue; + // Skip reference expressions like `$inputs.name` or + // `$step.field` — only flow `outputs:` use these and they + // aren't type declarations. + if (value.startsWith(r'$')) continue; + // Skip plainly empty values — the operator is typing, not + // declaring a typo. + if (value.trim().isEmpty) continue; + if (!kKnownTypes.contains(value)) { + out.add( + Issue( + line: entry.value.span.start.line, + message: + 'Unknown $kind type "$value". ' + 'Use one of: ${kKnownTypes.join(", ")}.', + type: IssueType.warning, + ), + ); + } + } + } } /// Strip the `@` tail from a capability spec so diff --git a/lib/src/flow_yaml_controller.dart b/lib/src/flow_yaml_controller.dart index 2f6d815..7cec615 100644 --- a/lib/src/flow_yaml_controller.dart +++ b/lib/src/flow_yaml_controller.dart @@ -39,13 +39,25 @@ class FlowYamlCodeController extends CodeController { analyzer = FlowAnalyzer(availableCapabilities: provider); } - // Matches `type:` followed by (optional) quote + one of our - // known type names + (optional) closing quote. The captured - // group is the type name only — we use the parent match span - // to locate it inside the document so we can recolour just - // the name (not the `type:` key itself). + // Matches an indented `key: value` line where the value is + // one of our known type tokens. This covers both shapes the + // F∆I YAML uses: + // + // inputs: + // pdf: bytes # field-name → type + // count: number + // + // inputs: + // - name: pdf + // type: bytes # explicit `type:` row + // + // We require at least one leading whitespace so top-level + // keys (`name: foo`, `version: 0.1.0`) can't false-match. + // The captured group is the value — we resolve its offset + // inside the document and recolour only that token. static final RegExp _typeAssignment = RegExp( - r'''\btype\s*:\s*(?:['"])?([A-Za-z_][\w-]*)(?:['"])?''', + r'''^[ \t]+[A-Za-z_][\w-]*\s*:\s*(?:['"])?([A-Za-z_][\w-]*)(?:['"])?\s*(?:#.*)?$''', + multiLine: true, ); @override diff --git a/pubspec.yaml b/pubspec.yaml index ca3613a..b354a4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fai_studio_flow_editor description: Swappable inline YAML editor for F∆I Studio flows. -version: 0.15.0 +version: 0.15.1 publish_to: 'none' repository: https://git.flemming.ai/fai/studio-flow-editor diff --git a/test/flow_analyzer_test.dart b/test/flow_analyzer_test.dart new file mode 100644 index 0000000..1fb58b2 --- /dev/null +++ b/test/flow_analyzer_test.dart @@ -0,0 +1,107 @@ +// Behavioural tests for FlowAnalyzer — the YAML-aware +// AbstractAnalyzer that feeds the flow editor's gutter + the +// custom CodeController's wavy-underline overrides. + +import 'package:fai_studio_flow_editor/src/flow_analyzer.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:highlight/languages/yaml.dart'; + +Code _wrap(String text) => Code(text: text, language: yaml); + +void main() { + group('FlowAnalyzer', () { + test('empty buffer reports no issues', () async { + final a = FlowAnalyzer(availableCapabilities: () => const []); + final r = await a.analyze(_wrap('')); + expect(r.issues, isEmpty); + }); + + test('valid hello flow reports no issues', () async { + final a = FlowAnalyzer( + availableCapabilities: () => const ['debug.echo'], + ); + final r = await a.analyze(_wrap(''' +name: hello +inputs: + name: text +steps: + - id: greet + use: debug.echo@^0 + with: + message: \$inputs.name +outputs: + greeting: \$greet.echoed +''')); + expect(r.issues, isEmpty); + }); + + test('flags unknown capability on the use line', () async { + final a = FlowAnalyzer( + availableCapabilities: () => const ['debug.echo'], + ); + final r = await a.analyze(_wrap(''' +name: hello +inputs: + msg: text +steps: + - id: t + use: text.translate@^1 +outputs: {} +''')); + expect(r.issues, hasLength(1)); + expect(r.issues.first.message, contains('text.translate')); + expect(r.issues.first.type, IssueType.error); + }); + + test('flags unknown input type', () async { + final a = FlowAnalyzer(availableCapabilities: () => const []); + final r = await a.analyze(_wrap(''' +name: hello +inputs: + pdf: byes +steps: [] +outputs: {} +''')); + expect(r.issues, hasLength(1)); + expect(r.issues.first.message, contains('byes')); + expect(r.issues.first.type, IssueType.warning); + }); + + test('YAML parse error pins to the offending line', () async { + final a = FlowAnalyzer(availableCapabilities: () => const []); + // Unterminated quoted string — a real parse error rather + // than a structural oddity YAML happens to tolerate. + final r = await a.analyze(_wrap('name: "unterminated\n')); + expect(r.issues, hasLength(1)); + expect(r.issues.first.type, IssueType.error); + expect(r.issues.first.message, startsWith('YAML:')); + }); + + test('matches the bare capability id ignoring @version', () async { + final a = FlowAnalyzer( + availableCapabilities: () => const ['text.echo'], + ); + final r = await a.analyze(_wrap(''' +name: x +steps: + - id: e + use: text.echo@^2 +''')); + expect(r.issues, isEmpty); + }); + + test('empty installed list silences capability checks', () async { + // When the host hasn't supplied a list (Studio still + // booting, or running in CLI-only mode), we deliberately + // don't false-positive every `use:` row. + final a = FlowAnalyzer(availableCapabilities: () => const []); + final r = await a.analyze(_wrap(''' +steps: + - id: e + use: text.echo@^2 +''')); + expect(r.issues, isEmpty); + }); + }); +}