diff --git a/.claude/skills/fix-issue.md b/.claude/skills/fix-issue.md index 0d105afd9..5f58754cc 100644 --- a/.claude/skills/fix-issue.md +++ b/.claude/skills/fix-issue.md @@ -171,6 +171,7 @@ to the symptom table below, so the next similar issue costs fewer reads. | `create or modify entity M.E ( )` on an entity that already has more attributes **silently drops** every attribute not re-listed (36→2 attrs seen in practice), then widgets/microflows still bound to them fail the build with CE1613 — and the "already exists" error that leads users here recommends the destructive `create or modify` for a partial edit | `create or modify` rebuilds the entity from the statement alone and REPLACEs the stored one, so any omitted attribute is deleted with no warning; the `NewAlreadyExistsMsg` hint pointed at `create or modify` without distinguishing "replace whole" from "add one" | `mdl/executor/cmd_entities.go` (`droppedEntityMembers`, the warn block before `UpdateEntity`, and the `execCreateEntity` already-exists message) | Warn-only (non-blocking, the user asked to modify): before `UpdateEntity`, diff existing vs replacement members (`droppedEntityMembers` — named attrs case-insensitive + the four audit flags) and print what's dropped + point at `alter entity … add attribute` for incremental edits. Fix the already-exists message to recommend `alter entity` for a member change and reserve `create or modify` for a full replace. Bug-test `mdl-examples/bug-tests/f24-create-or-modify-dataloss.mdl`. Findings #24 | | A microflow expression calls a function that doesn't exist (e.g. `randomInt(9)` — in some docs but not a Mendix built-in): parses, passes `mxcli check`, then fails the build with CE0117 "Error(s) in expression". Also: a Decimal-returning function (`random()`, `secondsBetween`, the duration `*Between` family) assigned to an Integer/Long variable fails CE0117, but `mxcli check` only caught bare `div` | (1) The func checker only validated arity for *known* functions; an unknown call name was ignored. (2) `checkNumericAssignment` used `SourceIsArithmeticDecimal`, which fires only on arithmetic roots, so a Decimal *function* result slipped through. Also the `*Between` duration funcs were mistyped Integer in `funcTable` | `mdl/exprcheck/unknown_funcs.go` (`UnknownFunctionCalls`, `SourceRejectedForIntegerTarget`, `nearestFunc`) + `mdl/exprcheck/func_checker.go` (`funcTable` between-date return kinds) + `mdl/executor/validate_microflow.go` (`checkExprFunctions` → MDL044; `checkNumericAssignment` now uses `SourceRejectedForIntegerTarget`) | Walk each expression for `CallExpr` names not in `funcTable` (which lists *every* built-in — a bare `name(...)` is always a built-in call) → **MDL044** with a Levenshtein/prefix "did you mean" hint. Extend the Decimal-into-Integer check to Decimal-returning non-rounding functions → **MDL041**. Correct `secondsBetween`/`minutesBetween`/`hoursBetween`/`daysBetween`/`weeksBetween` to Decimal (calendar variants stay Integer, millisecondsBetween stays Long). When adding a new Mendix built-in, add it to `funcTable` or MDL044 will false-positive. Bug-tests `f1-unknown-expression-function.fail.mdl`, `f2-decimal-func-into-integer.fail.mdl`. Findings #1, #2 | | `index name on (cols)` inside `create entity` (or `alter entity add index name on (cols)`) fails with `extraneous input 'on' expecting '('` — the SQL-like form docs/users expect. The bare `index name (cols)` worked | `indexDefinition` had no `ON` token: `IDENTIFIER? LPAREN indexAttributeList RPAREN` | `mdl/grammar/domains/MDLDomainModel.g4` (`indexDefinition`) | Make `ON` optional: `IDENTIFIER? ON? LPAREN indexAttributeList RPAREN`, regen grammar. `buildIndex` reads columns from `IndexAttributeList` only, so ON can't be mistaken for a column and no visitor change is needed. Covers both CREATE (entityOption) and ALTER ADD INDEX (shared rule). Bug-test `mdl-examples/bug-tests/f4-entity-index-on.mdl`. Findings #4 | +| `retrieve … where [Seq = $Game/MoveSeq + 1]` fails with a bare `mismatched input '+' expecting ']'` — no hint that Mendix XPath can't compute values (this is a Mendix limitation, not an mxcli bug) | Mendix XPath constraints take a literal/token/variable/path on the value side, never an arithmetic expression; the parse error named the token but not the cause | `mdl/visitor/visitor.go` (`enhanceErrorMessage`, `looksLikeXPathArithmetic`/`xpathArithmeticRe`) | Do NOT add grammar support (mxbuild would still reject the XPath). Add an error hint keyed on `mismatched input '<+|*|div|mod>' expecting ']'` (`expecting ']'` only occurs inside a `[…]` constraint) explaining the limitation and the workaround: compute into a variable first, then compare. Also documented in `xpath-constraints.md`. Bug-test `mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl`. Findings #8 | --- diff --git a/.claude/skills/mendix/xpath-constraints.md b/.claude/skills/mendix/xpath-constraints.md index 83c3bc1bd..9b1ebe8b4 100644 --- a/.claude/skills/mendix/xpath-constraints.md +++ b/.claude/skills/mendix/xpath-constraints.md @@ -21,6 +21,17 @@ This skill provides reference for writing XPath constraint expressions in MDL RE | Empty check | `= empty`, `!= empty` | `= empty` | | Token quoting | `'[%CurrentUser%]'` (quoted) | `[%CurrentUser%]` (unquoted) | | Nested filter | `Assoc/entity[pred]` | Not applicable | +| Arithmetic on value | **not supported** — pre-compute into a variable | `+`, `-`, `*`, `div`, `mod` | + +> **XPath constraints cannot compute values.** `where [Seq = $Game/MoveSeq + 1]` +> is a parse error (Mendix XPath has no arithmetic on the value side). Compute the +> value first, then compare against the variable: +> ```mdl +> $Next = $Game/MoveSeq + 1; +> retrieve $M from Mod.Move where [Seq = $Next] limit 1; +> ``` +> `mxcli check` explains this and shows the workaround when it sees `+`/`*`/`div`/ +> `mod` inside a constraint. ## Syntax Reference diff --git a/mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl b/mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl new file mode 100644 index 000000000..5a9a6c3b1 --- /dev/null +++ b/mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl @@ -0,0 +1,9 @@ +-- Findings #8: Mendix XPath constraints cannot compute values, so an arithmetic +-- operator on the value side (`+ 1`) is a parse error. check now explains the +-- limitation and the workaround (compute into a variable first, then compare). +-- Negative test: `mxcli check` MUST fail here. +create microflow Bug8.MF_NextMove (Game: Bug8.Game) returns Bug8.Move +begin + retrieve $M from Bug8.Move where [Seq = $Game/MoveSeq + 1] limit 1; + return $M; +end; diff --git a/mdl/visitor/visitor.go b/mdl/visitor/visitor.go index 836688f6a..c0d15fb90 100644 --- a/mdl/visitor/visitor.go +++ b/mdl/visitor/visitor.go @@ -112,6 +112,17 @@ func enhanceErrorMessage(msg, offendingLine string) string { " create enumeration Mod.E (Value1 = 'Caption 1'); (wrong — causes parse error)", msg) } + // Arithmetic inside an XPath constraint. Mendix XPath can't compute values + // (no +, -, *, div, mod on the value side) — compute into a variable first, + // then compare against it. (sudoku findings #8) + if looksLikeXPathArithmetic(msg) { + return fmt.Sprintf("%s\n\n Mendix XPath constraints cannot compute values (no +, -, *, div, mod).\n"+ + " Compute the value into a variable first, then compare against it:\n"+ + " $Next = $Game/MoveSeq + 1;\n"+ + " retrieve $M from Mod.Move where [Seq = $Next] limit 1; (correct)\n"+ + " retrieve $M from Mod.Move where [Seq = $Game/MoveSeq + 1] limit 1; (wrong — XPath can't compute)", msg) + } + // Check for a misplaced EXTENDS / GENERALIZATION clause. It must precede the // attribute list — `create entity Mod.Child extends Mod.Parent ( ... )` — but // users often append it after the closing parenthesis, where ANTLR reports a @@ -182,6 +193,19 @@ func enhanceErrorMessage(msg, offendingLine string) string { // stay false-positive-free (it won't fire on `not(...)`, `is not null`, etc.). var bareNotRe = regexp.MustCompile(`(?i)\bnot\s+\$`) +// xpathArithmeticRe matches an arithmetic operator that ANTLR rejected inside a +// bracketed XPath constraint. `expecting ']'` only occurs inside `[…]`, so a +// stray arithmetic operator there is a compute-in-constraint attempt. The op set +// is quoted-literal to avoid matching, say, a `-` inside a normal expression. +var xpathArithmeticRe = regexp.MustCompile(`mismatched input '(\+|\*|div|mod)' expecting '\]'`) + +// looksLikeXPathArithmetic detects an arithmetic operator used on the value side +// of an XPath constraint (e.g. `[Seq = $Game/MoveSeq + 1]`). Mendix XPath cannot +// compute values, so this must be pre-computed into a variable. (findings #8) +func looksLikeXPathArithmetic(msg string) bool { + return xpathArithmeticRe.MatchString(msg) +} + // looksLikeMisplacedExtends detects ANTLR errors caused by an EXTENDS / // GENERALIZATION clause placed after the entity's attribute parentheses instead // of before them. The offending token surfaces as extraneous/mismatched input. diff --git a/mdl/visitor/visitor_test.go b/mdl/visitor/visitor_test.go index ab37c5224..104dce328 100644 --- a/mdl/visitor/visitor_test.go +++ b/mdl/visitor/visitor_test.go @@ -2432,3 +2432,21 @@ func TestAlterEntityAddIndexOnKeyword(t *testing.T) { t.Errorf("expected column [Col], got %+v", alt.Index.Columns) } } + +// TestEnhanceErrorMessage_XPathArithmetic verifies the XPath-arithmetic hint +// fires for an arithmetic operator inside a bracketed constraint (findings #8) +// and not for arithmetic in a normal expression. +func TestEnhanceErrorMessage_XPathArithmetic(t *testing.T) { + xhint := "cannot compute values" + // Inside a constraint: `expecting ']'` + arithmetic op → hint. + for _, op := range []string{"+", "*", "div", "mod"} { + msg := "mismatched input '" + op + "' expecting ']'" + if got := enhanceErrorMessage(msg, ""); !strings.Contains(got, xhint) { + t.Errorf("op %q: expected XPath-arithmetic hint, got:\n%s", op, got) + } + } + // A normal expression arithmetic error (not `expecting ']'`) must not fire. + if got := enhanceErrorMessage("mismatched input '+' expecting ';'", ""); strings.Contains(got, xhint) { + t.Errorf("did not expect XPath hint for a non-constraint arithmetic error:\n%s", got) + } +}