fix(editor): match real F∆I YAML type shape + warn on bad types
The 0.15.0 type-token coloring regex hunted for `type: <name>`,
but the F∆I YAML actually shapes types as `<fieldname>: <name>`
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 <sf@flemming.it>
This commit is contained in:
parent
885d2db4e1
commit
911f368362
4 changed files with 162 additions and 7 deletions
|
|
@ -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
|
||||
// `<name>: <type>` 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<Issue> 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 `@<version>` tail from a capability spec so
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
107
test/flow_analyzer_test.dart
Normal file
107
test/flow_analyzer_test.dart
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue