diff --git a/examples/test_app.marte b/examples/test_app.marte index ec24a7d..fa26a70 100644 --- a/examples/test_app.marte +++ b/examples/test_app.marte @@ -748,10 +748,11 @@ $TbTestApp = { DataSource = Timer Type = uint32 } + //!cast(uint32, uint64): because... Time = { Frequency = 100 DataSource = Timer - Type = uint32 + Type = uint64 } AbsoluteTime = { DataSource = Timer @@ -759,6 +760,7 @@ $TbTestApp = { } } OutputSignals = { + //!implicit: defined because.... Counter_DDB1 = { DataSource = DDB1 Type = uint32 diff --git a/internal/index/index.go b/internal/index/index.go index f583a52..d6894c1 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -13,6 +13,7 @@ type ProjectTree struct { Root *ProjectNode References []Reference IsolatedFiles map[string]*ProjectNode + GlobalPragmas map[string][]string } func (pt *ProjectTree) ScanDirectory(rootPath string) error { @@ -59,6 +60,7 @@ type Fragment struct { Definitions []parser.Definition IsObject bool ObjectPos parser.Position + EndPos parser.Position Doc string // Documentation for this fragment (if object) } @@ -69,6 +71,7 @@ func NewProjectTree() *ProjectTree { Metadata: make(map[string]string), }, IsolatedFiles: make(map[string]*ProjectNode), + GlobalPragmas: make(map[string][]string), } } @@ -89,6 +92,7 @@ func (pt *ProjectTree) RemoveFile(file string) { pt.References = newRefs delete(pt.IsolatedFiles, file) + delete(pt.GlobalPragmas, file) pt.removeFileFromNode(pt.Root, file) } @@ -156,6 +160,14 @@ func (pt *ProjectTree) extractFieldMetadata(node *ProjectNode, f *parser.Field) func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { pt.RemoveFile(file) + // Collect global pragmas + for _, p := range config.Pragmas { + txt := strings.TrimSpace(p.Text) + if strings.HasPrefix(txt, "//!allow(") { + pt.GlobalPragmas[file] = append(pt.GlobalPragmas[file], txt) + } + } + if config.Package == nil { node := &ProjectNode{ Children: make(map[string]*ProjectNode), @@ -249,6 +261,7 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa File: file, IsObject: true, ObjectPos: obj.Position, + EndPos: obj.Subnode.EndPosition, Doc: doc, } @@ -462,3 +475,44 @@ func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) } return nil } + +func (pt *ProjectTree) GetNodeContaining(file string, pos parser.Position) *ProjectNode { + if isoNode, ok := pt.IsolatedFiles[file]; ok { + if found := pt.findNodeContaining(isoNode, file, pos); found != nil { + return found + } + return isoNode + } + if pt.Root != nil { + if found := pt.findNodeContaining(pt.Root, file, pos); found != nil { + return found + } + for _, frag := range pt.Root.Fragments { + if frag.File == file && !frag.IsObject { + return pt.Root + } + } + } + return nil +} + +func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos parser.Position) *ProjectNode { + for _, child := range node.Children { + if res := pt.findNodeContaining(child, file, pos); res != nil { + return res + } + } + + for _, frag := range node.Fragments { + if frag.File == file && frag.IsObject { + start := frag.ObjectPos + end := frag.EndPos + + if (pos.Line > start.Line || (pos.Line == start.Line && pos.Column >= start.Column)) && + (pos.Line < end.Line || (pos.Line == end.Line && pos.Column <= end.Column)) { + return node + } + } + } + return nil +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index cf6d780..0642ddd 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -581,8 +581,8 @@ func formatNodeInfo(node *index.ProjectNode) string { } // Size - dims := node.Metadata["NumberOfDimensions"] -elems := node.Metadata["NumberOfElements"] + dims := node.Metadata["NumberOfDimensions"] + elems := node.Metadata["NumberOfElements"] if dims != "" || elems != "" { sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims) } @@ -592,6 +592,57 @@ elems := node.Metadata["NumberOfElements"] if node.Doc != "" { info += fmt.Sprintf("\n\n%s", node.Doc) } + + // Find references + var refs []string + for _, ref := range tree.References { + if ref.Target == node { + container := tree.GetNodeContaining(ref.File, ref.Position) + if container != nil { + threadName := "" + stateName := "" + + curr := container + for curr != nil { + if cls, ok := curr.Metadata["Class"]; ok { + if cls == "RealTimeThread" { + threadName = curr.RealName + } + if cls == "RealTimeState" { + stateName = curr.RealName + } + } + curr = curr.Parent + } + + if threadName != "" || stateName != "" { + refStr := "" + if stateName != "" { + refStr += fmt.Sprintf("State: `%s`", stateName) + } + if threadName != "" { + if refStr != "" { + refStr += ", " + } + refStr += fmt.Sprintf("Thread: `%s`", threadName) + } + refs = append(refs, refStr) + } + } + } + } + + if len(refs) > 0 { + uniqueRefs := make(map[string]bool) + info += "\n\n**Referenced in**:\n" + for _, r := range refs { + if !uniqueRefs[r] { + uniqueRefs[r] = true + info += fmt.Sprintf("- %s\n", r) + } + } + } + return info } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index bed53e9..8012cd7 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -354,15 +354,17 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di } if targetNode == nil { - suppress := false - for _, p := range signalNode.Pragmas { - if strings.HasPrefix(p, "implicit:") { - suppress = true - break + suppressed := v.isGloballyAllowed("implicit") + if !suppressed { + for _, p := range signalNode.Pragmas { + if strings.HasPrefix(p, "implicit:") { + suppressed = true + break + } } } - if !suppress { + if !suppressed { 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), @@ -624,6 +626,9 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map // Heuristic for GAM if isGAM(node) { if !referenced[node] { + if v.isGloballyAllowed("unused") { + return + } suppress := false for _, p := range node.Pragmas { if strings.HasPrefix(p, "unused:") { @@ -647,6 +652,9 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map if signalsNode, ok := node.Children["Signals"]; ok { for _, signal := range signalsNode.Children { if !referenced[signal] { + if v.isGloballyAllowed("unused") { + continue + } suppress := false for _, p := range signal.Pragmas { if strings.HasPrefix(p, "unused:") { @@ -709,4 +717,16 @@ func (v *Validator) getNodeFile(node *index.ProjectNode) string { return node.Fragments[0].File } return "" +} + +func (v *Validator) isGloballyAllowed(warningType string) bool { + prefix := fmt.Sprintf("//!allow(%s)", warningType) + for _, pragmas := range v.Tree.GlobalPragmas { + for _, p := range pragmas { + if strings.HasPrefix(p, prefix) { + return true + } + } + } + return false } \ No newline at end of file diff --git a/mdt b/mdt index f1ee76f..652da78 100755 Binary files a/mdt and b/mdt differ diff --git a/specification.md b/specification.md index e969540..2c86e3a 100644 --- a/specification.md +++ b/specification.md @@ -84,7 +84,11 @@ The LSP server should provide the following capabilities: - **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object. - **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined). - **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field. -- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. +- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. Supported pragmas: + - `//!unused: REASON` - Suppress "Unused GAM" or "Unused Signal" warnings for a specific node. + - `//!implicit: REASON` - Suppress "Implicitly Defined Signal" warnings for a specific signal reference. + - `//!allow(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`). + - `//!cast(DEF_TYPE, CUR_TYPE): REASON` - Suppress "Type Inconsistency" errors if types match. - **Structure**: A configuration is composed by one or more definitions. ### Core MARTe Classes @@ -105,6 +109,7 @@ MARTe configurations typically involve several main categories of objects: - **Requirements**: - All signal definitions **must** include a `Type` field with a valid value. - **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`. + - **Property Matching**: Signal references in GAMs must match the properties (`Type`, `NumberOfElements`, `NumberOfDimensions`) of the defined signal in the `DataSource`. - **Extensibility**: Signal definitions can include additional fields as required by the specific application context. - **Signal Reference Syntax**: - Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats: @@ -186,13 +191,14 @@ The `fmt` command must format the code according to the following rules: The LSP and `check` command should report the following: - **Warnings**: - - **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler. - - **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`. - - **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`. + - **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler. (Suppress with `//!unused`) + - **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`. (Suppress with `//!unused`) + - **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`. (Suppress with `//!implicit`) - **Errors**: - - **Type Inconsistency**: A signal is referenced with a type different from its definition. + - **Type Inconsistency**: A signal is referenced with a type different from its definition. (Suppress with `//!cast`) - **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition. + - **Invalid Signal Content**: The `Signals` container of a `DataSource` contains invalid elements (e.g., fields instead of nodes). - **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files). - **Validation Errors**: - Missing mandatory fields. diff --git a/test/lsp_hover_context_test.go b/test/lsp_hover_context_test.go new file mode 100644 index 0000000..b2bb70c --- /dev/null +++ b/test/lsp_hover_context_test.go @@ -0,0 +1,73 @@ +package integration + +import ( + "testing" + + "github.com/marte-dev/marte-dev-tools/internal/index" + "github.com/marte-dev/marte-dev-tools/internal/parser" +) + +func TestGetNodeContaining(t *testing.T) { + content := ` ++App = { + Class = RealTimeApplication + +State1 = { + Class = RealTimeState + +Thread1 = { + Class = RealTimeThread + Functions = { GAM1 } + } + } +} ++GAM1 = { Class = IOGAM } +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + file := "hover_context.marte" + idx.AddFile(file, config) + idx.ResolveReferences() + + // Find reference to GAM1 + var gamRef *index.Reference + for i := range idx.References { + ref := &idx.References[i] + if ref.Name == "GAM1" { + gamRef = ref + break + } + } + + if gamRef == nil { + t.Fatal("Reference to GAM1 not found") + } + + // Check containing node + container := idx.GetNodeContaining(file, gamRef.Position) + if container == nil { + t.Fatal("Container not found") + } + + if container.RealName != "+Thread1" { + t.Errorf("Expected container +Thread1, got %s", container.RealName) + } + + // Check traversal up to State + curr := container + foundState := false + for curr != nil { + if curr.RealName == "+State1" { + foundState = true + break + } + curr = curr.Parent + } + + if !foundState { + t.Error("State parent not found") + } +} diff --git a/test/validator_global_pragma_test.go b/test/validator_global_pragma_test.go new file mode 100644 index 0000000..560b0e8 --- /dev/null +++ b/test/validator_global_pragma_test.go @@ -0,0 +1,67 @@ +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 TestGlobalPragma(t *testing.T) { + content := ` +//!allow(unused): Suppress all unused +//!allow(implicit): Suppress all implicit + ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = { + UnusedSig = { Type = uint32 } + } + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + 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("global_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") { + foundUnusedWarning = true + } + if strings.Contains(d.Message, "Implicitly Defined Signal") { + foundImplicitWarning = true + } + } + + if foundUnusedWarning { + t.Error("Expected warning for UnusedSig to be suppressed globally") + } + if foundImplicitWarning { + t.Error("Expected warning for ImplicitSig to be suppressed globally") + } +}