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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-17", "symptom": "`create or modify navigation` (or `create or replace`, deprecated or `mdl 1` spelling) naming a home page, role home, login page, not-found page or a menu item's page/microflow/nanoflow that does not exist passed `check -p` (\"Check passed!\") and `exec` (\"Navigation profile 'Responsive' updated.\"); only `mx check` objected: CE1613 \"The selected page 'Shop.NoSuchHome' no longer exists.\" at Navigation profile, and one per bad menu item. A page's own show page / call microflow and an unknown icon were refused. Same for `create menu`.", "cause": "The reference-resolution passes were added one reference kind at a time (validateWidgetReferences for widget trees, validateIconRefs, validateNavigationRoles for the role only), so navigation targets were in none of them. The exec handler built each menu action with a pageBuilder whose tolerateDanglingRefs is true for menus (so a target stored by name never failed), and read home/login/not-found pages straight into the spec; CREATE OR REPLACE NAVIGATION rewrites the whole profile.", "file": "`mdl/executor/validate_navigation_targets.go` (`navigationTargets`, `unresolvedNavigationTargets`, `validateNavigationTargets` called from `validateProgramWithWarnings`; `validateNavigationTargetsForExec` called first in `execAlterNavigation` and `execCreateMenu`); forward references via `navigationDefRefs` in `eagerDefRefs` and `Executor.enterScriptFlows` (`scriptTargets`)", "insight": "A reference pass written per kind leaves every kind it does not list silently unchecked, and the escape is silent both ways (check passes, exec writes, the build reports it one build later) — when adding a statement that names documents, grep every place a page/microflow name is resolved and ask which statements are NOT in the list. Navigation targets are written by a statement that replaces the whole profile, so the refusal has to come before the write, in the handler as well as in the pre-flight (`exec --no-check` skips the pre-flight). Reuse the same lookups (buildPageQualifiedNames and friends). A target stored by name and created further down the script must follow the flow-call rule (forwardFlowCalls, #1405) rather than statement order: a first version that made it an MDL-ORDER01 error broke a working script shape (navigation written before its pages: upstream execs it and mx check reports 0 errors) — measure what upstream does with the other order before enforcing one. Measured on a Mendix 11.12.5 app: 4 CE1613 before for the reported script, 0 errors after a valid navigation; an empty listing from the backend is silence, not a finding (as in validateForwardDefRefs)", "refs": ["mendixlabs/mxcli#1569"]}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ Beyond the page, microflow, entity and icon names a statement names directly,
- an access rule's `where […]` members;
- an entity event handler's microflow;
- `Module.Enum.Key` in an expression;
- a view entity's FROM / JOIN entities.
- a view entity's FROM / JOIN entities;
- the pages and flows a navigation profile or a menu document names: the home
page (and one per user role), a home flow, the login and not-found pages, and
every menu item's page, microflow or nanoflow at any depth. The target must
exist, or be created by the script (further down only in a run that rolls
back, as for a flow call); `exec` refuses the same statement
before it rewrites the profile or menu.

## The project's Mendix version

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **`check -p` and `exec` refuse a navigation profile or menu document that names a page or flow that does not exist** (mendixlabs/mxcli#1569) — `create or modify navigation` with a home page, role home, login page, not-found page or menu item (at any depth, in both the deprecated and the `mdl 1` spelling) pointing at a missing page, microflow or nanoflow passed `check -p` ("Check passed!") and `exec` ("Navigation profile 'Responsive' updated."), and only `mx check` objected: CE1613 "The selected page 'Shop.NoSuchHome' no longer exists.", once for the home page and once per menu item. A page's own `show page` and `call microflow` and an icon name were already resolved; a navigation statement was not, and it rewrites the whole profile. `check -p` now reports each unresolved target with the profile or menu document, the menu item path (`menu item 'Admin' > 'Users'`) and the missing name, and `exec` refuses the statement before it writes anything. A target the script creates resolves, wherever it stands in a run that rolls back (plain `exec`, `check`), as a call to a flow created further down does (mendixlabs/mxcli#1405); `exec --continue-on-error`, the REPL and `-c` refuse one created further down. A standalone `create menu` document is covered, and one stored as excluded only warns, as an excluded page does.

## [0.27.0] - 2026-10-15

Headline: **`exec` is all or nothing: when a statement fails at write time, the whole run is rolled back, so a script never leaves a project half-changed.** Alongside it: a wave of `describe` → `check` → `exec` fixes from round-trip runs over real apps (the Siemens FSM template, the GenAI showcase, Marketplace modules), Mendix 9 projects can take microflows, nanoflows and pluggable widgets again, `check -p` resolves many more of the references `exec` and the build would refuse, the classic `filemanager` widget and role-based `Visible: roles (…)` are in MDL, and the companion draws a module's domain model. The MongoDB driver is upgraded for the October 2026 BSON advisories.
Expand Down
57 changes: 57 additions & 0 deletions mdl-examples/bug-tests/1569-navigation-target-not-found.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
mdl 1;
-- A navigation profile and a menu document name pages and microflows
-- (mendixlabs/mxcli#1569).
--
-- A navigation statement points at documents in six places: the home page
-- (also one per user role), a home flow, the login page, the not-found page and
-- every menu item at any depth. None of them was resolved: `check -p` said
-- "Check passed!", `exec` wrote the profile, and `mx check` was the first to
-- object:
--
-- [CE1613] "The selected page 'Shop.NoSuchHome' no longer exists."
--
-- once for the home page and once per menu item with a missing page or flow.
--
-- This file is the shape that must PASS: every target exists by the time it is
-- needed, or is created by this script (a navigation written BEFORE the page it
-- names is accepted too, as a call to a flow created further down is: the target
-- is stored by name and a run that rolls back leaves nothing dangling). A target
-- that exists nowhere is only visible with a project, and `make check-mdl` runs
-- without one, so the refused cases are Go tests:
-- mdl/executor/validate_navigation_targets_test.go.

create module Zz2;

create page Zz2.Landing (
Title: 'Landing',
Layout: Atlas_Core.Atlas_Default
) {
dynamictext heading (Content: 'Landing', RenderMode: H1)
};

create microflow Zz2.ACT_Open ()
begin
log info 'open';
end;

-- Created further down than the navigation that opens it.
create or modify navigation Responsive
home page Zz2.Landing
{
menu item 'Landing' ( OnClick: show page Zz2.Landing, Icon: glyph 57377 )
menu 'Tools' ( Icon: glyph 57377 ) {
menu item 'Open' ( OnClick: call microflow Zz2.ACT_Open, Icon: glyph 57377 )
menu item 'Later' ( OnClick: show page Zz2.Later, Icon: glyph 57377 )
}
};

create page Zz2.Later (
Title: 'Later',
Layout: Atlas_Core.Atlas_Default
) {
dynamictext heading (Content: 'Later', RenderMode: H1)
};

create or modify menu Zz2.Main_Menu {
menu item 'Landing' ( OnClick: show page Zz2.Landing, Icon: glyph 57377 )
};
9 changes: 9 additions & 0 deletions mdl/executor/cmd_menus.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ func execCreateMenu(ctx *ExecContext, s *ast.CreateMenuStmt) error {
return mdlerrors.NewAlreadyExists("menu", s.Name.String())
}

// Resolve the pages and flows its items name before anything is written, a
// folder included. An excluded document is not validated by Mendix and keeps
// its dangling targets, as an excluded page does.
if existing == nil || !existing.Excluded {
if err := validateNavigationTargetsForExec(ctx, s); err != nil {
return err
}
}

var existingContainer model.ID
if existing != nil {
existingContainer = existing.ContainerID
Expand Down
7 changes: 7 additions & 0 deletions mdl/executor/cmd_navigation.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ func execAlterNavigation(ctx *ExecContext, s *ast.AlterNavigationStmt) error {
return err
}

// Resolve the pages and flows it names, for the same reason: this statement
// replaces the whole profile, so a target that does not exist would cost the
// stored menu and be reported only by the next build (CE1613).
if err := validateNavigationTargetsForExec(ctx, s); err != nil {
return err
}

nav, err := ctx.Backend.GetNavigation()
if err != nil {
return mdlerrors.NewBackend("get navigation", err)
Expand Down
6 changes: 6 additions & 0 deletions mdl/executor/exec_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ type ExecContext struct {
// flow the script creates further down is accepted (mendixlabs/mxcli#1405).
forwardFlowCalls bool

// scriptTargets are the pages, microflows and nanoflows the program being
// run declares, set under the same all-or-nothing run as scriptFlows
// (Executor.enterScriptFlows). A navigation target is stored by name, so one
// the script creates further down resolves there, as a flow call does.
scriptTargets map[defKey]bool

// Format controls output formatting (table, json, etc.).
Format OutputFormat

Expand Down
18 changes: 15 additions & 3 deletions mdl/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ type Executor struct {
// scriptFlows then holds every flow the program being run declares.
forwardFlowCalls bool
scriptFlows map[string]*selfFlow
scriptTargets map[defKey]bool // pages and flows the program declares, same run
settings map[string]any
cache *executorCache
catalog *catalog.Catalog
Expand Down Expand Up @@ -539,21 +540,32 @@ func (e *Executor) enterScriptFlows(prog *ast.Program) func() {
if !e.forwardFlowCalls {
return func() {}
}
prev := e.scriptFlows
prev, prevTargets := e.scriptFlows, e.scriptTargets
flows := make(map[string]*selfFlow, len(prev))
for k, v := range prev {
flows[k] = v
}
targets := make(map[defKey]bool, len(prevTargets))
for k, v := range prevTargets {
targets[k] = v
}
for _, stmt := range prog.Statements {
switch s := stmt.(type) {
case *ast.CreateMicroflowStmt:
declareScriptFlow(flows, false, s.Name, s.ReturnType)
case *ast.CreateNanoflowStmt:
declareScriptFlow(flows, true, s.Name, s.ReturnType)
}
// What a navigation target may name, wherever the declaration stands.
if key, _ := definedBy(stmt); key.name != "" {
switch key.kind {
case defPage, defMicroflow, defNanoflow:
targets[key] = true
}
}
}
e.scriptFlows = flows
return func() { e.scriptFlows = prev }
e.scriptFlows, e.scriptTargets = flows, targets
return func() { e.scriptFlows, e.scriptTargets = prev, prevTargets }
}

// enterLanguage runs the following statements under a program's language
Expand Down
1 change: 1 addition & 0 deletions mdl/executor/executor_dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func (e *Executor) newExecContext(ctx context.Context) *ExecContext {
LanguageVersion: e.langVersion,
forwardFlowCalls: e.forwardFlowCalls,
scriptFlows: e.scriptFlows,
scriptTargets: e.scriptTargets,
describeLang: e.describeLang,
SqlMgr: e.sqlMgr,
ThemeRegistry: e.themeRegistry,
Expand Down
5 changes: 5 additions & 0 deletions mdl/executor/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,11 @@ func validateProgramWithWarnings(ctx *ExecContext, prog *ast.Program) ([]error,
// worse than the two above: a module-qualified role here makes the project
// unloadable rather than merely failing the build (mendixlabs/mxcli#1001).
errors = append(errors, validateNavigationRoles(ctx, prog)...)
// Resolve the pages and flows a navigation profile or menu document names:
// home, login and not-found pages and every menu item. Written by name and
// unchecked, so a typo was CE1613 at build time, after the whole menu had
// been rewritten.
errors = append(errors, validateNavigationTargets(ctx, prog, sc)...)
// Resolve what a GRANT / REVOKE, a user role and a demo user name: the
// entity, document or member, and the module and user roles. exec refuses
// the grants after the statements before them are written, and writes the
Expand Down
26 changes: 24 additions & 2 deletions mdl/executor/validate_definition_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,20 @@ type defRef struct {
// stored by name, so a run that rolls back on failure resolves it against
// the whole script (mendixlabs/mxcli#1405); see flowCallRefs.
flowCall bool
// byName marks a reference stored by name that is only resolved by looking
// it up when the statement runs: a navigation profile's or menu document's
// page, microflow or nanoflow. Like a flow call, one the script creates
// further down is accepted by a run that rolls back (forwardFlowCalls) and
// refused by one that cannot; unlike a call, it is not a call, so
// forwardByNameRefusal words the refusal.
byName bool
}

// forwardByNameRefusal is why a run that cannot roll back refuses a navigation
// target the script creates further down.
const forwardByNameRefusal = "the target is stored by name and looked up when the statement runs, and this run cannot be rolled back (--continue-on-error, the REPL, -c), so a later failure would leave it dangling. " +
"Create the target first; plain `mxcli exec`, which rolls back, accepts the script as it is"

// flowCallRefs marks refs as flow-call targets.
func flowCallRefs(refs []defRef) []defRef {
for i := range refs {
Expand Down Expand Up @@ -124,7 +136,7 @@ func ValidateScriptDefinitionOrder(prog *ast.Program) []linter.Violation {
// Whether a flow call may run ahead of its callee depends on how
// the script runs, which this tier cannot know: the project tier
// (validateForwardDefRefs) and exec decide (mendixlabs/mxcli#1405).
if ref.flowCall {
if ref.flowCall || ref.byName {
continue
}
kind, at, ok := laterPlainCreate(plain, created, ref)
Expand Down Expand Up @@ -274,6 +286,11 @@ func eagerDefRefs(stmt ast.Statement) []defRef {
return oneDefRef(s.Nanoflow, defNanoflow, "grant execute on nanoflow "+s.Nanoflow.String())
case *ast.GrantPageAccessStmt:
return oneDefRef(s.Page, defPage, "grant view on page "+s.Page.String())

case *ast.AlterNavigationStmt, *ast.CreateMenuStmt:
// The home, login and not-found pages and every menu item's target,
// which the handler resolves before it writes the profile or menu.
return navigationDefRefs(stmt)
}
return nil
}
Expand Down Expand Up @@ -495,7 +512,7 @@ func validateForwardDefRefs(ctx *ExecContext, prog *ast.Program) []error {
break
}
}
if resolved || (ref.flowCall && ctx.forwardFlowCalls) {
if resolved || ((ref.flowCall || ref.byName) && ctx.forwardFlowCalls) {
continue
}
for _, k := range ref.kinds {
Expand All @@ -509,6 +526,11 @@ func validateForwardDefRefs(ctx *ExecContext, prog *ast.Program) []error {
i+1, ref.site, k, ref.name, at+1, forwardFlowCallRefusal))
break
}
if ref.byName {
errs = append(errs, fmt.Errorf("statement %d: %s references %s %s before it is created (statement %d): %s",
i+1, ref.site, k, ref.name, at+1, forwardByNameRefusal))
break
}
if _, isPlain := plain[key]; isPlain {
break // MDL-ORDER01 reports it, without a project
}
Expand Down
Loading
Loading