diff --git a/Makefile b/Makefile index 429415c..c48e42c 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ build: go build -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/mdt test: - go test -v ./... + go test -v ./test/... coverage: go test -cover -coverprofile=coverage.out ./test/... -coverpkg=./internal/... diff --git a/go.mod b/go.mod index 92ad08c..5726e55 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/marte-community/marte-dev-tools -go 1.25.6 +go 1.25 require cuelang.org/go v0.15.3 diff --git a/internal/formatter/formatter.go b/internal/formatter/formatter.go index 5d796f7..c2dcaae 100644 --- a/internal/formatter/formatter.go +++ b/internal/formatter/formatter.go @@ -45,17 +45,15 @@ func Format(config *parser.Configuration, w io.Writer) { } func fixComment(text string) string { - if strings.HasPrefix(text, "//!") { - if len(text) > 3 && text[3] != ' ' { - return "//! " + text[3:] - } - } else if strings.HasPrefix(text, "//#") { - if len(text) > 3 && text[3] != ' ' { - return "//# " + text[3:] - } - } else if strings.HasPrefix(text, "//") { - if len(text) > 2 && text[2] != ' ' && text[2] != '#' && text[2] != '!' { - return "// " + text[2:] + if !strings.HasPrefix(text, "//!") { + if strings.HasPrefix(text, "//#") { + if len(text) > 3 && text[3] != ' ' { + return "//# " + text[3:] + } + } else if strings.HasPrefix(text, "//") { + if len(text) > 2 && text[2] != ' ' && text[2] != '#' && text[2] != '!' { + return "// " + text[2:] + } } } return text diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 6f8741b..8330b58 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -936,6 +936,7 @@ func (v *Validator) CheckINOUTOrdering() { return } + suppress := v.isGloballyAllowed("not_consumed", v.getNodeFile(appNode)) for _, state := range statesNode.Children { var threads []*index.ProjectNode for _, child := range state.Children { @@ -961,24 +962,25 @@ func (v *Validator) CheckINOUTOrdering() { v.processGAMSignalsForOrdering(gam, "InputSignals", producedSignals, consumedSignals, true, thread, state) v.processGAMSignalsForOrdering(gam, "OutputSignals", producedSignals, consumedSignals, false, thread, state) } - - // Check for produced but not consumed - for ds, signals := range producedSignals { - for sigName, producers := range signals { - consumed := false - if cSet, ok := consumedSignals[ds]; ok { - if cSet[sigName] { - consumed = true + if !suppress { + // Check for produced but not consumed + for ds, signals := range producedSignals { + for sigName, producers := range signals { + consumed := false + if cSet, ok := consumedSignals[ds]; ok { + if cSet[sigName] { + consumed = true + } } - } - if !consumed { - for _, prod := range producers { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelWarning, - Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is produced in thread '%s' but never consumed in the same thread.", sigName, ds.RealName, thread.RealName), - Position: v.getNodePosition(prod), - File: v.getNodeFile(prod), - }) + if !consumed { + for _, prod := range producers { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelWarning, + Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is produced in thread '%s' but never consumed in the same thread.", sigName, ds.RealName, thread.RealName), + Position: v.getNodePosition(prod), + File: v.getNodeFile(prod), + }) + } } } } @@ -992,7 +994,7 @@ func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, contain if container == nil { return } - + not_produced_suppress := v.isGloballyAllowed("not_produced", v.getNodeFile(gam)) for _, sig := range container.Children { fields := v.getFields(sig) var dsNode *index.ProjectNode @@ -1033,22 +1035,31 @@ func (v *Validator) processGAMSignalsForOrdering(gam *index.ProjectNode, contain } if isInput { - isProduced := false - if set, ok := produced[dsNode]; ok { - if len(set[sigName]) > 0 { - isProduced = true + if !not_produced_suppress { + isProduced := false + if set, ok := produced[dsNode]; ok { + if len(set[sigName]) > 0 { + isProduced = true + } + } + locally_supressed := false + for _, p := range sig.Pragmas { + if strings.HasPrefix(p, "not_produced:") || strings.HasPrefix(p, "ignore(not_produced)") { + locally_supressed = true + break + } } - } - if !isProduced { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelError, - Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is consumed by GAM '%s' in thread '%s' (State '%s') before being produced by any previous GAM.", sigName, dsNode.RealName, gam.RealName, thread.RealName, state.RealName), - Position: v.getNodePosition(sig), - File: v.getNodeFile(sig), - }) - } + if !isProduced && !locally_supressed { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("INOUT Signal '%s' (DS '%s') is consumed by GAM '%s' in thread '%s' (State '%s') before being produced by any previous GAM.", sigName, dsNode.RealName, gam.RealName, thread.RealName, state.RealName), + Position: v.getNodePosition(sig), + File: v.getNodeFile(sig), + }) + } + } if consumed[dsNode] == nil { consumed[dsNode] = make(map[string]bool) } @@ -1120,16 +1131,16 @@ func (v *Validator) CheckVariables() { } v.Tree.Walk(checkNodeVars) -} - func (v *Validator) CheckUnresolvedVariables() { - for _, ref := range v.Tree.References { - if ref.IsVariable && ref.TargetVariable == nil { - v.Diagnostics = append(v.Diagnostics, Diagnostic{ - Level: LevelError, - Message: fmt.Sprintf("Unresolved variable reference: '@%s'", ref.Name), - Position: ref.Position, - File: ref.File, - }) - } - } - } +} +func (v *Validator) CheckUnresolvedVariables() { + for _, ref := range v.Tree.References { + if ref.IsVariable && ref.TargetVariable == nil { + v.Diagnostics = append(v.Diagnostics, Diagnostic{ + Level: LevelError, + Message: fmt.Sprintf("Unresolved variable reference: '@%s'", ref.Name), + Position: ref.Position, + File: ref.File, + }) + } + } +} diff --git a/test/lsp_binary_test.go b/test/lsp_binary_test.go deleted file mode 100644 index e304aa6..0000000 --- a/test/lsp_binary_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package integration - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" -) - -func TestLSPBinaryDiagnostics(t *testing.T) { - // 1. Build mdt - // Ensure we are in test directory context - buildCmd := exec.Command("go", "build", "-o", "../build/mdt", "../cmd/mdt") - if output, err := buildCmd.CombinedOutput(); err != nil { - t.Fatalf("Failed to build mdt: %v\nOutput: %s", err, output) - } - - // 2. Start mdt lsp - cmd := exec.Command("../build/mdt", "lsp") - stdin, _ := cmd.StdinPipe() - stdout, _ := cmd.StdoutPipe() - stderr, _ := cmd.StderrPipe() - - // Pipe stderr to test log for debugging - go func() { - scanner := bufio.NewScanner(stderr) - for scanner.Scan() { - t.Logf("LSP STDERR: %s", scanner.Text()) - } - }() - - if err := cmd.Start(); err != nil { - t.Fatalf("Failed to start mdt lsp: %v", err) - } - defer func() { - cmd.Process.Kill() - cmd.Wait() - }() - - reader := bufio.NewReader(stdout) - - send := func(m interface{}) { - body, _ := json.Marshal(m) - msg := fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(body), body) - stdin.Write([]byte(msg)) - } - - readCh := make(chan map[string]interface{}, 100) - - go func() { for { - // Parse Header - line, err := reader.ReadString('\n') - if err != nil { - close(readCh) - return - } - var length int - // Handle Content-Length: \r\n - if _, err := fmt.Sscanf(strings.TrimSpace(line), "Content-Length: %d", &length); err != nil { - // Maybe empty line or other header? - continue - } - - // Read until empty line (\r\n) - for { - l, err := reader.ReadString('\n') - if err != nil { - close(readCh) - return - } - if l == "\r\n" { - break - } - } - - body := make([]byte, length) - if _, err := io.ReadFull(reader, body); err != nil { - close(readCh) - return - } - - var m map[string]interface{} - if err := json.Unmarshal(body, &m); err == nil { - readCh <- m - } - } - }() - - cwd, _ := os.Getwd() - projectRoot := filepath.Dir(cwd) - absPath := filepath.Join(projectRoot, "examples/app_test.marte") - uri := "file://" + absPath - - // 3. Initialize - examplesDir := filepath.Join(projectRoot, "examples") - send(map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": map[string]interface{}{ - "rootUri": "file://" + examplesDir, - }, - }) - - // 4. Open app_test.marte - content, err := os.ReadFile(absPath) - if err != nil { - t.Fatalf("Failed to read test file: %v", err) - } - send(map[string]interface{}{ - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": map[string]interface{}{ - "textDocument": map[string]interface{}{ - "uri": uri, - "languageId": "marte", - "version": 1, - "text": string(content), - }, - }, - }) - - // 5. Wait for diagnostics - foundOrdering := false - foundVariable := false - - timeout := time.After(30 * time.Second) - - for { - select { - case msg, ok := <-readCh: - if !ok { - t.Fatal("LSP stream closed unexpectedly") - } - t.Logf("Received: %v", msg) - if method, ok := msg["method"].(string); ok && method == "textDocument/publishDiagnostics" { - params := msg["params"].(map[string]interface{}) - // Check URI match? - // if params["uri"] != uri { continue } // Might be absolute vs relative - -diags := params["diagnostics"].([]interface{}) - for _, d := range diags { - m := d.(map[string]interface{})["message"].(string) - if strings.Contains(m, "INOUT Signal 'A'") { - foundOrdering = true - t.Log("Found Ordering error") - } - if strings.Contains(m, "Unresolved variable reference: '@Value'") { - foundVariable = true - t.Log("Found Variable error") - } - } - if foundOrdering && foundVariable { - return // Success - } - } - case <-timeout: - t.Fatal("Timeout waiting for diagnostics") - } - } -} \ No newline at end of file