Skip to content

Commit ffa8616

Browse files
hjothahjothamendix
authored andcommitted
fix: support log node expressions in microflow roundtrip
The LOG statement's optional NODE clause only accepted a bare `STRING_LITERAL` (e.g. `LOG INFO NODE 'MyModule' 'msg'`), but Studio Pro persists projects where the node is any expression — a constant reference like `@MyModule.SecurityLogNode`, a variable, or a function call. Describing those microflows emitted MDL the parser rejected with `mismatched input '$' expecting STRING_LITERAL`, and `addLogMessageAction` was wrapping the already-quoted node in an extra pair of quotes whenever the input was a plain string. Widen the grammar and AST: logStatement : LOG logLevel? (NODE expression)? expression logTemplateParams? ; LogStmt.Node: Expression // was string The visitor now collects all child expressions and assigns the first to `Node` / second to `Message` when `NODE` is present; the builder formats `s.Node` through `exprToString` and defaults to `'Application'` when absent. Describer and diff emitter pass the node through the same expression formatter, and the format casing is lowercased to match the rest of the MDL output. `formatAction` for `LogMessageAction` keeps raw template text on the wire so Mendix-style `{1}` placeholders survive when WITH params are present; without that, the literal got re-quoted into `'''Order {1}'''` on every roundtrip. Regression tests cover: - `log info node @Module.Constant 'msg'` roundtrip through create -> describe -> re-exec - template LOG statements: `log info node 'App' 'Order {1}' with ...` - visitor builds `ConstantRefExpr` for `@Module.Constant` node - `formatAction` multiline escaping matches the new lowercased form
1 parent b1e13bd commit ffa8616

12 files changed

Lines changed: 253 additions & 44 deletions

mdl/ast/ast_microflow.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ func (p *TemplateParam) IsDataSourceRef() bool {
261261
return p.DataSourceName != ""
262262
}
263263

264-
// LogStmt represents: LOG LEVEL NODE 'node' 'message' [WITH params]
264+
// LogStmt represents: LOG LEVEL [NODE expr] message [WITH params]
265265
type LogStmt struct {
266266
Level LogLevel // Log level (INFO, WARNING, etc.)
267-
Node string // Log node name
267+
Node Expression // Optional log node expression
268268
Message Expression // Message expression
269269
Template []TemplateParam // Optional WITH template params
270270
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation

mdl/executor/bugfix_regression_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,82 @@ func TestAddWhileStatement_PreservesAnnotatedPosition(t *testing.T) {
140140
}
141141
}
142142

143+
func TestAddLogMessageAction_PreservesNodeExpression(t *testing.T) {
144+
fb := &flowBuilder{
145+
posX: 100,
146+
posY: 200,
147+
spacing: HorizontalSpacing,
148+
measurer: &layoutMeasurer{},
149+
}
150+
151+
stmt := &ast.LogStmt{
152+
Level: ast.LogInfo,
153+
Node: &ast.ConstantRefExpr{
154+
QualifiedName: ast.QualifiedName{Module: "TestModule", Name: "SecurityLogNode"},
155+
},
156+
Message: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "User added"},
157+
}
158+
159+
id := fb.addLogMessageAction(stmt)
160+
if id == "" {
161+
t.Fatal("expected log activity ID")
162+
}
163+
164+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
165+
if !ok {
166+
t.Fatalf("expected ActionActivity, got %T", fb.objects[len(fb.objects)-1])
167+
}
168+
169+
action, ok := activity.Action.(*microflows.LogMessageAction)
170+
if !ok {
171+
t.Fatalf("expected LogMessageAction, got %T", activity.Action)
172+
}
173+
174+
if action.LogNodeName != "@TestModule.SecurityLogNode" {
175+
t.Fatalf("got log node %q, want %q", action.LogNodeName, "@TestModule.SecurityLogNode")
176+
}
177+
}
178+
179+
func TestAddLogMessageAction_TemplateLiteralDoesNotKeepQuotes(t *testing.T) {
180+
fb := &flowBuilder{
181+
posX: 100,
182+
posY: 200,
183+
spacing: HorizontalSpacing,
184+
measurer: &layoutMeasurer{},
185+
}
186+
187+
stmt := &ast.LogStmt{
188+
Level: ast.LogInfo,
189+
Node: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "App"},
190+
Message: &ast.LiteralExpr{
191+
Kind: ast.LiteralString,
192+
Value: "Order {1}",
193+
},
194+
Template: []ast.TemplateParam{
195+
{Index: 1, Value: &ast.VariableExpr{Name: "OrderNumber"}},
196+
},
197+
}
198+
199+
id := fb.addLogMessageAction(stmt)
200+
if id == "" {
201+
t.Fatal("expected log activity ID")
202+
}
203+
204+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
205+
if !ok {
206+
t.Fatalf("expected ActionActivity, got %T", fb.objects[len(fb.objects)-1])
207+
}
208+
209+
action, ok := activity.Action.(*microflows.LogMessageAction)
210+
if !ok {
211+
t.Fatalf("expected LogMessageAction, got %T", activity.Action)
212+
}
213+
214+
if got := action.MessageTemplate.Translations["en_US"]; got != "Order {1}" {
215+
t.Fatalf("got message template %q, want %q", got, "Order {1}")
216+
}
217+
}
218+
143219
// =============================================================================
144220
// Issue #19: Long type must not be downgraded to Integer
145221
// =============================================================================

mdl/executor/cmd_diff_mdl.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,9 @@ func microflowStatementToMDL(ctx *ExecContext, stmt ast.MicroflowStatement, inde
345345
lines = append(lines, indentStr+"end loop;")
346346

347347
case *ast.LogStmt:
348-
nodeStr := s.Node
349-
if !strings.HasPrefix(nodeStr, "'") {
350-
nodeStr = "'" + nodeStr + "'"
348+
nodeStr := "'Application'"
349+
if s.Node != nil {
350+
nodeStr = diffExpressionToString(ctx, s.Node)
351351
}
352352
msgStr := diffExpressionToString(ctx, s.Message)
353353
stmt := fmt.Sprintf("%slog %s node %s %s", indentStr, strings.ToLower(s.Level.String()), nodeStr, msgStr)

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ func (fb *flowBuilder) addLogMessageAction(s *ast.LogStmt) model.ID {
3838

3939
if len(s.Template) > 0 {
4040
// Use provided template parameters
41-
templateText = fb.exprToString(s.Message)
41+
if lit, ok := s.Message.(*ast.LiteralExpr); ok && lit.Kind == ast.LiteralString {
42+
templateText = fmt.Sprintf("%v", lit.Value)
43+
} else {
44+
templateText = fb.exprToString(s.Message)
45+
}
4246
// Sort parameters by index to ensure correct order
4347
maxIndex := 0
4448
for _, p := range s.Template {
@@ -61,10 +65,15 @@ func (fb *flowBuilder) addLogMessageAction(s *ast.LogStmt) model.ID {
6165
templateParams = []string{fb.exprToString(s.Message)}
6266
}
6367

68+
logNodeName := "'Application'"
69+
if s.Node != nil {
70+
logNodeName = fb.exprToString(s.Node)
71+
}
72+
6473
action := &microflows.LogMessageAction{
6574
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
6675
LogLevel: logLevel,
67-
LogNodeName: "'" + s.Node + "'", // Store as expression (e.g., 'TEST')
76+
LogNodeName: logNodeName,
6877
MessageTemplate: &model.Text{
6978
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
7079
Translations: map[string]string{

mdl/executor/cmd_microflows_format_action_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,23 @@ func TestFormatAction_LogMessage_EscapesMultiline(t *testing.T) {
535535
},
536536
}
537537
got := e.formatAction(action, nil, nil)
538-
want := "LOG INFO NODE 'App' 'Line 1\\nLine 2';"
538+
want := "log info node 'App' 'Line 1\\nLine 2';"
539+
if got != want {
540+
t.Errorf("got %q, want %q", got, want)
541+
}
542+
}
543+
544+
func TestFormatAction_LogMessage_NodeExpression(t *testing.T) {
545+
e := newTestExecutor()
546+
action := &microflows.LogMessageAction{
547+
LogLevel: microflows.LogLevelInfo,
548+
LogNodeName: "@MyModule.SecurityLogNode",
549+
MessageTemplate: &model.Text{
550+
Translations: map[string]string{"en_US": "User added"},
551+
},
552+
}
553+
got := e.formatAction(action, nil, nil)
554+
want := "log info node @MyModule.SecurityLogNode 'User added';"
539555
if got != want {
540556
t.Errorf("got %q, want %q", got, want)
541557
}

mdl/executor/roundtrip_microflow_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,56 @@ func TestRoundtripMicroflow_LogWithTemplate(t *testing.T) {
107107
t.Error("Expected $CustomerName parameter in output")
108108
}
109109

110+
if err := env.executeMDL(output); err != nil {
111+
t.Fatalf("Describe output should execute without syntax errors: %v\n%s", err, output)
112+
}
113+
114+
output2, err := env.describeMDL(`DESCRIBE MICROFLOW ` + microflowName + `;`)
115+
if err != nil {
116+
t.Fatalf("Failed to describe microflow after re-exec: %v", err)
117+
}
118+
if strings.Contains(output2, "'''Processing order") {
119+
t.Fatalf("Roundtrip should not double-quote log template text:\n%s", output2)
120+
}
121+
110122
t.Logf("log with template roundtrip successful:\n%s", output)
111123
}
112124

