From 680c782be9b84290e64c4bd0dca559124eb8b192 Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Thu, 23 Apr 2026 17:14:29 +0200 Subject: [PATCH 1/2] refactor: introduce LintReader interface, remove sdk/mpr from linter and executor --- cmd/mxcli/cmd_lint.go | 7 +--- cmd/mxcli/cmd_report.go | 7 +--- mdl/executor/cmd_lint.go | 3 +- mdl/executor/exec_context.go | 17 ---------- mdl/executor/executor.go | 29 ++++++++--------- mdl/executor/roundtrip_doctype_test.go | 6 ++-- mdl/linter/context.go | 34 +++++++++++++------- mdl/linter/rules/page_navigation_security.go | 3 +- mdl/types/infrastructure.go | 8 +++++ 9 files changed, 51 insertions(+), 63 deletions(-) diff --git a/cmd/mxcli/cmd_lint.go b/cmd/mxcli/cmd_lint.go index 6fb3eb36..9893c128 100644 --- a/cmd/mxcli/cmd_lint.go +++ b/cmd/mxcli/cmd_lint.go @@ -120,14 +120,9 @@ Examples: } // Create lint context - ctx := linter.NewLintContext(cat) + ctx := linter.NewLintContext(cat, exec.Backend()) ctx.SetExcludedModules(excludeModules) - // Set reader so rules that inspect raw BSON (MPR004, MPR005) work - if reader := exec.Reader(); reader != nil { - ctx.SetReader(reader) - } - // Create linter and register rules lint := linter.New(ctx) lint.AddRule(rules.NewNamingConventionRule()) diff --git a/cmd/mxcli/cmd_report.go b/cmd/mxcli/cmd_report.go index 05d9f195..afa0e63b 100644 --- a/cmd/mxcli/cmd_report.go +++ b/cmd/mxcli/cmd_report.go @@ -78,14 +78,9 @@ Examples: } // Create lint context - ctx := linter.NewLintContext(cat) + ctx := linter.NewLintContext(cat, exec.Backend()) ctx.SetExcludedModules(excludeModules) - // Set reader so rules that inspect raw BSON work - if reader := exec.Reader(); reader != nil { - ctx.SetReader(reader) - } - // Create linter and register all rules lint := linter.New(ctx) diff --git a/mdl/executor/cmd_lint.go b/mdl/executor/cmd_lint.go index 15bd2ba3..783935be 100644 --- a/mdl/executor/cmd_lint.go +++ b/mdl/executor/cmd_lint.go @@ -33,8 +33,7 @@ func execLint(ctx *ExecContext, s *ast.LintStmt) error { } // Create lint context - lintCtx := linter.NewLintContext(ctx.Catalog) - lintCtx.SetReader(ctx.Reader()) + lintCtx := linter.NewLintContext(ctx.Catalog, ctx.Backend) // Load configuration projectDir := filepath.Dir(ctx.MprPath) diff --git a/mdl/executor/exec_context.go b/mdl/executor/exec_context.go index 92ef05ee..e6b29245 100644 --- a/mdl/executor/exec_context.go +++ b/mdl/executor/exec_context.go @@ -12,7 +12,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/catalog" "github.com/mendixlabs/mxcli/mdl/diaglog" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" sqllib "github.com/mendixlabs/mxcli/sql" ) @@ -215,19 +214,3 @@ func (ctx *ExecContext) ensureSqlMgr() *sqllib.Manager { } return ctx.SqlMgr } - -// Reader returns the MPR reader, or nil if not connected. -// Deprecated: External callers should migrate to using Backend methods directly. -// TODO(shared-types): remove once all callers use Backend — target: v0.next milestone. -func (ctx *ExecContext) Reader() *mpr.Reader { - if ctx.Backend == nil { - return nil - } - type readerProvider interface { - MprReader() *mpr.Reader - } - if rp, ok := ctx.Backend.(readerProvider); ok { - return rp.MprReader() - } - return nil -} diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index b4446129..e75e30be 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -18,7 +18,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" - "github.com/mendixlabs/mxcli/sdk/mpr" sqllib "github.com/mendixlabs/mxcli/sql" ) @@ -309,25 +308,23 @@ func (e *Executor) Catalog() *catalog.Catalog { return c } -// Reader returns the MPR reader, or nil if not connected. -// Deprecated: External callers should migrate to using Backend methods directly. -// TODO(shared-types): remove once all callers use Backend — target: v0.next milestone. -func (e *Executor) Reader() *mpr.Reader { - if e.backend == nil { +// IsConnected returns true if connected to a project. +func (e *Executor) IsConnected() bool { + return e.backend != nil && e.backend.IsConnected() +} + +// Backend returns the full backend, or nil if not connected. +func (e *Executor) Backend() backend.FullBackend { + if e.backend == nil || !e.backend.IsConnected() { return nil } - type readerProvider interface { - MprReader() *mpr.Reader - } - if rp, ok := e.backend.(readerProvider); ok { - return rp.MprReader() - } - return nil + return e.backend } -// IsConnected returns true if connected to a project. -func (e *Executor) IsConnected() bool { - return e.backend != nil && e.backend.IsConnected() +// Reader returns the backend for backward compatibility with callers +// that used the former sdk/mpr.Reader accessor. Prefer Backend() in new code. +func (e *Executor) Reader() backend.FullBackend { + return e.Backend() } // Close closes the connection to the project and all SQL connections. diff --git a/mdl/executor/roundtrip_doctype_test.go b/mdl/executor/roundtrip_doctype_test.go index d3bf3a7e..f6f3993e 100644 --- a/mdl/executor/roundtrip_doctype_test.go +++ b/mdl/executor/roundtrip_doctype_test.go @@ -15,8 +15,8 @@ import ( "testing" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/mdl/visitor" - "github.com/mendixlabs/mxcli/sdk/mpr/version" ) // scriptModuleDeps maps script filenames to marketplace module MPKs they require. @@ -228,7 +228,7 @@ type versionConstraint struct { } // matches returns true if the project version satisfies this constraint. -func (vc *versionConstraint) matches(pv *version.ProjectVersion) bool { +func (vc *versionConstraint) matches(pv *types.ProjectVersion) bool { if vc.minMajor >= 0 { if !pv.IsAtLeast(vc.minMajor, vc.minMinor) { return false @@ -321,7 +321,7 @@ func parseMajorMinor(s string) (int, int, bool) { // Sections are delimited by "-- @version: " directives. // A directive applies to all following lines until the next directive or end of file. // "-- @version: any" resets to unconditional inclusion. -func filterByVersion(content string, pv *version.ProjectVersion) (string, int) { +func filterByVersion(content string, pv *types.ProjectVersion) (string, int) { var result strings.Builder var currentConstraint *versionConstraint // nil = no constraint (always include) skippedLines := 0 diff --git a/mdl/linter/context.go b/mdl/linter/context.go index 310e74e6..fd5ebcf8 100644 --- a/mdl/linter/context.go +++ b/mdl/linter/context.go @@ -7,33 +7,45 @@ import ( "iter" "github.com/mendixlabs/mxcli/mdl/catalog" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/sdk/pages" + "github.com/mendixlabs/mxcli/sdk/security" ) +// LintReader provides read access to MPR document data needed by lint rules. +// Implemented by MprBackend (and any backend satisfying these signatures). +type LintReader interface { + GetMicroflow(id model.ID) (*microflows.Microflow, error) + GetProjectSecurity() (*security.ProjectSecurity, error) + GetNavigation() (*types.NavigationDocument, error) + ListPages() ([]*pages.Page, error) + ListModules() ([]*model.Module, error) + ListFolders() ([]*types.FolderInfo, error) + GetRawUnit(id model.ID) (map[string]any, error) +} + // LintContext wraps a catalog and provides rule-friendly APIs. type LintContext struct { catalog *catalog.Catalog db catalog.CatalogDB excluded map[string]bool - reader *mpr.Reader -} - -// SetReader sets the MPR reader for rules that need to inspect full document data. -func (ctx *LintContext) SetReader(reader *mpr.Reader) { - ctx.reader = reader + reader LintReader } -// Reader returns the MPR reader, or nil if not set. -func (ctx *LintContext) Reader() *mpr.Reader { +// Reader returns the LintReader, or nil if not set. +func (ctx *LintContext) Reader() LintReader { return ctx.reader } -// NewLintContext creates a new LintContext from a catalog. -func NewLintContext(cat *catalog.Catalog) *LintContext { +// NewLintContext creates a new LintContext from a catalog and an optional reader. +func NewLintContext(cat *catalog.Catalog, reader LintReader) *LintContext { return &LintContext{ catalog: cat, db: cat.CatalogDB(), excluded: make(map[string]bool), + reader: reader, } } diff --git a/mdl/linter/rules/page_navigation_security.go b/mdl/linter/rules/page_navigation_security.go index 33a73f56..c3e454b8 100644 --- a/mdl/linter/rules/page_navigation_security.go +++ b/mdl/linter/rules/page_navigation_security.go @@ -8,7 +8,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/linter" "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // PageNavigationSecurityRule checks that pages used in navigation have at least @@ -134,7 +133,7 @@ func collectMenuPages(items []*types.NavMenuItem, profileName string, navPages m } // buildPageRoleCountMap builds a map of qualified page name → number of allowed roles. -func buildPageRoleCountMap(reader *mpr.Reader) map[string]int { +func buildPageRoleCountMap(reader linter.LintReader) map[string]int { result := make(map[string]int) pages, err := reader.ListPages() diff --git a/mdl/types/infrastructure.go b/mdl/types/infrastructure.go index 75b5bf6b..5e1e4735 100644 --- a/mdl/types/infrastructure.go +++ b/mdl/types/infrastructure.go @@ -23,6 +23,14 @@ type ProjectVersion struct { PatchVersion int } +// IsAtLeast returns true if this version is at least the specified major.minor version. +func (v *ProjectVersion) IsAtLeast(major, minor int) bool { + if v.MajorVersion > major { + return true + } + return v.MajorVersion == major && v.MinorVersion >= minor +} + // FolderInfo is a lightweight folder descriptor. type FolderInfo struct { ID model.ID From fac023a090d888f51a80351500b04d5393725946 Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Fri, 24 Apr 2026 15:12:03 +0200 Subject: [PATCH 2/2] address review: dedup ProjectVersion, drop Reader() shim, add LintReader assertion --- mdl/backend/mpr/backend.go | 4 +- mdl/backend/mpr/convert.go | 16 ----- mdl/backend/mpr/convert_roundtrip_test.go | 21 ------ mdl/executor/executor.go | 6 -- mdl/executor/roundtrip_doctype_test.go | 2 +- mdl/executor/roundtrip_helpers_test.go | 2 +- mdl/linter/context.go | 1 + mdl/types/infrastructure.go | 24 +++++++ sdk/mpr/version/version.go | 81 ++++------------------- 9 files changed, 44 insertions(+), 113 deletions(-) diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index c4eb74f5..11f04313 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -7,6 +7,7 @@ package mprbackend import ( "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/linter" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" @@ -20,6 +21,7 @@ import ( ) var _ backend.FullBackend = (*MprBackend)(nil) +var _ linter.LintReader = (*MprBackend)(nil) // MprBackend implements backend.FullBackend by delegating to mpr.Reader // and mpr.Writer. @@ -87,7 +89,7 @@ func (b *MprBackend) MprReader() *mpr.Reader { return b.reader } func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } func (b *MprBackend) ProjectVersion() *types.ProjectVersion { - return convertProjectVersion(b.reader.ProjectVersion()) + return b.reader.ProjectVersion() } func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } diff --git a/mdl/backend/mpr/convert.go b/mdl/backend/mpr/convert.go index 472b213e..fa2e0648 100644 --- a/mdl/backend/mpr/convert.go +++ b/mdl/backend/mpr/convert.go @@ -5,7 +5,6 @@ package mprbackend import ( "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr" - "github.com/mendixlabs/mxcli/sdk/mpr/version" ) // --------------------------------------------------------------------------- @@ -14,21 +13,6 @@ import ( func convertMPRVersion(v mpr.MPRVersion) types.MPRVersion { return types.MPRVersion(v) } -func convertProjectVersion(v *version.ProjectVersion) *types.ProjectVersion { - if v == nil { - return nil - } - return &types.ProjectVersion{ - ProductVersion: v.ProductVersion, - BuildVersion: v.BuildVersion, - FormatVersion: v.FormatVersion, - SchemaHash: v.SchemaHash, - MajorVersion: v.MajorVersion, - MinorVersion: v.MinorVersion, - PatchVersion: v.PatchVersion, - } -} - func convertFolderInfoSlice(in []*mpr.FolderInfo, err error) ([]*types.FolderInfo, error) { if err != nil || in == nil { return nil, err diff --git a/mdl/backend/mpr/convert_roundtrip_test.go b/mdl/backend/mpr/convert_roundtrip_test.go index 157572f0..1292f6ee 100644 --- a/mdl/backend/mpr/convert_roundtrip_test.go +++ b/mdl/backend/mpr/convert_roundtrip_test.go @@ -13,7 +13,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" - "github.com/mendixlabs/mxcli/sdk/mpr/version" "go.mongodb.org/mongo-driver/bson" ) @@ -23,26 +22,6 @@ var errTest = errors.New("test error") // Forward conversions: sdk/mpr -> mdl/types // --------------------------------------------------------------------------- -func TestConvertProjectVersion(t *testing.T) { - in := &version.ProjectVersion{ - ProductVersion: "10.18.0", BuildVersion: "1234", - FormatVersion: 42, SchemaHash: "abc123", - MajorVersion: 10, MinorVersion: 18, PatchVersion: 0, - } - out := convertProjectVersion(in) - if out.ProductVersion != "10.18.0" || out.BuildVersion != "1234" || - out.FormatVersion != 42 || out.SchemaHash != "abc123" || - out.MajorVersion != 10 || out.MinorVersion != 18 || out.PatchVersion != 0 { - t.Errorf("field mismatch: %+v", out) - } -} - -func TestConvertProjectVersion_Nil(t *testing.T) { - if convertProjectVersion(nil) != nil { - t.Error("expected nil for nil input") - } -} - func TestConvertFolderInfoSlice(t *testing.T) { in := []*mpr.FolderInfo{ {ID: model.ID("f1"), ContainerID: model.ID("c1"), Name: "Folder1"}, diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index e75e30be..6f60f23a 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -321,12 +321,6 @@ func (e *Executor) Backend() backend.FullBackend { return e.backend } -// Reader returns the backend for backward compatibility with callers -// that used the former sdk/mpr.Reader accessor. Prefer Backend() in new code. -func (e *Executor) Reader() backend.FullBackend { - return e.Backend() -} - // Close closes the connection to the project and all SQL connections. func (e *Executor) Close() error { var closeErr error diff --git a/mdl/executor/roundtrip_doctype_test.go b/mdl/executor/roundtrip_doctype_test.go index f6f3993e..3a8c678f 100644 --- a/mdl/executor/roundtrip_doctype_test.go +++ b/mdl/executor/roundtrip_doctype_test.go @@ -142,7 +142,7 @@ func TestMxCheck_DoctypeScripts(t *testing.T) { } // Filter out version-gated sections that don't match this project's Mendix version - pv := env.executor.Reader().ProjectVersion() + pv := env.executor.Backend().ProjectVersion() filtered, skippedLines := filterByVersion(string(content), pv) if skippedLines > 0 { t.Logf("Mendix %s: skipped %d version-gated lines", pv.ProductVersion, skippedLines) diff --git a/mdl/executor/roundtrip_helpers_test.go b/mdl/executor/roundtrip_helpers_test.go index f7b1b25e..43bd6fc8 100644 --- a/mdl/executor/roundtrip_helpers_test.go +++ b/mdl/executor/roundtrip_helpers_test.go @@ -596,7 +596,7 @@ func (e *testEnv) assertContains(createMDL string, expectedProps []string, opts // requireMinVersion skips the test if the project's Mendix version is below the given minimum. func (e *testEnv) requireMinVersion(t *testing.T, major, minor int) { t.Helper() - pv := e.executor.Reader().ProjectVersion() + pv := e.executor.Backend().ProjectVersion() if !pv.IsAtLeast(major, minor) { t.Skipf("Requires Mendix %d.%d+ (project is %s)", major, minor, pv.ProductVersion) } diff --git a/mdl/linter/context.go b/mdl/linter/context.go index fd5ebcf8..f3113bac 100644 --- a/mdl/linter/context.go +++ b/mdl/linter/context.go @@ -40,6 +40,7 @@ func (ctx *LintContext) Reader() LintReader { } // NewLintContext creates a new LintContext from a catalog and an optional reader. +// reader may be nil; rules that require backend access must check Reader() != nil. func NewLintContext(cat *catalog.Catalog, reader LintReader) *LintContext { return &LintContext{ catalog: cat, diff --git a/mdl/types/infrastructure.go b/mdl/types/infrastructure.go index 5e1e4735..6c11f348 100644 --- a/mdl/types/infrastructure.go +++ b/mdl/types/infrastructure.go @@ -31,6 +31,30 @@ func (v *ProjectVersion) IsAtLeast(major, minor int) bool { return v.MajorVersion == major && v.MinorVersion >= minor } +// IsAtLeastFull returns true if this version is at least the specified major.minor.patch version. +func (v *ProjectVersion) IsAtLeastFull(major, minor, patch int) bool { + if v.MajorVersion > major { + return true + } + if v.MajorVersion == major && v.MinorVersion > minor { + return true + } + if v.MajorVersion == major && v.MinorVersion == minor && v.PatchVersion >= patch { + return true + } + return false +} + +// String returns the product version string. +func (v *ProjectVersion) String() string { + return v.ProductVersion +} + +// IsMPRv2 returns true if the project uses MPR v2 format (mprcontents folder). +func (v *ProjectVersion) IsMPRv2() bool { + return v.FormatVersion >= 2 +} + // FolderInfo is a lightweight folder descriptor. type FolderInfo struct { ID model.ID diff --git a/sdk/mpr/version/version.go b/sdk/mpr/version/version.go index 6eb1c8e5..6e8d36df 100644 --- a/sdk/mpr/version/version.go +++ b/sdk/mpr/version/version.go @@ -9,32 +9,14 @@ import ( "strconv" "strings" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/versions" ) -// ProjectVersion contains version information for a Mendix project. -type ProjectVersion struct { - // ProductVersion is the full Mendix version string (e.g., "10.18.0", "11.6.0") - ProductVersion string - - // BuildVersion is the build version, usually same as ProductVersion - BuildVersion string - - // FormatVersion is the MPR format version (1 for legacy, 2 for mprcontents) - FormatVersion int - - // SchemaHash is the SHA256 hash of the metamodel schema - SchemaHash string - - // MajorVersion is the major version number (e.g., 10, 11) - MajorVersion int - - // MinorVersion is the minor version number (e.g., 18, 6) - MinorVersion int - - // PatchVersion is the patch version number (e.g., 0, 1) - PatchVersion int -} +// ProjectVersion is an alias for types.ProjectVersion. +// All version comparison methods (IsAtLeast, IsAtLeastFull, String, IsMPRv2) +// are defined on types.ProjectVersion directly. +type ProjectVersion = types.ProjectVersion // DefaultVersion returns the default version (11.6.0) used when detection fails. func DefaultVersion() *ProjectVersion { @@ -102,41 +84,6 @@ func parseVersion(version string) (major, minor, patch int) { return } -// String returns the product version string. -func (v *ProjectVersion) String() string { - return v.ProductVersion -} - -// IsMPRv2 returns true if the project uses MPR v2 format (mprcontents folder). -func (v *ProjectVersion) IsMPRv2() bool { - return v.FormatVersion >= 2 -} - -// IsAtLeast returns true if this version is at least the specified major.minor version. -func (v *ProjectVersion) IsAtLeast(major, minor int) bool { - if v.MajorVersion > major { - return true - } - if v.MajorVersion == major && v.MinorVersion >= minor { - return true - } - return false -} - -// IsAtLeastFull returns true if this version is at least the specified major.minor.patch version. -func (v *ProjectVersion) IsAtLeastFull(major, minor, patch int) bool { - if v.MajorVersion > major { - return true - } - if v.MajorVersion == major && v.MinorVersion > minor { - return true - } - if v.MajorVersion == major && v.MinorVersion == minor && v.PatchVersion >= patch { - return true - } - return false -} - // SupportedVersionRange defines the range of Mendix versions supported for read/write. var SupportedVersionRange = struct { MinMajor int @@ -146,22 +93,22 @@ var SupportedVersionRange = struct { MaxMajor: 11, } -// IsSupported returns true if this version is within the supported range for writing. -func (v *ProjectVersion) IsSupported() bool { - return v.MajorVersion >= SupportedVersionRange.MinMajor && - v.MajorVersion <= SupportedVersionRange.MaxMajor +// IsSupported returns true if pv is within the supported range for writing. +func IsSupported(pv *ProjectVersion) bool { + return pv.MajorVersion >= SupportedVersionRange.MinMajor && + pv.MajorVersion <= SupportedVersionRange.MaxMajor } -// SupportsFeature checks if a specific feature is available in this version. +// SupportsFeature checks if a specific feature is available in the given version. // It first checks the YAML-based version registry, falling back to the // hardcoded featureVersions map for features not yet in the registry. -func (v *ProjectVersion) SupportsFeature(feature Feature) bool { +func SupportsFeature(pv *ProjectVersion, feature Feature) bool { // Try the YAML registry first via the feature-to-registry mapping. if mapping, ok := featureRegistry[feature]; ok { reg, err := versions.Load() if err == nil { - pv := versions.SemVer{Major: v.MajorVersion, Minor: v.MinorVersion, Patch: v.PatchVersion} - return reg.IsAvailable(mapping.Area, mapping.Name, pv) + sv := versions.SemVer{Major: pv.MajorVersion, Minor: pv.MinorVersion, Patch: pv.PatchVersion} + return reg.IsAvailable(mapping.Area, mapping.Name, sv) } } @@ -170,7 +117,7 @@ func (v *ProjectVersion) SupportsFeature(feature Feature) bool { if !ok { return false } - return v.IsAtLeast(minVersion.Major, minVersion.Minor) + return pv.IsAtLeast(minVersion.Major, minVersion.Minor) } // Feature represents a Mendix feature that may or may not be available.