diff --git a/internal/index/index.go b/internal/index/index.go index 327a9b1..f583a52 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -51,6 +51,7 @@ type ProjectNode struct { Parent *ProjectNode Metadata map[string]string // Store extra info like Class, Type, Size Target *ProjectNode // Points to referenced node (for Direct References/Links) + Pragmas []string } type Fragment struct { @@ -201,6 +202,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars for _, def := range config.Definitions { doc := pt.findDoc(config.Comments, def.Pos()) + pragmas := pt.findPragmas(config.Pragmas, def.Pos()) switch d := def.(type) { case *parser.Field: @@ -229,7 +231,11 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars child.Doc += doc } - pt.addObjectFragment(child, file, d, doc, config.Comments) + if len(pragmas) > 0 { + child.Pragmas = append(child.Pragmas, pragmas...) + } + + pt.addObjectFragment(child, file, d, doc, config.Comments, config.Pragmas) } } @@ -238,7 +244,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars } } -func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment) { +func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment, pragmas []parser.Pragma) { frag := &Fragment{ File: file, IsObject: true, @@ -248,6 +254,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa for _, def := range obj.Subnode.Definitions { subDoc := pt.findDoc(comments, def.Pos()) + subPragmas := pt.findPragmas(pragmas, def.Pos()) switch d := def.(type) { case *parser.Field: @@ -277,7 +284,11 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa child.Doc += subDoc } - pt.addObjectFragment(child, file, d, subDoc, comments) + if len(subPragmas) > 0 { + child.Pragmas = append(child.Pragmas, subPragmas...) + } + + pt.addObjectFragment(child, file, d, subDoc, comments, pragmas) } } @@ -322,6 +333,30 @@ func (pt *ProjectTree) findDoc(comments []parser.Comment, pos parser.Position) s return docBuilder.String() } +func (pt *ProjectTree) findPragmas(pragmas []parser.Pragma, pos parser.Position) []string { + var found []string + targetLine := pos.Line - 1 + + for i := len(pragmas) - 1; i >= 0; i-- { + p := pragmas[i] + if p.Position.Line > pos.Line { + continue + } + if p.Position.Line == pos.Line { + continue + } + + if p.Position.Line == targetLine { + txt := strings.TrimSpace(strings.TrimPrefix(p.Text, "//!")) + found = append(found, txt) + targetLine-- + } else if p.Position.Line < targetLine { + break + } + } + return found +} + func (pt *ProjectTree) indexValue(file string, val parser.Value) { switch v := val.(type) { case *parser.ReferenceValue: diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 5630120..bed53e9 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -2,6 +2,7 @@ package validator import ( "fmt" + "strings" "github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-dev/marte-dev-tools/internal/parser" @@ -353,12 +354,22 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di } 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), - }) + suppress := false + for _, p := range signalNode.Pragmas { + if strings.HasPrefix(p, "implicit:") { + suppress = true + break + } + } + + if !suppress { + 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{ @@ -367,6 +378,17 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di Position: v.getNodePosition(signalNode), File: v.getNodeFile(signalNode), }) + } else { + // Check Type validity even for implicit + typeVal := v.getFieldValue(typeFields[0]) + if !isValidType(typeVal) { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeVal, signalNode.RealName), + Position: typeFields[0].Position, + File: v.getNodeFile(signalNode), + }) + } } } else { signalNode.Target = targetNode @@ -376,9 +398,71 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, targetNode) } } + + // Property checks + v.checkSignalProperty(signalNode, targetNode, "Type") + v.checkSignalProperty(signalNode, targetNode, "NumberOfElements") + v.checkSignalProperty(signalNode, targetNode, "NumberOfDimensions") + + // Check Type validity if present + if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 { + typeVal := v.getFieldValue(typeFields[0]) + if !isValidType(typeVal) { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeVal, signalNode.RealName), + Position: typeFields[0].Position, + File: v.getNodeFile(signalNode), + }) + } + } } } +func (v *Validator) checkSignalProperty(gamSig, dsSig *index.ProjectNode, prop string) { + gamVal := gamSig.Metadata[prop] + dsVal := dsSig.Metadata[prop] + + if gamVal == "" { + return + } + + if dsVal != "" && gamVal != dsVal { + if prop == "Type" { + if v.checkCastPragma(gamSig, dsVal, gamVal) { + return + } + } + + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Signal '%s' property '%s' mismatch: defined '%s', referenced '%s'", gamSig.RealName, prop, dsVal, gamVal), + Position: v.getNodePosition(gamSig), + File: v.getNodeFile(gamSig), + }) + } +} + +func (v *Validator) checkCastPragma(node *index.ProjectNode, defType, curType string) bool { + for _, p := range node.Pragmas { + if strings.HasPrefix(p, "cast(") { + content := strings.TrimPrefix(p, "cast(") + if idx := strings.Index(content, ")"); idx != -1 { + content = content[:idx] + parts := strings.Split(content, ",") + if len(parts) == 2 { + d := strings.TrimSpace(parts[0]) + c := strings.TrimSpace(parts[1]) + if d == defType && c == curType { + return true + } + } + } + } + } + return false +} + func (v *Validator) updateReferenceTarget(file string, pos parser.Position, target *index.ProjectNode) { for i := range v.Tree.References { ref := &v.Tree.References[i] @@ -409,6 +493,10 @@ func (v *Validator) getFieldValue(f *parser.Field) string { return val.Value case *parser.ReferenceValue: return val.Value + case *parser.IntValue: + return val.Raw + case *parser.FloatValue: + return val.Raw } return "" } @@ -533,14 +621,24 @@ func (v *Validator) collectTargetUsage(node *index.ProjectNode, referenced map[* } func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) { + // Heuristic for GAM if isGAM(node) { if !referenced[node] { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelWarning, - Message: fmt.Sprintf("Unused GAM: %s is defined but not referenced in any thread or scheduler", node.RealName), - Position: v.getNodePosition(node), - File: v.getNodeFile(node), - }) + suppress := false + for _, p := range node.Pragmas { + if strings.HasPrefix(p, "unused:") { + suppress = true + break + } + } + if !suppress { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelWarning, + Message: fmt.Sprintf("Unused GAM: %s is defined but not referenced in any thread or scheduler", node.RealName), + Position: v.getNodePosition(node), + File: v.getNodeFile(node), + }) + } } } @@ -549,12 +647,21 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map if signalsNode, ok := node.Children["Signals"]; ok { for _, signal := range signalsNode.Children { if !referenced[signal] { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelWarning, - Message: fmt.Sprintf("Unused Signal: %s is defined in DataSource %s but never referenced", signal.RealName, node.RealName), - Position: v.getNodePosition(signal), - File: v.getNodeFile(signal), - }) + suppress := false + for _, p := range signal.Pragmas { + if strings.HasPrefix(p, "unused:") { + suppress = true + break + } + } + if !suppress { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelWarning, + Message: fmt.Sprintf("Unused Signal: %s is defined in DataSource %s but never referenced", signal.RealName, node.RealName), + Position: v.getNodePosition(signal), + File: v.getNodeFile(signal), + }) + } } } } @@ -602,4 +709,4 @@ func (v *Validator) getNodeFile(node *index.ProjectNode) string { return node.Fragments[0].File } return "" -} +} \ No newline at end of file diff --git a/mdt b/mdt index 9eee343..f1ee76f 100755 Binary files a/mdt and b/mdt differ diff --git a/test/validator_implicit_signal_test.go b/test/validator_implicit_signal_test.go new file mode 100644 index 0000000..a2e0163 --- /dev/null +++ b/test/validator_implicit_signal_test.go @@ -0,0 +1,107 @@ +package integration + +import ( + "strings" + "testing" + + "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 TestImplicitSignal(t *testing.T) { + content := ` ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = { + ExplicitSig = { Type = uint32 } + } + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + ExplicitSig = { + DataSource = MyDS + Type = uint32 + } + ImplicitSig = { + DataSource = MyDS + Type = uint32 + } + } +} +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile("implicit_signal.marte", config) + idx.ResolveReferences() + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + + foundWarning := false + foundError := false + + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Implicitly Defined Signal") { + if strings.Contains(d.Message, "ImplicitSig") { + foundWarning = true + } + } + if strings.Contains(d.Message, "Signal 'ExplicitSig' not found") { + foundError = true + } + } + + if !foundWarning || foundError { + for _, d := range v.Diagnostics { + t.Logf("Diagnostic: %s", d.Message) + } + } + + if !foundWarning { + t.Error("Expected warning for ImplicitSig") + } + if foundError { + t.Error("Unexpected error for ExplicitSig") + } + + // Test missing Type for implicit + contentMissingType := ` ++Data = { Class = ReferenceContainer +DS={Class=FileReader Filename="" Signals={}} } ++GAM = { Class = IOGAM InputSignals = { Impl = { DataSource = DS } } } +` + p2 := parser.NewParser(contentMissingType) + config2, err2 := p2.Parse() + if err2 != nil { + t.Fatalf("Parse2 failed: %v", err2) + } + idx2 := index.NewProjectTree() + idx2.AddFile("missing_type.marte", config2) + idx2.ResolveReferences() + v2 := validator.NewValidator(idx2, ".") + v2.ValidateProject() + + foundTypeErr := false + for _, d := range v2.Diagnostics { + if strings.Contains(d.Message, "Implicit signal 'Impl' must define Type") { + foundTypeErr = true + } + } + if !foundTypeErr { + for _, d := range v2.Diagnostics { + t.Logf("Diagnostic2: %s", d.Message) + } + t.Error("Expected error for missing Type in implicit signal") + } +} diff --git a/test/validator_pragma_test.go b/test/validator_pragma_test.go new file mode 100644 index 0000000..79b7531 --- /dev/null +++ b/test/validator_pragma_test.go @@ -0,0 +1,69 @@ +package integration + +import ( + "strings" + "testing" + + "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 TestPragmaSuppression(t *testing.T) { + content := ` ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = { + //!unused: Ignore this + UnusedSig = { Type = uint32 } + UsedSig = { Type = uint32 } + } + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + UsedSig = { DataSource = MyDS Type = uint32 } + + //!implicit: Ignore this implicit + ImplicitSig = { DataSource = MyDS Type = uint32 } + } +} +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile("pragma.marte", config) + idx.ResolveReferences() + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + v.CheckUnused() + + foundUnusedWarning := false + foundImplicitWarning := false + + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Unused Signal") && strings.Contains(d.Message, "UnusedSig") { + foundUnusedWarning = true + } + if strings.Contains(d.Message, "Implicitly Defined Signal") && strings.Contains(d.Message, "ImplicitSig") { + foundImplicitWarning = true + } + } + + if foundUnusedWarning { + t.Error("Expected warning for UnusedSig to be suppressed") + } + if foundImplicitWarning { + t.Error("Expected warning for ImplicitSig to be suppressed") + } +} diff --git a/test/validator_signal_properties_test.go b/test/validator_signal_properties_test.go new file mode 100644 index 0000000..42c954e --- /dev/null +++ b/test/validator_signal_properties_test.go @@ -0,0 +1,108 @@ +package integration + +import ( + "strings" + "testing" + + "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 TestSignalProperties(t *testing.T) { + content := ` ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = { + Correct = { Type = uint32 NumberOfElements = 10 } + } + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + // Correct reference + Correct = { DataSource = MyDS Type = uint32 NumberOfElements = 10 } + + // Mismatch Type + BadType = { + Alias = Correct + DataSource = MyDS + Type = float32 // Error + } + + // Mismatch Elements + BadElements = { + Alias = Correct + DataSource = MyDS + Type = uint32 + NumberOfElements = 20 // Error + } + + // Valid Cast + //!cast(uint32, float32): Cast reason + CastSig = { + Alias = Correct + DataSource = MyDS + Type = float32 // OK + } + + // Invalid Cast (Wrong definition type in pragma) + //!cast(int32, float32): Wrong def type + BadCast = { + Alias = Correct + DataSource = MyDS + Type = float32 // Error because pragma mismatch + } + } +} +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile("signal_props.marte", config) + idx.ResolveReferences() + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + + foundBadType := false + foundBadElements := false + foundBadCast := false + + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "property 'Type' mismatch") { + if strings.Contains(d.Message, "'BadType'") { + foundBadType = true + } + if strings.Contains(d.Message, "'BadCast'") { + foundBadCast = true + } + if strings.Contains(d.Message, "'CastSig'") { + t.Error("Unexpected error for CastSig (should be suppressed by pragma)") + } + } + + if strings.Contains(d.Message, "property 'NumberOfElements' mismatch") { + foundBadElements = true + } + } + + if !foundBadType { + t.Error("Expected error for BadType") + } + if !foundBadElements { + t.Error("Expected error for BadElements") + } + if !foundBadCast { + t.Error("Expected error for BadCast (pragma mismatch)") + } +}