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
17 changes: 13 additions & 4 deletions .claude/lint-rules/example_microflow.star
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
# VAL_ - Validation microflows
# SCH_ - Scheduled event microflows
#
# Customize the prefixes and patterns to match your project standards.
# Customize the prefixes in lint-config.yaml rather than here (`mxcli init`
# rewrites this file):
#
# rules:
# CUSTOM002:
# options:
# valid_prefixes: [ACT_, SUB_, DS_, TEST_] # or "ACT_, SUB_, DS_, TEST_"
#
# The option replaces VALID_PREFIXES below; unset keeps it.
#
# Microflow properties:
# .id - Document ID
Expand All @@ -28,21 +36,22 @@ DESCRIPTION = "Microflows should have standard naming prefixes (ACT_, SUB_, DS_,
CATEGORY = "naming"
SEVERITY = "info"

# Define valid prefixes - customize for your project
# Built-in valid prefixes, used when the valid_prefixes option is unset
VALID_PREFIXES = ["ACT_", "SUB_", "DS_", "VAL_", "SCH_", "IVK_", "OCH_"]

def check():
"""
Check that microflows use standard naming prefixes.
"""
violations = []
valid_prefixes = get_list_option("valid_prefixes", VALID_PREFIXES)

for mf in microflows():
name = mf.name # Simple name

# Check if name starts with any valid prefix
has_valid_prefix = False
for prefix in VALID_PREFIXES:
for prefix in valid_prefixes:
if name.startswith(prefix):
has_valid_prefix = True
break
Expand All @@ -57,7 +66,7 @@ def check():
message="{} '{}' should start with a standard prefix ({})".format(
mf.document_noun_title,
name,
", ".join(VALID_PREFIXES)
", ".join(valid_prefixes)
),
location=loc
)
Expand Down
20 changes: 15 additions & 5 deletions .claude/lint-rules/orphaned_elements.star
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@
# - Entities with no references at all
#
# This rule requires REFRESH CATALOG FULL to populate the refs table.
#
# Options (lint-config.yaml, rules.QUAL004.options):
# entry_point_prefixes - microflow name prefixes that mark an entry point,
# as a YAML list or comma-separated text. Replaces
# the built-in list below; unset keeps it.
#
# Edit the option, not this file: `mxcli init` rewrites the shipped rules.

RULE_ID = "QUAL004"
RULE_NAME = "Orphaned Elements"
DESCRIPTION = "Elements should be referenced somewhere in the project or marked as entry points"
CATEGORY = "quality"
SEVERITY = "info"

# Prefixes for microflows that are expected to be entry points
# Prefixes for microflows that are expected to be entry points, unless the
# entry_point_prefixes option says otherwise
ENTRY_POINT_PREFIXES = ["ACT_", "SCH_", "WS_", "REST_", "OData_"]

