Skip to content
Merged
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
1 change: 1 addition & 0 deletions .claude/commands/mendix/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ mxcli lint -p app.mpr --exclude System --exclude Administration
| CONV012 | quality | ExclusiveSplitCaption - Exclusive splits need meaningful captions |
| CONV013 | quality | ErrorHandlingOnCalls - External calls (REST/WS/Java) need custom error handling |
| CONV014 | quality | NoContinueErrorHandling - Don't silently swallow errors with Continue |
| CONV020 | quality | CreatedObjectNeverPersisted - A microflow creates a persistent object without commit and never commits, returns or passes it on, so it is discarded |

## Bundled Starlark Rules

Expand Down
19 changes: 10 additions & 9 deletions .claude/skills/mendix/assess-quality/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ description: "Audit a Mendix project against best practices for naming, security
|---|---|
| 29-36 | When to Use This Skill |
| 37-96 | Assessment Workflow |
| 97-352 | Quality Guidelines Reference |
| 353-363 | Assessment Report Template |
| 364-376 | Executive Summary |
| 377-387 | project Metrics |
| 388-400 | Automated Findings |
| 401-420 | Manual Review Findings |
| 421-436 | Recommendations |
| 437-447 | Sources |
| 97-353 | Quality Guidelines Reference |
| 354-364 | Assessment Report Template |
| 365-377 | Executive Summary |
| 378-388 | project Metrics |
| 389-401 | Automated Findings |
| 402-421 | Manual Review Findings |
| 422-437 | Recommendations |
| 438-448 | Sources |

<!-- /skill-index -->
# Assess Mendix Project Quality
Expand Down Expand Up @@ -200,7 +200,7 @@ The Dutch Institute for Vulnerability Disclosure (DIVD) found widespread authori

### D. Maintainability

**Enforced by rules:** QUAL001-QUAL004, CONV009, CONV012, CONV014, MPR002, MPR004, MPR006
**Enforced by rules:** QUAL001-QUAL004, CONV009, CONV012, CONV014, CONV020, MPR002, MPR004, MPR006

| Guideline | Rule/Check | Threshold |
|-----------|-----------|-----------|
Expand All @@ -211,6 +211,7 @@ The Dutch Institute for Vulnerability Disclosure (DIVD) found widespread authori
| No empty microflows | MPR002 | Every microflow should have at least one activity |
| Caption on exclusive splits | CONV012 | All decision points must have captions |
| No "Continue" error handling | CONV014 | Never swallow errors silently |
| No created object silently discarded | CONV020 | A persistent object created without commit is committed, returned or passed on |
| ACT_ microflows: thin controllers | CONV010 | Only page activities + submicroflow calls |

