Skip to content

Commit cdb0b73

Browse files
akoclaude
andcommitted
feat: add DROP FOLDER command for removing empty folders
Adds DROP FOLDER 'path' IN Module syntax across the full MDL pipeline (grammar, AST, visitor, executor, writer). The command refuses to delete non-empty folders, returning an error with the child unit count. Closes #46 (partial — adds missing folder cleanup capability) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 163b37c commit cdb0b73

File tree

11 files changed

+6573
-6367
lines changed

11 files changed

+6573
-6367
lines changed

mdl/ast/ast_enumeration.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ type DropModuleStmt struct {
2727

2828
func (s *DropModuleStmt) isStatement() {}
2929

30+
// DropFolderStmt represents: DROP FOLDER 'path' IN Module
31+
type DropFolderStmt struct {
32+
FolderPath string // Folder path (e.g., "Resources/Images")
33+
Module string // Module name
34+
}
35+
36+
func (s *DropFolderStmt) isStatement() {}
37+
3038
// CreateEnumerationStmt represents: CREATE ENUMERATION Module.Name (values) COMMENT '...'
3139
type CreateEnumerationStmt struct {
3240
Name QualifiedName

mdl/executor/cmd_folders.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
// Package executor - DROP FOLDER command
4+
package executor
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
"github.com/mendixlabs/mxcli/mdl/ast"
11+
"github.com/mendixlabs/mxcli/model"
12+
)
13+
14+
// execDropFolder handles DROP FOLDER 'path' IN Module statements.
15+
// The folder must be empty (no child documents or sub-folders).
16+
func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error {
17+
if e.writer == nil {
18+
return fmt.Errorf("not connected to a project")
19+
}
20+
21+
// Find the module
22+
module, err := e.findModule(s.Module)
23+
if err != nil {
24+
return fmt.Errorf("module not found: %s", s.Module)
25+
}
26+
27+
// List all folders
28+
folders, err := e.reader.ListFolders()
29+
if err != nil {
30+
return fmt.Errorf("failed to list folders: %w", err)
31+
}
32+
33+
// Walk the folder path to find the target folder
34+
parts := strings.Split(s.FolderPath, "/")
35+
currentContainerID := module.ID
36+
37+
var targetFolderID model.ID
38+
for i, part := range parts {
39+
if part == "" {
40+
continue
41+
}
42+
43+
var found bool
44+
for _, f := range folders {
45+
if f.ContainerID == currentContainerID && f.Name == part {
46+
currentContainerID = f.ID
47+
if i == len(parts)-1 {
48+
targetFolderID = f.ID
49+
}
50+
found = true
51+
break
52+
}
53+
}
54+
55+
if !found {
56+
return fmt.Errorf("folder not found: '%s' in %s", s.FolderPath, s.Module)
57+
}
58+
}
59+
60+
if targetFolderID == "" {
61+
return fmt.Errorf("folder not found: '%s' in %s", s.FolderPath, s.Module)
62+
}
63+
64+
// Delete the folder (writer checks if empty)
65+
if err := e.writer.DeleteFolder(targetFolderID); err != nil {
66+
return fmt.Errorf("failed to delete folder '%s': %w", s.FolderPath, err)
67+
}
68+
69+
e.invalidateHierarchy()
70+
fmt.Fprintf(e.output, "Dropped folder: '%s' in %s\n", s.FolderPath, s.Module)
71+
return nil
72+
}

mdl/executor/executor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ func (e *Executor) executeInner(stmt ast.Statement) error {
227227
return e.execDropJavaAction(s)
228228
case *ast.CreateJavaActionStmt:
229229
return e.execCreateJavaAction(s)
230+
case *ast.DropFolderStmt:
231+
return e.execDropFolder(s)
230232
case *ast.MoveStmt:
231233
return e.execMove(s)
232234

mdl/grammar/MDLParser.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ dropStatement
251251
| DROP WORKFLOW qualifiedName
252252
| DROP IMAGE COLLECTION qualifiedName
253253
| DROP REST CLIENT qualifiedName
254+
| DROP FOLDER STRING_LITERAL IN (qualifiedName | IDENTIFIER)
254255
;
255256

256257
renameStatement

mdl/grammar/parser/MDLParser.interp

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

mdl/grammar/parser/mdl_lexer.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdl_parser.go

Lines changed: 6449 additions & 6363 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdlparser_base_listener.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdlparser_listener.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/visitor/visitor_entity.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,19 @@ func (b *Builder) ExitDropStatement(ctx *parser.DropStatementContext) {
665665
b.statements = append(b.statements, &ast.DropRestClientStmt{
666666
Name: buildQualifiedName(names[0]),
667667
})
668+
} else if ctx.FOLDER() != nil {
669+
folderPath := unquoteString(ctx.STRING_LITERAL().GetText())
670+
// Module can be a qualifiedName or IDENTIFIER
671+
var moduleName string
672+
if len(names) > 0 {
673+
moduleName = getQualifiedNameText(names[0])
674+
} else if ctx.IDENTIFIER() != nil {
675+
moduleName = ctx.IDENTIFIER().GetText()
676+
}
677+
b.statements = append(b.statements, &ast.DropFolderStmt{
678+
FolderPath: folderPath,
679+
Module: moduleName,
680+
})
668681
}
669682
}
670683

0 commit comments

Comments
 (0)