diff --git a/examples/pragma_test.marte b/examples/pragma_test.marte new file mode 100644 index 0000000..0b0c901 --- /dev/null +++ b/examples/pragma_test.marte @@ -0,0 +1,27 @@ +//!allow(unused): Ignore unused GAMs in this file +//!allow(implicit): Ignore implicit signals in this file + ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = {} + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + // Implicit signal (not in MyDS) + ImplicitSig = { + DataSource = MyDS + Type = uint32 + } + } +} + +// Unused GAM ++UnusedGAM = { + Class = IOGAM +} diff --git a/internal/index/index.go b/internal/index/index.go index d6894c1..6636262 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -162,8 +162,9 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) { // Collect global pragmas for _, p := range config.Pragmas { - txt := strings.TrimSpace(p.Text) - if strings.HasPrefix(txt, "//!allow(") { + txt := strings.TrimSpace(strings.TrimPrefix(p.Text, "//!")) + normalized := strings.ReplaceAll(txt, " ", "") + if strings.HasPrefix(normalized, "allow(") || strings.HasPrefix(normalized, "ignore(") { pt.GlobalPragmas[file] = append(pt.GlobalPragmas[file], txt) } } diff --git a/internal/parser/parser_strictness_test.go b/internal/parser/parser_strictness_test.go new file mode 100644 index 0000000..9e08bc0 --- /dev/null +++ b/internal/parser/parser_strictness_test.go @@ -0,0 +1,35 @@ +package parser_test + +import ( + "testing" + + "github.com/marte-dev/marte-dev-tools/internal/parser" +) + +func TestParserStrictness(t *testing.T) { + // Case 1: content not a definition (missing =) + invalidDef := ` +A = { + Field = 10 + XXX +} +` + p := parser.NewParser(invalidDef) + _, err := p.Parse() + if err == nil { + t.Error("Expected error for invalid definition XXX, got nil") + } + + // Case 2: Missing closing bracket + missingBrace := ` +A = { + SUBNODE = { + FIELD = 10 +} +` + p2 := parser.NewParser(missingBrace) + _, err2 := p2.Parse() + if err2 == nil { + t.Error("Expected error for missing closing bracket, got nil") + } +} diff --git a/internal/schema/marte.json b/internal/schema/marte.json index 02cfb27..c47e658 100644 --- a/internal/schema/marte.json +++ b/internal/schema/marte.json @@ -12,6 +12,16 @@ {"name": "States", "type": "node", "mandatory": false} ] }, + "RealTimeState": { + "fields": [ + {"name": "Threads", "type": "node", "mandatory": true} + ] + }, + "RealTimeThread": { + "fields": [ + {"name": "Functions", "type": "array", "mandatory": true} + ] + }, "GAMScheduler": { "fields": [ {"name": "TimingDataSource", "type": "reference", "mandatory": true} @@ -143,7 +153,7 @@ {"name": "StateChangeResetName", "type": "string", "mandatory": false}, {"name": "InputSignals", "type": "node", "mandatory": false}, {"name": "OutputSignals", "type": "node", "mandatory": false} - ] + ] }, "Interleaved2FlatGAM": { "fields": [] }, "FlattenedStructIOGAM": { "fields": [] }, @@ -152,7 +162,7 @@ {"name": "Expression", "type": "string", "mandatory": true}, {"name": "InputSignals", "type": "node", "mandatory": false}, {"name": "OutputSignals", "type": "node", "mandatory": false} - ] + ] }, "MessageGAM": { "fields": [] }, "MuxGAM": { "fields": [] }, @@ -224,4 +234,4 @@ "SysLogger": { "fields": [] }, "GAMDataSource": { "fields": [], "direction": "INOUT" } } -} \ No newline at end of file +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 8012cd7..2106cd4 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -133,6 +133,10 @@ func (v *Validator) validateNode(node *index.ProjectNode) { File: file, }) } + + if className == "RealTimeThread" { + v.checkFunctionsArray(node, fields) + } } // 3. Schema Validation @@ -354,10 +358,10 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di } if targetNode == nil { - suppressed := v.isGloballyAllowed("implicit") + suppressed := v.isGloballyAllowed("implicit", v.getNodeFile(signalNode)) if !suppressed { for _, p := range signalNode.Pragmas { - if strings.HasPrefix(p, "implicit:") { + if strings.HasPrefix(p, "implicit:") || strings.HasPrefix(p, "ignore(implicit)") { suppressed = true break } @@ -626,14 +630,13 @@ 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:") { - suppress = true - break + suppress := v.isGloballyAllowed("unused", v.getNodeFile(node)) + if !suppress { + for _, p := range node.Pragmas { + if strings.HasPrefix(p, "unused:") || strings.HasPrefix(p, "ignore(unused)") { + suppress = true + break + } } } if !suppress { @@ -652,12 +655,12 @@ 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") { + if v.isGloballyAllowed("unused", v.getNodeFile(signal)) { continue } suppress := false for _, p := range signal.Pragmas { - if strings.HasPrefix(p, "unused:") { + if strings.HasPrefix(p, "unused:") || strings.HasPrefix(p, "ignore(unused)") { suppress = true break } @@ -719,11 +722,59 @@ func (v *Validator) getNodeFile(node *index.ProjectNode) string { return "" } -func (v *Validator) isGloballyAllowed(warningType string) bool { - prefix := fmt.Sprintf("//!allow(%s)", warningType) - for _, pragmas := range v.Tree.GlobalPragmas { +func (v *Validator) checkFunctionsArray(node *index.ProjectNode, fields map[string][]*parser.Field) { + if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 { + f := funcs[0] + if arr, ok := f.Value.(*parser.ArrayValue); ok { + for _, elem := range arr.Elements { + if ref, ok := elem.(*parser.ReferenceValue); ok { + target := v.resolveReference(ref.Value, v.getNodeFile(node), isGAM) + if target == nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Function '%s' not found or is not a valid GAM", ref.Value), + Position: ref.Position, + File: v.getNodeFile(node), + }) + } + } else { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: "Functions array must contain references", + Position: f.Position, + File: v.getNodeFile(node), + }) + } + } + } + } +} + +func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bool { + prefix1 := fmt.Sprintf("allow(%s)", warningType) + prefix2 := fmt.Sprintf("ignore(%s)", warningType) + + // If context file is isolated, only check its own pragmas + if _, isIsolated := v.Tree.IsolatedFiles[contextFile]; isIsolated { + if pragmas, ok := v.Tree.GlobalPragmas[contextFile]; ok { + for _, p := range pragmas { + normalized := strings.ReplaceAll(p, " ", "") + if strings.HasPrefix(normalized, prefix1) || strings.HasPrefix(normalized, prefix2) { + return true + } + } + } + return false + } + + // If project file, check all non-isolated files + for file, pragmas := range v.Tree.GlobalPragmas { + if _, isIsolated := v.Tree.IsolatedFiles[file]; isIsolated { + continue + } for _, p := range pragmas { - if strings.HasPrefix(p, prefix) { + normalized := strings.ReplaceAll(p, " ", "") + if strings.HasPrefix(normalized, prefix1) || strings.HasPrefix(normalized, prefix2) { return true } } diff --git a/mdt b/mdt index 11ff859..d64bcfa 100755 Binary files a/mdt and b/mdt differ diff --git a/specification.md b/specification.md index 2e52149..37cd7ed 100644 --- a/specification.md +++ b/specification.md @@ -85,9 +85,9 @@ The LSP server should provide the following capabilities: - **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. 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`). + - `//!unused: REASON` or `//!ignore(unused): REASON` - Suppress "Unused GAM" or "Unused Signal" warnings. + - `//!implicit: REASON` or `//!ignore(implicit): REASON` - Suppress "Implicitly Defined Signal" warnings. + - `//!allow(WARNING_TYPE): REASON` or `//!ignore(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. - **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition (Field, Node, or Object) is **not allowed** and must generate a parsing error. @@ -205,6 +205,7 @@ The LSP and `check` command should report the following: - Missing mandatory fields. - Field type mismatches. - Grammar errors (e.g., missing closing brackets). + - **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes. ## Logging diff --git a/test/lsp_test.go b/test/lsp_test.go index 2374fd1..cee130c 100644 --- a/test/lsp_test.go +++ b/test/lsp_test.go @@ -140,3 +140,16 @@ func TestLSPHover(t *testing.T) { t.Errorf("Expected +MyObject, got %s", res.Node.RealName) } } + +func TestParserError(t *testing.T) { + invalidContent := ` +A = { + Field = +} +` + p := parser.NewParser(invalidContent) + _, err := p.Parse() + if err == nil { + t.Fatal("Expected parser error, got nil") + } +} diff --git a/test/validator_functions_array_test.go b/test/validator_functions_array_test.go new file mode 100644 index 0000000..7568a5e --- /dev/null +++ b/test/validator_functions_array_test.go @@ -0,0 +1,74 @@ +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 TestFunctionsArrayValidation(t *testing.T) { + content := ` ++App = { + Class = RealTimeApplication + +State = { + Class = RealTimeState + +Thread = { + Class = RealTimeThread + Functions = { + ValidGAM, + InvalidGAM, // Not a GAM (DataSource) + MissingGAM, // Not found + "String", // Not reference + } + } + } +} + ++ValidGAM = { Class = IOGAM InputSignals = {} } ++InvalidGAM = { Class = FileReader } +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + idx := index.NewProjectTree() + idx.AddFile("funcs.marte", config) + idx.ResolveReferences() + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + + foundInvalid := false + foundMissing := false + foundNotRef := false + + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "not found or is not a valid GAM") { + // This covers both InvalidGAM and MissingGAM cases + if strings.Contains(d.Message, "InvalidGAM") { + foundInvalid = true + } + if strings.Contains(d.Message, "MissingGAM") { + foundMissing = true + } + } + if strings.Contains(d.Message, "must contain references") { + foundNotRef = true + } + } + + if !foundInvalid { + t.Error("Expected error for InvalidGAM") + } + if !foundMissing { + t.Error("Expected error for MissingGAM") + } + if !foundNotRef { + t.Error("Expected error for non-reference element") + } +} diff --git a/test/validator_global_pragma_debug_test.go b/test/validator_global_pragma_debug_test.go new file mode 100644 index 0000000..8060cc7 --- /dev/null +++ b/test/validator_global_pragma_debug_test.go @@ -0,0 +1,65 @@ +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 TestGlobalPragmaDebug(t *testing.T) { + content := `//! allow(implicit): Debugging +//! allow(unused): Debugging ++Data={Class=ReferenceContainer} ++GAM={Class=IOGAM InputSignals={Impl={DataSource=Data Type=uint32}}} ++UnusedGAM={Class=IOGAM} +` + p := parser.NewParser(content) + config, err := p.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Check if pragma parsed + if len(config.Pragmas) == 0 { + t.Fatal("Pragma not parsed") + } + t.Logf("Parsed Pragma 0: %s", config.Pragmas[0].Text) + + idx := index.NewProjectTree() + idx.AddFile("debug.marte", config) + idx.ResolveReferences() + + // Check if added to GlobalPragmas + pragmas, ok := idx.GlobalPragmas["debug.marte"] + if !ok || len(pragmas) == 0 { + t.Fatal("GlobalPragmas not populated") + } + t.Logf("Global Pragma stored: %s", pragmas[0]) + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + v.CheckUnused() // Must call this for unused check! + + foundImplicitWarning := false + foundUnusedWarning := false + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Implicitly Defined Signal") { + foundImplicitWarning = true + t.Logf("Found warning: %s", d.Message) + } + if strings.Contains(d.Message, "Unused GAM") { + foundUnusedWarning = true + t.Logf("Found warning: %s", d.Message) + } + } + + if foundImplicitWarning { + t.Error("Expected implicit warning to be suppressed") + } + if foundUnusedWarning { + t.Error("Expected unused warning to be suppressed") + } +} diff --git a/test/validator_global_pragma_update_test.go b/test/validator_global_pragma_update_test.go new file mode 100644 index 0000000..9245bdb --- /dev/null +++ b/test/validator_global_pragma_update_test.go @@ -0,0 +1,75 @@ +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 TestGlobalPragmaUpdate(t *testing.T) { + // Scenario: Project scope. File A has pragma. File B has warning. + + fileA := "fileA.marte" + contentA_WithPragma := ` +#package my.project +//!allow(unused): Suppress +` + contentA_NoPragma := ` +#package my.project +// No pragma +` + + fileB := "fileB.marte" + contentB := ` +#package my.project ++Data={Class=ReferenceContainer +DS={Class=FileReader Filename="t" Signals={Unused={Type=uint32}}}} +` + + idx := index.NewProjectTree() + + // Helper to validate + check := func() bool { + idx.ResolveReferences() + v := validator.NewValidator(idx, ".") + v.ValidateProject() + v.CheckUnused() + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Unused Signal") { + return true // Found warning + } + } + return false + } + + // 1. Add A (with pragma) and B + pA := parser.NewParser(contentA_WithPragma) + cA, _ := pA.Parse() + idx.AddFile(fileA, cA) + + pB := parser.NewParser(contentB) + cB, _ := pB.Parse() + idx.AddFile(fileB, cB) + + if check() { + t.Error("Step 1: Expected warning to be suppressed") + } + + // 2. Update A (remove pragma) + pA2 := parser.NewParser(contentA_NoPragma) + cA2, _ := pA2.Parse() + idx.AddFile(fileA, cA2) + + if !check() { + t.Error("Step 2: Expected warning to appear") + } + + // 3. Update A (add pragma back) + idx.AddFile(fileA, cA) // Re-use config A + + if check() { + t.Error("Step 3: Expected warning to be suppressed again") + } +} diff --git a/test/validator_ignore_pragma_test.go b/test/validator_ignore_pragma_test.go new file mode 100644 index 0000000..14f16fc --- /dev/null +++ b/test/validator_ignore_pragma_test.go @@ -0,0 +1,59 @@ +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 TestIgnorePragma(t *testing.T) { + content := ` +//!ignore(unused): Suppress global unused ++Data = { + Class = ReferenceContainer + +MyDS = { + Class = FileReader + Filename = "test" + Signals = { + Unused1 = { Type = uint32 } + + //!ignore(unused): Suppress local unused + Unused2 = { Type = uint32 } + } + } +} + ++MyGAM = { + Class = IOGAM + InputSignals = { + //!ignore(implicit): Suppress local 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("ignore.marte", config) + idx.ResolveReferences() + + v := validator.NewValidator(idx, ".") + v.ValidateProject() + v.CheckUnused() + + for _, d := range v.Diagnostics { + if strings.Contains(d.Message, "Unused Signal") { + t.Errorf("Unexpected warning: %s", d.Message) + } + if strings.Contains(d.Message, "Implicitly Defined Signal") { + t.Errorf("Unexpected warning: %s", d.Message) + } + } +}