From c685def87b0981f9678743529da514be56f27aae Mon Sep 17 00:00:00 2001 From: kris-bom <339814660+kris-bom@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:08:39 +0100 Subject: [PATCH] fix(javaactions): write the class javadoc from the action's documentation Studio Pro and mxbuild write a Java action's documentation as a javadoc block directly above `public class `. mxcli stored the documentation but wrote the .java file without the block, so a new action had none and every `create or modify java action` removed the one Studio Pro wrote; the next build put it back. GenerateSource and WriteJavaSourceFile now take the documentation, and the executor passes the action's documentation after the carry rule (the stated doc comment, else the stored one). It is written as mxbuild does: `/**`, one ` * ` per line (an empty line keeps the trailing space), ` */`, CRLF. No documentation, no block. The generated backend stubs are regenerated for the new signature. Closes #1653 Co-Authored-By: Claude Opus 5.5 --- ...a-action-source-without-class-javadoc.json | 1 + CHANGELOG.md | 1 + mdl/backend/java.go | 2 +- mdl/backend/mcp/unsupported_gen.go | 2 +- mdl/backend/mock/backend.go | 2 +- mdl/backend/mock/mock_java.go | 4 +- mdl/backend/modelsdk/java_write.go | 4 +- mdl/backend/modelsdk/unimplemented_gen.go | 2 +- mdl/executor/cmd_javaactions.go | 2 +- .../cmd_javaactions_bare_enum_935_test.go | 2 +- .../cmd_javaactions_export_level_test.go | 4 +- .../cmd_javaactions_expose_preserve_test.go | 2 +- mdl/executor/cmd_javaactions_javadoc_test.go | 59 +++++++++++++++++++ .../cmd_javaactions_param_description_test.go | 2 +- .../describe_java_action_type_params_test.go | 2 +- sdk/javaactions/javadoc_test.go | 59 +++++++++++++++++++ sdk/javaactions/mxbuild_golden_test.go | 4 +- sdk/javaactions/retain_test.go | 2 +- sdk/javaactions/source.go | 22 ++++++- 19 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/sdk/2026-10-25-java-action-source-without-class-javadoc.json create mode 100644 mdl/executor/cmd_javaactions_javadoc_test.go create mode 100644 sdk/javaactions/javadoc_test.go diff --git a/.claude/skills/fix-issue/findings/sdk/2026-10-25-java-action-source-without-class-javadoc.json b/.claude/skills/fix-issue/findings/sdk/2026-10-25-java-action-source-without-class-javadoc.json new file mode 100644 index 0000000000..588a21bea8 --- /dev/null +++ b/.claude/skills/fix-issue/findings/sdk/2026-10-25-java-action-source-without-class-javadoc.json @@ -0,0 +1 @@ +{"area": "sdk/javaactions", "date": "2026-10-25", "symptom": "mendixlabs/mxcli#1653: `create java action` with a `/** … */` doc comment wrote a .java file without the class javadoc mxbuild generates from the documentation, and `create or modify java action` removed the block Studio Pro wrote; the next build put it back (diff there and back in git)", "cause": "javaactions.GenerateSource had no documentation argument and wrote the imports, a blank line, then `public class …`; WriteJavaSourceFile (backend interface, modelsdk, mock, generated stubs) did not pass it either, although the executor had ja.Documentation at hand", "file": "sdk/javaactions/source.go (GenerateSource, writeClassJavadoc), mdl/backend/java.go + modelsdk/java_write.go + mock (WriteJavaSourceFile), mdl/executor/cmd_javaactions.go", "insight": "GenerateSource aims at byte-identical output with mxbuild, so anything mxbuild derives from the model (here the documentation) must reach it, not only what MDL states about the file. Pass the documentation as stored on the action after the carry rule (stated doc comment, else the stored one), so a rewrite without a doc comment keeps the block. After changing a backend interface method, regenerate unimplemented_gen.go (go run ./mdl/backend/modelsdk/gen_unimplemented.go) and unsupported_gen.go (go generate in mdl/backend/mcp).", "refs": ["mendixlabs/mxcli#1653", "mendixlabs/mxcli#1588", "ako/mxcli#705"], "ce": [], "rules": []} diff --git a/CHANGELOG.md b/CHANGELOG.md index b291e68f24..d8f3754a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **`create [or modify] java action` writes the class javadoc into the .java file** (mendixlabs/mxcli#1653) — Studio Pro and mxbuild write a Java action's documentation as a javadoc block directly above `public class `. mxcli stored the documentation in the model but wrote the file without the block, so a new action had none and every `create or modify java action` removed the one Studio Pro wrote; the next build put it back, leaving a pair of unrelated diffs in version control. `GenerateSource` and `WriteJavaSourceFile` now take the documentation and write it the way mxbuild does (`/**`, one ` * ` per line, ` */`, CRLF); an action without documentation still gets no block, and a rewrite without a doc comment writes the stored documentation. - **A pluggable widget's text-template parameter across an association builds, and describes so it re-executes** (mendixlabs/mxcli#1494) — `fieldset fs (legend: '{1}', legendParams: ({1} = M.Parent_Child/Label))` passed `check` and `exec` and failed the build with `[CE1365] "Attribute 'M.Child.Label' is not an attribute of entity 'M.Parent'."`: the parameter was written with its attribute but without the association hop, so mxbuild read the attribute against the widget's own entity. The same writer serves an object-list item's template (an Accordion group's `headerText`) and a Data grid 2 column's dynamic text, and all three failed; on Mendix 11.12.4 they now build with 0 errors. `describe page` printed such a parameter as the bare attribute (`{1} = Label`), which re-executed into CE1613 "The selected attribute 'M.Parent.Label' no longer exists"; it now prints `M.Parent_Child/Label`. The companion report, an expression parameter (`{1} = if 1 = 1 then 'A' else 'B'`) stored as an attribute name (mendixlabs/mxcli#1495), was fixed in v0.25.0 (#823) and is now pinned by a test. - **`alter entity … add event handler` adds a second handler for the same event** (mendixlabs/mxcli#1540) — `add event handler on before commit call M.Second($currentObject)` failed with "event handler already exists for Before Commit" when the entity already had one, though Studio Pro allows several handlers for one event and `create or modify entity` with two `on before commit` clauses stores both (measured on 11.12.4: two Before Commit handlers are 0 errors, a void-returning one is CE7247, so the check does look at them). A handler is now identified by its moment, event and microflow: another microflow adds a second handler, the same microflow again is still a duplicate. The same key had made `add event handler if not exists … call M.New` skip — reporting "already exists" — whenever any handler for the event existed, and `drop event handler on before commit` remove whichever handler was stored first. `drop event handler` now takes `call M.Mf` to say which one; without it, a drop that matches several handlers is refused and names them. diff --git a/mdl/backend/java.go b/mdl/backend/java.go index b906eabb99..cfd556d8da 100644 --- a/mdl/backend/java.go +++ b/mdl/backend/java.go @@ -20,7 +20,7 @@ type JavaBackend interface { CreateJavaAction(ja *javaactions.JavaAction) error UpdateJavaAction(ja *javaactions.JavaAction) error DeleteJavaAction(id model.ID) error - WriteJavaSourceFile(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string) error + WriteJavaSourceFile(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string, documentation string) error DeleteJavaSourceFile(moduleName, actionName string) error RenameJavaSourceFile(moduleName, oldName, newName string) error ReadJavaSourceFile(moduleName, actionName string) (string, error) diff --git a/mdl/backend/mcp/unsupported_gen.go b/mdl/backend/mcp/unsupported_gen.go index 0be8f9a70a..8f34e838b0 100644 --- a/mdl/backend/mcp/unsupported_gen.go +++ b/mdl/backend/mcp/unsupported_gen.go @@ -1450,7 +1450,7 @@ func (unsupportedBackend) WriteJavaScriptSourceFile(_ string, _ string, _ string return } -func (unsupportedBackend) WriteJavaSourceFile(_ string, _ string, _ string, _ []*types.JavaActionParameter, _ types.CodeActionReturnType, _ []string, _ string) (err0 error) { +func (unsupportedBackend) WriteJavaSourceFile(_ string, _ string, _ string, _ []*types.JavaActionParameter, _ types.CodeActionReturnType, _ []string, _ string, _ string) (err0 error) { err0 = errUnsupported("WriteJavaSourceFile") return } diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index 3df8fc29dc..cc04eeefc6 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -260,7 +260,7 @@ type MockBackend struct { CreateJavaActionFunc func(ja *javaactions.JavaAction) error UpdateJavaActionFunc func(ja *javaactions.JavaAction) error DeleteJavaActionFunc func(id model.ID) error - WriteJavaSourceFileFunc func(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string) error + WriteJavaSourceFileFunc func(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string, documentation string) error DeleteJavaSourceFileFunc func(moduleName, actionName string) error RenameJavaSourceFileFunc func(moduleName, oldName, newName string) error ReadJavaSourceFileFunc func(moduleName, actionName string) (string, error) diff --git a/mdl/backend/mock/mock_java.go b/mdl/backend/mock/mock_java.go index 81965ccd1f..17d2479ecf 100644 --- a/mdl/backend/mock/mock_java.go +++ b/mdl/backend/mock/mock_java.go @@ -64,9 +64,9 @@ func (m *MockBackend) DeleteJavaAction(id model.ID) error { return nil } -func (m *MockBackend) WriteJavaSourceFile(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string) error { +func (m *MockBackend) WriteJavaSourceFile(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string, documentation string) error { if m.WriteJavaSourceFileFunc != nil { - return m.WriteJavaSourceFileFunc(moduleName, actionName, javaCode, params, returnType, extraImports, extraCode) + return m.WriteJavaSourceFileFunc(moduleName, actionName, javaCode, params, returnType, extraImports, extraCode, documentation) } return nil } diff --git a/mdl/backend/modelsdk/java_write.go b/mdl/backend/modelsdk/java_write.go index 488f904ce3..7a3e94030e 100644 --- a/mdl/backend/modelsdk/java_write.go +++ b/mdl/backend/modelsdk/java_write.go @@ -92,7 +92,7 @@ func (b *Backend) UpdateJavaAction(ja *javaactions.JavaAction) error { // WriteJavaSourceFile writes the .java stub for a java action under // javasource//actions/.java, using the shared generator so the // output is byte-identical to the legacy engine. -func (b *Backend) WriteJavaSourceFile(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string) error { +func (b *Backend) WriteJavaSourceFile(moduleName, actionName string, javaCode string, params []*javaactions.JavaActionParameter, returnType javaactions.CodeActionReturnType, extraImports []string, extraCode string, documentation string) error { if b.path == "" { return fmt.Errorf("WriteJavaSourceFile: no project path") } @@ -107,7 +107,7 @@ func (b *Backend) WriteJavaSourceFile(moduleName, actionName string, javaCode st // into Java that does not compile (ako/mxcli#705). existing, _ := os.ReadFile(path) extraImports, extraCode = javaactions.RetainSections(string(existing), extraImports, extraCode) - source := javaactions.GenerateSource(moduleName, actionName, javaCode, params, returnType, extraImports, extraCode) + source := javaactions.GenerateSource(moduleName, actionName, javaCode, params, returnType, extraImports, extraCode, documentation) changed, err := javaactions.WriteSourceIfChanged(path, source) b.noteFileWrite(changed) if err != nil { diff --git a/mdl/backend/modelsdk/unimplemented_gen.go b/mdl/backend/modelsdk/unimplemented_gen.go index 531969ef61..48a2787539 100644 --- a/mdl/backend/modelsdk/unimplemented_gen.go +++ b/mdl/backend/modelsdk/unimplemented_gen.go @@ -1292,7 +1292,7 @@ func (unimplemented) WriteJavaScriptSourceFile(_ string, _ string, _ string, _ [ return errUnimplemented("WriteJavaScriptSourceFile") } -func (unimplemented) WriteJavaSourceFile(_ string, _ string, _ string, _ []*types.JavaActionParameter, _ types.CodeActionReturnType, _ []string, _ string) error { +func (unimplemented) WriteJavaSourceFile(_ string, _ string, _ string, _ []*types.JavaActionParameter, _ types.CodeActionReturnType, _ []string, _ string, _ string) error { return errUnimplemented("WriteJavaSourceFile") } diff --git a/mdl/executor/cmd_javaactions.go b/mdl/executor/cmd_javaactions.go index ea98f08f6b..b10bb98049 100644 --- a/mdl/executor/cmd_javaactions.go +++ b/mdl/executor/cmd_javaactions.go @@ -690,7 +690,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { // module's compiled one. The statement says nothing about the code, so the // file is left alone. if s.JavaCode != "" && strings.TrimSpace(s.JavaCode) != javaSourceOmittedBody { - if err := ctx.Backend.WriteJavaSourceFile(moduleName, s.Name.Name, s.JavaCode, ja.Parameters, ja.ReturnType, s.Imports, s.ExtraCode); err != nil { + if err := ctx.Backend.WriteJavaSourceFile(moduleName, s.Name.Name, s.JavaCode, ja.Parameters, ja.ReturnType, s.Imports, s.ExtraCode, ja.Documentation); err != nil { return mdlerrors.NewBackend("write java source file", err) } } diff --git a/mdl/executor/cmd_javaactions_bare_enum_935_test.go b/mdl/executor/cmd_javaactions_bare_enum_935_test.go index 97ecbef4d1..3961357c9d 100644 --- a/mdl/executor/cmd_javaactions_bare_enum_935_test.go +++ b/mdl/executor/cmd_javaactions_bare_enum_935_test.go @@ -58,7 +58,7 @@ func newCodeActionProject() *codeActionProject { return nil }, WriteJavaSourceFileFunc: func(string, string, string, []*javaactions.JavaActionParameter, - javaactions.CodeActionReturnType, []string, string) error { + javaactions.CodeActionReturnType, []string, string, string) error { return nil }, WriteJavaScriptSourceFileFunc: func(string, string, string, []*types.JavaActionParameter, types.CodeActionReturnType) error { diff --git a/mdl/executor/cmd_javaactions_export_level_test.go b/mdl/executor/cmd_javaactions_export_level_test.go index 2d08d7e9c1..7142c9eccf 100644 --- a/mdl/executor/cmd_javaactions_export_level_test.go +++ b/mdl/executor/cmd_javaactions_export_level_test.go @@ -58,7 +58,7 @@ func TestCreateJavaActionWritesStudioProDefaults(t *testing.T) { return nil }, WriteJavaSourceFileFunc: func(string, string, string, []*javaactions.JavaActionParameter, - javaactions.CodeActionReturnType, []string, string) error { + javaactions.CodeActionReturnType, []string, string, string) error { return nil }, } @@ -160,7 +160,7 @@ func TestJavaActionBody_PlaceholderIsNotWritten(t *testing.T) { ctx, _ := exposeRewriteCtx(t, stored, mod) wrote := false ctx.Backend.(*mock.MockBackend).WriteJavaSourceFileFunc = func(string, string, string, []*javaactions.JavaActionParameter, - javaactions.CodeActionReturnType, []string, string) error { + javaactions.CodeActionReturnType, []string, string, string) error { wrote = true return nil } diff --git a/mdl/executor/cmd_javaactions_expose_preserve_test.go b/mdl/executor/cmd_javaactions_expose_preserve_test.go index 477365376a..f179dd4a7b 100644 --- a/mdl/executor/cmd_javaactions_expose_preserve_test.go +++ b/mdl/executor/cmd_javaactions_expose_preserve_test.go @@ -71,7 +71,7 @@ func exposeRewriteCtx(t *testing.T, stored *javaactions.JavaAction, mod *model.M return nil }, WriteJavaSourceFileFunc: func(string, string, string, []*javaactions.JavaActionParameter, - javaactions.CodeActionReturnType, []string, string) error { + javaactions.CodeActionReturnType, []string, string, string) error { return nil }, } diff --git a/mdl/executor/cmd_javaactions_javadoc_test.go b/mdl/executor/cmd_javaactions_javadoc_test.go new file mode 100644 index 0000000000..94c8cd4bde --- /dev/null +++ b/mdl/executor/cmd_javaactions_javadoc_test.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/javaactions" +) + +// mendixlabs/mxcli#1653: the .java file is written with the action's +// documentation, so the class javadoc Studio Pro and mxbuild generate from it +// is not removed by every `create or modify java action`. The documentation +// handed to WriteJavaSourceFile is the one stored on the action: the stated +// doc comment, or the stored one when the statement has none. +func TestCreateJavaAction_WritesTheSourceWithItsDocumentation(t *testing.T) { + cases := []struct { + name, stored, stated string + set bool + want string + }{ + {"stated doc comment", "Old text.", "Formats an amount.", true, "Formats an amount."}, + {"no doc comment keeps the stored documentation", "Stored text.", "", false, "Stored text."}, + {"an empty doc comment clears it", "Stored text.", "", true, ""}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + mod := mkModule("MyModule") + stored := storedExposedAction(mod) + ctx, _ := exposeRewriteCtx(t, stored, mod) + mb := ctx.Backend.(*mock.MockBackend) + // The stored documentation is read from the action list. + mb.ListJavaActionsFunc = func() ([]*types.JavaAction, error) { + return []*types.JavaAction{{BaseElement: model.BaseElement{ID: stored.ID}, + ContainerID: stored.ContainerID, Name: stored.Name, Documentation: c.stored}}, nil + } + var wrote *string + mb.WriteJavaSourceFileFunc = func(_, _, _ string, _ []*javaactions.JavaActionParameter, + _ javaactions.CodeActionReturnType, _ []string, _ string, documentation string) error { + wrote = &documentation + return nil + } + + s := rewriteStmt(true) + s.Documentation, s.DocumentationSet = c.stated, c.set + assertNoError(t, execCreateJavaAction(ctx, s)) + + if wrote == nil { + t.Fatal("WriteJavaSourceFile was never called") + } + if *wrote != c.want { + t.Errorf("documentation written to the .java file = %q, want %q", *wrote, c.want) + } + }) + } +} diff --git a/mdl/executor/cmd_javaactions_param_description_test.go b/mdl/executor/cmd_javaactions_param_description_test.go index b06e3b58ff..b292cb3322 100644 --- a/mdl/executor/cmd_javaactions_param_description_test.go +++ b/mdl/executor/cmd_javaactions_param_description_test.go @@ -63,7 +63,7 @@ func TestCreateJavaAction_WritesStatedParameterDescriptionAndReturnName(t *testi return nil }, WriteJavaSourceFileFunc: func(string, string, string, []*javaactions.JavaActionParameter, - javaactions.CodeActionReturnType, []string, string) error { + javaactions.CodeActionReturnType, []string, string, string) error { return nil }, } diff --git a/mdl/executor/describe_java_action_type_params_test.go b/mdl/executor/describe_java_action_type_params_test.go index 996c66e70c..cd1bfe30a5 100644 --- a/mdl/executor/describe_java_action_type_params_test.go +++ b/mdl/executor/describe_java_action_type_params_test.go @@ -50,7 +50,7 @@ func createJACtx(t *testing.T) (*ExecContext, **javaactions.JavaAction) { return nil }, WriteJavaSourceFileFunc: func(string, string, string, []*javaactions.JavaActionParameter, - javaactions.CodeActionReturnType, []string, string) error { + javaactions.CodeActionReturnType, []string, string, string) error { return nil }, } diff --git a/sdk/javaactions/javadoc_test.go b/sdk/javaactions/javadoc_test.go new file mode 100644 index 0000000000..25c086703e --- /dev/null +++ b/sdk/javaactions/javadoc_test.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 + +package javaactions + +import ( + "strings" + "testing" +) + +// mendixlabs/mxcli#1653: Studio Pro and mxbuild write a Java action's +// documentation as a javadoc block directly above the class; GenerateSource +// wrote none, so every `create or modify java action` removed the block and +// the next build put it back. The format is the one measured on generated +// files: `/**`, one ` * ` per line (an empty line keeps the trailing +// space), ` */`, CRLF like the rest of the file. +func TestGenerateSource_WritesTheDocumentationAsClassJavadoc(t *testing.T) { + src := GenerateSource("MyFirstModule", "FormatAmount", "return null;", nil, &StringType{}, nil, "", + "Line one.\n\nLine three.") + want := "\r\n/**\r\n * Line one.\r\n * \r\n * Line three.\r\n */\r\npublic class FormatAmount extends UserAction\r\n" + if !strings.Contains(src, want) { + t.Errorf("want the javadoc directly above the class:\n%q\ngot:\n%s", want, src) + } +} + +// Documentation stored with CRLF line ends (as Studio Pro may store it) gives +// the same block, not one with stray carriage returns. +func TestGenerateSource_JavadocFromCRLFDocumentation(t *testing.T) { + lf := GenerateSource("MyFirstModule", "A", "return null;", nil, &StringType{}, nil, "", "One.\nTwo.") + crlf := GenerateSource("MyFirstModule", "A", "return null;", nil, &StringType{}, nil, "", "One.\r\nTwo.") + if lf != crlf { + t.Errorf("CRLF documentation gives a different file:\n%s", lineDiff(lf, crlf)) + } +} + +// Control: no documentation, no javadoc — the file mxbuild writes for an +// undocumented action (the goldens in testdata/mxbuild). +func TestGenerateSource_NoDocumentationNoJavadoc(t *testing.T) { + src := GenerateSource("MyFirstModule", "A", "return null;", nil, &StringType{}, nil, "", "") + head, _, _ := strings.Cut(src, "public class ") + if strings.Contains(head, "/**") { + t.Errorf("javadoc written for an action without documentation:\n%s", head) + } +} + +// Regenerating over a file that already has the javadoc — what a build left +// behind — gives that file back, so a rewrite with unchanged documentation +// leaves no diff. +func TestGenerateSource_RegeneratesOverAFileWithJavadoc(t *testing.T) { + doc := "Formats an amount as text with two decimals.\n\nSecond paragraph." + params := []*JavaActionParameter{param("Amount", &DecimalType{})} + first := GenerateSource("MyFirstModule", "FormatAmount", `return String.format("%.2f", Amount);`, + params, &StringType{}, nil, "", doc) + imports, extra := RetainSections(first, nil, "") + again := GenerateSource("MyFirstModule", "FormatAmount", `return String.format("%.2f", Amount);`, + params, &StringType{}, imports, extra, doc) + if again != first { + t.Errorf("regenerating over a file with javadoc changed it:\n%s", lineDiff(first, again)) + } +} diff --git a/sdk/javaactions/mxbuild_golden_test.go b/sdk/javaactions/mxbuild_golden_test.go index 85a0252521..4046afd43d 100644 --- a/sdk/javaactions/mxbuild_golden_test.go +++ b/sdk/javaactions/mxbuild_golden_test.go @@ -109,7 +109,7 @@ func TestGenerateSource_RegeneratesWhatMxbuildWrote(t *testing.T) { t.Run(c.name, func(t *testing.T) { golden := readGolden(t, c.name) imports, extra := RetainSections(golden, nil, "") - got := GenerateSource("MyFirstModule", c.name, c.userCode, c.params, c.ret, imports, extra) + got := GenerateSource("MyFirstModule", c.name, c.userCode, c.params, c.ret, imports, extra, "") if got != golden { t.Errorf("generated source differs from mxbuild's:\n%s", lineDiff(golden, got)) } @@ -129,7 +129,7 @@ func TestGenerateSource_NewFileMatchesMxbuild(t *testing.T) { t.Run(c.name, func(t *testing.T) { golden := readGolden(t, c.name) imports, extra := RetainSections("", nil, "") - got := GenerateSource("MyFirstModule", c.name, stub, c.params, c.ret, imports, extra) + got := GenerateSource("MyFirstModule", c.name, stub, c.params, c.ret, imports, extra, "") // mxcli pads the user code with a blank line on each side. got = strings.Replace(got, "// BEGIN USER CODE\r\n\r\n", "// BEGIN USER CODE\r\n", 1) got = strings.Replace(got, "\r\n\r\n\t\t// END USER CODE", "\r\n\t\t// END USER CODE", 1) diff --git a/sdk/javaactions/retain_test.go b/sdk/javaactions/retain_test.go index 48cb3e6737..8ac23cdf78 100644 --- a/sdk/javaactions/retain_test.go +++ b/sdk/javaactions/retain_test.go @@ -94,7 +94,7 @@ func TestRetainSections_MergesWithWhatTheStatementSaid(t *testing.T) { func TestGenerateSource_WithRetainedSectionsKeepsTheHelper(t *testing.T) { imports, extra := RetainSections(studioProSource, nil, "") src := GenerateSource("FeedbackModule", "XSS_Sanitizer", "return sanitize(stringToSanitize);", - nil, &StringType{}, imports, extra) + nil, &StringType{}, imports, extra, "") for _, want := range []string{ "import java.util.regex.Pattern;", "public static String sanitize(String input) {", diff --git a/sdk/javaactions/source.go b/sdk/javaactions/source.go index 572be82a44..70a6175eb1 100644 --- a/sdk/javaactions/source.go +++ b/sdk/javaactions/source.go @@ -29,11 +29,13 @@ const ( // is its proxy enum — measured on 10.24.27 and 11.14.0, which write the same // bytes (testdata/mxbuild). // +// documentation is the action's documentation, written as the class javadoc. +// // imports is the import list to keep, in order — the existing file's, see // RetainSections. The imports the generated code needs and the list lacks are // appended, sorted. That is what mxbuild does: it never reorders or drops an // import, so for a new file it writes just the needed ones, sorted. -func GenerateSource(moduleName, actionName string, userCode string, params []*JavaActionParameter, returnType CodeActionReturnType, imports []string, extraCode string) string { +func GenerateSource(moduleName, actionName string, userCode string, params []*JavaActionParameter, returnType CodeActionReturnType, imports []string, extraCode string, documentation string) string { moduleNameLower := strings.ToLower(moduleName) returnTypeStr := javaReturnType(returnType) @@ -56,6 +58,7 @@ func GenerateSource(moduleName, actionName string, userCode string, params []*Ja } sb.WriteString("\n") + writeClassJavadoc(&sb, documentation) sb.WriteString(fmt.Sprintf("public class %s extends UserAction<%s>\n", actionName, returnTypeStr)) sb.WriteString("{\n") @@ -154,6 +157,23 @@ func GenerateSource(moduleName, actionName string, userCode string, params []*Ja return strings.ReplaceAll(src, "\n", "\r\n") } +// writeClassJavadoc writes the action's documentation as the javadoc block +// mxbuild puts directly above the class: `/**`, one ` * ` per line (an +// empty line is ` * `, trailing space included), ` */`. No documentation, no +// block (mendixlabs/mxcli#1653). +func writeClassJavadoc(sb *strings.Builder, documentation string) { + if documentation == "" { + return + } + sb.WriteString("/**\n") + for line := range strings.SplitSeq(documentation, "\n") { + sb.WriteString(" * ") + sb.WriteString(strings.TrimSuffix(line, "\r")) + sb.WriteString("\n") + } + sb.WriteString(" */\n") +} + // completeImports returns imports, deduplicated, with the ones the generated // code needs and imports lacks appended in sorted order. func completeImports(imports []string, params []*JavaActionParameter, returnType CodeActionReturnType) []string {