Skip to content

Commit 5617d4c

Browse files
akoclaude
andcommitted
Align mxcli check and lint reporting with shared Violation format (issue #10)
Convert all mxcli check validators (ValidateMicroflow, ValidateEnumeration, ValidateEntity, ValidateOQLSyntax, ValidateOQLTypes) from returning []string to []linter.Violation with structured rule IDs (MDL prefix for source-level checks). Rename built-in lint rules from MDL001-MDL007 to MPR001-MPR007 since they operate on the project model. Add --format flag to mxcli check for JSON/SARIF output. Simplify LSP diagnostics to run validators directly instead of subprocess + regex parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a4c8280 commit 5617d4c

24 files changed

Lines changed: 400 additions & 199 deletions

.claude/commands/mendix/lint.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ mxcli lint -p app.mpr --exclude System --exclude Administration
2828

2929
| Rule | Category | Description |
3030
|------|----------|-------------|
31-
| MDL001 | naming | NamingConvention - PascalCase with 21 microflow prefixes (ACT_, SUB_, DS_, VAL_, SCH_, IVK_, BCO_, ACO_, etc.) |
32-
| MDL002 | quality | EmptyMicroflow - Microflows should have at least one activity |
33-
| MDL003 | design | DomainModelSize - Max persistent entities per domain model |
34-
| MDL004 | quality | ValidationFeedback - Validation feedback with empty message |
35-
| MDL005 | quality | ImageSource - IMAGE widgets with no source configured |
36-
| MDL006 | quality | EmptyContainer - Empty layout containers |
37-
| MDL007 | security | PageNavigationSecurity - Navigation pages need allowed roles (CE0557) |
31+
| MPR001 | naming | NamingConvention - PascalCase with 21 microflow prefixes (ACT_, SUB_, DS_, VAL_, SCH_, IVK_, BCO_, ACO_, etc.) |
32+
| MPR002 | quality | EmptyMicroflow - Microflows should have at least one activity |
33+
| MPR003 | design | DomainModelSize - Max persistent entities per domain model |
34+
| MPR004 | quality | ValidationFeedback - Validation feedback with empty message |
35+
| MPR005 | quality | ImageSource - IMAGE widgets with no source configured |
36+
| MPR006 | quality | EmptyContainer - Empty layout containers |
37+
| MPR007 | security | PageNavigationSecurity - Navigation pages need allowed roles (CE0557) |
3838
| SEC001 | security | NoEntityAccessRules - Persistent entities need access rules |
3939
| SEC002 | security | WeakPasswordPolicy - Password minimum length should be 8+ |
4040
| SEC003 | security | DemoUsersActive - Demo users should be off at Production security |
@@ -109,11 +109,11 @@ Place `.star` files in `.claude/lint-rules/` to add project-specific rules. They
109109
```
110110
Sales
111111
-----
112-
⚠ Entity name 'customer_info' should use PascalCase [MDL001]
112+
⚠ Entity name 'customer_info' should use PascalCase [MPR001]
113113
at Sales.customer_info
114114
→ CustomerInfo
115115
116-
⚠ Microflow 'test' has no activities [MDL002]
116+
⚠ Microflow 'test' has no activities [MPR002]
117117
at Sales.test
118118
→ Add activities or remove unused microflow
119119

.claude/skills/mendix/assess-quality.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ After reviewing automated results, assess the following areas manually. The guid
6565

6666
### A. Naming Conventions
6767

68-
**Enforced by rules:** MDL001, CONV001, CONV003, CONV004, CONV005
68+
**Enforced by rules:** MPR001, CONV001, CONV003, CONV004, CONV005
6969

7070
| Element | Convention | Example |
7171
|---------|-----------|---------|
@@ -119,7 +119,7 @@ After reviewing automated results, assess the following areas manually. The guid
119119

120120
### C. Security
121121

122-
**Enforced by rules:** SEC001-SEC009, CONV006, CONV007, CONV008, MDL007
122+
**Enforced by rules:** SEC001-SEC009, CONV006, CONV007, CONV008, MPR007
123123

124124
#### C.1 Entity Access
125125

@@ -137,7 +137,7 @@ After reviewing automated results, assess the following areas manually. The guid
137137
| Guideline | Rule | Priority |
138138
|-----------|------|----------|
139139
| 1:1 mapping between module roles and user roles | CONV008 | High |
140-
| Pages in navigation must have allowed roles | MDL007 | High |
140+
| Pages in navigation must have allowed roles | MPR007 | High |
141141
| No guest/anonymous access to sensitive data | SEC004 | Critical |
142142
| Strict security mode enabled | SEC005 | High |
143143
| No demo users in production | SEC003 | Critical |
@@ -165,15 +165,15 @@ The Dutch Institute for Vulnerability Disclosure (DIVD) found widespread authori
165165

166166
### D. Maintainability
167167

168-
**Enforced by rules:** QUAL001-QUAL004, CONV009, CONV012, CONV014, MDL002, MDL004, MDL006
168+
**Enforced by rules:** QUAL001-QUAL004, CONV009, CONV012, CONV014, MPR002, MPR004, MPR006
169169

170170
| Guideline | Rule/Check | Threshold |
171171
|-----------|-----------|-----------|
172172
| Microflow complexity (McCabe) | QUAL001 | Cyclomatic complexity <= 10 |
173173
| Microflow size | CONV009 | Max 15 activities |
174174
| Documentation on public microflows | QUAL002 | All ACT_, DS_, IVK_ should be documented |
175175
| No orphaned/unused elements | QUAL004 | Remove unused microflows, pages, entities |
176-
| No empty microflows | MDL002 | Every microflow should have at least one activity |
176+
| No empty microflows | MPR002 | Every microflow should have at least one activity |
177177
| Caption on exclusive splits | CONV012 | All decision points must have captions |
178178
| No "Continue" error handling | CONV014 | Never swallow errors silently |
179179
| ACT_ microflows: thin controllers | CONV010 | Only page activities + submicroflow calls |
@@ -211,14 +211,14 @@ The Dutch Institute for Vulnerability Disclosure (DIVD) found widespread authori
211211

212212
### F. Architecture
213213

214-
**Enforced by rules:** ARCH001-ARCH003, CONV010, MDL003
214+
**Enforced by rules:** ARCH001-ARCH003, CONV010, MPR003
215215

216216
| Guideline | Rule/Check | Details |
217217
|-----------|-----------|---------|
218218
| No cross-module direct data access | ARCH001 | Access data through microflows, not direct entity references |
219219
| Data changes through microflows only | ARCH002 | Don't change entities from pages directly |
220220
| Business key on entities | ARCH003 | Persistent entities should have a business key attribute |
221-
| Domain model size | MDL003 | Max 15 persistent entities per module |
221+
| Domain model size | MPR003 | Max 15 persistent entities per module |
222222
| Entity attribute count | DESIGN001 | Max 10 attributes per entity (consider splitting) |
223223

224224
**Manual checks:**
@@ -404,7 +404,7 @@ Overall Score: [X]/100 (from `mxcli report`)
404404
This skill incorporates guidelines from:
405405
- **Conventions.pdf** — Squad Apps internal best practices (14 categories, 80+ guidelines)
406406
- **CONV001-CONV017 lint rules** — Automated checks derived from Conventions.pdf
407-
- **MDL001-MDL007, SEC001-SEC009** — Built-in linter rules
407+
- **MPR001-MPR007, SEC001-SEC009** — Built-in linter rules
408408
- **ARCH, DESIGN, QUAL series** — Starlark architecture/quality rules
409409
- **Mendix Performance Best Practices** — Official Mendix documentation on performance optimization
410410
- **Mendix Security Best Practices** — Official Mendix documentation on security configuration

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ ModelSDKGo/
8989
│ ├── executor/ # Executes AST against modelsdk-go
9090
│ ├── catalog/ # SQLite-based catalog for querying project metadata
9191
│ ├── linter/ # Extensible linting framework
92-
│ │ └── rules/ # Built-in lint rules (MDL001, MDL002, etc.)
92+
│ │ └── rules/ # Built-in lint rules (MPR001, MPR002, etc.)
9393
│ └── repl/ # Interactive REPL interface
9494
9595
├── sql/ # External database connectivity (PostgreSQL, Oracle, SQL Server)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ mxcli lint -p app.mpr --list-rules
268268
mxcli lint -p app.mpr --exclude System --exclude Administration
269269
```
270270

271-
14 built-in Go rules (MDL001-MDL007, SEC001-SEC003, CONV011-CONV014) plus 27 bundled Starlark rules covering security (SEC004-SEC009), architecture (ARCH001-003), quality (QUAL001-004), design (DESIGN001), and Mendix best practice conventions (CONV001-CONV010, CONV015-CONV017). Custom `.star` rules in `.claude/lint-rules/` are loaded automatically.
271+
14 built-in Go rules (MPR001-MPR007, SEC001-SEC003, CONV011-CONV014) plus 27 bundled Starlark rules covering security (SEC004-SEC009), architecture (ARCH001-003), quality (QUAL001-004), design (DESIGN001), and Mendix best practice conventions (CONV001-CONV010, CONV015-CONV017). Custom `.star` rules in `.claude/lint-rules/` are loaded automatically.
272272

273273
### Best Practices Report
274274

cmd/mxcli/cmd_check.go

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/mendixlabs/mxcli/mdl/ast"
1111
"github.com/mendixlabs/mxcli/mdl/executor"
12+
"github.com/mendixlabs/mxcli/mdl/linter"
1213
"github.com/mendixlabs/mxcli/mdl/visitor"
1314
"github.com/spf13/cobra"
1415
)
@@ -26,18 +27,25 @@ that are created within the script itself. For example, if your script creates
2627
a module "MyModule" and then creates entities in it, no error will be reported
2728
for the module reference.
2829
30+
Output includes structured rule IDs (MDL prefix) for each validation issue.
31+
2932
Examples:
3033
# Check syntax only (no project needed)
3134
mxcli check script.mdl
3235
3336
# Check syntax and validate references against a project
3437
mxcli check script.mdl -p app.mpr --references
38+
39+
# Output as JSON or SARIF
40+
mxcli check script.mdl --format json
41+
mxcli check script.mdl --format sarif
3542
`,
3643
Args: cobra.ExactArgs(1),
3744
Run: func(cmd *cobra.Command, args []string) {
3845
filePath := args[0]
3946
projectPath, _ := cmd.Flags().GetString("project")
4047
checkRefs, _ := cmd.Flags().GetBool("references")
48+
format, _ := cmd.Flags().GetString("format")
4149

4250
// Read the file
4351
content, err := os.ReadFile(filePath)
@@ -67,54 +75,41 @@ Examples:
6775
fmt.Printf("✓ Syntax OK (%d statements)\n", len(prog.Statements))
6876

6977
// Validate statements (doesn't require project connection)
70-
var oqlErrors []string
71-
for i, stmt := range prog.Statements {
78+
var violations []linter.Violation
79+
for _, stmt := range prog.Statements {
7280
// Check enumeration values for reserved words
7381
if enumStmt, ok := stmt.(*ast.CreateEnumerationStmt); ok {
74-
if errs := executor.ValidateEnumeration(enumStmt); len(errs) > 0 {
75-
for _, e := range errs {
76-
oqlErrors = append(oqlErrors, fmt.Sprintf("statement %d (%s): %s", i+1, enumStmt.Name.String(), e))
77-
}
78-
}
82+
violations = append(violations, executor.ValidateEnumeration(enumStmt)...)
7983
}
8084
// Check entity attributes for reserved system names
8185
if entityStmt, ok := stmt.(*ast.CreateEntityStmt); ok {
82-
if errs := executor.ValidateEntity(entityStmt); len(errs) > 0 {
83-
for _, e := range errs {
84-
oqlErrors = append(oqlErrors, fmt.Sprintf("statement %d (%s): %s", i+1, entityStmt.Name.String(), e))
85-
}
86-
}
86+
violations = append(violations, executor.ValidateEntity(entityStmt)...)
8787
}
8888
// Check microflow body for common issues
8989
if mfStmt, ok := stmt.(*ast.CreateMicroflowStmt); ok {
90-
if warns := executor.ValidateMicroflow(mfStmt); len(warns) > 0 {
91-
for _, w := range warns {
92-
oqlErrors = append(oqlErrors, fmt.Sprintf("statement %d (%s): %s", i+1, mfStmt.Name.String(), w))
93-
}
94-
}
90+
violations = append(violations, executor.ValidateMicroflow(mfStmt)...)
9591
}
9692
// Check view entity OQL
9793
if viewStmt, ok := stmt.(*ast.CreateViewEntityStmt); ok {
9894
if viewStmt.Query.RawQuery != "" {
99-
if errs := executor.ValidateOQLSyntax(viewStmt.Query.RawQuery); len(errs) > 0 {
100-
for _, e := range errs {
101-
oqlErrors = append(oqlErrors, fmt.Sprintf("statement %d (%s): %s", i+1, viewStmt.Name.String(), e))
102-
}
103-
}
104-
if errs := executor.ValidateOQLTypes(viewStmt.Query.RawQuery, viewStmt.Attributes); len(errs) > 0 {
105-
for _, e := range errs {
106-
oqlErrors = append(oqlErrors, fmt.Sprintf("statement %d (%s): %s", i+1, viewStmt.Name.String(), e))
107-
}
108-
}
95+
violations = append(violations, executor.ValidateOQLSyntax(viewStmt.Query.RawQuery)...)
96+
violations = append(violations, executor.ValidateOQLTypes(viewStmt.Query.RawQuery, viewStmt.Attributes)...)
10997
}
11098
}
11199
}
112-
if len(oqlErrors) > 0 {
113-
fmt.Fprintf(os.Stderr, "\nValidation errors found:\n")
114-
for _, e := range oqlErrors {
115-
fmt.Fprintf(os.Stderr, " - %s\n", e)
100+
101+
if len(violations) > 0 {
102+
// Use structured output
103+
outputFormat := linter.OutputFormat(format)
104+
formatter := linter.GetFormatter(outputFormat, format == "" || format == "text")
105+
fmt.Fprintln(os.Stderr)
106+
formatter.Format(violations, os.Stderr)
107+
108+
// Exit with error if there are any error-severity violations
109+
summary := linter.Summarize(violations)
110+
if summary.Errors > 0 {
111+
os.Exit(1)
116112
}
117-
os.Exit(1)
118113
}
119114

120115
// If reference checking requested

cmd/mxcli/cmd_lint.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ var lintCmd = &cobra.Command{
2020
Long: `Run linting rules against a Mendix project to find potential issues.
2121
2222
Built-in rules check for:
23-
- Naming conventions (MDL001) - entities, microflows, pages, enumerations
24-
- Empty microflows (MDL002) - microflows with no activities
25-
- Domain model size (MDL003) - max persistent entities per domain model
26-
- Empty validation feedback (MDL004) - validation feedback with empty message
27-
- Unconfigured images (MDL005) - IMAGE widgets with no source configured
28-
- Empty containers (MDL006) - layout containers with no children
29-
- Navigation page security (MDL007) - pages in navigation need allowed roles
23+
- Naming conventions (MPR001) - entities, microflows, pages, enumerations
24+
- Empty microflows (MPR002) - microflows with no activities
25+
- Domain model size (MPR003) - max persistent entities per domain model
26+
- Empty validation feedback (MPR004) - validation feedback with empty message
27+
- Unconfigured images (MPR005) - IMAGE widgets with no source configured
28+
- Empty containers (MPR006) - layout containers with no children
29+
- Navigation page security (MPR007) - pages in navigation need allowed roles
3030
- Entity access rules (SEC001) - persistent entities need access rules
3131
- Password policy (SEC002) - password minimum length should be 8+
3232
- Demo users (SEC003) - demo users should be off at Production security
@@ -123,7 +123,7 @@ Examples:
123123
ctx := linter.NewLintContext(cat)
124124
ctx.SetExcludedModules(excludeModules)
125125

