Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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": []}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Action>`. 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 ` * <line>` 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.

Expand Down
2 changes: 1 addition & 1 deletion mdl/backend/java.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion mdl/backend/mcp/unsupported_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mdl/backend/mock/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions mdl/backend/mock/mock_java.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions mdl/backend/modelsdk/java_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (b *Backend) UpdateJavaAction(ja *javaactions.JavaAction) error {
// WriteJavaSourceFile writes the .java stub for a java action under
// javasource/<module>/actions/<Action>.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")
}
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion mdl/backend/modelsdk/unimplemented_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mdl/executor/cmd_javaactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/cmd_javaactions_bare_enum_935_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions mdl/executor/cmd_javaactions_export_level_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/cmd_javaactions_expose_preserve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
Expand Down
59 changes: 59 additions & 0 deletions mdl/executor/cmd_javaactions_javadoc_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
2 changes: 1 addition & 1 deletion mdl/executor/cmd_javaactions_param_description_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/describe_java_action_type_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
Expand Down
59 changes: 59 additions & 0 deletions sdk/javaactions/javadoc_test.go
Original file line number Diff line number Diff line change
@@ -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 ` * <line>` 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<java.lang.String>\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))
}
}
4 changes: 2 additions & 2 deletions sdk/javaactions/mxbuild_golden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sdk/javaactions/retain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {",
Expand Down
Loading
Loading