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/skills/fix-issue/findings/mdl-other.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,5 @@
{"area": "mdl/linter", "date": "2026-10-07", "symptom": "upstream mendixlabs/mxcli#1217: \"Lint CONV011 (NoCommitInLoop) misses CHANGE … COMMIT (and CREATE … COMMIT) inside a loop; only a separate COMMIT activity is flagged\". `change $T (\"Done\" = true) commit;` in a loop passed lint and `check`; `change …; commit $T;` was flagged. Studio Pro's recommender flags both (MXP004).", "cause": "CONV011 matched only `*microflows.CommitObjectsAction`; the Commit property of CreateObjectAction / ChangeObjectAction was never consulted. MDL-PERF01 (check-time twin, #1186) pinned its boundary to CONV011's, so it inherited the same gap by design.", "file": "`mdl/linter/rules/conv_loop_commit.go` (`committingActionKind`), `mdl/executor/validate_commit_in_loop.go` (ChangeObjectStmt/CreateObjectStmt cases); tests `conv_loop_commit_test.go`, `validate_commit_in_loop_test.go`; example `mdl-examples/bug-tests/1217-commit-clause-in-loop.mdl`", "insight": "**A rule that detects a database effect must enumerate every action that HAS the effect, not the action NAMED after it.** Commit is a property on create/change as well as an activity of its own; grep the action types for a `Commit` field before trusting a commit rule's coverage. Any value other than `No` commits — YesWithoutEvents skips handlers, not the round trip. When two rules share a pinned boundary (CONV011 ↔ MDL-PERF01), a coverage fix lands in both in the same change or they drift. Verified end to end: exec the repro into testdata/expr-checker/minimal.mpr and `lint -r CONV011` — reverted build flags 1 of 3 (only the separate commit), fixed build 3 of 3, list-commit-after-loop control quiet in both.", "refs": ["mendixlabs/mxcli#1217", "mendixlabs/mxcli#1186"], "rules": ["CONV011", "MDL-PERF01"]}
{"area": "mdl/exprcheck", "date": "2026-10-07", "symptom": "mendixlabs/mxcli#1216: `declare $D DateTime = parseDateTimeUTC($Text, 'yyyy-MM-dd', empty);` — `mxcli check -p` reports `parseDateTimeUTC() expects 2 argument(s), got 3. [E006]` and exec refuses, while mx check on 11.14.0 reports 0 errors; only --no-check writes it", "cause": "funcTable listed the parse functions without their default-value overloads: parseDateTime/parseDateTimeUTC(value, format [, default]), parseInteger(value [, default]), parseDecimal(value [, format [, default]]) were all fixed at their minimum arity", "file": "`mdl/exprcheck/func_checker.go` (funcTable), test `mdl/exprcheck/parse_default_arity_test.go`, `mdl-examples/bug-tests/1216-parse-datetime-default-value.mdl`", "insight": "funcTable arities are a transcription, so widen each one only by measurement, and measure the siblings the reporter says 'presumably' — two of them (parseInteger/parseDecimal) had the same false E006, but parseBoolean($s, false) is CE0117 and stays 1-arg. Isolate each case in its own project copy: mx check names the activity by its caption ('Create Date and time variable'), so several cases in one project are indistinguishable. Plausible wrong turn: a default of currentDateTime() fails CE0117, which reads as 'the 3-arg UTC form is invalid' — it is currentDateTime() itself, rejected on its own in a microflow; vary the default ($var, empty) before concluding. E006 only fires with -p, so a project-less `mxcli check` of the repro passes and proves nothing.", "ce": ["CE0117"], "rules": ["E006"]}
{"area": "mdl/exprcheck", "date": "2026-10-07", "symptom": "`declare $D DateTime = currentDateTime();` passes `mxcli check` and `exec`, then mx check on 11.14.0 fails `[error] [CE0117] \"Error(s) in expression.\" at Create variable activity 'Create Date and time variable'` — in a microflow and a nanoflow alike", "cause": "funcTable listed `currentDateTime` as a zero-argument built-in. Mendix has no such function; the current time is the `[%CurrentDateTime%]` token. funcTable is MDL044's sole allow-list, so the entry silenced the one rule that would have caught it", "file": "`mdl/exprcheck/func_checker.go` (entry removed), `mdl/exprcheck/unknown_funcs.go` (tokenFuncs → FuncRef.Token), `mdl/executor/validate_microflow.go` (MDL044 hint), bug tests `current-datetime-function.fail.mdl` / `current-datetime-token.mdl`", "insight": "Found by accident while measuring #1216: a default of currentDateTime() made parseDateTime look like it rejected a third argument. Before blaming the outer construct, build the inner expression on its own. The removal alone would give a useless hint — nearestFunc offers a spelling match, and the right answer is a token rather than a function — so name the token in the hint. Nothing in the repo emitted or recommended currentDateTime() (every example uses the token), which suggests the entry came from transcription, like the year()/month()/trunc() entries before it. The remaining unverified extraction names (dayOfYear, hour, …) are the same risk.", "ce": ["CE0117"], "rules": ["MDL044"]}
{"area": "mdl/linter", "date": "2026-10-07", "symptom": "A Starlark lint rule cannot tell which project languages are enabled: strings() returns rows for every stored translation (a fresh en_US-only 11.12.5 app already has 124 nl_NL rows), enabling nl_NL changes nothing a rule sees, and a rule calling languages() is skipped with 'undefined: languages (a builtin this mxcli does not have — the rule may need a newer mxcli)'", "cause": "No builtin exposed Settings$LanguageSettings.Languages; LintReader had no GetProjectSettings, so the only language data a rule could reach was the catalog's strings table, which indexes stored translations, not enabled languages", "fix": "languages() builtin returning struct language{code,is_default,check_completeness} from LintReader.GetProjectSettings (added to the interface; the backend already implements it). Classified CatalogFast; a reader error fails the rule instead of answering []", "insight": "Stored translations are not the enabled set: Studio Pro and the starter app keep texts in languages the project never enabled, so any 'per language' check built on strings() over-reports. Project-level facts belong on the reader path (like project_security()), not in the catalog, so they need no FULL build. A new builtin trips three guards at once — builtinModes, the skill's builtin list and its struct table — which is the checklist", "issue": "mendixlabs/mxcli#1306", "file": "mdl/linter/starlark.go (builtinLanguages); mdl/linter/context.go (LintReader)", "test": "mdl/linter/starlark_languages_test.go"}
{"area": "mdl/catalog", "date": "2026-10-07", "symptom": "activities_for() / CATALOG.ACTIVITIES returns every call and delete with its target empty: `MicroflowCallAction: action_ref=\"\" entity_ref=\"\" service_ref=\"\"` (also NanoflowCallAction, JavaActionCallAction, JavaScriptActionCallAction, DeleteObjectAction); no column says which call runs in a task queue; a nanoflow's JavaScript action call has no refs_from() row. refs_from() names the call/delete targets fine, and the lint run reports nothing.", "cause": "describeAction (builder_microflows.go) filled ActionRef/EntityRef only for REST/web service/OData calls and create/retrieve; the call and delete cases were never written. The delete's entity was resolvable all along via buildVarEntityMap, which only the refs builder used. microflowActionRef had no JavaScriptActionCallAction case.", "file": "`mdl/catalog/builder_microflows.go` (describeAction, insertFlowActivities), `mdl/catalog/builder_references.go` (microflowActionRef), `mdl/catalog/tables.go` (QueueRef, schema 23)", "insight": "Two builders derive per-action facts from the same parsed actions -- the refs switch and the activities switch -- and drift independently: an action can have its target in refs and an empty column in activities. When a row field is empty, check whether the other switch already resolves it before writing new resolution; here the delete's entity reused buildVarEntityMap (params, create/retrieve outputs, loop iterators) per flow. The queue lives on MicroflowCall.QueueSettings.Queue (and JavaActionCallAction.QueueSettings), not a top-level Queue. A new activities column breaks hand-built `CREATE TABLE activities` fixtures in linter tests (COALESCE does not cover a missing column) and needs a CatalogSchemaVersion bump. Control: HEAD binary on the issue's repro prints exactly the reported empty rows.", "refs": ["mendixlabs/mxcli#1305", "mendixlabs/mxcli#1266", "mendixlabs/mxcli#1267"]}
11 changes: 11 additions & 0 deletions .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Without this, a rule that reads a full-only table under a fast build gets
| `module_roles()` | list of module_role | All module roles (deduplicated from role mappings) |
| `role_mappings()` | list of role_mapping | User role to module role assignments |
| `project_security()` | project_security or None | Project-level security settings (requires MPR reader) |
| `languages()` | list of language | The languages **enabled** in the project settings, in settings order (requires MPR reader; `[]` without one). Use it to scope per-language checks: `strings()` has a row for every stored translation, including languages the project never enabled |
| `xpath_expressions()` | list of xpath_expression | All XPath constraint expressions in the catalog (access rules, retrieve actions, widgets) (full catalog — auto-detected) |
| `modules()` | list of module | The user's modules (not System, not Marketplace), with their domain model's documentation |
| `associations()` | list of association | All non-system associations, same-module and cross-module, with the delete behaviour of both ends |
Expand Down Expand Up @@ -572,6 +573,16 @@ Returned by `project_security()`. Returns `none` if no MPR reader is available.
| `require_mixed_case` | bool | Must contain upper and lower case |
| `require_symbol` | bool | Must contain a symbol |

