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
7 changes: 7 additions & 0 deletions cmd/mxcli/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ run.
Output includes structured rule IDs (MDL prefix for reference and script rules,
E0xx for expression type rules) for each validation issue.

A deprecated MDL spelling — an alias left over from consolidating MDL onto one
canonical form, such as "create or replace" for "create or modify" or "show" for
"list" — is reported as an MDL-DEPRnnn warning naming the canonical form. Pass
--deprecations=error to fail the run on one instead, e.g. in CI over docs.

Use --post-migration to scan an existing project (independent of the script)
for legacy native widgets that have pluggable replacements — Studio Pro does
not auto-migrate these on a Mendix major-version upgrade.
Expand Down Expand Up @@ -99,6 +104,7 @@ Examples:
checkRefs, _ := cmd.Flags().GetBool("references")
checkRefs = checkRefs || projectPath != ""
postMigration, _ := cmd.Flags().GetBool("post-migration")
depPolicy := deprecationPolicy(cmd)
format := resolveFormat(cmd, "text")
isStructured := format != "" && format != "text"

Expand Down Expand Up @@ -194,6 +200,7 @@ Examples:
// refuses exactly what `mxcli check` reports. Adding a check there gives
// both commands it at once.
violations := append(testProblems, executor.ValidateProgram(prog, projectPath)...)
violations = executor.ApplyDeprecationPolicy(violations, depPolicy)

