From ecc703930607cc2d39b095b36ec920522d1560fe Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Thu, 29 Jan 2026 23:03:46 +0100 Subject: [PATCH] Improved scoping --- internal/index/index.go | 49 +++++++++++-- internal/validator/validator.go | 8 ++ test/scoping_test.go | 65 +++++++++++++++++ test/validator_variable_usage_test.go | 101 ++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 test/scoping_test.go create mode 100644 test/validator_variable_usage_test.go diff --git a/internal/index/index.go b/internal/index/index.go index 1a630b2..682195c 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -427,11 +427,8 @@ func (pt *ProjectTree) ResolveReferences() { continue } - if isoNode, ok := pt.IsolatedFiles[ref.File]; ok { - ref.Target = pt.FindNode(isoNode, ref.Name, nil) - } else { - ref.Target = pt.FindNode(pt.Root, ref.Name, nil) - } + container := pt.GetNodeContaining(ref.File, ref.Position) + ref.Target = pt.resolveScopedName(container, ref.Name) } } @@ -612,3 +609,45 @@ func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos pa } return nil } + +func (pt *ProjectTree) resolveScopedName(ctx *ProjectNode, name string) *ProjectNode { + if ctx == nil { + return pt.FindNode(pt.Root, name, nil) + } + + parts := strings.Split(name, ".") + first := parts[0] + normFirst := NormalizeName(first) + + var startNode *ProjectNode + curr := ctx + + for curr != nil { + if child, ok := curr.Children[normFirst]; ok { + startNode = child + break + } + curr = curr.Parent + } + + if startNode == nil && ctx != pt.Root { + if child, ok := pt.Root.Children[normFirst]; ok { + startNode = child + } + } + + if startNode == nil { + return nil + } + + curr = startNode + for i := 1; i < len(parts); i++ { + norm := NormalizeName(parts[i]) + if child, ok := curr.Children[norm]; ok { + curr = child + } else { + return nil + } + } + return curr +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go index f29ed3a..a4a20ce 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -221,6 +221,14 @@ func (v *Validator) valueToInterface(val parser.Value) interface{} { return t.Value case *parser.ReferenceValue: return t.Value + case *parser.VariableReferenceValue: + name := strings.TrimPrefix(t.Name, "$") + if info, ok := v.Tree.Variables[name]; ok { + if info.Def.DefaultValue != nil { + return v.valueToInterface(info.Def.DefaultValue) + } + } + return nil case *parser.ArrayValue: var arr []interface{} for _, e := range t.Elements { diff --git a/test/scoping_test.go b/test/scoping_test.go new file mode 100644 index 0000000..1407b55 --- /dev/null +++ b/test/scoping_test.go @@ -0,0 +1,65 @@ +package integration + +import ( + "testing" + + "github.com/marte-community/marte-dev-tools/internal/index" + "github.com/marte-community/marte-dev-tools/internal/parser" +) + +func TestNameScoping(t *testing.T) { + // App1 = { A = { Data = 10 } B = { Ref = A } } + // App2 = { C = { Data = 10 } A = { Data = 12 } D = { Ref = A } } + + content := ` ++App1 = { + Class = App + +A = { Class = Node Data = 10 } + +B = { Class = Node Ref = A } +} ++App2 = { + Class = App + +C = { Class = Node Data = 10 } + +A = { Class = Node Data = 12 } + +D = { Class = Node Ref = A } +} +` + pt := index.NewProjectTree() + p := parser.NewParser(content) + cfg, err := p.Parse() + if err != nil { t.Fatal(err) } + pt.AddFile("main.marte", cfg) + + pt.ResolveReferences() + + // Helper to find ref target + findRefTarget := func(refName string, containerName string) *index.ProjectNode { + for _, ref := range pt.References { + if ref.Name == refName { + container := pt.GetNodeContaining(ref.File, ref.Position) + if container != nil && container.RealName == containerName { + return ref.Target + } + } + } + return nil + } + + targetB := findRefTarget("A", "+B") + if targetB == nil { + t.Fatal("Could not find reference A in +B") + } + // Check if targetB is App1.A + if targetB.Parent == nil || targetB.Parent.RealName != "+App1" { + t.Errorf("App1.B.Ref resolved to wrong target: %v (Parent %v)", targetB.RealName, targetB.Parent.RealName) + } + + targetD := findRefTarget("A", "+D") + if targetD == nil { + t.Fatal("Could not find reference A in +D") + } + // Check if targetD is App2.A + if targetD.Parent == nil || targetD.Parent.RealName != "+App2" { + t.Errorf("App2.D.Ref resolved to wrong target: %v (Parent %v)", targetD.RealName, targetD.Parent.RealName) + } +} diff --git a/test/validator_variable_usage_test.go b/test/validator_variable_usage_test.go new file mode 100644 index 0000000..0d43951 --- /dev/null +++ b/test/validator_variable_usage_test.go @@ -0,0 +1,101 @@ +package integration + +import ( + "strings" + "testing" + + "github.com/marte-community/marte-dev-tools/internal/index" + "github.com/marte-community/marte-dev-tools/internal/parser" + "github.com/marte-community/marte-dev-tools/internal/validator" +) + +func TestVariableValidation(t *testing.T) { + // Need a schema that enforces strict types to test usage validation. + // We can use built-in types or rely on Variable Definition validation. + + // Test Case 1: Variable Definition Mismatch + contentDef := ` +#var Positive: uint = -5 +` + pt := index.NewProjectTree() + p := parser.NewParser(contentDef) + cfg, err := p.Parse() + if err != nil { t.Fatal(err) } + pt.AddFile("def.marte", cfg) + + v := validator.NewValidator(pt, ".") + v.CheckVariables() + + foundError := false + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Variable 'Positive' value mismatch") { + foundError = true + } + } + if !foundError { + t.Error("Expected error for invalid variable definition") + } + + // Test Case 2: Variable Usage Mismatch + // We need a class with specific field type. + // PIDGAM.Kp is float | int. + // Let's use string variable. + contentUsage := ` +#var MyStr: string = "hello" ++MyPID = { + Class = PIDGAM + Kp = $MyStr + Ki = 0.0 + Kd = 0.0 +} +` + pt2 := index.NewProjectTree() + p2 := parser.NewParser(contentUsage) + cfg2, err := p2.Parse() + if err != nil { t.Fatal(err) } + pt2.AddFile("usage.marte", cfg2) + + v2 := validator.NewValidator(pt2, ".") + v2.ValidateProject() // Should run CUE validation on nodes + + foundUsageError := false + for _, d := range v2.Diagnostics { + // Schema validation error + if strings.Contains(d.Message, "Schema Validation Error") && + (strings.Contains(d.Message, "conflicting values") || strings.Contains(d.Message, "mismatched types")) { + foundUsageError = true + } + } + + if !foundUsageError { + t.Error("Expected error for invalid variable usage in PIDGAM.Kp") + for _, d := range v2.Diagnostics { + t.Logf("Diag: %s", d.Message) + } + } + + // Test Case 3: Valid Usage + contentValid := ` +#var MyGain: float = 1.5 ++MyPID = { + Class = PIDGAM + Kp = $MyGain + Ki = 0.0 + Kd = 0.0 +} +` + pt3 := index.NewProjectTree() + p3 := parser.NewParser(contentValid) + cfg3, err := p3.Parse() + if err != nil { t.Fatal(err) } + pt3.AddFile("valid.marte", cfg3) + + v3 := validator.NewValidator(pt3, ".") + v3.ValidateProject() + + for _, d := range v3.Diagnostics { + if strings.Contains(d.Message, "Schema Validation Error") { + t.Errorf("Unexpected schema error: %s", d.Message) + } + } +}