### language

Returned by `languages()`, one per language enabled in the project settings. A per-language check skips `strings()` rows whose `language` is not among these codes.

| Property | Type | Description |
|----------|------|-------------|
| `code` | string | Language code: `"en_US"`, `"nl_NL"` |
| `is_default` | bool | Whether this is the project's default language |
| `check_completeness` | bool | Whether Studio Pro checks this language's translations for completeness |

## Helper Functions

| Function | Description |
Expand Down
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/).

### Added

- **`languages()` in Starlark lint rules — the languages enabled in the project** (mendixlabs/mxcli#1306) — a rule had no way to tell which languages are enabled: `strings()` has a row for every stored translation, and a fresh en_US-only app already carries nl_NL texts, so enabling a language changed nothing a rule could see, and `languages()` failed to load with `undefined: languages`. It returns one `language` per enabled language, from the project settings, with `code`, `is_default` and `check_completeness`, so a per-language check (every page title translated into each enabled language) can filter `strings()` to the enabled codes. It reads the project settings, not the catalog, so it needs no full catalog build; a failed settings read fails the rule instead of answering `[]`.
- **A published REST service's authentication** (mendixlabs/mxcli#1331) — `create [or modify] published rest service M.Api (…, Authentication: (basic, session, microflow M.Authenticate))` sets Studio Pro's **Requires authentication** and its methods: `basic` (username and password), `session` (active session), `microflow M.F` (custom); `Authentication: none` is "Requires authentication: No". The methods are stored in the order written, which is how Studio Pro stores the ones ticked, and `describe` prints them in the stored order (it omits `none`). Left out, `create or modify` and `alter` keep the stored setting, as before. `alter published rest service M.Api set ( Key: value, … )` now takes create's property list (`Path`, `Version`, `ServiceName`, `Authentication`); `set Key = '…'` still works. The authentication microflow must return `System.User` and take only a `System.HttpRequest` and/or `System.HttpResponse` (Mendix: CE0334, CE0336, measured with `mx check` on 11.14.0); `exec` and `check --references` refuse anything else as **MDL-REST04**. Executing the `describe` output of each of ako/TestApp's Studio Pro services writes nothing.
- **`send email` — the built-in Send Email activity (Mendix 11.13+, beta)** — `send email ( From: …, To: …, Subject: 'Order {1}' with ({1} = …), Body: template '…', HtmlBody: template '…', Headers: ('X-Name': 'value'), Attachment: $Doc, Host: …, Port: …, SecurityType: ssl, CheckServerIdentity: true, ConnectionTimeout: 30000, Authentication: basic (Username: …, Password: …) ) [on error …];` creates a `Microflows$SendEmailAction`, which sends SMTP mail without the Email Connector module. The settings are one property list (ADR-0013), keyed by the metamodel's names; an unknown, repeated or misshapen key is an error, and so is a missing From, Host, Port or recipient (mxbuild: CE0166). `describe microflow` renders the activity instead of `-- Unsupported action: Microflows$SendEmailAction`, and the description re-executes to the same activity. `check` type-checks its expressions (E009: String addresses, host and credentials, Integer/Long port, String template parameters — what mxbuild reports as CE9528/CE0117), and warns on a server-identity check without SSL and on header names Studio Pro would refuse (MDL-EMAIL02/03). A 10.x–11.12 project is refused; a stored activity MDL cannot restate (authentication document, pre-11.13 message) still describes as unsupported rather than being rewritten smaller. `mxcli syntax microflow.send-email`. (mendixlabs/mxcli#1315)
- **`mxcli playwright check` — a text verdict for pages of a running app, in one call** — `mxcli playwright check /p/a /p/b -p app.mpr` loads each page in one headless browser and prints the verdict `run --page-check` prints (title, heading, rows, visible text, error banners, console errors) plus failed same-origin requests and the HTTP status, then `OK n page(s)` or `FAIL k of n page(s)`. **Exit status** 0 all passed, 1 a page failed (HTTP error, sign-in form or a 401 instead of the page, an error banner or error dialog, a console error, a failed request, a failed assertion), 2 the check could not run. It signs in when needed — `--user/--password`, `--role R` (the project's demo user with that user role), or with only `-p` a demo user — saves the session under `.mxcli/playwright-check/`, reuses it on the next check and renews it when the runtime has restarted. `--assert-text`, `--assert-count 'SELECTOR>=N'`, and `--screenshot out.png`, which prints the path and never the image. It replaces the hand-written playwright-cli login/goto/sleep/eval/screenshot sequences that were about a sixth of the tool calls in a measured app-building session, each ending in a PNG read; the `test-app` and `verify-in-runtime` skills and `/mendix:test` now route "check a page" to it.
Expand Down
21 changes: 21 additions & 0 deletions mdl-examples/bug-tests/1306-lint-languages-builtin.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
mdl 1;
-- ============================================================================
-- mendixlabs/mxcli#1306 — a Starlark lint rule could not read the enabled
-- project languages
-- ============================================================================
--
-- Reported: strings() returns rows for every stored translation, including
-- languages that are not enabled (a fresh en_US-only app already carries nl_NL
-- texts), so enabling nl_NL changed nothing a rule could see; a rule calling
-- languages() was skipped with "undefined: languages".
--
-- The assertion is a Go test (mdl/linter/starlark_languages_test.go): the
-- symptom is in the lint API, which `make check-mdl` does not exercise. This
-- script is the model change from the report. To reproduce by hand on a fresh
-- `mxcli new` app, run it, then lint a rule that returns
-- [violation(message = l.code) for l in languages()]
-- before it, languages() lists en_US only; after it, en_US and nl_NL, while
-- strings() reports the same per-language counts both times.
-- ============================================================================

alter settings LANGUAGE add or modify 'nl_NL' (CheckCompleteness: true);
1 change: 1 addition & 0 deletions mdl/linter/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type LintReader interface {
GetMicroflow(id model.ID) (*microflows.Microflow, error)
ListMicroflows() ([]*microflows.Microflow, error)
GetProjectSecurity() (*security.ProjectSecurity, error)
GetProjectSettings() (*model.ProjectSettings, error)
GetNavigation() (*types.NavigationDocument, error)
ListPages() ([]*pages.Page, error)
ListModules() ([]*model.Module, error)
Expand Down
11 changes: 6 additions & 5 deletions mdl/linter/rules/dataview_layout_grid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,12 @@ func (r rawUnitReader) ListMicroflows() ([]*microflows.Microflow, error) { r
func (r rawUnitReader) GetProjectSecurity() (*security.ProjectSecurity, error) {
return nil, nil
}
func (r rawUnitReader) GetNavigation() (*types.NavigationDocument, error) { return nil, nil }
func (r rawUnitReader) ListPages() ([]*pages.Page, error) { return nil, nil }
func (r rawUnitReader) ListModules() ([]*model.Module, error) { return nil, nil }
func (r rawUnitReader) ListFolders() ([]*types.FolderInfo, error) { return nil, nil }
func (r rawUnitReader) GetRawUnit(id model.ID) (map[string]any, error) { return r.units[id], nil }
func (r rawUnitReader) GetProjectSettings() (*model.ProjectSettings, error) { return nil, nil }
func (r rawUnitReader) GetNavigation() (*types.NavigationDocument, error) { return nil, nil }
func (r rawUnitReader) ListPages() ([]*pages.Page, error) { return nil, nil }
func (r rawUnitReader) ListModules() ([]*model.Module, error) { return nil, nil }
func (r rawUnitReader) ListFolders() ([]*types.FolderInfo, error) { return nil, nil }
func (r rawUnitReader) GetRawUnit(id model.ID) (map[string]any, error) { return r.units[id], nil }
func (r rawUnitReader) ListScheduledEvents() ([]*model.ScheduledEvent, error) {
return nil, nil
}
Expand Down
34 changes: 34 additions & 0 deletions mdl/linter/starlark.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ func (r *StarlarkRule) buildPredeclared() starlark.StringDict {
"module_roles": starlark.NewBuiltin("module_roles", r.builtinModuleRoles),
"role_mappings": starlark.NewBuiltin("role_mappings", r.builtinRoleMappings),
"project_security": starlark.NewBuiltin("project_security", r.builtinProjectSecurity),
"languages": starlark.NewBuiltin("languages", r.builtinLanguages),

// XPath / expression analysis
"xpath_expressions": starlark.NewBuiltin("xpath_expressions", r.builtinXPathExpressions),
Expand Down Expand Up @@ -914,6 +915,39 @@ func (r *StarlarkRule) builtinProjectSecurity(_ *starlark.Thread, _ *starlark.Bu
}), nil
}

// builtinLanguages returns the languages enabled in the project settings
// (mendixlabs/mxcli#1306). strings() cannot answer this: it has a row for every
// stored translation, and a project carries texts in languages it never
// enabled. A failed settings read fails the rule rather than answering [],
// which a per-language rule would report as clean.
func (r *StarlarkRule) builtinLanguages(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := starlark.UnpackArgs(b.Name(), args, kwargs); err != nil {
return nil, err
}
if r.ctx == nil || r.ctx.Reader() == nil {
return starlark.NewList(nil), nil
}
ps, err := r.ctx.Reader().GetProjectSettings()
if err != nil {
return nil, fmt.Errorf("%s: reading project settings: %w", b.Name(), err)
}
if ps == nil || ps.Language == nil {
return starlark.NewList(nil), nil
}
var out []starlark.Value
for _, l := range ps.Language.Languages {
if l.Code == "" {
continue
}
out = append(out, starlarkstruct.FromStringDict(starlark.String("language"), starlark.StringDict{
"code": starlark.String(l.Code),
"is_default": starlark.Bool(l.Code == ps.Language.DefaultLanguageCode),
"check_completeness": starlark.Bool(l.CheckCompleteness),
}))
}
return starlark.NewList(out), nil
}

// entityToStarlark converts an Entity to a Starlark struct.
func entityToStarlark(e Entity) starlark.Value {
return starlarkstruct.FromStringDict(starlark.String("entity"), starlark.StringDict{
Expand Down
Loading
Loading