From fea2f53ac99759f278fd5e81fa857580871e5d60 Mon Sep 17 00:00:00 2001 From: stagknee <241821830+stagknee@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:26:29 -0500 Subject: [PATCH] fix(check): accept a Data grid 2's own settings without a project (mendixlabs/mxcli#1573) `mxcli check` without `-p` warned, for every setting a Data grid 2 takes by its own key, widget `dgAttr` (datagrid) property `configurationAttribute` is not recognized and will be silently dropped on write [MDL-WIDGET07] although `exec -p` stores it and `check -p` accepts it, and it refused an Expression-typed one (`rowClass`, a column's `columnClass`) as an MDL-WIDGET42 ERROR, "a plain-value property cannot hold the expression". A script exec takes therefore failed `make check-mdl` (exit 1 with 2 errors, measured), and so would the output of `describe page` once it prints such settings. DATAGRID has no embedded definition: its .def.json is generated from the project's own package, and an embedded one would take precedence over that package on every write (RefreshWidgetDefinitions skips a widget id the registry already holds), so adding one would change the write path to fix a check. Without a project the keyword resolved to no definition at all and was judged against the vocabulary of the built-in widgets. The check now reads the property list of the Data grid 2 template mxcli embeds, for the grid's own keys and, for a `column` inside one, the item type of `columns`. A key the template declares (or the `Params` of a text template) is accepted, and its declared type decides MDL-WIDGET42. The template is the oldest-supported baseline, so a key it does not declare is still reported by MDL-WIDGET07, with a did-you-mean from the template's names, but as what is true without a project: it cannot be verified, and is dropped on write unless the installed widget declares it. The Data grid 2 template carries the same 40 grid keys and 21 column keys as the 11.12.5 package. With a project, and for every other widget, nothing changes. Co-Authored-By: Claude Sonnet 5.5 --- ...ject-reports-datagrid-keys-as-dropped.json | 1 + CHANGELOG.md | 4 + docs-site/src/internals/widget-engine.md | 2 + ...agrid-by-key-settings-check-no-project.mdl | 83 +++++++ .../validate_widget_embedded_schema.go | 184 +++++++++++++++ .../validate_widget_embedded_schema_test.go | 218 ++++++++++++++++++ .../validate_widget_expression_value.go | 15 +- mdl/executor/validate_widgets.go | 57 ++++- 8 files changed, 553 insertions(+), 11 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-executor/2026-10-17-check-without-project-reports-datagrid-keys-as-dropped.json create mode 100644 mdl-examples/bug-tests/1573-datagrid-by-key-settings-check-no-project.mdl create mode 100644 mdl/executor/validate_widget_embedded_schema.go create mode 100644 mdl/executor/validate_widget_embedded_schema_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-executor/2026-10-17-check-without-project-reports-datagrid-keys-as-dropped.json b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-17-check-without-project-reports-datagrid-keys-as-dropped.json new file mode 100644 index 0000000000..8e197daa25 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-17-check-without-project-reports-datagrid-keys-as-dropped.json @@ -0,0 +1 @@ +{"area": "mdl/executor", "date": "2026-10-17", "symptom": "`mxcli check pers.mdl` without `-p` warned `widget dgAttr (datagrid) property configurationAttribute is not recognized and will be silently dropped on write [MDL-WIDGET07]` (and the same for configurationStorageType, storeFiltersInPersonalization, refreshInterval, columnsHidable, showEmptyPlaceholder, a column's minWidth/minWidthLimit/allowEventPropagation/exportValue), although `exec -p` stores each of them and `check -p` accepts them (mendixlabs/mxcli#1573). An Expression-typed key (rowClass, a column's columnClass) was worse: MDL-WIDGET42 ERROR \"a plain-value property cannot hold the expression\", so a script `exec` takes failed `check` outright, and so did `describe` output carrying one.", "cause": "DATAGRID has no embedded definition (sdk/widgets/definitions holds combobox, gallery, the filters, image): its .def.json is generated from the project's own package, and an embedded one would take precedence over that package for every write because RefreshWidgetDefinitions skips an id the registry already holds. With no project the keyword therefore resolved to no definition, and validateWidgetTreeIn fell through to validateStaticWidgetUnknownProps, whose vocabulary is the union of what the built-in widgets' builders read and describe prints; validateWidgetExpressionValues, given no definition and no item mapping, called every key a plain-value property.", "fix": "validate_widget_embedded_schema.go: for a keyword widget the registry has no definition for (DATAGRID, and its `column`), read the property list of the template mxcli embeds for it (widgets.GetTemplate; the grid's own keys, the `columns` item type for a column). A key the template declares (or the `Params` of a text template) is accepted by MDL-WIDGET07, and its declared type decides MDL-WIDGET42 (Expression takes an expression). A key it does not declare still draws MDL-WIDGET07 with a did-you-mean from the template's names, now worded as what is true: it cannot be verified without a project. Nothing changes with a project, or for a built-in widget.", "file": "mdl/executor/validate_widget_embedded_schema.go", "test": "mdl/executor/validate_widget_embedded_schema_test.go (every key the template declares, derived from it); mdl-examples/bug-tests/1573-datagrid-by-key-settings-check-no-project.mdl", "insight": "`make check-mdl` runs `check` with no project, so the symptom is invisible to the usual loop (`check -p`) and shows only when a script is checked in a repo or by a tool without a project; the reproduction needs the pair `check` vs `check -p` on one file. The embedded template is the 11.6 baseline, kept conservative on purpose (widget-engine.md), so it is evidence of what a key IS and never of what it is not: the Data grid 2 template carries the same 40 grid keys and 21 column keys as the 11.12.5 package (3.4.0), but a newer widget version can add more, which is why an undeclared key is reported as unverifiable rather than as dropped, and why the misspelled-key case keeps its warning. Adding an embedded datagrid.def.json looks like the obvious fix and is the wrong one: it would change every write, not just the check."} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0798c1db7b..f85ed43c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed + +- **`check` without a project no longer reports a Data grid 2's own settings as dropped, or refuses an expression on one** (mendixlabs/mxcli#1573) — `exec -p` stores `configurationStorageType`, `configurationAttribute`, `storeFiltersInPersonalization`, `refreshInterval`, `columnsHidable`, `rowClass`, a column's `minWidth` and the rest of the widget's own keys, and `check -p` accepts them, but `check` alone warned MDL-WIDGET07 "not recognized and will be silently dropped on write" for each, and refused an Expression-typed one (`rowClass`, a column's `columnClass`) as an MDL-WIDGET42 error, so a script `exec` takes failed `make check-mdl`. Data grid 2 has no embedded definition (it is generated from the project's package, and an embedded one would take precedence over that package on every write), so the check judged its keys against the built-in widgets' vocabulary. It now reads the property list of the Data grid 2 template mxcli embeds: a key the template declares is accepted and its declared type decides MDL-WIDGET42. A key it does not declare still warns — a misspelled `configurationStorageTyp` too, with the spelling it probably meant — but says what is true without a project: that the key cannot be verified, and is dropped on write unless the installed widget declares it. With `-p`, and for every other widget, nothing changes. + ## [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. diff --git a/docs-site/src/internals/widget-engine.md b/docs-site/src/internals/widget-engine.md index 630f42b334..bd61096bbb 100644 --- a/docs-site/src/internals/widget-engine.md +++ b/docs-site/src/internals/widget-engine.md @@ -228,6 +228,8 @@ Definitions are loaded in priority order. Higher-priority definitions override l Definitions are looked up by MDL name (case-insensitive): `COMBOBOX`, `GALLERY`, or any custom name. +**DATAGRID has no embedded definition, on purpose.** Its `.def.json` is generated from the project's own package, and an embedded one would sit ahead of that package for every write (`RefreshWidgetDefinitions` skips a widget id the registry already holds). So without a project the registry has no definition for it, and `mxcli check` reads the property list of the template mxcli embeds for the widget (`sdk/widgets/templates/`, via `embeddedSchemaFor`): a key the template declares is accepted and its declared type decides whether an expression is allowed (MDL-WIDGET42). The template is the oldest-supported baseline, so a key it does not declare is reported as one that cannot be verified without a project (MDL-WIDGET07), not as one that is dropped. + ### Validation at Load Time When a definition is loaded, it is validated: diff --git a/mdl-examples/bug-tests/1573-datagrid-by-key-settings-check-no-project.mdl b/mdl-examples/bug-tests/1573-datagrid-by-key-settings-check-no-project.mdl new file mode 100644 index 0000000000..2ae33377eb --- /dev/null +++ b/mdl-examples/bug-tests/1573-datagrid-by-key-settings-check-no-project.mdl @@ -0,0 +1,83 @@ +mdl 1; +-- ============================================================================ +-- Bug: `mxcli check` without a project called a Data grid 2's own settings +-- "not recognized and silently dropped" (mendixlabs/mxcli#1573) +-- ============================================================================ +-- +-- A Data grid 2 takes every one of its settings by the widget's own property +-- key. `exec -p` stores each of these and `check -p` accepts them. `check` +-- without a project — which is what `make check-mdl` runs on this file — warned +-- MDL-WIDGET07 for each of them ("property `configurationAttribute` is not +-- recognized and will be silently dropped on write"), and REFUSED the two +-- Expression-typed ones as MDL-WIDGET42 ("a plain-value property cannot hold +-- the expression"), which makes the file fail `check` outright. +-- +-- Without a project DATAGRID has no definition (it is generated from the +-- project's own package), so the check judged its keys against the vocabulary +-- of the built-in widgets. It now reads the property list of the Data grid 2 +-- template mxcli embeds: a key that template declares is accepted, and an +-- Expression-typed one takes an expression. A key it does not declare is still +-- reported, now as one that cannot be verified without a project — the +-- template is a baseline, not the installed widget. +-- +-- This file must pass `mxcli check` with no MDL-WIDGET07 and no MDL-WIDGET42. +-- The negative case (a misspelled key still warns) is a Go test: +-- mdl/executor/validate_widget_embedded_schema_test.go. +-- ============================================================================ + +CREATE MODULE BugDgCheck; + +CREATE OR MODIFY PERSISTENT ENTITY BugDgCheck.Ord ( + Code: String(20), + Amount: Decimal, + Open: Boolean +); + +CREATE OR MODIFY PERSISTENT ENTITY BugDgCheck.GridPref ( + Config: String(unlimited) +); + +-- Keyword settings and settings by key, grid and column. +CREATE PAGE BugDgCheck.Orders_Settings (Title: 'Settings', Layout: Atlas_Core.Atlas_Default) { + datagrid dgSettings ( + DataSource: database BugDgCheck.Ord, + PageSize: 25, + Pagination: virtualScrolling, + Selection: Single, + refreshInterval: 30, + columnsHidable: false, + showEmptyPlaceholder: custom, + rowClass: if $currentObject/Open then 'row-open' else 'row-closed', + exportDialogLabel: 'Exporting' + ) { + column (Attribute: Code, Caption: 'Code', Sortable: true, columnClass: if $currentObject/Open then 'is-open' else '') { textfilter tf } + column (Attribute: Amount, ShowContentAs: customContent, ColumnWidth: autoFit, minWidth: manual, minWidthLimit: 150, allowEventPropagation: false) { + dynamictext amt (Content: 'amt') + } + } +}; + +-- Personalization kept in the browser. +CREATE PAGE BugDgCheck.Orders_Local (Title: 'Local', Layout: Atlas_Core.Atlas_Default) { + datagrid dgLocal ( + DataSource: database BugDgCheck.Ord, + configurationStorageType: localStorage, + storeFiltersInPersonalization: true + ) { + column (Attribute: Code) { textfilter tf } + } +}; + +-- Personalization kept in an attribute of the surrounding object. +CREATE PAGE BugDgCheck.Orders_Attribute (Title: 'Attribute', Layout: Atlas_Core.Atlas_Default, Params: ( $Pref: BugDgCheck.GridPref )) { + dataview dvPref (DataSource: $Pref) { + datagrid dgAttr ( + DataSource: database BugDgCheck.Ord, + configurationStorageType: attribute, + configurationAttribute: Config, + storeFiltersInPersonalization: true + ) { + column (Attribute: Code) { textfilter tf } + } + } +}; diff --git a/mdl/executor/validate_widget_embedded_schema.go b/mdl/executor/validate_widget_embedded_schema.go new file mode 100644 index 0000000000..5e2bec0b96 --- /dev/null +++ b/mdl/executor/validate_widget_embedded_schema.go @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "sort" + "strings" + "sync" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/sdk/widgets" +) + +// What a check without a project knows about Data grid 2. +// +// A widget's properties come from its package (.mpk), which only a project +// has. The widgets mxcli embeds a definition for (combobox, gallery, the grid +// filters, image) are known without one. DATAGRID is not among them: its +// definition is generated from the project's own package, and embedding one +// would put it ahead of that package for every write +// (RefreshWidgetDefinitions skips an id the registry already holds). +// +// So without a project the widget had no definition at all, and the check fell +// back on the vocabulary of the built-in widgets — a union of what their +// builders read and what describe prints. Every Data grid 2 setting outside it +// was reported as "not recognized and will be silently dropped on write" +// (MDL-WIDGET07) although exec writes it, and an Expression-typed one was an +// ERROR (MDL-WIDGET42, "a plain-value property cannot hold the expression"). +// `mxcli check` of a script `exec` takes, and `describe` prints, failed. +// +// What mxcli does have is the template it embeds for the widget, which declares +// every property. That is a baseline (the oldest supported Mendix version, see +// widget-engine.md), so it is evidence of what a key IS, never of what it is +// not: a key it declares is accepted, a key it does not is reported as one that +// cannot be verified without a project. + +// embeddedWidgetSchema is the property list of a widget template mxcli embeds. +type embeddedWidgetSchema struct { + // Title and Version are the template's: "Data grid 2", "11.6.3". + Title string + Version string + // Names are the declared property keys, in declaration order. + Names []string + // types maps a lower-cased key to its declared type ("Expression", + // "TextTemplate", "Boolean", …). + types map[string]string +} + +// declares reports whether the template declares key, case-insensitively — or +// the `Params` companion of a text-template property, which carries its +// `{N}` parameters. +func (s *embeddedWidgetSchema) declares(key string) bool { + if s == nil { + return false + } + lower := strings.ToLower(key) + if _, ok := s.types[lower]; ok { + return true + } + if base, found := strings.CutSuffix(lower, "params"); found { + return s.types[base] == "TextTemplate" + } + return false +} + +// operation is the engine operation that writes key ("expression", +// "texttemplate", "attribute", "primitive"), or "" for a kind with none. +func (s *embeddedWidgetSchema) operation(key string) (string, bool) { + if s == nil { + return "", false + } + t, ok := s.types[strings.ToLower(key)] + if !ok { + return "", false + } + return operationForValueType(t), true +} + +// pluggableWidgetIDOfKeyword is the id of the pluggable widget an MDL keyword +// names, whatever the Mendix version (DATAGRID → Data grid 2), or "". +func pluggableWidgetIDOfKeyword(keyword string) string { + upper := strings.ToUpper(keyword) + for _, m := range keywordDispatchTable { + if m.Keyword != upper { + continue + } + for _, b := range m.Bindings { + if b.Kind == bindingKindPluggable && b.WidgetID != "" { + return b.WidgetID + } + } + } + return "" +} + +var embeddedSchemas sync.Map // widgetID + "/" + listKey -> *embeddedWidgetSchema (nil when there is none) + +// embeddedSchemaFor returns the embedded schema for the keyword widget w — +// the grid's own properties for `datagrid`, a column's for a `column` inside +// one — or nil when w is not a keyword widget the registry lacks a definition +// for (everything else is judged exactly as before). +func embeddedSchemaFor(w, parent *ast.WidgetV3) *embeddedWidgetSchema { + if w == nil || w.TypeIsGeneric || explicitWidgetID(w) != "" { + return nil + } + if id := pluggableWidgetIDOfKeyword(w.Type); id != "" { + return loadEmbeddedSchema(id, "") + } + if strings.EqualFold(w.Type, "column") && parent != nil && !parent.TypeIsGeneric && explicitWidgetID(parent) == "" { + if id := pluggableWidgetIDOfKeyword(parent.Type); id != "" { + return loadEmbeddedSchema(id, "columns") + } + } + return nil +} + +// loadEmbeddedSchema reads the property list of the embedded template for +// widgetID — or, with listKey, the item type of that object-list property. +func loadEmbeddedSchema(widgetID, listKey string) *embeddedWidgetSchema { + cacheKey := widgetID + "/" + listKey + if v, ok := embeddedSchemas.Load(cacheKey); ok { + s, _ := v.(*embeddedWidgetSchema) + return s + } + var schema *embeddedWidgetSchema + if tmpl, err := widgets.GetTemplate(widgetID); err == nil && tmpl != nil { + if propTypes := embeddedTemplatePropertyTypes(tmpl.Type, listKey); len(propTypes) > 0 { + schema = &embeddedWidgetSchema{Title: tmpl.Name, Version: tmpl.Version, types: map[string]string{}} + if listKey != "" { + schema.Title += " column" + } + for _, pt := range propTypes { + key, _ := pt["PropertyKey"].(string) + if key == "" { + continue + } + kind := "" + if vt, ok := pt["ValueType"].(map[string]any); ok { + kind, _ = vt["Type"].(string) + } + schema.Names = append(schema.Names, key) + schema.types[strings.ToLower(key)] = kind + } + sort.Strings(schema.Names) + } + } + embeddedSchemas.Store(cacheKey, schema) + return schema +} + +// embeddedTemplatePropertyTypes lists the property types of a template's widget type — +// or of the item type of its object-list property listKey. +func embeddedTemplatePropertyTypes(widgetType map[string]any, listKey string) []map[string]any { + objType, _ := widgetType["ObjectType"].(map[string]any) + list := embeddedPropertyTypeMaps(objType) + if listKey == "" { + return list + } + for _, pt := range list { + if key, _ := pt["PropertyKey"].(string); key != listKey { + continue + } + vt, _ := pt["ValueType"].(map[string]any) + itemType, _ := vt["ObjectType"].(map[string]any) + return embeddedPropertyTypeMaps(itemType) + } + return nil +} + +// embeddedPropertyTypeMaps is an object type's PropertyTypes as maps; the typed-array +// marker that opens the array is not one. +func embeddedPropertyTypeMaps(objType map[string]any) []map[string]any { + var out []map[string]any + if objType == nil { + return nil + } + arr, _ := objType["PropertyTypes"].([]any) + for _, e := range arr { + if m, ok := e.(map[string]any); ok { + out = append(out, m) + } + } + return out +} diff --git a/mdl/executor/validate_widget_embedded_schema_test.go b/mdl/executor/validate_widget_embedded_schema_test.go new file mode 100644 index 0000000000..5cee163494 --- /dev/null +++ b/mdl/executor/validate_widget_embedded_schema_test.go @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 + +// `mxcli check` without a project must not call a Data grid 2 setting "not +// recognized and silently dropped" when the widget declares it. +// +// datagrid dgAttr (DataSource: database Shop.Ord, configurationStorageType: attribute, +// configurationAttribute: Config) +// +// `exec` stores both, and `check -p` accepts them. `check` alone warned +// MDL-WIDGET07 for each — and refused an Expression-typed one (rowClass) as +// MDL-WIDGET42, an ERROR — because DATAGRID has no embedded definition and the +// check fell back on the vocabulary of the built-in widgets. +// +// The keys here come from the Data grid 2 template mxcli embeds, not from a +// list typed for the test. +package executor + +import ( + "fmt" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/linter" +) + +// noProjectFindings is what `mxcli check script.mdl` (no -p) reports for the +// widget rules in question. +func noProjectFindings(t *testing.T, script string) []linter.Violation { + t.Helper() + var out []linter.Violation + for _, v := range ValidateProgram(parseMDL(t, script), "") { + if v.RuleID == "MDL-WIDGET07" || v.RuleID == "MDL-WIDGET42" { + out = append(out, v) + } + } + return out +} + +func describeFindings(vs []linter.Violation) string { + var lines []string + for _, v := range vs { + lines = append(lines, fmt.Sprintf("[%s %s] %s", v.RuleID, v.Severity, v.Message)) + } + return strings.Join(lines, "\n ") +} + +// The personalization settings the issue is about, in the three shapes a script +// uses them (exec stores every one). +func TestCheckWithoutProject_DataGridDeclaredKeysAreNotReportedAsDropped(t *testing.T) { + script := `create page Shop.Ord_Overview ( Title: 'Orders', Layout: Atlas_Core.Atlas_Default ) { + datagrid dg (DataSource: database Shop.Ord, PageSize: 25, Pagination: virtualScrolling, + itemSelection: Single, refreshInterval: 30, columnsHidable: false, showEmptyPlaceholder: custom) { + column (Attribute: Code, Caption: 'Code', Sortable: true) { textfilter tf } + } +}; +create page Shop.Ord_Local ( Title: 'Local', Layout: Atlas_Core.Atlas_Default ) { + datagrid dgLocal (DataSource: database Shop.Ord, configurationStorageType: localStorage, storeFiltersInPersonalization: true) { + column (Attribute: Code) { textfilter tf } + } +}; +create page Shop.Ord_Attr ( Title: 'Attr', Layout: Atlas_Core.Atlas_Default, Params: ( $Pref: Shop.GridPref ) ) { + dataview dvPref (DataSource: $Pref) { + datagrid dgAttr (DataSource: database Shop.Ord, configurationStorageType: attribute, + configurationAttribute: Config, storeFiltersInPersonalization: true) { + column (Attribute: Code) { textfilter tf2 } + } + } +};` + if got := noProjectFindings(t, script); len(got) != 0 { + t.Errorf("check without a project reported declared Data grid 2 keys:\n %s", describeFindings(got)) + } +} + +// Expression-typed keys, grid and column: an error before, because the check +// could not tell the key takes one. +func TestCheckWithoutProject_DataGridExpressionKeysTakeAnExpression(t *testing.T) { + script := `create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) { + datagrid dg (DataSource: database Shop.Ord, rowClass: if $currentObject/Open then 'open' else 'closed') { + column (Attribute: Code, columnClass: if $currentObject/Open then 'a' else 'b') { textfilter tf } + } +};` + if got := noProjectFindings(t, script); len(got) != 0 { + t.Errorf("check without a project refused an expression on an Expression-typed key:\n %s", describeFindings(got)) + } +} + +// The column settings that have no MDL keyword. +func TestCheckWithoutProject_DataGridColumnDeclaredKeysAreNotReportedAsDropped(t *testing.T) { + script := `create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) { + datagrid dg (DataSource: database Shop.Ord) { + column (Attribute: Code, ShowContentAs: customContent, ColumnWidth: autoFit, minWidth: manual, + minWidthLimit: 150, allowEventPropagation: false, exportValue: 'x') { + dynamictext t (Content: 'x') + } + } +};` + if got := noProjectFindings(t, script); len(got) != 0 { + t.Errorf("check without a project reported declared column keys:\n %s", describeFindings(got)) + } +} + +// A misspelled key is still reported — the point of the rule — and says what is +// true: it cannot be verified without a project, and it names the key it +// probably meant. +func TestCheckWithoutProject_DataGridMisspelledKeyStillWarns(t *testing.T) { + script := `create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) { + datagrid dg (DataSource: database Shop.Ord, configurationStorageTyp: localStorage) { + column (Attribute: Code, minWidthLimt: 150) { textfilter tf } + } +};` + got := noProjectFindings(t, script) + if len(got) != 2 { + t.Fatalf("want 2 findings (the grid key and the column key), got %d:\n %s", len(got), describeFindings(got)) + } + for _, v := range got { + if v.RuleID != "MDL-WIDGET07" || v.Severity != linter.SeverityWarning { + t.Errorf("want a MDL-WIDGET07 warning, got %s", describeFindings([]linter.Violation{v})) + } + } + grid, column := got[0].Message, got[1].Message + if !strings.Contains(grid, "`configurationStorageTyp`") || !strings.Contains(grid, "did you mean `configurationStorageType`") { + t.Errorf("grid finding does not name the key and its likely spelling:\n %s", grid) + } + if !strings.Contains(column, "`minWidthLimt`") || !strings.Contains(column, "did you mean `minWidthLimit`") { + t.Errorf("column finding does not name the key and its likely spelling:\n %s", column) + } + for _, msg := range []string{grid, column} { + if !strings.Contains(msg, "without a project") { + t.Errorf("finding does not say it cannot be verified without a project:\n %s", msg) + } + } +} + +// The embedded definition is a baseline, not the installed widget: a key it does +// not declare may be one a newer widget version added. The message must not +// claim the value is lost as a fact; it says the key cannot be verified. +func TestCheckWithoutProject_UndeclaredKeyIsNotClaimedToBeDropped(t *testing.T) { + got := noProjectFindings(t, `create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) { + datagrid dg (DataSource: database Shop.Ord, aSettingFromANewerWidget: true) +};`) + if len(got) != 1 { + t.Fatalf("want 1 finding, got %d:\n %s", len(got), describeFindings(got)) + } + msg := got[0].Message + if !strings.Contains(msg, "cannot be verified without a project") { + t.Errorf("message does not say the key cannot be verified:\n %s", msg) + } + if strings.Contains(msg, "is not recognized and will be silently dropped") { + t.Errorf("message states as fact that the value is dropped:\n %s", msg) + } +} + +// A built-in widget has no embedded template; its message is the one it always +// had, so the widgets the vocabulary was written for are judged as before. +func TestCheckWithoutProject_BuiltInWidgetUnknownKeyIsUnchanged(t *testing.T) { + got := noProjectFindings(t, `create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) { + textbox tb (Attribute: Code, columnsHidable: false) +};`) + if len(got) != 1 || !strings.Contains(got[0].Message, "is not recognized and will be silently dropped on write") { + t.Errorf("want the unchanged MDL-WIDGET07 message for a text box, got:\n %s", describeFindings(got)) + } +} + +// Derived from the schema, not typed: every scalar key the embedded Data grid 2 +// template declares, written as the value its kind takes, draws neither rule. +// A key added to the template is covered the day it is added. +func TestCheckWithoutProject_EveryDeclaredDataGridKeyIsAccepted(t *testing.T) { + schema := loadEmbeddedSchema(embeddedDataGridID, "") + if schema == nil || len(schema.Names) < 30 { + t.Fatalf("Data grid 2 schema = %+v, want the template's ~40 keys", schema) + } + values := map[string]string{ + "Boolean": "true", "Integer": "1", "Decimal": "1", "String": "'x'", "Enumeration": "'x'", + "TextTemplate": "'x'", "Expression": "if true then 'a' else 'b'", "Attribute": "Code", + } + var props []string + for _, key := range schema.Names { + kind := schema.types[strings.ToLower(key)] + v, ok := values[kind] + if !ok { + continue // datasource, selection, action, widgets, object: other rules and other syntax + } + props = append(props, key+": "+v) + } + if len(props) < 25 { + t.Fatalf("only %d scalar keys in the template", len(props)) + } + script := "create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) {\n datagrid dg (DataSource: database Shop.Ord, " + + strings.Join(props, ", ") + ")\n};" + if got := noProjectFindings(t, script); len(got) != 0 { + t.Errorf("a key the template declares was reported:\n %s", describeFindings(got)) + } +} + +// The column item type, the same way. +func TestCheckWithoutProject_EveryDeclaredDataGridColumnKeyIsAccepted(t *testing.T) { + schema := loadEmbeddedSchema(embeddedDataGridID, "columns") + if schema == nil || len(schema.Names) < 15 { + t.Fatalf("column schema = %+v, want the template's ~20 keys", schema) + } + values := map[string]string{ + "Boolean": "true", "Integer": "1", "Decimal": "1", "String": "'x'", "Enumeration": "'x'", + "TextTemplate": "'x'", "Expression": "if true then 'a' else 'b'", "Attribute": "Code", + } + var props []string + for _, key := range schema.Names { + if v, ok := values[schema.types[strings.ToLower(key)]]; ok { + props = append(props, key+": "+v) + } + } + script := "create page Shop.P ( Title: 'P', Layout: Atlas_Core.Atlas_Default ) {\n datagrid dg (DataSource: database Shop.Ord) {\n column (" + + strings.Join(props, ", ") + ")\n }\n};" + if got := noProjectFindings(t, script); len(got) != 0 { + t.Errorf("a column key the template declares was reported:\n %s", describeFindings(got)) + } +} + +const embeddedDataGridID = "com.mendix.widget.web.datagrid.Datagrid" diff --git a/mdl/executor/validate_widget_expression_value.go b/mdl/executor/validate_widget_expression_value.go index 9f6ab6f174..4ba898ed02 100644 --- a/mdl/executor/validate_widget_expression_value.go +++ b/mdl/executor/validate_widget_expression_value.go @@ -26,6 +26,16 @@ import ( // built-in widget or an item. func validateWidgetExpressionValues(w *ast.WidgetV3, def *WidgetDefinition, item *ObjectListMapping, isObjectListItem bool, locationPrefix string) []linter.Violation { + return validateWidgetExpressionValuesIn(w, def, item, isObjectListItem, nil, locationPrefix) +} + +// validateWidgetExpressionValuesIn is validateWidgetExpressionValues for a +// widget that may have an embedded schema (embeddedSchemaFor), which stands in +// for the definition a check without a project does not have: the kind it +// declares for a key decides, and a key it does not declare is left to +// MDL-WIDGET07 rather than called a plain-value property. +func validateWidgetExpressionValuesIn(w *ast.WidgetV3, def *WidgetDefinition, item *ObjectListMapping, + isObjectListItem bool, schema *embeddedWidgetSchema, locationPrefix string) []linter.Violation { if w == nil || len(w.Properties) == 0 { return nil } @@ -52,13 +62,16 @@ func validateWidgetExpressionValues(w *ast.WidgetV3, def *WidgetDefinition, item continue } op, known := expressionValueOperation(key, def, item) + if !known && def == nil && item == nil { + op, known = schema.operation(key) + } switch { case op == "expression": continue case op == "image" && isImageObjectVariable(text): // `Image: $currentObject` binds the object in context (#1476). continue - case isExpr && !known && (def != nil || item != nil): + case isExpr && !known && (def != nil || item != nil || schema != nil): // A key the definition does not map: MDL-WIDGET01 (unknown) or // MDL-WIDGET06 (known, unmapped) already speaks for it, and the // builder resolves its kind from the template. diff --git a/mdl/executor/validate_widgets.go b/mdl/executor/validate_widgets.go index e62f53dfc6..d15a9c7e4b 100644 --- a/mdl/executor/validate_widgets.go +++ b/mdl/executor/validate_widgets.go @@ -311,12 +311,20 @@ func validateWidgetTreeIn(widgets []*ast.WidgetV3, registry *WidgetRegistry, loc // so gets past every other rule. Needs the parent's definition, and stays // quiet without one for the same reason MDL-WIDGET26 does. out = append(out, validateUnroutedChildren(w, def, locationPrefix)...) + // A keyword widget the registry has no definition for (DATAGRID: its + // definition is generated from the project's package) is judged by the + // property list of the template mxcli embeds for it, not by the + // vocabulary of the built-in widgets. + var embedded *embeddedWidgetSchema + if def == nil && !isObjectListItem { + embedded = embeddedSchemaFor(w, parent) + } // An expression on a generic key whose schema kind takes none — which // used to be written as an empty value (MDL-WIDGET42). if mapping != nil { out = append(out, validateWidgetExpressionValues(w, nil, mapping, true, locationPrefix)...) } else { - out = append(out, validateWidgetExpressionValues(w, def, nil, isObjectListItem, locationPrefix)...) + out = append(out, validateWidgetExpressionValuesIn(w, def, nil, isObjectListItem, embedded, locationPrefix)...) } // A generic widget type that resolved to nothing is already reported as // MDL-WIDGET25 (the kind is wrong). Validating its properties on top of @@ -330,7 +338,7 @@ func validateWidgetTreeIn(widgets []*ast.WidgetV3, registry *WidgetRegistry, loc // no definition for the project's own widget — the built-in allow-list // would report every property it has. if def == nil && !isObjectListItem && !w.TypeIsGeneric && explicitWidgetID(w) == "" { - out = append(out, validateStaticWidgetUnknownProps(w, locationPrefix)...) + out = append(out, validateStaticWidgetUnknownPropsIn(w, locationPrefix, embedded, registryProjectPath(registry) != "")...) // #1465: the vocabulary above is a cross-widget union, so a key a // tab page cannot store (CaptionParams, Class, Style) passes it. out = append(out, validateTabPageProps(w, locationPrefix)...) @@ -1106,9 +1114,19 @@ func isKnownStaticWidgetProp(key string) bool { // could false-positive on a valid property. Pluggable widgets are validated // separately (MDL-WIDGET01) and must not reach here. func validateStaticWidgetUnknownProps(w *ast.WidgetV3, locationPrefix string) []linter.Violation { + return validateStaticWidgetUnknownPropsIn(w, locationPrefix, nil, false) +} + +// validateStaticWidgetUnknownPropsIn is validateStaticWidgetUnknownProps for a +// widget that may have an embedded schema (embeddedSchemaFor): Data grid 2, +// whose definition only a project can supply. A key that schema declares is +// known. A key it does not is reported as one that cannot be verified, not as +// one that is dropped — the schema is the baseline template, and a newer +// widget version can declare more. +func validateStaticWidgetUnknownPropsIn(w *ast.WidgetV3, locationPrefix string, schema *embeddedWidgetSchema, projectOpen bool) []linter.Violation { var out []linter.Violation for _, key := range sortedPropertyKeys(w) { - if isKnownStaticWidgetProp(key) { + if isKnownStaticWidgetProp(key) || schema.declares(key) { continue } // Dynamic-text format keys placed at the widget level are reported by @@ -1130,18 +1148,37 @@ func validateStaticWidgetUnknownProps(w *ast.WidgetV3, locationPrefix string) [] if isInputSubmitProp(w.Type, key) { continue } + candidates := staticWidgetKnownPropList + if schema != nil { + // The widget's own names first: they are what the key was meant to be. + candidates = append(append([]string(nil), schema.Names...), staticWidgetKnownPropList...) + } hint := "" - if suggestion := nearestKey(key, staticWidgetKnownPropList); suggestion != "" { + if suggestion := nearestKey(key, candidates); suggestion != "" { hint = fmt.Sprintf(" — did you mean `%s`?", suggestion) } - out = append(out, linter.Violation{ + message := fmt.Sprintf( + "%s: %s property `%s` is not recognized and will be silently dropped on write%s", + locationPrefix, widgetLabel(w.Name, w.Type), key, hint, + ) + if schema != nil { + // Not "will be dropped": the template is the oldest supported version, + // and the installed widget may declare more. + message = fmt.Sprintf( + "%s: %s property `%s` is not declared by the embedded %s definition (%s), so it cannot be verified without a project — "+ + "it is silently dropped on write unless the installed widget declares it%s", + locationPrefix, widgetLabel(w.Name, w.Type), key, schema.Title, schema.Version, hint, + ) + } + v := linter.Violation{ RuleID: "MDL-WIDGET07", Severity: linter.SeverityWarning, - Message: fmt.Sprintf( - "%s: %s property `%s` is not recognized and will be silently dropped on write%s", - locationPrefix, widgetLabel(w.Name, w.Type), key, hint, - ), - }) + Message: message, + } + if schema != nil && !projectOpen { + v.Suggestion = "Run check with -p to look the key up in the installed widget." + } + out = append(out, v) } return out }