126-
// Set reader so rules that inspect raw BSON (MDL004, MDL005) work
126+
// Set reader so rules that inspect raw BSON (MPR004, MPR005) work
127127
if reader := exec.Reader(); reader != nil {
128128
ctx.SetReader(reader)
129129
}

cmd/mxcli/init.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -647,13 +647,13 @@ func generateClaudeMD(projectName, mprFile string) string {
647647
w("### Built-in Rules\n\n")
648648
w("| Rule | Category | Description |\n")
649649
w("|------|----------|-------------|\n")
650-
w("| MDL001 | quality | PascalCase naming conventions (entities, microflows, pages, enumerations) |\n")
651-
w("| MDL002 | quality | Empty microflows (no activities) |\n")
652-
w("| MDL003 | design | Domain model size (>15 persistent entities per module) |\n")
653-
w("| MDL004 | correctness | Empty validation feedback message (CE0091) |\n")
654-
w("| MDL005 | correctness | Unconfigured image widget source |\n")
655-
w("| MDL006 | correctness | Empty containers (runtime crash) |\n")
656-
w("| MDL007 | security | Navigation page without allowed role (CE0557) |\n")
650+
w("| MPR001 | quality | PascalCase naming conventions (entities, microflows, pages, enumerations) |\n")
651+
w("| MPR002 | quality | Empty microflows (no activities) |\n")
652+
w("| MPR003 | design | Domain model size (>15 persistent entities per module) |\n")
653+
w("| MPR004 | correctness | Empty validation feedback message (CE0091) |\n")
654+
w("| MPR005 | correctness | Unconfigured image widget source |\n")
655+
w("| MPR006 | correctness | Empty containers (runtime crash) |\n")
656+
w("| MPR007 | security | Navigation page without allowed role (CE0557) |\n")
657657
w("| SEC001 | security | Persistent entity without access rules |\n")
658658
w("| SEC002 | security | Weak password policy (minimum length < 8) |\n")
659659
w("| SEC003 | security | Demo users active at non-development security level |\n")

cmd/mxcli/lsp_diagnostics.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"strconv"
99
"strings"
1010

11+
"github.com/mendixlabs/mxcli/mdl/ast"
12+
"github.com/mendixlabs/mxcli/mdl/executor"
13+
"github.com/mendixlabs/mxcli/mdl/linter"
1114
"github.com/mendixlabs/mxcli/mdl/visitor"
1215
"go.lsp.dev/protocol"
1316
"go.lsp.dev/uri"
@@ -71,6 +74,10 @@ func (s *mdlServer) publishDiagnostics(ctx context.Context, docURI uri.URI, text
7174
}
7275

7376
diags := parseMDLDiagnostics(text)
77+
// If no parse errors, run semantic validation inline
78+
if len(diags) == 0 {
79+
diags = append(diags, runSemanticValidation(text)...)
80+
}
7481
if diags == nil {
7582
diags = []protocol.Diagnostic{} // send empty array to clear diagnostics
7683
}
@@ -258,3 +265,73 @@ func mapStatementLines(text string) []uint32 {
258265
}
259266
return stmtLines
260267
}
268+
269+
// runSemanticValidation runs the same validators as cmd_check.go directly on parsed text,
270+
// returning LSP diagnostics with structured rule IDs.
271+
func runSemanticValidation(text string) []protocol.Diagnostic {
272+
prog, errs := visitor.Build(text)
273+
if len(errs) > 0 || prog == nil {
274+
return nil
275+
}
276+
277+
stmtLines := mapStatementLines(text)
278+
279+
var diags []protocol.Diagnostic
280+
for i, stmt := range prog.Statements {
281+
var violations []linter.Violation
282+
if enumStmt, ok := stmt.(*ast.CreateEnumerationStmt); ok {
283+
violations = append(violations, executor.ValidateEnumeration(enumStmt)...)
284+
}
285+
if entityStmt, ok := stmt.(*ast.CreateEntityStmt); ok {
286+
violations = append(violations, executor.ValidateEntity(entityStmt)...)
287+
}
288+
if mfStmt, ok := stmt.(*ast.CreateMicroflowStmt); ok {
289+
violations = append(violations, executor.ValidateMicroflow(mfStmt)...)
290+
}
291+
if viewStmt, ok := stmt.(*ast.CreateViewEntityStmt); ok {
292+
if viewStmt.Query.RawQuery != "" {
293+
violations = append(violations, executor.ValidateOQLSyntax(viewStmt.Query.RawQuery)...)
294+
violations = append(violations, executor.ValidateOQLTypes(viewStmt.Query.RawQuery, viewStmt.Attributes)...)
295+
}
296+
}
297+
298+
lineNum := uint32(0)
299+
if i < len(stmtLines) {
300+
lineNum = stmtLines[i]
301+
}
302+
303+
for _, v := range violations {
304+
msg := v.Message
305+
if v.Suggestion != "" {
306+
msg += " → " + v.Suggestion
307+
}
308+
diags = append(diags, protocol.Diagnostic{
309+
Range: protocol.Range{
310+
Start: protocol.Position{Line: lineNum, Character: 0},
311+
End: protocol.Position{Line: lineNum, Character: 0},
312+
},
313+
Severity: violationToLSPSeverity(v.Severity),
314+
Source: "mdl-check",
315+
Code: v.RuleID,
316+
Message: msg,
317+
})
318+
}
319+
}
320+
return diags
321+
}
322+
323+
// violationToLSPSeverity maps linter.Severity to protocol.DiagnosticSeverity.
324+
func violationToLSPSeverity(s linter.Severity) protocol.DiagnosticSeverity {
325+
switch s {
326+
case linter.SeverityError:
327+
return protocol.DiagnosticSeverityError
328+
case linter.SeverityWarning:
329+
return protocol.DiagnosticSeverityWarning
330+
case linter.SeverityInfo:
331+
return protocol.DiagnosticSeverityInformation
332+
case linter.SeverityHint:
333+
return protocol.DiagnosticSeverityHint
334+
default:
335+
return protocol.DiagnosticSeverityWarning
336+
}
337+
}

cmd/mxcli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ func init() {
191191

192192
// Check command flags
193193
checkCmd.Flags().BoolP("references", "r", false, "Validate references against the project")
194+
checkCmd.Flags().String("format", "text", "Output format: text, json, sarif")
194195

195196
// Diff command flags
196197
diffCmd.Flags().StringP("format", "f", "unified", "Output format: unified, side, struct")

0 commit comments

Comments
 (0)