diff --git a/internal/index/index.go b/internal/index/index.go index eaafa04..327a9b1 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -50,6 +50,7 @@ type ProjectNode struct { Children map[string]*ProjectNode Parent *ProjectNode Metadata map[string]string // Store extra info like Class, Type, Size + Target *ProjectNode // Points to referenced node (for Direct References/Links) } type Fragment struct { @@ -384,6 +385,22 @@ func (pt *ProjectTree) Query(file string, line, col int) *QueryResult { return pt.queryNode(pt.Root, file, line, col) } +func (pt *ProjectTree) Walk(visitor func(*ProjectNode)) { + if pt.Root != nil { + pt.walkRecursive(pt.Root, visitor) + } + for _, node := range pt.IsolatedFiles { + pt.walkRecursive(node, visitor) + } +} + +func (pt *ProjectTree) walkRecursive(node *ProjectNode, visitor func(*ProjectNode)) { + visitor(node) + for _, child := range node.Children { + pt.walkRecursive(child, visitor) + } +} + func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) *QueryResult { for _, frag := range node.Fragments { if frag.File == file { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 8c75524..cf6d780 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -406,7 +406,11 @@ func handleHover(params HoverParams) *Hover { var content string if res.Node != nil { - content = formatNodeInfo(res.Node) + if res.Node.Target != nil { + content = fmt.Sprintf("**Link**: `%s` -> `%s`\n\n%s", res.Node.RealName, res.Node.Target.RealName, formatNodeInfo(res.Node.Target)) + } else { + content = formatNodeInfo(res.Node) + } } else if res.Field != nil { content = fmt.Sprintf("**Field**: `%s`", res.Field.Name) } else if res.Reference != nil { @@ -454,7 +458,11 @@ func handleDefinition(params DefinitionParams) any { if res.Reference != nil && res.Reference.Target != nil { targetNode = res.Reference.Target } else if res.Node != nil { - targetNode = res.Node + if res.Node.Target != nil { + targetNode = res.Node.Target + } else { + targetNode = res.Node + } } if targetNode != nil { @@ -497,23 +505,30 @@ func handleReferences(params ReferenceParams) []Location { return nil } + // Resolve canonical target (follow link if present) + canonical := targetNode + if targetNode.Target != nil { + canonical = targetNode.Target + } + var locations []Location if params.Context.IncludeDeclaration { - for _, frag := range targetNode.Fragments { + for _, frag := range canonical.Fragments { if frag.IsObject { locations = append(locations, Location{ URI: "file://" + frag.File, Range: Range{ Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1}, - End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(targetNode.RealName)}, + End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(canonical.RealName)}, }, }) } } } + // 1. References from index (Aliases) for _, ref := range tree.References { - if ref.Target == targetNode { + if ref.Target == canonical { locations = append(locations, Location{ URI: "file://" + ref.File, Range: Range{ @@ -524,6 +539,23 @@ func handleReferences(params ReferenceParams) []Location { } } + // 2. References from Node Targets (Direct References) + tree.Walk(func(node *index.ProjectNode) { + if node.Target == canonical { + for _, frag := range node.Fragments { + if frag.IsObject { + locations = append(locations, Location{ + URI: "file://" + frag.File, + Range: Range{ + Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1}, + End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(node.RealName)}, + }, + }) + } + } + } + }) + return locations } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index f66156c..5630120 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -285,7 +285,7 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di return // Ignore implicit signals or missing datasource (handled elsewhere if mandatory) } - dsNode := v.resolveReference(dsName, v.getNodeFile(signalNode)) + dsNode := v.resolveReference(dsName, v.getNodeFile(signalNode), isDataSource) if dsNode == nil { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, @@ -335,8 +335,8 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di targetSignalName = v.getFieldValue(aliasFields[0]) // Alias is usually the name in DataSource } + var targetNode *index.ProjectNode if signalsContainer, ok := dsNode.Children["Signals"]; ok { - var targetNode *index.ProjectNode targetNorm := index.NormalizeName(targetSignalName) if child, ok := signalsContainer.Children[targetNorm]; ok { @@ -350,20 +350,30 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di } } } + } - if targetNode == nil { + if targetNode == nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelWarning, + Message: fmt.Sprintf("Implicitly Defined Signal: '%s' is defined in GAM '%s' but not in DataSource '%s'", targetSignalName, gamNode.RealName, dsName), + Position: v.getNodePosition(signalNode), + File: v.getNodeFile(signalNode), + }) + + if typeFields, ok := fields["Type"]; !ok || len(typeFields) == 0 { v.Diagnostics = append(v.Diagnostics, Diagnostic{ Level: LevelError, - Message: fmt.Sprintf("Signal '%s' not found in DataSource '%s'", targetSignalName, dsName), + Message: fmt.Sprintf("Implicit signal '%s' must define Type", targetSignalName), Position: v.getNodePosition(signalNode), File: v.getNodeFile(signalNode), }) - } else { - // Link Alias reference - if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 { - if val, ok := aliasFields[0].Value.(*parser.ReferenceValue); ok { - v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, targetNode) - } + } + } else { + signalNode.Target = targetNode + // Link Alias reference + if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 { + if val, ok := aliasFields[0].Value.(*parser.ReferenceValue); ok { + v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, targetNode) } } } @@ -403,9 +413,9 @@ func (v *Validator) getFieldValue(f *parser.Field) string { return "" } -func (v *Validator) resolveReference(name string, file string) *index.ProjectNode { +func (v *Validator) resolveReference(name string, file string, predicate func(*index.ProjectNode) bool) *index.ProjectNode { if isoNode, ok := v.Tree.IsolatedFiles[file]; ok { - if found := v.findNodeRecursive(isoNode, name); found != nil { + if found := v.findNodeRecursive(isoNode, name, predicate); found != nil { return found } return nil @@ -413,24 +423,20 @@ func (v *Validator) resolveReference(name string, file string) *index.ProjectNod if v.Tree.Root == nil { return nil } - return v.findNodeRecursive(v.Tree.Root, name) + return v.findNodeRecursive(v.Tree.Root, name, predicate) } -func (v *Validator) findNodeRecursive(root *index.ProjectNode, name string) *index.ProjectNode { +func (v *Validator) findNodeRecursive(root *index.ProjectNode, name string, predicate func(*index.ProjectNode) bool) *index.ProjectNode { // Simple recursive search matching name if root.RealName == name || root.Name == index.NormalizeName(name) { - return root - } - - // Fast lookup in children - norm := index.NormalizeName(name) - if child, ok := root.Children[norm]; ok { - return child + if predicate == nil || predicate(root) { + return root + } } // Recursive for _, child := range root.Children { - if found := v.findNodeRecursive(child, name); found != nil { + if found := v.findNodeRecursive(child, name, predicate); found != nil { return found } } @@ -495,7 +501,6 @@ func (v *Validator) getFileForField(f *parser.Field, node *index.ProjectNode) st } func (v *Validator) CheckUnused() { - // ... (same as before) referencedNodes := make(map[*index.ProjectNode]bool) for _, ref := range v.Tree.References { if ref.Target != nil { @@ -503,6 +508,13 @@ func (v *Validator) CheckUnused() { } } + if v.Tree.Root != nil { + v.collectTargetUsage(v.Tree.Root, referencedNodes) + } + for _, node := range v.Tree.IsolatedFiles { + v.collectTargetUsage(node, referencedNodes) + } + if v.Tree.Root != nil { v.checkUnusedRecursive(v.Tree.Root, referencedNodes) } @@ -511,9 +523,16 @@ func (v *Validator) CheckUnused() { } } +func (v *Validator) collectTargetUsage(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) { + if node.Target != nil { + referenced[node.Target] = true + } + for _, child := range node.Children { + v.collectTargetUsage(child, referenced) + } +} + func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) { - // ... (same as before) - // Heuristic for GAM if isGAM(node) { if !referenced[node] { v.Diagnostics = append(v.Diagnostics, Diagnostic{ diff --git a/mdt b/mdt index 351abf0..9eee343 100755 Binary files a/mdt and b/mdt differ diff --git a/test/lsp_signal_test.go b/test/lsp_signal_test.go index f3f1de9..ae34a44 100644 --- a/test/lsp_signal_test.go +++ b/test/lsp_signal_test.go @@ -5,16 +5,30 @@ import ( "github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-dev/marte-dev-tools/internal/parser" + "github.com/marte-dev/marte-dev-tools/internal/validator" ) -func TestLSPSignalMetadata(t *testing.T) { +func TestLSPSignalReferences(t *testing.T) { content := ` -+MySignal = { - Class = Signal - Type = uint32 - NumberOfElements = 10 - NumberOfDimensions = 1 - DataSource = DDB1 ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = { + MySig = { Type = uint32 } + } + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + MySig = { + DataSource = MyDS + Type = uint32 + } + } } ` p := parser.NewParser(content) @@ -24,26 +38,50 @@ func TestLSPSignalMetadata(t *testing.T) { } idx := index.NewProjectTree() - file := "signal.marte" - idx.AddFile(file, config) - - res := idx.Query(file, 2, 2) // Query +MySignal - if res == nil || res.Node == nil { - t.Fatal("Query failed for signal definition") + idx.AddFile("signal_refs.marte", config) + idx.ResolveReferences() + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + + // Find definition of MySig in MyDS + root := idx.IsolatedFiles["signal_refs.marte"] + if root == nil { + t.Fatal("Root node not found") } - meta := res.Node.Metadata - if meta["Class"] != "Signal" { - t.Errorf("Expected Class Signal, got %s", meta["Class"]) - } - if meta["Type"] != "uint32" { - t.Errorf("Expected Type uint32, got %s", meta["Type"]) - } - if meta["NumberOfElements"] != "10" { - t.Errorf("Expected 10 elements, got %s", meta["NumberOfElements"]) - } + // Traverse to MySig + dataNode := root.Children["Data"] + if dataNode == nil { t.Fatal("Data node not found") } - // Since handleHover logic is in internal/lsp which we can't easily test directly without - // exposing formatNodeInfo, we rely on the fact that Metadata is populated correctly. - // If Metadata is correct, server.go logic (verified by code review) should display it. -} + myDS := dataNode.Children["MyDS"] + if myDS == nil { t.Fatal("MyDS node not found") } + + signals := myDS.Children["Signals"] + if signals == nil { t.Fatal("Signals node not found") } + + mySigDef := signals.Children["MySig"] + if mySigDef == nil { + t.Fatal("Definition of MySig not found in tree") + } + + // Now simulate "Find References" on mySigDef + foundRefs := 0 + idx.Walk(func(node *index.ProjectNode) { + if node.Target == mySigDef { + foundRefs++ + // Check if node is the GAM signal + if node.RealName != "MySig" { // In GAM it is MySig + t.Errorf("Unexpected reference node name: %s", node.RealName) + } + // Check parent is InputSignals -> MyGAM + if node.Parent == nil || node.Parent.Parent == nil || node.Parent.Parent.RealName != "+MyGAM" { + t.Errorf("Reference node not in MyGAM") + } + } + }) + + if foundRefs != 1 { + t.Errorf("Expected 1 reference (Direct), found %d", foundRefs) + } +} \ No newline at end of file