125+
func TestRoundtripMicroflow_LogWithNodeExpression(t *testing.T) {
126+
env := setupTestEnv(t)
127+
defer env.teardown()
128+
129+
if err := env.executeMDL(`CREATE CONSTANT ` + testModule + `.SecurityLogNode TYPE String DEFAULT 'Security';`); err != nil {
130+
t.Fatalf("Failed to create constant for log node: %v", err)
131+
}
132+
133+
microflowName := testModule + ".TestLogNodeExpression"
134+
env.registerCleanup("microflow", microflowName)
135+
136+
createMDL := `CREATE MICROFLOW ` + microflowName + ` () RETURNS Boolean
137+
BEGIN
138+
LOG INFO NODE @` + testModule + `.SecurityLogNode 'User added';
139+
RETURN true;
140+
END;`
141+
142+
if err := env.executeMDL(createMDL); err != nil {
143+
t.Fatalf("Failed to create microflow with node expression: %v", err)
144+
}
145+
146+
output, err := env.describeMDL(`DESCRIBE MICROFLOW ` + microflowName + `;`)
147+
if err != nil {
148+
t.Fatalf("Failed to describe microflow: %v", err)
149+
}
150+
151+
if !containsProperty(output, "LOG INFO NODE @"+testModule+".SecurityLogNode") {
152+
t.Fatalf("Expected constant node expression in output, got:\n%s", output)
153+
}
154+
155+
if err := env.executeMDL(output); err != nil {
156+
t.Fatalf("Describe output should execute without syntax errors: %v\n%s", err, output)
157+
}
158+
}
159+
113160
// --- DESCRIBE MICROFLOW Roundtrip Tests ---
114161
// These tests verify that DESCRIBE MICROFLOW produces correct output for various
115162
// microflow patterns. They serve as regression guards for bugs in findBranchFlows

mdl/executor/validate_microflow.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ func referencedVars(stmt ast.MicroflowStatement) []string {
400400
case *ast.RemoveFromListStmt:
401401
refs = append(refs, s.Item, s.List)
402402
case *ast.LogStmt:
403+
refs = append(refs, exprVarRefs(s.Node)...)
403404
refs = append(refs, exprVarRefs(s.Message)...)
404405
}
405406
return refs

mdl/grammar/MDLParser.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1400,7 +1400,7 @@ raiseErrorStatement
14001400

14011401
// LOG INFO NODE 'TEST' 'Message'; or LOG INFO 'Message'; or LOG WARNING 'Message' WITH ({1} = $var);
14021402
logStatement
1403-
: LOG logLevel? (NODE STRING_LITERAL)? expression logTemplateParams?
1403+
: LOG logLevel? (NODE expression)? expression logTemplateParams?
14041404
;
14051405

14061406
logLevel

mdl/grammar/parser/MDLParser.interp

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

mdl/grammar/parser/mdl_parser.go

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

0 commit comments

Comments
 (0)