Skip to content

Commit e744810

Browse files
authored
Merge pull request #259 from hjotha/submit/qualified-function-call-grammar
fix: accept qualified function calls in expression parser
2 parents 75a2a7f + 0118360 commit e744810

8 files changed

Lines changed: 1140 additions & 986 deletions

mdl/grammar/MDLParser.g4

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3598,8 +3598,16 @@ aggregateFunction
35983598
: (COUNT | SUM | AVG | MIN | MAX) LPAREN (DISTINCT? expression | STAR) RPAREN
35993599
;
36003600

3601+
/** Function call: built-in (`length($s)`) or qualified rule / sub-microflow
3602+
* (`Module.Rule($arg)`, `Module.Mf(Param = $v)`).
3603+
*
3604+
* Named arguments parse naturally as equality expressions, i.e. a single
3605+
* `Name = expr` argument is a `BinaryExpr` with operator `=`. This matches
3606+
* the form the describer emits for `RuleSplitCondition` / rule calls in
3607+
* expression position and restores describe → check roundtrip symmetry.
3608+
*/
36013609
functionCall
3602-
: functionName LPAREN argumentList? RPAREN
3610+
: (functionName | qualifiedName) LPAREN argumentList? RPAREN
36033611
;
36043612

36053613
/** Function names - includes identifiers and keywords that are valid function names */

mdl/grammar/parser/MDLParser.interp

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

mdl/grammar/parser/mdl_lexer.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdl_parser.go

Lines changed: 1016 additions & 980 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdlparser_base_listener.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdlparser_listener.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/visitor/visitor_microflow_expression.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,14 @@ func buildFunctionCall(ctx parser.IFunctionCallContext) ast.Expression {
555555

556556
funcExpr := &ast.FunctionCallExpr{}
557557

558-
// Get function name from FunctionName rule
558+
// Get function name. `functionCall` accepts either a built-in `functionName`
559+
// (length, find, ...) or a `qualifiedName` (Module.Rule, Module.SubMicroflow)
560+
// so that describe → check roundtrips preserve rule-based split conditions
561+
// and sub-microflow calls used in expression position.
559562
if fn := funcCtx.FunctionName(); fn != nil {
560563
funcExpr.Name = fn.GetText()
564+
} else if qn := funcCtx.QualifiedName(); qn != nil {
565+
funcExpr.Name = buildQualifiedName(qn).String()
561566
}
562567

563568
// Get arguments
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package visitor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
)
10+
11+
// TestQualifiedCallInIfCondition covers the describe → check roundtrip case
12+
// where `describe microflow --format mdl` emits a rule / sub-microflow call
13+
// with a qualified name and named arguments as the IF condition. Before the
14+
// grammar was widened to accept `qualifiedName` in `functionCall`, this form
15+
// failed to parse with `mismatched input '(' expecting THEN`, blocking the
16+
// roundtrip for microflows whose ExclusiveSplit uses a RuleSplitCondition.
17+
func TestQualifiedCallInIfCondition(t *testing.T) {
18+
input := `CREATE OR MODIFY MICROFLOW MxAdmin.Test ($S: String) returns Boolean
19+
BEGIN
20+
IF ControlCenterCommons.IsNotEmptyString(String = $S) THEN
21+
RETURN true;
22+
ELSE
23+
RETURN false;
24+
END IF;
25+
END`
26+
27+
prog, errs := Build(input)
28+
if len(errs) > 0 {
29+
t.Fatalf("parse failed: %v", errs)
30+
}
31+
32+
mf, ok := prog.Statements[0].(*ast.CreateMicroflowStmt)
33+
if !ok {
34+
t.Fatalf("expected CreateMicroflowStmt, got %T", prog.Statements[0])
35+
}
36+
if len(mf.Body) == 0 {
37+
t.Fatalf("empty microflow body")
38+
}
39+
ifStmt, ok := mf.Body[0].(*ast.IfStmt)
40+
if !ok {
41+
t.Fatalf("expected IfStmt, got %T", mf.Body[0])
42+
}
43+
44+
call, ok := ifStmt.Condition.(*ast.FunctionCallExpr)
45+
if !ok {
46+
t.Fatalf("expected FunctionCallExpr as if-condition, got %T", ifStmt.Condition)
47+
}
48+
if call.Name != "ControlCenterCommons.IsNotEmptyString" {
49+
t.Errorf("call name = %q, want %q", call.Name, "ControlCenterCommons.IsNotEmptyString")
50+
}
51+
if len(call.Arguments) != 1 {
52+
t.Fatalf("expected 1 argument, got %d", len(call.Arguments))
53+
}
54+
55+
// Named argument `String = $S` parses as an equality BinaryExpr: this is
56+
// how the describer already emits RuleSplitCondition parameter mappings
57+
// and how the re-parse preserves the textual form.
58+
bin, ok := call.Arguments[0].(*ast.BinaryExpr)
59+
if !ok {
60+
t.Fatalf("expected BinaryExpr argument, got %T", call.Arguments[0])
61+
}
62+
if bin.Operator != "=" {
63+
t.Errorf("operator = %q, want %q", bin.Operator, "=")
64+
}
65+
if id, ok := bin.Left.(*ast.IdentifierExpr); !ok || id.Name != "String" {
66+
t.Errorf("left = %+v, want IdentifierExpr{String}", bin.Left)
67+
}
68+
if v, ok := bin.Right.(*ast.VariableExpr); !ok || v.Name != "S" {
69+
t.Errorf("right = %+v, want VariableExpr{S}", bin.Right)
70+
}
71+
}
72+
73+
// TestQualifiedCallPositionalArgs keeps the bare `Module.Func($arg1, $arg2)`
74+
// form parseable (no named args) — common for sub-microflow calls that take
75+
// a single entity / list argument and appear as the IF condition.
76+
func TestQualifiedCallPositionalArgs(t *testing.T) {
77+
input := `CREATE OR MODIFY MICROFLOW M.Test ($L: list of M.Item) returns Boolean
78+
BEGIN
79+
IF M.HasItems($L) THEN
80+
RETURN true;
81+
ELSE
82+
RETURN false;
83+
END IF;
84+
END`
85+
86+
prog, errs := Build(input)
87+
if len(errs) > 0 {
88+
t.Fatalf("parse failed: %v", errs)
89+
}
90+
mf := prog.Statements[0].(*ast.CreateMicroflowStmt)
91+
ifStmt := mf.Body[0].(*ast.IfStmt)
92+
call, ok := ifStmt.Condition.(*ast.FunctionCallExpr)
93+
if !ok {
94+
t.Fatalf("expected FunctionCallExpr, got %T", ifStmt.Condition)
95+
}
96+
if call.Name != "M.HasItems" {
97+
t.Errorf("call name = %q, want %q", call.Name, "M.HasItems")
98+
}
99+
if len(call.Arguments) != 1 {
100+
t.Fatalf("expected 1 argument, got %d", len(call.Arguments))
101+
}
102+
if v, ok := call.Arguments[0].(*ast.VariableExpr); !ok || v.Name != "L" {
103+
t.Errorf("arg = %+v, want VariableExpr{L}", call.Arguments[0])
104+
}
105+
}

0 commit comments

Comments
 (0)