**Manual checks:**
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- **Lint rule CONV020: an object created in a microflow and never committed, returned or passed on** (mendixlabs/mxcli#1518) — `$o = create Shop.Item (…)` without `commit` makes an object in memory only; if the microflow then never commits it or hands it to anything that could, the object is discarded when the microflow ends, and `check`, `mx check` and the build all pass. In the field an after-startup seed microflow logged "Seeded 20" and the table had 0 rows. CONV020 (warning) reports a create with commit No of a persistent entity in a microflow when nothing in the flow uses the variable except to read a member (`$o/Cnt`) or change it without commit. Any other mention — commit, change with commit, return, an argument to a microflow, Java/JavaScript action, workflow, mapping or page, a list, an association value, delete, rollback, or the bare variable in any expression — counts as passing it on. It is conservative by design: flow-insensitive (a commit on one branch or in an error handler counts), and nanoflows, non-persistent entities and System/Marketplace modules are not reported; anything it cannot classify keeps it quiet. Docs: "Built-in Rules" › CONV020 details.
- **`layoutheader` and native layout parts — every Atlas layout copies whole** — `describe layout` → `create layout` of Atlas's Phone_ and Tablet_ layouts dropped their header bar (`Forms$Header`, printed as NOT re-executable), so pages bound to its `HeaderLeft` / `HeaderRight` placeholders failed CE1613 and a copy of Phone_Sidebar or Tablet_Sidebar failed CE0611 "A sidebar toggle button is required if a region can toggle". `layoutheader header1 { region left { … } region right { … } }` writes it. A native layout's copy silently lost its header placeholder, bottom bar and sidebar — pages bound to `Header` or a sidebar placeholder then failed CE1613; they are `region header { placeholder Header }`, `showbottombar: true` and `region sidebar { … }`, and the native content is written with the eight keys Studio Pro stores. Measured on a new 11.13.0 app: all 22 Atlas_Core layouts copied, with pages bound to the copied header and sidebar placeholders, build with 0 errors.
- **`sidebartoggle` — Atlas's responsive sidebar toggle (`Forms$SidebarToggleButton`) in MDL** (mendixlabs/mxcli#1220) — copying `Atlas_Core.Atlas_Default` with `describe layout` → `create layout` dropped the button that opens the sidebar on phones and tablets: `describe` printed it as a NOT re-executable comment, and `exec` gave no warning. `sidebartoggle sidebarToggle3 (Tooltip: 'Toggle Menu', ButtonStyle: Primary, Icon: 'Atlas_Core.Atlas_Filled.navigation-menu', Class: 'toggle-btn')` takes an action button's caption, tooltip, style and icon, and `RenderMode: Link`; it has no action. The written widget has the keys of Atlas's own, including its caption under `CaptionTemplate` rather than the `Caption` key the vendored metamodel bound. A scroll container region's `ToggleMode` (`ToggleMode: 'ShrinkContentInitiallyClosed'`), which the toggle opens, is described and written too — without it the copy failed CE0628.
- **`mxcli marketplace package` — export a module as a `.mpk`** (mendixlabs/mxcli#1382) — `mxcli marketplace package MyModule -p app.mpr [-o dir] [--all-libs] [--exclude REGEX] [--exclude-managed-dependencies] [--json]` is the reverse of `marketplace install --file`: it wraps Mendix's own `mx create-module-package` (resolved for the project's Mendix version), so the package is what Studio Pro's *Export module package* makes, and it installs back with `install --file`. By default only the module's own required `userlib/` JARs go in (`mx -l`; measured 12.5 KB against 24 MB with Business Events installed); `--all-libs` bundles every JAR. `-l` filters only JARs — mx still exports the app's whole `resources/` folder (measured: another module's `resources/EventLog/` came along) — so after the export mxcli opens the package, warns on stderr about each `resources/<X>/`, `javasource/<x>/` or `themesource/<x>/` file of another module, and prints the `--exclude` pattern that drops them in the one form mx accepts (`^resources.EventLog.`; a character class or backslash is rejected by mx with exit 1 and a usage message, and an exit 0 that wrote no package is reported as an error too). A module name that is not in the project lists the project's modules instead of failing with mx's exit 3, and mx's exit codes 1–4 are reported with what they mean and what mx printed. `mx create-module-package` writes into the project it is given — measured on a blank 11.12.5 app: `theme-cache/web/theme.compiled.css` and its map rewritten, `deployment/sass/main.scss` added, the module's `themesource/` folder created, model files untouched — so it runs on a temporary copy, as `docker check` does; a before/after listing of the project (sizes and modification times) was identical. The `mx` that runs must be the project's Mendix version: `ResolveMxForVersion` prefers an `mx` on PATH and falls back to any cached one, so mxcli reads the version of the `mx` it found (its `mx.deps.json`, else its version-named folder) and refuses a different major.minor.patch, or one it cannot identify, naming both versions and the fix (`mxcli setup mxbuild --version X`); `--mxbuild-path` is the override, with a warning on stderr. mx's own output on a successful export goes to stderr, cancelling the command stops mx, and an output directory inside the project is not copied into the temporary copy. Round trip on 11.12.5: export, `install --file` into a second fresh app, `mx check` — 0 errors.
Expand Down
6 changes: 4 additions & 2 deletions cmd/mxcli/cmd_lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Bundled Starlark rules (in .claude/lint-rules/):
- No validation rules (CONV015) - use microflows instead of entity validation rules
- No event handlers (CONV016) - avoid entity event handlers
- No calculated attributes (CONV017) - avoid calculated attributes
- Created object never persisted (CONV020) - a created object that is never committed, returned or passed on

Custom Starlark rules in .claude/lint-rules/*.star are loaded automatically.

Expand Down Expand Up @@ -432,7 +433,8 @@ func builtinLintRules() []linter.Rule {
rules.NewExclusiveSplitCaptionRule(),
rules.NewErrorHandlingOnCallsRule(),
rules.NewNoContinueErrorHandlingRule(),
rules.NewIrreducibleFlowGraphRule(), // MDL-FLOW01 - graph structure vs MDL nesting
rules.NewImportRangeObjectMappingRule(), // MDL-MAP04 - First over an object-rooted mapping in the model
rules.NewCreatedObjectNeverPersistedRule(), // CONV020 - create without commit, never persisted or passed on
rules.NewIrreducibleFlowGraphRule(), // MDL-FLOW01 - graph structure vs MDL nesting
rules.NewImportRangeObjectMappingRule(), // MDL-MAP04 - First over an object-rooted mapping in the model
}
}
27 changes: 27 additions & 0 deletions docs-site/src/tools/builtin-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,36 @@ The **lint rules** below run with `mxcli lint`. There is also a separate group o
| **CONV012** | Exclusive split captions -- Checks that decision branches have meaningful captions |
| **CONV013** | Error handling on external calls -- Ensures external service calls have error handling |
| **CONV014** | No continue error handling -- Warns against using CONTINUE error handling without logging |
| **CONV020** | Created object never persisted -- A microflow creates an object of a persistent entity without commit, and never commits, returns or passes it on, so the object is discarded when the microflow ends ([details](#conv020-details)) |
| **MDL-FLOW01** | Un-describable branch structure -- Decision branches that re-enter each other's paths, so `DESCRIBE MICROFLOW` cannot render them as nested `IF`s without changing what they mean |
| **MDL-MAP04** | First over an object-rooted import mapping -- An `import from mapping` or `rest call … returns mapping` activity already in the model stores Studio Pro's *First* (`ForceSingleOccurrence` or `Range.SingleObject`) over a mapping that returns one object. `mx check` reports 0 errors; the activity throws `key not found: Path(QName(None,),None,)` when it runs. `mxcli check` refuses the same statement in a script under the same ID; this finds activities written before that check existed, or with `exec --no-check`. A list-rooted mapping's *First* is legitimate and is not reported |

### CONV020 details {#conv020-details}

`$Item = create Shop.Item (...)` without `commit` makes an object in memory only.
If the microflow then neither commits it nor hands it to anything that could,
the object is gone when the microflow ends. Nothing else reports this: `check`,
`mx check` and the build all pass, and the microflow runs without error. In the
report that prompted the rule, an after-startup seed microflow logged
"Seeded 20" and the table stayed empty.

The rule reports a create only when **all** of these hold:

- it is in a **microflow** (not a nanoflow or a rule) and its commit setting is
**No**;
- the entity is **persistent** (a non-persistent entity is meant to be
temporary), and is not a System or Marketplace entity;
- nothing in the microflow, on any path, uses the variable except to read one
of its members (`$Item/Name`) or to change it without commit.

Any other mention counts as passing the object on and keeps the rule quiet:
`commit`, `change … commit`, returning it, an argument to a microflow, Java or
JavaScript action, workflow, mapping or page, adding it to a list, using it as
an association value on another object, `delete`, `rollback`, or the bare
variable anywhere in an expression. The check is flow-insensitive: a commit on
one branch only, or only in an error handler, counts, so that case is not
reported. When the rule cannot tell, it stays quiet.

## Running Built-in Rules

Built-in rules run automatically with `mxcli lint`:
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/tools/linting.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The linting system provides:
|----------|--------|-------|
| MDL | MDL001-MDL007 | Naming conventions, empty microflows, domain model size |
| Security | SEC001-SEC010 | Access rules, password policy, demo users, PII exposure |
| Convention | CONV001-CONV019 | Best practice conventions, error handling |
| Convention | CONV001-CONV020 | Best practice conventions, error handling |
| Quality | QUAL001-QUAL004 | Complexity, documentation, long microflows |
| Architecture | ARCH001-ARCH003 | Cross-module data, entity business keys |
| Design | DESIGN001 | Entity attribute count |
Expand Down
47 changes: 47 additions & 0 deletions mdl-examples/bug-tests/1518-created-object-never-persisted.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
mdl 1;
-- mendixlabs/mxcli#1518: an object created without commit and never committed,
-- returned or passed on is discarded when the microflow ends. check, mx check
-- and the build are silent; lint rule CONV020 reports it.
--
-- mxcli exec 1518-created-object-never-persisted.mdl -p app.mpr
-- mxcli lint -p app.mpr -r CONV020
--
-- Expected: exactly one CONV020 warning, on Tmp1518.Seed. The other three are
-- controls: committed, a non-persistent entity, and a nanoflow.

create module Tmp1518;

create persistent entity Tmp1518.Item (Cnt: Integer);
create non-persistent entity Tmp1518.Helper (Cnt: Integer);

-- Reported: $o is never committed, returned or passed on.
create microflow Tmp1518.Seed () returns Boolean as $b
begin
declare $b Boolean = true;
$o = create Tmp1518.Item (Cnt = 1);
log info node 'Seed' 'Seeded {1}' with ({1} = toString($o/Cnt));
return $b;
end;

-- Not reported: committed.
create microflow Tmp1518.SeedCommitted () returns Boolean as $b
begin
declare $b Boolean = true;
$o = create Tmp1518.Item (Cnt = 1);
commit $o;
return $b;
end;

-- Not reported: a non-persistent entity is meant to be temporary.
create microflow Tmp1518.TempHelper () returns Boolean as $b
begin
declare $b Boolean = true;
$h = create Tmp1518.Helper (Cnt = 1);
return $b;
end;

-- Not reported: nanoflows are out of scope.
create nanoflow Tmp1518.SeedInClient ()
begin
$o = create Tmp1518.Item (Cnt = 1);
end;
1 change: 1 addition & 0 deletions mdl/linter/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ var categoryMapping = map[string]string{
"CONV015": "Quality",
"CONV018": "Quality",
"CONV019": "Quality",
"CONV020": "Quality",

// Architecture
"ARCH001": "Architecture",
Expand Down
Loading
Loading