Skip to content

fix: templates with an expression and with(…); Java action javadoc and EXTRA CODE round trip (#1683, #1700, #1702) - #1736

Merged
ako merged 3 commits into
mainfrom
fix/1683-templates-java-action-roundtrip
Oct 27, 2026
Merged

ako merged 3 commits into
mainfrom
fix/1683-templates-java-action-roundtrip

Conversation

@ako

@ako ako commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Closes #1683. Closes #1700 (mirror of mendixlabs#1653). Closes #1702 (mirror of mendixlabs#1658).

Three issues, one commit each.

#1683 — log / send email stored an expression followed by with (…) as its source text

log info node 'P' $Msg with ({1} = 'x'); logged the literal $Msg; Subject: $Subj with ({1} = '42') sent $Subj — check, exec and mx check all passed. Cause (cmd_microflows_builder_calls.go): addLogMessageAction and emailTemplate stored fb.exprToString(message) as the template text. show message and validation feedback had the same shape broken differently — they stored '{1}' with $Msg as its parameter and appended the stated ones, so 'x' became a {2} nothing reads and the message showed its own unfilled {1}.

Fix: one shared decision, messageTemplateRefusals (new message_templates.go), rule MDL-TEMPLATE01: an expression followed by with (…) (or the deprecated objects [...]) is refused on log, send email (subject, body, HtmlBody), show message and validation feedback — all four builders call it (exec refuses), and check reports it from the microflow, nanoflow, alter-fragment and rule validators. An expression has no {n} for with to fill; the same rule as MDL-REST05 (#1669) and MDL-JA02 (#1692). Sibling fixed: a multi-line email Body: string literal with parameters stored its source, quotes included, as the template; it now stores the literal's value. Kept inside the template paths (PR #1731 touches the call builders/grammar).

Verified: message_templates_test.go (check and exec on every statement and flow kind, controls, the multi-line body; reverted: every refused case and the body case fail). 11.13.0: before, all four flows written and mx check 0 errors; after, check and exec --no-check refuse with nothing written; the control forms build at 0 errors.

#1700 — the generated .java file lacked the class javadoc

Cause: sdk/javaactions/source.go GenerateSource took no documentation. Fix: GenerateSource / WriteJavaSourceFile take it; the executor passes the action's carried documentation, so create or modify without a doc comment keeps the block. The format was measured against mxbuild 11.13.0: \r\n, \r and \n each end a * line; whitespace kept (whitespace-only documentation still gets a block); a backslash is doubled unless u/U + four hex digits follow (the upstream proposal missed this); mxbuild rejects */ in documentation as a model error, which an MDL doc comment cannot produce.
Verified: 14 goldens from a real mxbuild deploy build (sdk/javaactions/testdata/mxbuild/JA_Doc*.java.golden, CRLF preserved) reproduced byte for byte; reverted, all but the no-documentation control fail. Real app, 15 actions create or modifyd over mxbuild's files: 13/15 byte-identical (baseline 1/15; the other 2 differ only in mxcli's long-standing user-code blank-line padding), and the next mxbuild build rewrote 0/15 files (baseline 14/15).

#1702 — no EXTRA CODE clause, so describe → exec lost hand-written helpers

Cause: the grammar ended create java action at AS $$ … $$ (the AST already had an unset ExtraCode), and describe read only the USER CODE section. New clause:

create or modify java action ModJ.JA_Twice(N: Integer not null) returns Integer
as $$
import java.util.Objects;
return twice(N);
$$
extra code $$
private static long twice(long n) { return Objects.requireNonNull(n) * 2L; }
$$;

extra code $$ … $$ sets the EXTRA CODE section (blank lines at either end trimmed; an import line inside it moves to the import list, as in the body); omitted keeps the stored section (like EXPOSED AS); extra code $$ $$ clears it. Describe prints it, plus the shortest leading part of the import list that regenerates the stored list in order. EXTRA and CODE are new keywords, both in the keyword rule (a test shows Code still works as an attribute name).
Verified: visitor/sdk/executor tests — describe → exec rebuilds the report's file byte for byte; leaving the clause out hands the writer nil (keep). Reverted: all fail. Real app: baseline replay lost the import and the helper and mxbuild failed cannot find symbol; fixed replay is byte-identical, builds, and the build leaves it unchanged. Doctype example in 07-java-action-examples.mdl passes -tags integration exec + mx check (0 errors).

All

Full go test ./... (incl. the conformance gate), go vet, gofmt; CI's doc checks pass locally. Repros: log-email-1683-expression-with-params.fail.mdl, the #1700 and java-action-1702-extra-code-roundtrip.mdl bug-tests. Findings recorded. Docs: mxcli syntax (logging, send-email, validation, java-action.create), MDL_QUICK_REFERENCE.md, write-microflows pitfalls §16, java-actions skill, docs-site create-java-action.md and error appendix.

Noticed, not fixed

  • JavaScript actions have the same EXTRA CODE round-trip gap (describe prints it only as a comment; WriteJavaScriptSourceFile has no extra-code parameter).
  • TUI preview stripJavaCodeBlock looks for "\nAS $$" but describe prints lowercase as $$.
  • A $$ inside Java code ends the dollar string (body and extra code alike); documentation containing // BEGIN USER CODE would confuse section extraction.

🤖 Generated with Claude Code

ako and others added 3 commits October 26, 2026 21:39
…urce text (#1683)

`log info node 'P' $Msg with ({1} = 'x');` logged the literal text `$Msg`,
and `send email (… Subject: $Subj with ({1} = '42') …)` sent the subject
`$Subj`. `show message` and `validation feedback` stored the same statement
as '{1}' with the expression and then the stated parameters, so 'x' became a
{2} no placeholder reads and the message showed its own unfilled {1}. check,
exec and mx check (0 errors, 11.13.0) passed all four.

Cause: a message template is a text with {n} placeholders; an expression has
none for `with (…)` to fill, and Mendix cannot store an expression as a
template's text. addLogMessageAction and emailTemplate
(mdl/executor/cmd_microflows_builder_calls.go) fell back to
fb.exprToString(message) as the template text when parameters were stated;
addShowMessageAction / addValidationFeedbackAction appended them after the
expression's {1}. Nothing in check refused the shape.

Fix: one decision, messageTemplateRefusals (mdl/executor/message_templates.go),
refuses an expression followed by `with (…)` (or the deprecated `objects [...]`)
on all four statements — the shape MDL-REST05 and MDL-JA02 refuse for REST
calls and Java action arguments. The builders call it (exec, for microflows,
nanoflows, rules and alter fragments alike), and check reports it as
MDL-TEMPLATE01 from the microflow, nanoflow, alter-fragment and rule
validators. Sibling on the same branch: a multi-line string-literal email
Body with parameters arrives as a SourceExpr and stored its source, quotes
included, as the template; it is now stored as the literal's value.

Verification: message_templates_test.go covers check and exec on every
statement with controls (a string template, an expression without
`with (…)`), every flow kind, and the multi-line body; reverting the
production change fails all refused cases and the body case. On a blank
11.13.0 app the baseline wrote the four flows (describe printed '$Msg' /
'$Subj'; mx check 0 errors); the fix refuses them at check and at
exec --no-check, nothing written. The controls build at mx check 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oc (#1700)

mxbuild writes a Java action's documentation as a javadoc block above
`public class`. The file mxcli wrote never had it: a new action got none, and
`create or modify java action` removed the one a build had written, so the
next build put it back and every rewrite left a pair of unrelated VCS diffs.
The model itself was intact. Mirror of mendixlabs#1653.

Cause: GenerateSource (sdk/javaactions/source.go) took no documentation, and
neither did backend.JavaBackend.WriteJavaSourceFile or its executor caller,
although the executor had the documentation at hand.

Fix: GenerateSource and WriteJavaSourceFile take the documentation; the
executor passes ja.Documentation, the one the model ends up with, so a rewrite
without a doc comment keeps the block. classJavadoc writes the block as
measured on mxbuild 11.13.0 with documentation set straight in the model:
`\r\n`, `\r` and `\n` each end a ` * ` line (so leading/trailing breaks give
empty lines), whitespace is kept, whitespace-only documentation still gets a
block, and a backslash is doubled unless `u`/`U` and four hex digits follow
it. mxbuild refuses `*/` in the documentation as a model error. Mock, mcp and
modelsdk stubs regenerated.

Verification: 14 goldens from an mxbuild deploy build
(testdata/mxbuild/JA_Doc*.java.golden, two of them generated from no file)
are reproduced byte for byte; executor tests check the stated and the carried
documentation reach the writer. Reverting the generator and the executor
argument fails all of them except the no-documentation control. Real 11.13.0
app, 15 actions: after `create or modify` over mxbuild's files, 13 of 15 are
byte-identical to mxbuild's with the fix (the other two differ only in the
user-code padding mxcli has always written) against 1 of 15 with the baseline;
the next mxbuild build rewrote 0 of 15 files after the fix and 14 of 15 after
the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1702)

Studio Pro keeps three parts of a Java action's .java file when it
regenerates it: the import list, the USER CODE and the EXTRA CODE section.
MDL could author the first two (an import line in the body is hoisted to the
import list) but had no clause for the third, and `describe java action`
printed only the body. `create or modify` kept a hand-written helper in place,
but executing the describe output on another project wrote a body calling a
helper that no longer existed, and the build failed. Mirror of
mendixlabs#1658.

Cause: createJavaActionStatement ended at `AS $$ … $$` (the AST's ExtraCode
field was never set), and describeJavaAction read only the USER CODE section.

Fix:
  create or modify java action ModJ.JA_Twice(N: Integer not null) returns Integer
  as $$
  import java.util.Objects;
  return twice(N);
  $$
  extra code $$
  private static long twice(long n) { return Objects.requireNonNull(n) * 2L; }
  $$;
`extra code $$ … $$` (new EXTRA/CODE keywords, both still names) states the
section, kept as written apart from its blank edge lines; an import line in it
goes to the import list too. Omitted on `create or modify`, the stored section
is kept, as an omitted EXPOSED AS keeps the toolbox entry; `extra code $$ $$`
clears it, so WriteJavaSourceFile/RetainSections take the section as a
*string (nil keeps). describe prints the section and, at the top of the body,
the shortest leading part of the stored import list that regenerates it
(javaactions.StatedImports) — nothing for mxbuild's own list.

Verification: visitor, sdk and executor tests, including describe → exec of
the report's file rebuilding it byte for byte and the omitted-clause control
handing the writer nil; stubbing the visitor, describe and RetainSections
changes fails them. Real 11.13.0 app with the report's steps: the baseline
replay on a fresh project lost the import and helper and mxbuild failed with
`cannot find symbol`; the fix's replay is byte-identical to the source file,
builds, and the build leaves it unchanged. `create or modify` without the
clause leaves the file unchanged. 07-java-action-examples.mdl passes exec + mx
check with the new example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ako
ako merged commit 9bdf723 into main Oct 27, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment