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
80 changes: 31 additions & 49 deletions .claude/lint-rules/orphaned_elements.star
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
# - Work in progress that was forgotten
#
# Checks:
# - Microflows with no callers (except ACT_ which are UI entry points)
# - Pages with no show_page references (except entry pages)
# - Entities with no references at all
# - Microflows, nanoflows and rules nothing references (except entry-point
# prefixes such as ACT_)
# - Pages nothing references (except likely entry pages)
#
# "Referenced" means what `mxcli refs` and CATALOG.GRAPH_DEAD_ASSETS mean: any
# edge in the catalog's reference graph that points at the document.
#
# This rule requires REFRESH CATALOG FULL to populate the refs table.
#
Expand All @@ -34,13 +37,23 @@ ENTRY_POINT_PREFIXES = ["ACT_", "SCH_", "WS_", "REST_", "OData_"]
# Page name patterns that are likely entry points
ENTRY_PAGE_PATTERNS = ["Home", "Login", "Index", "Dashboard"]

# Reference kinds that mean "something causes this microflow to run". These are
# catalog RefKind values (mdl/catalog/builder_references.go); a kind missing here
# turns a live document into a false "not called from anywhere" finding.
MICROFLOW_ENTRY_KINDS = ["call", "schedule", "publish", "datasource", "action", "calculate", "settings", "event"]
# There is deliberately no list of the reference kinds that "count". Every edge
# into a microflow or a page is something that runs or opens it — a call, a
# widget, a calculated attribute, a scheduled event, a published service, an
# event handler, a project setting, the navigation — and the catalog keeps
# adding ways. A hand-kept list of them fell behind: it had the page kinds for a
# home page and a menu item but not the microflow ones, so a microflow started
# only from the navigation was reported with "Remove if unused" while
# `mxcli refs` listed the reference (ako/mxcli#1484). With no list, a new kind
# counts the day the catalog emits it.
#
# TestQUAL004CountsEveryRefKind (mdl/linter) drives every kind the catalog
# declares at a microflow, a nanoflow and a page, and fails on one that does not
# count.

# Reference kinds that mean "something opens this page".
PAGE_ENTRY_KINDS = ["show_page", "home_page", "login_page", "menu_item", "action"]
def is_referenced(qualified_name):
"""True when anything in the catalog's reference graph points at the document."""
return len(refs_to(qualified_name)) > 0

def is_entry_point_microflow(name, prefixes):
"""Check if a microflow name suggests it's a UI/scheduled entry point."""
Expand Down Expand Up @@ -70,34 +83,14 @@ def check():
if is_entry_point_microflow(mf.name, entry_prefixes):
continue

# Get references to this microflow
refs = refs_to(mf.qualified_name)

# Anything that causes the microflow to run counts as a caller, not just
# a literal "call" edge. A microflow reached only through one of the other
# kinds was reported as orphaned with the suggestion "Remove if unused":
#
# schedule a scheduled event runs it (Mendix's cron)
# datasource a page or widget uses it as a data source
# action a widget button calls it
# calculate a calculated attribute computes with it
# settings a project setting names it (after-startup, before-shutdown,
# health-check) — the RUNTIME calls it, and nothing in the
# model does
#
# The banking-app report hit the 'datasource' case: DS_CurrentCustomer and
# DS_MyAccounts are both page data sources and both were flagged. The
# CapTrackV4 report hit 'settings': the project's own AfterStartupMicroflow
# was reported as "not called from anywhere. Remove if unused", and taking
# that advice left a dangling name mx check did not catch either — only the
# runtime refused to start.
has_callers = False
for ref in refs:
if ref.ref_kind in MICROFLOW_ENTRY_KINDS:
has_callers = True
break

if not has_callers:
# a literal "call" edge. Each of these once reached a live microflow
# that this rule then advised deleting: a page data source (DS_ microflows in
# the banking-app report), the project's AfterStartupMicroflow (whose
# removal mx check did not catch — the runtime refused to start), a
# scheduled event, a published REST operation, an entity event handler,
# and a home or menu-item microflow (ako/mxcli#1484).
if not is_referenced(mf.qualified_name):
loc = location(
module=mf.module_name,
document_type=mf.document_noun_title,
Expand All @@ -116,21 +109,10 @@ def check():
if is_entry_point_page(page.name):
continue

# Get references to this page
refs = refs_to(page.qualified_name)

# A page is reachable if anything opens it. Navigation counts: a page that
# is only a home page, a login page or a menu item is reached by the
# client, not by a microflow. Counting only 'show_page' reported those as
# orphaned — masked until now by ENTRY_PAGE_PATTERNS, which happens to
# cover the pages most likely to be navigation targets.
is_shown = False
for ref in refs:
if ref.ref_kind in PAGE_ENTRY_KINDS:
is_shown = True
break

if not is_shown:
# client, not by a microflow.
if not is_referenced(page.qualified_name):
loc = location(
module=page.module_name,
document_type="Page",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/linter", "date": "2026-10-27", "symptom": "Every QUAL004 finding (and CONV001-010, the SEC rules, most bundled Starlark rules, and missing_translations) prints its location with the module twice: `at M.M.NAV_Home`, `at Sales.Sales.Order.Status`; CONV018's module-level finding prints `at Sales.Sales`", "cause": "`Location.DocumentName` is documented as the document's own name and `QualifiedName()` prefixes the module, but ~30 bundled Starlark rules pass `document_name=<x>.qualified_name`, as does the Go rule missing_translations. CONV018 passes the module as the document of a module-level finding", "file": "`mdl/linter/linter.go` (`Location.QualifiedName`); test `mdl/linter/location_test.go`", "insight": "Fix the one place the name is assembled, not the rules: text, Markdown, SARIF and `lint -d` all go through `QualifiedName()`, and fixing ~30 rule files would leave every project's stale `mxcli init` copies and every user-written rule still doubling. It is safe to strip on prefix because a Mendix document name cannot contain a dot, so a leading `<module>.` is always the module; the controls are a document named like its module (`Sales.Sales` stays) and a module name that is a prefix of the document's (`M` + `MX_Home`). The JSON `document` field still carries whichever spelling the rule passed — `lint -d` already accepts both. Found while reproducing ako/mxcli#1484", "refs": ["ako/mxcli#1484"], "rules": ["QUAL004", "CONV018"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/catalog", "date": "2026-10-27", "symptom": "`mxcli refs <Module.Read_X>` says \"no references found\" for a microflow that is a published OData resource's `ReadMode: microflow`, and likewise for the service's `Authentication: (microflow X)`; `impact` agrees and QUAL004 says \"Remove if unused\" — 35 of one backend's 43 QUAL004 findings were the read microflows and the authentication microflow of six published services (ako/mxcli#1639)", "cause": "`buildODataServices` wrote only service-level columns and emitted no edge for any microflow the service names: entity-set read/change modes (`CallMicroflow:<qn>` from the reader), published OData actions (`svc.Microflows`), `AuthMicroflow`. Same shape on three siblings the reader already populates: a published REST service's `AuthenticationMicroflow`, a business event service operation's `Microflow` (stored in business_events_data and dropped), a consumed OData service's configuration / headers / error-handling microflows", "file": "`mdl/catalog/builder_service_refs.go` (new: `serviceMicroflowRef`, `addServiceMicroflowRefs`, `publishedODataMicroflows`, `extractServiceMicroflowRefs`), collection in `builder_modules.go` (`buildODataServices`, `buildODataClients`, `buildBusinessEventServices`) and `builder_rest.go`, emission in `builder_references.go`, `RefObject*Service` source types, `CatalogSchemaVersion` 28->29; tests `mdl/catalog/builder_service_refs_test.go`", "insight": "Fifth instance of the class after widget actions, scheduled events, settings, published REST operations and entity events — **audit the whole document family when one pointer is missing**: the reported two (ReadMode, Authentication) sat beside the change modes, the OData actions and three sibling services with the identical gap, found by grepping modelsdk/gen for `*Microflow*QualifiedName()` and checking each against the builders. **Reuse a kind whose consumers already treat it as an entry point** rather than minting one: `publish` (a published service runs it), `event` (an event handler — entity or business event; detail stays in CATALOG.BUSINESS_EVENTS), `call` (the consumed service calls its configuration microflow, as a workflow calls its handlers). A new kind would have needed `callerRefKinds`, `graphRefKinds`, the skill's ref_kind row and the docs; reusing one needed none. The source is the service with its catalog id in SourceId, which joins to the service's own table and to `objects` (the RefObject values equal the objects ObjectType values). Bumped CatalogSchemaVersion: no column changed, but a cached catalog would keep the old edge set until the .mpr changed. Controls: stubbing the collection hooks empties every edge test and GRAPH_DEAD_ASSETS lists Read_Rows again. Real 11.13 app, same model, baseline vs fixed binary: refs for the read, authentication and action microflows, the REST authentication microflow and a business event handler go from \"no references\" to one row each; QUAL004 drops them, an unreferenced microflow is still reported. Studio Pro-authored ako/TestApp: its OData actions (GetOrder, GetCustomerOrders, SaveOrder) gain publish edges; no Studio Pro document with a read microflow or authentication microflow was available, so those two read paths are verified on mxcli-written documents (keys match generated/metamodel). Not fixed, reported: published SOAP web service operations and header-auth microflow, import/export mapping microflows, ProjectSecurity.SignInMicroflow, Microflow.ConcurrencyErrorMicroflow, the navigation not-found page, document-template data sources, app services", "refs": ["ako/mxcli#1639", "ako/mxcli#1484"], "rules": ["QUAL004"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/linter", "date": "2026-10-27", "symptom": "QUAL004 reports a microflow started only from the navigation — a profile's home microflow, or a menu item's `call microflow` — as \"is not called from anywhere. Remove if unused\", while `mxcli refs` lists its `home_page` / `menu_item` edge (ako/mxcli#1484, upstream mendixlabs/mxcli#1579; the 4 NAV_ findings left in ako/mxcli#1639's Formula1Frontend)", "cause": "The rule decided \"has a caller\" from its own copy of the reference vocabulary, MICROFLOW_ENTRY_KINDS. The page list (PAGE_ENTRY_KINDS) had `home_page` and `menu_item`; the microflow list did not, though the catalog emits both kinds into a MICROFLOW too (a home microflow, a menu item that calls one)", "file": "`.claude/lint-rules/orphaned_elements.star` (`is_referenced`), `mdl/catalog/builder_references.go` (`RefKinds`); tests `mdl/linter/starlark_qual004_refs_test.go`, `mdl/catalog/lint_rule_doc_vocabulary_test.go` (`RefKind` row of `TestEveryObjectTypeConstantIsInAPublishedList`)", "insight": "This was the fourth time the same list fell behind (datasource/action/calculate, schedule, settings/publish, event, now the navigation pair) — **a consumer that keeps a copy of an allowlist of edge kinds will fall behind the producer again**, and the upstream one-line fix (add the two kinds) would have reset the clock. The rule now counts any edge into the document, i.e. the graph `refs` and GRAPH_DEAD_ASSETS already read; no kind targeting a microflow or page means anything but \"this runs/opens it\", so no exclusion list was needed. Drift is guarded behaviourally rather than by a second list: `catalog.RefKinds` is held complete against the RefKind constants (AST scan), and TestQUAL004CountsEveryRefKind drives every one of them at a microflow, a nanoflow and a page through the real rule, with an unreferenced microflow as the per-kind control — so a new kind is tested the moment it is declared, and a reintroduced allowlist fails by kind name. Verified on a real 11.13 app with the BASELINE binary and only the rule swapped: NAV_Home/NAV_Orders drop out, an unreferenced microflow and page are still reported. The OData microflows in the same report are a catalog gap, not a rule gap: `refs` was empty for them too (ako/mxcli#1639)", "refs": ["ako/mxcli#1484", "mendixlabs/mxcli#1579", "ako/mxcli#1639"], "rules": ["QUAL004"]}
6 changes: 3 additions & 3 deletions .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,13 +561,13 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific)
### reference
| Property | Type | Example |
|----------|------|---------|
| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PROJECT_SETTINGS"`, `"IMPORT_MAPPING"`, `"EXPORT_MAPPING"` |
| `source_type` | string | The document the edge comes FROM, upper-case: `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"SNIPPET"`, `"ENTITY"`, `"ASSOCIATION"`, `"WORKFLOW"`, `"NAVIGATION"`, `"SCHEDULED_EVENT"`, `"PUBLISHED_REST_OPERATION"`, `"PUBLISHED_REST_SERVICE"`, `"PUBLISHED_ODATA_SERVICE"`, `"CONSUMED_ODATA_SERVICE"`, `"BUSINESS_EVENT_SERVICE"`, `"PROJECT_SETTINGS"`, `"IMPORT_MAPPING"`, `"EXPORT_MAPPING"` |
| `source_id` | string | Source UUID |
| `source_name` | string | `"Sales.ACT_Customer_Create"` |
| `target_type` | string | What it points AT, upper-case: `"ENTITY"`, `"ASSOCIATION"`, `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"LAYOUT"`, `"WORKFLOW"`, `"WIDGET"`, `"JAVA_ACTION"`, `"JAVASCRIPT_ACTION"`, `"REST_OPERATION"`, `"REGULAR_EXPRESSION"`, `"ATTRIBUTE"`, `"ENUMERATION"`, `"ENUMERATION_VALUE"`. `LAYOUT`, `WIDGET`, `ATTRIBUTE`, `ENUMERATION` and `ENUMERATION_VALUE` are only ever targets; `SCHEDULED_EVENT` and `PROJECT_SETTINGS` only ever sources |
| `target_type` | string | What it points AT, upper-case: `"ENTITY"`, `"ASSOCIATION"`, `"MICROFLOW"`, `"NANOFLOW"`, `"RULE"`, `"PAGE"`, `"LAYOUT"`, `"WORKFLOW"`, `"WIDGET"`, `"JAVA_ACTION"`, `"JAVASCRIPT_ACTION"`, `"REST_OPERATION"`, `"REGULAR_EXPRESSION"`, `"ATTRIBUTE"`, `"ENUMERATION"`, `"ENUMERATION_VALUE"`. `LAYOUT`, `WIDGET`, `ATTRIBUTE`, `ENUMERATION` and `ENUMERATION_VALUE` are only ever targets; `SCHEDULED_EVENT`, `PROJECT_SETTINGS` and the service types only ever sources |
| `target_id` | string | Target UUID |
| `target_name` | string | `"Sales.Customer"`; three-part for an attribute or an enumeration value: `"Sales.Order.Total"`, `"Sales.OrderStatus.Open"` |
| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"commit"` (a commit action, or a create/change that commits — beside its `"create"`/`"change"` edge; a commit of a variable whose entity the flow cannot tell has no edge), `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"`, `"event"`, `"member"` (binds/reads/writes an attribute or navigates an association), `"xpath"` (an XPath constraint names it), `"type"` (typed as an enumeration), `"value"` (an expression names an enumeration value), `"mapping"` (an import/export mapping maps the entity) — lower-case, unlike the types above. Attribute names used only through a variable in a free-text expression (`$Order/Total`) have no edge |
| `ref_kind` | string | How it references: `"call"`, `"create"`, `"retrieve"`, `"change"`, `"delete"`, `"commit"` (a commit action, or a create/change that commits — beside its `"create"`/`"change"` edge; a commit of a variable whose entity the flow cannot tell has no edge), `"show_page"`, `"datasource"`, `"action"`, `"layout"`, `"parameter"`, `"return"`, `"generalize"`, `"associate"`, `"home_page"`, `"login_page"`, `"menu_item"`, `"calculate"`, `"schedule"`, `"validate"`, `"settings"`, `"widget"`, `"sync"`, `"publish"` (a published REST operation, OData resource or action, or a service's authentication microflow), `"event"` (an entity event or a business event handler), `"member"` (binds/reads/writes an attribute or navigates an association), `"xpath"` (an XPath constraint names it), `"type"` (typed as an enumeration), `"value"` (an expression names an enumeration value), `"mapping"` (an import/export mapping maps the entity) — lower-case, unlike the types above. Attribute names used only through a variable in a free-text expression (`$Order/Total`) have no edge |
| `module_name` | string | Source module |

### project_security
Expand Down
1 change: 1 addition & 0 deletions cmd/mxcli/lint_rules_shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ bbd935d1ee0fe6c635572f19c030f511e1010f13b38f49622f28060c3b3e8305 missing_docume
a5fa3f0f37f7fde0206df2880fdeb676d0db656957ce89d7b09033af8a7a4476 orphaned_elements.star
c27185493f9b8f0b26eff68634c522b61ddf41a59b614d14e046c28bbc60a7d3 orphaned_elements.star
e05678135cbc750a3cfef07ddf348b7f207196f0a7762907e416e1a3c973de79 orphaned_elements.star
d5d37b24359dd2d8110860ced7107781016a730ac883e2353f3ae797ede58055 orphaned_elements.star
4500272ad6d0ae8876081de0825c633117b471913079be08fb78311e10470071 sec_guest_access.star
922394f50a7d8b8fab406dcd623536f98dc0d01b9970053776cd609900b2dc39 sec_missing_member_restriction.star
46d8bf64c7a30453685c4c470d6864e9b8f7cfd69d33985a50e24a358c309324 sec_pii_attributes.star
Expand Down
Loading
Loading