# Page name patterns that are likely entry points
Expand All @@ -33,9 +41,9 @@ MICROFLOW_ENTRY_KINDS = ["call", "schedule", "publish", "datasource", "action",
# Reference kinds that mean "something opens this page".
PAGE_ENTRY_KINDS = ["show_page", "home_page", "login_page", "menu_item", "action"]

def is_entry_point_microflow(name):
def is_entry_point_microflow(name, prefixes):
"""Check if a microflow name suggests it's a UI/scheduled entry point."""
for prefix in ENTRY_POINT_PREFIXES:
for prefix in prefixes:
if name.startswith(prefix):
return True
return False
Expand All @@ -52,11 +60,13 @@ def check():
Check for orphaned elements that have no incoming references.
"""
violations = []
entry_prefixes = get_list_option("entry_point_prefixes", ENTRY_POINT_PREFIXES)
rename_hint = "/".join(entry_prefixes[:2]) if entry_prefixes else "an entry-point"

# Check microflows
for mf in microflows():
# Skip entry point microflows
if is_entry_point_microflow(mf.name):
if is_entry_point_microflow(mf.name, entry_prefixes):
continue

# Get references to this microflow
Expand Down Expand Up @@ -95,7 +105,7 @@ def check():
v = violation(
message="{} '{}' is not called from anywhere.".format(mf.document_noun_title, mf.name),
location=loc,
suggestion="Remove if unused, or rename with ACT_/SCH_ prefix if it's an entry point."
suggestion="Remove if unused, rename with {} prefix if it's an entry point, or add its prefix to rules.QUAL004.options.entry_point_prefixes in lint-config.yaml.".format(rename_hint)
)
violations.append(v)

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ Returned by `languages()`, one per language enabled in the project settings. A p
| `is_camel_case(s)` | Returns True if string is camelCase |
| `matches(s, pattern)` | Returns True if string matches regex |
| `find_all(s, pattern)` | Every match of the regex in `s`: a tuple of the capture groups per match, or the matched string when the pattern has no groups |
| `get_option(key, default?)` | The rule's option `key` from the `options:` block under its rule ID in `.claude/lint-config.yaml`, or `default` (`None` if omitted) when unset |
| `get_option(key, default?)` / `get_list_option(key, default?)` | The rule's option `key` from the `options:` block under its rule ID in `.claude/lint-config.yaml`, or `default` (`None` if omitted) when unset. YAML lists and maps arrive as lists and dicts. `get_list_option` returns a list of strings from a YAML list or comma-separated text (`"ACT_, SUB_"`) — use it, not a constant list a project would have to edit (`mxcli init` rewrites shipped rules). A value of the wrong shape is reported as a warning and yields `default` |
| `struct(**kwargs)` | Build an ad-hoc struct, e.g. `struct(name="x", count=1)`, to group values inside a rule |

## Common Patterns
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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.
- **QUAL004's entry-point prefixes and CUSTOM002's valid prefixes are `lint-config.yaml` options** (mendixlabs/mxcli#1509) — both lists were constants in the rule files, and `mxcli init` rewrites those files, so a project whose convention names microflows `TEST_`, `GET_` or `PRS_` could not keep an edit and got one finding per such microflow (84 QUAL004 and 121 CUSTOM002 findings on the reporting project). `rules.QUAL004.options.entry_point_prefixes` and `rules.CUSTOM002.options.valid_prefixes` take a YAML list or comma-separated text and replace the built-in list; unset, the built-in list applies as before. Underneath, a YAML list or map under any rule's `options` reached a Starlark rule as `None` — `get_option` converted scalars only — so a list-valued option was dropped without a word; lists and maps now convert recursively, the new `get_list_option(key, default)` builtin reads a list of strings in either spelling, and an option value a rule cannot use is reported as a warning naming `rules.<ID>.options.<key>` (kept out of the score) while the rule runs on its default. QUAL004 still counts only callers in the model: a microflow called only from `tests/*.test.mdl` is reported, and its prefix belongs in `entry_point_prefixes`. Docs: "Starlark Rules" › QUAL004 and CUSTOM002 options.
- **`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
3 changes: 3 additions & 0 deletions cmd/mxcli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,9 @@ excludeModules:
# severity: warning
# options:
# maxObjects: 20
# QUAL004: # orphaned elements: name prefixes of entry points
# options:
# entry_point_prefixes: [ACT_, SCH_, WS_, REST_, OData_]
rules: {}
`

Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/tools/mxcli-lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ failure is an error (`Starlark rule error: …`). A rule's configured severity i
`lint-config.yaml` applies to its findings, never to its failure. `mxcli report`
keeps all rule failures out of the score.

An option in `lint-config.yaml` that a rule cannot use — for example a number
where the rule expects a list of prefixes — is reported at **warning** level as
`lint-config.yaml: rules.<ID>.options.<key> … the rule ran with its default`.
The rule still runs, on its built-in default, and the warning is kept out of the
score like any other rule failure.

A rule file that does not load at all (for example, one calling a builtin this
mxcli lacks) is skipped with a warning on stderr.

Expand Down
40 changes: 39 additions & 1 deletion docs-site/src/tools/starlark-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ In addition to the built-in Go rules, mxcli bundles 29 Starlark-based lint rules
| **QUAL001** | McCabe complexity -- Flags microflows with high cyclomatic complexity |
| **QUAL002** | Documentation -- Missing documentation across every document type: modules, entities, pages, microflows, workflows, Java/JavaScript actions and their parameters, REST services, mappings, constants ([options](#qual002-options)) |
| **QUAL003** | Long microflows -- Warns about microflows with too many activities |
| **QUAL004** | Orphaned elements -- Detects unused entities, microflows, or pages |
| **QUAL004** | Orphaned elements -- Detects unused entities, microflows, or pages ([options](#qual004-and-custom002-options)) |

### Design Rules (DESIGN001)

Expand Down Expand Up @@ -127,6 +127,44 @@ and one in `_DOC_KINDS` (`missing_documentation.star`) giving the option name an
the suggestion text. `TestQUAL002_SweepsEveryAdvertisedDocumentType` fails if the
Go side advertises a kind the tests do not cover.

### QUAL004 and CUSTOM002 options {#qual004-and-custom002-options}

Both rules decide by name prefix. QUAL004 (orphaned elements) does not report a
microflow whose name starts with an **entry-point prefix**, because something
outside the model calls it; CUSTOM002 (microflow prefix convention) reports a
microflow whose name starts with none of the **valid prefixes**. Projects name
things differently, so both lists are options — set them in `lint-config.yaml`
rather than in the rule files, which `mxcli init` rewrites:

| Rule | Option | Default |
|------|--------|---------|
| QUAL004 | `entry_point_prefixes` | `ACT_, SCH_, WS_, REST_, OData_` |
| CUSTOM002 | `valid_prefixes` | `ACT_, SUB_, DS_, VAL_, SCH_, IVK_, OCH_` |

```yaml
rules:
QUAL004:
options:
entry_point_prefixes: [ACT_, SCH_, WS_, REST_, OData_, TEST_]
CUSTOM002:
options:
valid_prefixes: "ACT_, SUB_, DS_, VAL_, SCH_, TEST_, GET_"
```

Write the list as a YAML list or as one comma-separated string; spaces around
the commas are ignored. A configured list **replaces** the default, so repeat the
default prefixes you still want. An empty list is a real setting: for QUAL004 it
means no microflow is exempt by name. A value of any other shape (a number, a
list holding a number) is not silently ignored — the rule runs with its default
and the run reports a warning naming the option, listed with the rules that could
not run and kept out of the score.

QUAL004 counts a microflow as called only when something **in the model** runs
it (a call, a schedule, a page or widget, a published service, a project
setting). A call from an MDL test script (`tests/*.test.mdl`) is not in the
model, so a helper used only by tests is reported; give such helpers a prefix
and list it in `entry_point_prefixes`.

### CONV018 options {#conv018-options}

Studio Pro lets a module hold folders, and every Mendix style guide expects them
Expand Down
Loading
Loading