if isStructured {
// Always emit structured output (even when clean)
Expand Down
6 changes: 5 additions & 1 deletion cmd/mxcli/cmd_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ applies statements one at a time and cannot roll back, so running a script with
a known error leaves the model partly updated. Warnings are printed and do not
stop the run. Use --no-check to apply a script anyway.

A deprecated MDL spelling (MDL-DEPRnnn, e.g. "create or replace" for "create or
modify") is a warning; --deprecations=error makes it an error.

By default execution stops at the first error. With --continue-on-error, every
statement is attempted; each failure is reported (prefixed with its statement
number) and execution continues, exiting non-zero if any statement failed. This
Expand All @@ -50,6 +53,7 @@ Example:
projectPath, _ := cmd.Flags().GetString("project")
continueOnError, _ := cmd.Flags().GetBool("continue-on-error")
skipCheck, _ := cmd.Flags().GetBool("no-check")
depPolicy := deprecationPolicy(cmd)

// Read the script (a path, or "-" for stdin)
content, err := readMDLSource(filePath)
Expand Down Expand Up @@ -103,7 +107,7 @@ Example:
// exec is not transactional, so "run it and see" means a half-applied
// model. Warnings are printed and do not stop the run.
if !skipCheck {
violations := executor.ValidateProgram(prog, projectPath)
violations := executor.ApplyDeprecationPolicy(executor.ValidateProgram(prog, projectPath), depPolicy)
if len(violations) > 0 {
formatter := linter.GetFormatter(linter.OutputFormatText, true)
formatter.Format(violations, os.Stderr)
Expand Down
36 changes: 36 additions & 0 deletions cmd/mxcli/deprecations_flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0

package main

import (
"fmt"
"os"

"github.com/mendixlabs/mxcli/mdl/deprecation"
"github.com/spf13/cobra"
)

// The --deprecations flag decides what `check` and `exec` do with a deprecated
// MDL spelling (an MDL-DEPRnnn warning, registry in mdl/deprecation). The
// default warns; `error` fails the run, so CI over docs, skills and examples
// can hold them to the canonical form.
const deprecationsFlagUsage = "What to do with a deprecated MDL spelling (MDL-DEPRnnn): " +
"warn, or error to fail the run (for CI over docs, skills and examples)"

func init() {
checkCmd.Flags().String("deprecations", "warn", deprecationsFlagUsage)
execCmd.Flags().String("deprecations", "warn", deprecationsFlagUsage)
}

// deprecationPolicy reads --deprecations, exiting with a usage error on a value
// it does not know: silently treating a typo as `warn` would let a CI gate pass
// that was meant to fail.
func deprecationPolicy(cmd *cobra.Command) deprecation.Policy {
raw, _ := cmd.Flags().GetString("deprecations")
policy, err := deprecation.ParsePolicy(raw)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
}
return policy
}
15 changes: 15 additions & 0 deletions docs-site/src/appendixes/error-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ iterator name exists, so mxbuild rejects this with CE0109 "Undefined variable

The rule keys on **scope, not on the name**. `$item` is perfectly valid in a predicate when it is the enclosing loop's iterator, which is how the O(N) lookup idiom is written — inside `loop $item in $L`, `find($Others, Key = $item/Key)` navigates the loop's variable and is not flagged.

### MDL-DEPRnnn: Deprecated spelling

```
line 1: `create or replace …` (enumeration) is deprecated; write `create or modify …`
— same meaning. Refused from `mdl 2`. [MDL-DEPR001]
```

**Cause:** The script uses an alias that MDL is consolidating away (ADR-0010). The alias means exactly what the canonical form means, so the statement runs unchanged. The warning names the canonical form, and the language version whose header (`mdl <n>;`) will refuse the alias (ADR-0011).

**Solution:** Write the canonical form the warning names. The rewrite is a mechanical keyword swap, and the suggestion line says which one.

`check` and `exec` report these as **warnings**. To fail the run on one, for example in CI over documentation and examples, pass `--deprecations=error`.

The registry of deprecated spellings is `mdl/deprecation/deprecation.go`. It holds the code, old form, canonical form, rewrite and removal version of each entry. A spelling is only registered where it means exactly the same as its canonical form. `create or replace view entity`, for example, drops and recreates the view entity, so it is not reported. Likewise `show` is reported only where its canonical form is `list` (plurals and relationship queries); `show entity X` or `show version` is not, because it becomes `describe` or a REPL command, not `list`.

## mxcli Parser Errors


Expand Down
18 changes: 18 additions & 0 deletions mdl/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ type Program struct {
// next to knownActivityAnnotations, instead of being spread across the seven
// visitor sites that read them.
DocumentAnnotations []DocumentAnnotation
// Deprecations records every use of a deprecated spelling registered in
// mdl/deprecation, in source order. Both spellings build the same
// statements, so this is the only trace of which one the source used; it
// drives the MDL-DEPRnnn warnings and nothing else may branch on it.
Deprecations []DeprecatedSpelling

// LanguageVersion is the MDL language version the script is written in:
// the number in its `mdl <n>;` header, or mdl 0 when it has none
Expand All @@ -71,6 +76,19 @@ type Program struct {
LanguageNotes []LanguageNote
}

// DeprecatedSpelling is one use of a deprecated spelling in the source.
type DeprecatedSpelling struct {
// Code is the registry code, MDL-DEPRnnn.
Code string
// Line and Column locate the deprecated token (1-based line, 0-based
// column, as ANTLR reports them).
Line int
Column int
// Subject says what the spelling was used on, in MDL's own words ("entity",
// "microflow", …); empty when there is nothing more specific to say.
Subject string
}

// LanguageNote is one construct whose meaning depends on the language version,
// kept at the meaning of the version the script is written in.
type LanguageNote struct {
Expand Down
154 changes: 154 additions & 0 deletions mdl/deprecation/deprecation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: Apache-2.0

// Package deprecation is the single registry of deprecated MDL spellings
// (ADR-0011, decision 1).
//
// A deprecated spelling is a respelling: it means exactly what its canonical
// form means, so it keeps parsing, `check` and `exec` warn on it, and
// `mxcli fmt --upgrade` can rewrite it mechanically. A spelling that means
// something different from the proposed canonical form is NOT an alias there,
// and is not reported — rewriting it would silently change a script.
//
// # How an alias is marked
//
// Every grammar token or alternative that exists only as an alias carries a
// block comment naming its registry code, next to the alias itself:
//
// CREATE (OR (MODIFY | REPLACE /* @alias MDL-DEPR001 */))?
//
// ANTLR ignores the comment. TestGrammarAliasesAreRegistered (mdl/grammar)
// reads the .g4 sources and fails when a marker names a code with no entry
// here, or an entry is named by no marker. The marker is what makes a missing
// entry detectable: an alias is marked in the edit that adds it, and the marker
// cannot be satisfied without an entry.
//
// # How a use is detected
//
// Both spellings build the same AST, so the parse tree is the only place the
// source spelling is still visible. The visitor records every use of a
// registered spelling on ast.Program.Deprecations, and the executor's
// ValidateDeprecations turns the records into warnings (errors under
// --deprecations=error). This generalises MDL065, where the AST node itself
// carries the spelling flags.
package deprecation

import (
"fmt"
"strings"
)

// Entry is one deprecated spelling.
type Entry struct {
// Code is the stable warning code, MDL-DEPRnnn. Never reused.
Code string
// Old is the deprecated form, as a reader would write it.
Old string
// Canonical is the form that replaces it (ADR-0010).
Canonical string
// Rewrite is the mechanical rewrite `fmt --upgrade` applies.
Rewrite Rewrite
// RemovedIn is the MDL language version (the `mdl <n>;` header) under which
// the old form is refused. Under earlier versions it warns (ADR-0011).
RemovedIn int
// Note is shown with the warning: scope limits, or where the canonical form
// is expected to move next.
Note string
// Example is a complete statement in the old form. A test parses it and
// requires exactly this code to be recorded.
Example string
// CanonicalExample is Example after Rewrite. A test requires it to record no
// deprecation and to build the same AST as Example — the proof that the
// rewrite does not change meaning.
CanonicalExample string
}

// Rewrite replaces one keyword token of the deprecated form with another. It is
// the only shape the seeded entries need; a richer one is added with the first
// entry that needs it.
type Rewrite struct {
// Token is the keyword to replace, lower-case.
Token string
// Replacement is the keyword written in its place, lower-case.
Replacement string
}

// Codes of the registered entries, for the visitor to record.
const (
CreateOrReplace = "MDL-DEPR001"
Show = "MDL-DEPR002"
)

// entries is the registry. Append only: a code is never reused or renumbered,
// because scripts, CI allowlists and docs refer to it.
var entries = []Entry{
{
Code: CreateOrReplace,
Old: "create or replace …",
Canonical: "create or modify …",
Rewrite: Rewrite{Token: "replace", Replacement: "modify"},
RemovedIn: 2,
Note: "Not reported where `or replace` means something else today: " +
"`create or replace view entity` (drops and recreates), " +
"`create or replace translations` (replaces the whole set), and " +
"`create or replace user role` / `demo user` (the `replace` is ignored).",
Example: "create or replace enumeration M.Color (Red 'Red');",
CanonicalExample: "create or modify enumeration M.Color (Red 'Red');",
},
{
Code: Show,
Old: "show …",
Canonical: "list …",
Rewrite: Rewrite{Token: "show", Replacement: "list"},
RemovedIn: 2,
Note: "Reported only for plurals and relationship queries, whose canonical " +
"form is `list`. Forms that name a single thing (`show entity X`, " +
"`show navigation`, `show project security`, …) become `describe`, and " +
"session state (`show version`, `show status`) a REPL command; they are " +
"not reported until those forms exist.",
Example: "show entities in M;",
CanonicalExample: "list entities in M;",
},
}

// All returns every registered entry, in code order.
func All() []Entry {
out := make([]Entry, len(entries))
copy(out, entries)
return out
}

// Lookup returns the entry for code.
func Lookup(code string) (Entry, bool) {
for _, e := range entries {
if e.Code == code {
return e, true
}
}
return Entry{}, false
}

// IsDeprecationCode reports whether a violation rule ID is a registry code.
func IsDeprecationCode(ruleID string) bool {
return strings.HasPrefix(ruleID, "MDL-DEPR")
}

// Policy is what `check` and `exec` do with a deprecated spelling.
type Policy int

const (
// Warn reports it and carries on. The default.
Warn Policy = iota
// Error fails the run, for CI over docs, skills and examples.
Error
)

// ParsePolicy reads the --deprecations flag value.
func ParsePolicy(s string) (Policy, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "warn":
return Warn, nil
case "error":
return Error, nil
}
return Warn, fmt.Errorf("invalid --deprecations value %q: want warn or error", s)
}
62 changes: 62 additions & 0 deletions mdl/deprecation/deprecation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0

package deprecation

import (
"regexp"
"testing"
)

// The registry is append-only and its codes are what scripts, CI allowlists
// and docs key on, so each entry must be complete and its code well-formed and
// unique.
func TestRegistryEntriesAreWellFormed(t *testing.T) {
code := regexp.MustCompile(`^MDL-DEPR\d{3}$`)
seen := map[string]bool{}
for _, e := range All() {
if !code.MatchString(e.Code) {
t.Errorf("code %q is not MDL-DEPRnnn", e.Code)
}
if seen[e.Code] {
t.Errorf("code %s registered twice", e.Code)
}
seen[e.Code] = true
if !IsDeprecationCode(e.Code) {
t.Errorf("IsDeprecationCode(%q) = false", e.Code)
}
if got, ok := Lookup(e.Code); !ok || got.Code != e.Code {
t.Errorf("Lookup(%q) = %v, %v", e.Code, got.Code, ok)
}
if e.Old == "" || e.Canonical == "" || e.Rewrite.Token == "" || e.Rewrite.Replacement == "" ||
e.Example == "" || e.CanonicalExample == "" {
t.Errorf("%s is incomplete: %+v", e.Code, e)
}
// ADR-0011: an alias warns under the version that deprecates it (1)
// and is refused from a later one.
if e.RemovedIn < 2 {
t.Errorf("%s RemovedIn = %d, want >= 2", e.Code, e.RemovedIn)
}
}
if _, ok := Lookup("MDL-DEPR999"); ok {
t.Error("Lookup found an unregistered code")
}
if IsDeprecationCode("MDL065") {
t.Error("MDL065 treated as a registry code")
}
}

// A typo in --deprecations must be an error, not a silent `warn`: that would
// let a CI gate pass that was meant to fail.
func TestParsePolicy(t *testing.T) {
for in, want := range map[string]Policy{"": Warn, "warn": Warn, "error": Error, " ERROR ": Error} {
got, err := ParsePolicy(in)
if err != nil || got != want {
t.Errorf("ParsePolicy(%q) = %v, %v; want %v", in, got, err, want)
}
}
for _, in := range []string{"errors", "fail", "warning", "bogus"} {
if _, err := ParsePolicy(in); err == nil {
t.Errorf("ParsePolicy(%q) accepted an unknown value", in)
}
}
}
Loading
Loading