Skip to content

Commit 2e8ad25

Browse files
akoclaude
andcommitted
fix(cli): escape -p path so Windows backslashes survive CONNECT LOCAL (#644)
The CLI fabricated `CONNECT LOCAL '<path>'` MDL from the -p flag value and re-parsed it, so the lexer's unquoteString interpreted `\t`/`\n`/`\r` and dropped escape backslashes -- e.g. `C:\temp\App.mpr` became `C:<TAB>emp\App.mpr`, failing with "unable to open database file (14)". The positional .mdl argument is a raw path, so it was unaffected; forward slashes worked as a workaround. Add visitor.QuoteString (the exact inverse of unquoteString: `\`->`\\`, `'`->`''`) and wrap the project path at every `CONNECT LOCAL '%s'` interpolation site (check, describe, report, diff, query, lint, rename, exec, main.go). A raw filesystem path must never be interpolated into MDL source unescaped. Round-trip tests in visitor_connection_test.go cover \t/\n/\r paths, UNC paths, apostrophes, and unix paths (unchanged). Symptom-table row added to fix-issue.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2fc8038 commit 2e8ad25

12 files changed

Lines changed: 71 additions & 12 deletions

File tree

.claude/skills/fix-issue.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
5252
| `ALTER STYLING ON PAGE/SNIPPET ... SET ...` fails with `unsupported container type: PAGE` (and `DESCRIBE STYLING` silently shows "No widgets found"); even past that, design-property writes never reached builder-created pages | Two layered bugs: (1) the visitor emits uppercase `ContainerType` `"PAGE"`/`"SNIPPET"` but `execAlterStyling`/`execDescribeStyling` compared lowercase, so it fell through to the unsupported-container error; (2) ALTER STYLING used the reflection walker `walkPageWidgets` (legacy `ListPages`/`UpdatePage`), which can't locate widgets in MDL-builder pages and violates the mutator-pattern rule | `mdl/executor/cmd_styling.go` (`execAlterStyling`, `execDescribeStyling`) + `mdl/backend/mpr/page_mutator.go` | Normalise container type with `strings.ToLower`. Route ALTER STYLING through `ctx.Backend.OpenPageForMutation(unitID)` like ALTER PAGE; check `mutator.FindWidget`. Add `SetDesignProperty`/`RemoveDesignProperty`/`ClearDesignProperties` to the `PageMutator` interface, writing the widget's `Appearance.DesignProperties` BSON array (`Forms$DesignPropertyValue` → `Toggle`/`Option`/`Custom` value), preserving an existing custom kind on option updates. When an ALTER uses a container-type discriminator, mirror the casing fix already done for ALTER PAGE (#402). Issue #631 |
5353
| `declare $x list of T = empty;` (or any list-typed `declare`) passes `mxcli check` but Studio Pro rejects with CE0053 ("type not allowed") + CE0038 ("value required") | `declare` maps to a Create Variable activity, which Mendix forbids from producing a list — but the validator only flagged an empty list *used as a loop source* (MDL002), never the declaration itself | `mdl/executor/validate_microflow.go``walkBody` `*ast.DeclareStmt` case | Emit `MDL040` (SeverityError) for any `stmt.Type.Kind == ast.TypeListOf`, regardless of initializer. Lists must come from a microflow parameter, a `retrieve`, or `$x = create list of T;`. Also fix the synced skills that present declare-list as valid (`write-microflows.md`, `cheatsheet-variables.md`, `check-syntax.md`, `patterns-*`). Issue #607 |
5454
| `DESCRIBE PAGE` emits a widget whose name is a reserved keyword (`container List`, `dynamictext Template`) and the output fails `mxcli check` (`mismatched input 'List' expecting IDENTIFIER`); quoting it by hand (`container "List"`) also failed | The widget-name position `widgetV3` accepted only a bare `IDENTIFIER`, so neither the keyword nor a `QUOTED_IDENTIFIER` parsed; and DESCRIBE emitted the name unquoted. Note this is *not* the general reserved-word problem — qualified names / params / attributes already go through `identifierOrKeyword` (a 552-entry keyword allowlist). The gap is the ~103 strict-`IDENTIFIER` positions, of which widget names are the highest-impact | grammar `mdl/grammar/domains/MDLPage.g4` (`widgetV3`) + `mdl/visitor/visitor_page_v3.go` (`buildWidgetV3`) + `mdl/executor/cmd_pages_describe_output.go` | Widen the name position to `(IDENTIFIER \| QUOTED_IDENTIFIER)`, `make grammar`, and `unquoteIdentifier` it in the visitor. On output, quote via `executor.mdlIdent` (which lexes the name and quotes only when it does *not* lex as a bare `IDENTIFIER` — no hardcoded keyword list, no false positives like a widget named "Dot"). For other strict-`IDENTIFIER` name positions apply the same pattern: `pageParameter`/`snippetParameter` bare names were widened the same way (issue #114 — note the SHOW_PAGE colon arg it reported already worked via `identifierOrKeyword`, and DESCRIBE emits params `$`-prefixed so output was already safe; the fix only closed the bare-declaration gap). Still strict and unfixed: security `read(...)`/`write(...)` member lists. Issues #619, #114 |
55+
| `mxcli <cmd> -p "C:\\path\\App.mpr"` (Windows backslash path) fails with `unable to open database file (14)` and the echoed path is mangled (`C:\temp``C:<TAB>emp`, backslashes dropped); forward slashes (`-p C:/path/App.mpr`) work | The CLI fabricates `CONNECT LOCAL '<path>'` MDL from the flag value and re-parses it, so the lexer's `unquoteString` interprets `\t`/`\n`/`\r` and escape backslashes in the path. The positional `.mdl` arg is a raw path, so it's unaffected | every `cmd/mxcli/*.go` `fmt.Sprintf("CONNECT LOCAL '%s'...", projectPath)` site + `mdl/visitor/visitor_helpers.go` | Never interpolate a raw filesystem path into MDL source. Wrap it in `visitor.QuoteString(projectPath)` (escapes `\``\\`, `'``''` — the exact inverse of `unquoteString`) at every CONNECT-string site. Round-trip test in `mdl/visitor/visitor_connection_test.go`. Issue #644 |
5556

5657
---
5758

cmd/mxcli/cmd_check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Examples:
166166
defer exec.Close()
167167

168168
// Connect to project
169-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
169+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
170170
for _, stmt := range connectProg.Statements {
171171
if err := exec.Execute(stmt); err != nil {
172172
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)

cmd/mxcli/cmd_describe.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Example:
170170
exec.SetQuiet(true) // suppress status messages for programmatic output
171171

172172
// Connect
173-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
173+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
174174
for _, stmt := range connectProg.Statements {
175175
if err := exec.Execute(stmt); err != nil {
176176
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

cmd/mxcli/cmd_diff.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Examples:
7272
defer logger.Close()
7373
defer exec.Close()
7474

75-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
75+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
7676
for _, stmt := range connectProg.Statements {
7777
if err := exec.Execute(stmt); err != nil {
7878
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)
@@ -145,7 +145,7 @@ Examples:
145145
defer logger.Close()
146146
defer exec.Close()
147147

148-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
148+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
149149
for _, stmt := range connectProg.Statements {
150150
if err := exec.Execute(stmt); err != nil {
151151
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)

cmd/mxcli/cmd_exec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Example:
3939

4040
// Auto-connect if project specified
4141
if projectPath != "" {
42-
connectCmd := fmt.Sprintf("CONNECT LOCAL '%s';", projectPath)
42+
connectCmd := fmt.Sprintf("CONNECT LOCAL '%s';", visitor.QuoteString(projectPath))
4343
prog, _ := visitor.Build(connectCmd)
4444
for _, stmt := range prog.Statements {
4545
if err := exec.Execute(stmt); err != nil {

cmd/mxcli/cmd_lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Examples:
9595
defer logger.Close()
9696
defer exec.Close()
9797

98-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
98+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
9999
for _, stmt := range connectProg.Statements {
100100
if err := exec.Execute(stmt); err != nil {
101101
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)

cmd/mxcli/cmd_query.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ Examples:
242242
defer exec.Close()
243243

244244
// Connect to project
245-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
245+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
246246
for _, stmt := range connectProg.Statements {
247247
if err := exec.Execute(stmt); err != nil {
248248
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)

cmd/mxcli/cmd_rename.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Example:
8383
defer exec.Close()
8484

8585
// Connect
86-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s' FOR WRITING", projectPath))
86+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s' FOR WRITING", visitor.QuoteString(projectPath)))
8787
for _, stmt := range connectProg.Statements {
8888
if err := exec.Execute(stmt); err != nil {
8989
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

cmd/mxcli/cmd_report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Examples:
5252
defer logger.Close()
5353
defer exec.Close()
5454

55-
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", projectPath))
55+
connectProg, _ := visitor.Build(fmt.Sprintf("CONNECT LOCAL '%s'", visitor.QuoteString(projectPath)))
5656
for _, stmt := range connectProg.Statements {
5757
if err := exec.Execute(stmt); err != nil {
5858
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)

cmd/mxcli/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ Examples:
129129

130130
// Auto-connect if project specified
131131
if projectPath != "" {
132-
commands = fmt.Sprintf("CONNECT LOCAL '%s'; %s", projectPath, commands)
132+
commands = fmt.Sprintf("CONNECT LOCAL '%s'; %s", visitor.QuoteString(projectPath), commands)
133133
}
134134

135135
prog, errs := visitor.Build(commands)
@@ -158,7 +158,7 @@ Examples:
158158

159159
// Auto-connect if project specified
160160
if projectPath != "" {
161-
if err := r.ExecuteString(fmt.Sprintf("CONNECT LOCAL '%s';", projectPath)); err != nil {
161+
if err := r.ExecuteString(fmt.Sprintf("CONNECT LOCAL '%s';", visitor.QuoteString(projectPath))); err != nil {
162162
fmt.Fprintf(os.Stderr, "Error connecting: %v\n", err)
163163
}
164164
}
@@ -216,7 +216,7 @@ func executeMDL(projectPath, mdlCmd string) {
216216
defer logger.Close()
217217
defer exec.Close()
218218

219-
fullCmd := fmt.Sprintf("CONNECT LOCAL '%s'; %s", projectPath, mdlCmd)
219+
fullCmd := fmt.Sprintf("CONNECT LOCAL '%s'; %s", visitor.QuoteString(projectPath), mdlCmd)
220220
prog, errs := visitor.Build(fullCmd)
221221
if len(errs) > 0 {
222222
for _, err := range errs {

0 commit comments

Comments
 (0)