Compare commits

..

17 Commits

Author SHA1 Message Date
Martino Ferrari
0ffcecf19e simple makefile 2026-01-23 14:30:17 +01:00
Martino Ferrari
761cf83b8e Added *.out rule 2026-01-23 14:30:02 +01:00
Martino Ferrari
7caf3a5da5 Renamed files 2026-01-23 14:24:43 +01:00
Martino Ferrari
94ee7e4880 added support to enum in completion 2026-01-23 14:18:41 +01:00
Martino Ferrari
ce9b68200e More tests 2026-01-23 14:09:17 +01:00
Martino Ferrari
e3c84fcf60 Moved tests in test folder (and made methods public in server.go) 2026-01-23 14:04:24 +01:00
Martino Ferrari
4a515fd6c3 completion test 2026-01-23 14:01:35 +01:00
Martino Ferrari
14cba1b530 Working 2026-01-23 14:01:26 +01:00
Martino Ferrari
462c832651 improved suggestions 2026-01-23 13:20:22 +01:00
Martino Ferrari
77fe3e9cac Improved LSP reactivity 2026-01-23 13:14:34 +01:00
Martino Ferrari
0ee44c0a27 Readme file added 2026-01-23 13:02:53 +01:00
Martino Ferrari
d450d358b4 add MIT Licensing 2026-01-23 13:02:34 +01:00
Martino Ferrari
2cdcfe2812 Updated specifications 2026-01-23 13:02:12 +01:00
Martino Ferrari
ef7729475a Implemented auto completion 2026-01-23 12:01:35 +01:00
Martino Ferrari
99bd5bffdd Changed project uri 2026-01-23 11:46:59 +01:00
Martino Ferrari
4379960835 Removed wrong test 2026-01-23 11:42:34 +01:00
Martino Ferrari
2aeec1e5f6 better validation of statemachine 2026-01-23 11:42:29 +01:00
45 changed files with 1201 additions and 285 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
build build
*.log *.log
mdt mdt
*.out

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 MARTe Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
Makefile Normal file
View File

@@ -0,0 +1,24 @@
BINARY_NAME=mdt
BUILD_DIR=build
.PHONY: all build test coverage clean install
all: test build
build:
mkdir -p $(BUILD_DIR)
go build -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/mdt
test:
go test -v ./...
coverage:
go test -cover -coverprofile=coverage.out ./test/... -coverpkg=./internal/...
go tool cover -func=coverage.out
clean:
rm -rf $(BUILD_DIR)
rm -f coverage.out
install:
go install ./cmd/mdt

96
README.md Normal file
View File

@@ -0,0 +1,96 @@
# MARTe Development Tools (mdt)
`mdt` is a comprehensive toolkit for developing, validating, and building configurations for the MARTe real-time framework. It provides a CLI and a Language Server Protocol (LSP) server to enhance the development experience.
## Features
- **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, and navigation (Go to Definition/References).
- **Builder**: Merges multiple configuration files into a single, ordered output file.
- **Formatter**: Standardizes configuration file formatting.
- **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness.
## Installation
### From Source
Requirements: Go 1.21+
```bash
go install github.com/marte-community/marte-dev-tools/cmd/mdt@latest
```
## Usage
### CLI Commands
- **Check**: Run validation on a file or project.
```bash
mdt check path/to/project
```
- **Build**: Merge project files into a single output.
```bash
mdt build -o output.marte main.marte
```
- **Format**: Format configuration files.
```bash
mdt fmt path/to/file.marte
```
- **LSP**: Start the language server (used by editor plugins).
```bash
mdt lsp
```
### Editor Integration
`mdt lsp` implements the Language Server Protocol. You can use it with any LSP-compatible editor (VS Code, Neovim, Emacs, etc.).
## MARTe Configuration
The tools support the MARTe configuration format with extended features:
- **Objects**: `+Node = { Class = ... }`
- **Signals**: `Signal = { Type = ... }`
- **Namespaces**: `#package PROJECT.NODE` for organizing multi-file projects.
### Validation & Schema
Validation is fully schema-driven using CUE.
- **Built-in Schema**: Covers standard MARTe classes (`StateMachine`, `GAM`, `DataSource`, `RealTimeApplication`, etc.).
- **Custom Schema**: Add a `.marte_schema.cue` file to your project root to extend or override definitions.
**Example `.marte_schema.cue`:**
```cue
package schema
#Classes: {
MyCustomGAM: {
Param1: int
Param2?: string
...
}
}
```
### Pragmas (Suppressing Warnings)
Use comments starting with `//!` to control validation behavior:
- `//!unused: Reason` - Suppress "Unused GAM" or "Unused Signal" warnings.
- `//!implicit: Reason` - Suppress "Implicitly Defined Signal" warnings.
- `//!cast(DefinedType, UsageType)` - Allow type mismatch between definition and usage (e.g. `//!cast(uint32, int32)`).
- `//!allow(unused)` - Global suppression for the file.
## Development
### Building
```bash
go build ./cmd/mdt
```
### Running Tests
```bash
go test ./...
```
## License
MIT

View File

@@ -4,13 +4,13 @@ import (
"bytes" "bytes"
"os" "os"
"github.com/marte-dev/marte-dev-tools/internal/builder" "github.com/marte-community/marte-dev-tools/internal/builder"
"github.com/marte-dev/marte-dev-tools/internal/formatter" "github.com/marte-community/marte-dev-tools/internal/formatter"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/logger" "github.com/marte-community/marte-dev-tools/internal/logger"
"github.com/marte-dev/marte-dev-tools/internal/lsp" "github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func main() { func main() {

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/marte-dev/marte-dev-tools module github.com/marte-community/marte-dev-tools
go 1.25.6 go 1.25.6

View File

@@ -6,8 +6,8 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
type Builder struct { type Builder struct {

View File

@@ -6,7 +6,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
type Insertable struct { type Insertable struct {

View File

@@ -5,8 +5,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/marte-dev/marte-dev-tools/internal/logger" "github.com/marte-community/marte-dev-tools/internal/logger"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
type ProjectTree struct { type ProjectTree struct {

View File

@@ -7,15 +7,51 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"regexp"
"strings" "strings"
"github.com/marte-dev/marte-dev-tools/internal/formatter" "github.com/marte-community/marte-dev-tools/internal/formatter"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/logger" "github.com/marte-community/marte-dev-tools/internal/logger"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/schema"
"github.com/marte-community/marte-dev-tools/internal/validator"
"cuelang.org/go/cue"
) )
type CompletionParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"`
Context CompletionContext `json:"context,omitempty"`
}
type CompletionContext struct {
TriggerKind int `json:"triggerKind"`
}
type CompletionItem struct {
Label string `json:"label"`
Kind int `json:"kind"`
Detail string `json:"detail,omitempty"`
Documentation string `json:"documentation,omitempty"`
InsertText string `json:"insertText,omitempty"`
InsertTextFormat int `json:"insertTextFormat,omitempty"` // 1: PlainText, 2: Snippet
SortText string `json:"sortText,omitempty"`
}
type CompletionList struct {
IsIncomplete bool `json:"isIncomplete"`
Items []CompletionItem `json:"items"`
}
var Tree = index.NewProjectTree()
var Documents = make(map[string]string)
var ProjectRoot string
var GlobalSchema *schema.Schema
type JsonRpcMessage struct { type JsonRpcMessage struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
@@ -135,9 +171,6 @@ type TextEdit struct {
NewText string `json:"newText"` NewText string `json:"newText"`
} }
var tree = index.NewProjectTree()
var documents = make(map[string]string)
var projectRoot string
func RunServer() { func RunServer() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
@@ -151,7 +184,7 @@ func RunServer() {
continue continue
} }
handleMessage(msg) HandleMessage(msg)
} }
} }
@@ -181,7 +214,7 @@ func readMessage(reader *bufio.Reader) (*JsonRpcMessage, error) {
return &msg, err return &msg, err
} }
func handleMessage(msg *JsonRpcMessage) { func HandleMessage(msg *JsonRpcMessage) {
switch msg.Method { switch msg.Method {
case "initialize": case "initialize":
var params InitializeParams var params InitializeParams
@@ -194,12 +227,13 @@ func handleMessage(msg *JsonRpcMessage) {
} }
if root != "" { if root != "" {
projectRoot = root ProjectRoot = root
logger.Printf("Scanning workspace: %s\n", root) logger.Printf("Scanning workspace: %s\n", root)
if err := tree.ScanDirectory(root); err != nil { if err := Tree.ScanDirectory(root); err != nil {
logger.Printf("ScanDirectory failed: %v\n", err) logger.Printf("ScanDirectory failed: %v\n", err)
} }
tree.ResolveReferences() Tree.ResolveReferences()
GlobalSchema = schema.LoadFullSchema(ProjectRoot)
} }
} }
@@ -210,6 +244,9 @@ func handleMessage(msg *JsonRpcMessage) {
"definitionProvider": true, "definitionProvider": true,
"referencesProvider": true, "referencesProvider": true,
"documentFormattingProvider": true, "documentFormattingProvider": true,
"completionProvider": map[string]any{
"triggerCharacters": []string{"=", " "},
},
}, },
}) })
case "initialized": case "initialized":
@@ -221,18 +258,18 @@ func handleMessage(msg *JsonRpcMessage) {
case "textDocument/didOpen": case "textDocument/didOpen":
var params DidOpenTextDocumentParams var params DidOpenTextDocumentParams
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
handleDidOpen(params) HandleDidOpen(params)
} }
case "textDocument/didChange": case "textDocument/didChange":
var params DidChangeTextDocumentParams var params DidChangeTextDocumentParams
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
handleDidChange(params) HandleDidChange(params)
} }
case "textDocument/hover": case "textDocument/hover":
var params HoverParams var params HoverParams
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
logger.Printf("Hover: %s:%d", params.TextDocument.URI, params.Position.Line) logger.Printf("Hover: %s:%d", params.TextDocument.URI, params.Position.Line)
res := handleHover(params) res := HandleHover(params)
if res != nil { if res != nil {
logger.Printf("Res: %v", res.Contents) logger.Printf("Res: %v", res.Contents)
} else { } else {
@@ -246,17 +283,22 @@ func handleMessage(msg *JsonRpcMessage) {
case "textDocument/definition": case "textDocument/definition":
var params DefinitionParams var params DefinitionParams
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, handleDefinition(params)) respond(msg.ID, HandleDefinition(params))
} }
case "textDocument/references": case "textDocument/references":
var params ReferenceParams var params ReferenceParams
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, handleReferences(params)) respond(msg.ID, HandleReferences(params))
}
case "textDocument/completion":
var params CompletionParams
if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, HandleCompletion(params))
} }
case "textDocument/formatting": case "textDocument/formatting":
var params DocumentFormattingParams var params DocumentFormattingParams
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, handleFormatting(params)) respond(msg.ID, HandleFormatting(params))
} }
} }
} }
@@ -265,41 +307,51 @@ func uriToPath(uri string) string {
return strings.TrimPrefix(uri, "file://") return strings.TrimPrefix(uri, "file://")
} }
func handleDidOpen(params DidOpenTextDocumentParams) { func HandleDidOpen(params DidOpenTextDocumentParams) {
path := uriToPath(params.TextDocument.URI) path := uriToPath(params.TextDocument.URI)
documents[params.TextDocument.URI] = params.TextDocument.Text Documents[params.TextDocument.URI] = params.TextDocument.Text
p := parser.NewParser(params.TextDocument.Text) p := parser.NewParser(params.TextDocument.Text)
config, err := p.Parse() config, err := p.Parse()
if err != nil { if err != nil {
publishParserError(params.TextDocument.URI, err) publishParserError(params.TextDocument.URI, err)
return } else {
publishParserError(params.TextDocument.URI, nil)
} }
tree.AddFile(path, config)
tree.ResolveReferences() if config != nil {
Tree.AddFile(path, config)
Tree.ResolveReferences()
runValidation(params.TextDocument.URI) runValidation(params.TextDocument.URI)
}
} }
func handleDidChange(params DidChangeTextDocumentParams) { func HandleDidChange(params DidChangeTextDocumentParams) {
if len(params.ContentChanges) == 0 { if len(params.ContentChanges) == 0 {
return return
} }
text := params.ContentChanges[0].Text text := params.ContentChanges[0].Text
documents[params.TextDocument.URI] = text Documents[params.TextDocument.URI] = text
path := uriToPath(params.TextDocument.URI) path := uriToPath(params.TextDocument.URI)
p := parser.NewParser(text) p := parser.NewParser(text)
config, err := p.Parse() config, err := p.Parse()
if err != nil { if err != nil {
publishParserError(params.TextDocument.URI, err) publishParserError(params.TextDocument.URI, err)
return } else {
publishParserError(params.TextDocument.URI, nil)
} }
tree.AddFile(path, config)
tree.ResolveReferences() if config != nil {
Tree.AddFile(path, config)
Tree.ResolveReferences()
runValidation(params.TextDocument.URI) runValidation(params.TextDocument.URI)
}
} }
func handleFormatting(params DocumentFormattingParams) []TextEdit { func HandleFormatting(params DocumentFormattingParams) []TextEdit {
uri := params.TextDocument.URI uri := params.TextDocument.URI
text, ok := documents[uri] text, ok := Documents[uri]
if !ok { if !ok {
return nil return nil
} }
@@ -331,7 +383,7 @@ func handleFormatting(params DocumentFormattingParams) []TextEdit {
} }
func runValidation(uri string) { func runValidation(uri string) {
v := validator.NewValidator(tree, projectRoot) v := validator.NewValidator(Tree, ProjectRoot)
v.ValidateProject() v.ValidateProject()
v.CheckUnused() v.CheckUnused()
@@ -340,7 +392,7 @@ func runValidation(uri string) {
// Collect all known files to ensure we clear diagnostics for fixed files // Collect all known files to ensure we clear diagnostics for fixed files
knownFiles := make(map[string]bool) knownFiles := make(map[string]bool)
collectFiles(tree.Root, knownFiles) collectFiles(Tree.Root, knownFiles)
// Initialize all known files with empty diagnostics // Initialize all known files with empty diagnostics
for f := range knownFiles { for f := range knownFiles {
@@ -385,6 +437,19 @@ func runValidation(uri string) {
} }
func publishParserError(uri string, err error) { func publishParserError(uri string, err error) {
if err == nil {
notification := JsonRpcMessage{
Jsonrpc: "2.0",
Method: "textDocument/publishDiagnostics",
Params: mustMarshal(PublishDiagnosticsParams{
URI: uri,
Diagnostics: []LSPDiagnostic{},
}),
}
send(notification)
return
}
var line, col int var line, col int
var msg string var msg string
// Try parsing "line:col: message" // Try parsing "line:col: message"
@@ -436,12 +501,12 @@ func mustMarshal(v any) json.RawMessage {
return b return b
} }
func handleHover(params HoverParams) *Hover { func HandleHover(params HoverParams) *Hover {
path := uriToPath(params.TextDocument.URI) path := uriToPath(params.TextDocument.URI)
line := params.Position.Line + 1 line := params.Position.Line + 1
col := params.Position.Character + 1 col := params.Position.Character + 1
res := tree.Query(path, line, col) res := Tree.Query(path, line, col)
if res == nil { if res == nil {
logger.Printf("No object/node/reference found") logger.Printf("No object/node/reference found")
return nil return nil
@@ -488,12 +553,314 @@ func handleHover(params HoverParams) *Hover {
} }
} }
func handleDefinition(params DefinitionParams) any { func HandleCompletion(params CompletionParams) *CompletionList {
uri := params.TextDocument.URI
path := uriToPath(uri)
text, ok := Documents[uri]
if !ok {
return nil
}
lines := strings.Split(text, "\n")
if params.Position.Line >= len(lines) {
return nil
}
lineStr := lines[params.Position.Line]
col := params.Position.Character
if col > len(lineStr) {
col = len(lineStr)
}
prefix := lineStr[:col]
// Case 1: Assigning a value (Ends with "=" or "= ")
if strings.Contains(prefix, "=") {
lastIdx := strings.LastIndex(prefix, "=")
beforeEqual := prefix[:lastIdx]
// Find the last identifier before '='
key := ""
re := regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9_\-]*`)
matches := re.FindAllString(beforeEqual, -1)
if len(matches) > 0 {
key = matches[len(matches)-1]
}
if key == "Class" {
return suggestClasses()
}
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
if container != nil {
return suggestFieldValues(container, key, path)
}
return nil
}
// Case 2: Typing a key inside an object
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
if container != nil {
return suggestFields(container)
}
return nil
}
func suggestClasses() *CompletionList {
if GlobalSchema == nil {
return nil
}
classesVal := GlobalSchema.Value.LookupPath(cue.ParsePath("#Classes"))
if classesVal.Err() != nil {
return nil
}
iter, err := classesVal.Fields()
if err != nil {
return nil
}
var items []CompletionItem
for iter.Next() {
label := iter.Selector().String()
label = strings.Trim(label, "?!#")
items = append(items, CompletionItem{
Label: label,
Kind: 7, // Class
Detail: "MARTe Class",
})
}
return &CompletionList{Items: items}
}
func suggestFields(container *index.ProjectNode) *CompletionList {
cls := container.Metadata["Class"]
if cls == "" {
return &CompletionList{Items: []CompletionItem{{
Label: "Class",
Kind: 10, // Property
InsertText: "Class = ",
Detail: "Define object class",
}}}
}
if GlobalSchema == nil {
return nil
}
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s", cls))
classVal := GlobalSchema.Value.LookupPath(classPath)
if classVal.Err() != nil {
return nil
}
iter, err := classVal.Fields()
if err != nil {
return nil
}
existing := make(map[string]bool)
for _, frag := range container.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
existing[f.Name] = true
}
}
}
for name := range container.Children {
existing[name] = true
}
var items []CompletionItem
for iter.Next() {
label := iter.Selector().String()
label = strings.Trim(label, "?!#")
// Skip if already present
if existing[label] {
continue
}
isOptional := iter.IsOptional()
kind := 10 // Property
detail := "Mandatory"
if isOptional {
detail = "Optional"
}
insertText := label + " = "
val := iter.Value()
if val.Kind() == cue.StructKind {
// Suggest as node
insertText = "+" + label + " = {\n\t$0\n}"
kind = 9 // Module
}
items = append(items, CompletionItem{
Label: label,
Kind: kind,
Detail: detail,
InsertText: insertText,
InsertTextFormat: 2, // Snippet
})
}
return &CompletionList{Items: items}
}
func suggestFieldValues(container *index.ProjectNode, field string, path string) *CompletionList {
var root *index.ProjectNode
if iso, ok := Tree.IsolatedFiles[path]; ok {
root = iso
} else {
root = Tree.Root
}
if field == "DataSource" {
return suggestObjects(root, "DataSource")
}
if field == "Functions" {
return suggestObjects(root, "GAM")
}
if field == "Type" {
return suggestSignalTypes()
}
if list := suggestCUEEnums(container, field); list != nil {
return list
}
return nil
}
func suggestSignalTypes() *CompletionList {
types := []string{
"uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64",
"float32", "float64", "string", "bool", "char8",
}
var items []CompletionItem
for _, t := range types {
items = append(items, CompletionItem{
Label: t,
Kind: 13, // EnumMember
Detail: "Signal Type",
})
}
return &CompletionList{Items: items}
}
func suggestCUEEnums(container *index.ProjectNode, field string) *CompletionList {
if GlobalSchema == nil {
return nil
}
cls := container.Metadata["Class"]
if cls == "" {
return nil
}
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.%s", cls, field))
val := GlobalSchema.Value.LookupPath(classPath)
if val.Err() != nil {
return nil
}
op, args := val.Expr()
var values []cue.Value
if op == cue.OrOp {
values = args
} else {
values = []cue.Value{val}
}
var items []CompletionItem
for _, v := range values {
if !v.IsConcrete() {
continue
}
str, err := v.String() // Returns quoted string for string values?
if err != nil {
continue
}
// Ensure strings are quoted
if v.Kind() == cue.StringKind && !strings.HasPrefix(str, "\"") {
str = fmt.Sprintf("\"%s\"", str)
}
items = append(items, CompletionItem{
Label: str,
Kind: 13, // EnumMember
Detail: "Enum Value",
})
}
if len(items) > 0 {
return &CompletionList{Items: items}
}
return nil
}
func suggestObjects(root *index.ProjectNode, filter string) *CompletionList {
if root == nil {
return nil
}
var items []CompletionItem
var walk func(*index.ProjectNode)
walk = func(node *index.ProjectNode) {
match := false
if filter == "GAM" {
if isGAM(node) {
match = true
}
} else if filter == "DataSource" {
if isDataSource(node) {
match = true
}
}
if match {
items = append(items, CompletionItem{
Label: node.Name,
Kind: 6, // Variable
Detail: node.Metadata["Class"],
})
}
for _, child := range node.Children {
walk(child)
}
}
walk(root)
return &CompletionList{Items: items}
}
func isGAM(node *index.ProjectNode) bool {
if node.RealName == "" || (node.RealName[0] != '+' && node.RealName[0] != '$') {
return false
}
_, hasInput := node.Children["InputSignals"]
_, hasOutput := node.Children["OutputSignals"]
return hasInput || hasOutput
}
func isDataSource(node *index.ProjectNode) bool {
if node.Parent != nil && node.Parent.Name == "Data" {
return true
}
_, hasSignals := node.Children["Signals"]
return hasSignals
}
func HandleDefinition(params DefinitionParams) any {
path := uriToPath(params.TextDocument.URI) path := uriToPath(params.TextDocument.URI)
line := params.Position.Line + 1 line := params.Position.Line + 1
col := params.Position.Character + 1 col := params.Position.Character + 1
res := tree.Query(path, line, col) res := Tree.Query(path, line, col)
if res == nil { if res == nil {
return nil return nil
} }
@@ -528,12 +895,12 @@ func handleDefinition(params DefinitionParams) any {
return nil return nil
} }
func handleReferences(params ReferenceParams) []Location { func HandleReferences(params ReferenceParams) []Location {
path := uriToPath(params.TextDocument.URI) path := uriToPath(params.TextDocument.URI)
line := params.Position.Line + 1 line := params.Position.Line + 1
col := params.Position.Character + 1 col := params.Position.Character + 1
res := tree.Query(path, line, col) res := Tree.Query(path, line, col)
if res == nil { if res == nil {
return nil return nil
} }
@@ -571,7 +938,7 @@ func handleReferences(params ReferenceParams) []Location {
} }
// 1. References from index (Aliases) // 1. References from index (Aliases)
for _, ref := range tree.References { for _, ref := range Tree.References {
if ref.Target == canonical { if ref.Target == canonical {
locations = append(locations, Location{ locations = append(locations, Location{
URI: "file://" + ref.File, URI: "file://" + ref.File,
@@ -584,7 +951,7 @@ func handleReferences(params ReferenceParams) []Location {
} }
// 2. References from Node Targets (Direct References) // 2. References from Node Targets (Direct References)
tree.Walk(func(node *index.ProjectNode) { Tree.Walk(func(node *index.ProjectNode) {
if node.Target == canonical { if node.Target == canonical {
for _, frag := range node.Fragments { for _, frag := range node.Fragments {
if frag.IsObject { if frag.IsObject {
@@ -638,9 +1005,9 @@ func formatNodeInfo(node *index.ProjectNode) string {
// Find references // Find references
var refs []string var refs []string
for _, ref := range tree.References { for _, ref := range Tree.References {
if ref.Target == node { if ref.Target == node {
container := tree.GetNodeContaining(ref.File, ref.Position) container := Tree.GetNodeContaining(ref.File, ref.Position)
if container != nil { if container != nil {
threadName := "" threadName := ""
stateName := "" stateName := ""

View File

@@ -11,6 +11,7 @@ type Parser struct {
buf []Token buf []Token
comments []Comment comments []Comment
pragmas []Pragma pragmas []Pragma
errors []error
} }
func NewParser(input string) *Parser { func NewParser(input string) *Parser {
@@ -19,6 +20,10 @@ func NewParser(input string) *Parser {
} }
} }
func (p *Parser) addError(pos Position, msg string) {
p.errors = append(p.errors, fmt.Errorf("%d:%d: %s", pos.Line, pos.Column, msg))
}
func (p *Parser) next() Token { func (p *Parser) next() Token {
if len(p.buf) > 0 { if len(p.buf) > 0 {
t := p.buf[0] t := p.buf[0]
@@ -71,72 +76,82 @@ func (p *Parser) Parse() (*Configuration, error) {
continue continue
} }
def, err := p.parseDefinition() def, ok := p.parseDefinition()
if err != nil { if ok {
return nil, err
}
config.Definitions = append(config.Definitions, def) config.Definitions = append(config.Definitions, def)
} else {
// Synchronization: skip token if not consumed to make progress
if p.peek() == tok {
p.next()
}
}
} }
config.Comments = p.comments config.Comments = p.comments
config.Pragmas = p.pragmas config.Pragmas = p.pragmas
return config, nil
var err error
if len(p.errors) > 0 {
err = p.errors[0]
}
return config, err
} }
func (p *Parser) parseDefinition() (Definition, error) { func (p *Parser) parseDefinition() (Definition, bool) {
tok := p.next() tok := p.next()
switch tok.Type { switch tok.Type {
case TokenIdentifier: case TokenIdentifier:
// Could be Field = Value OR Node = { ... }
name := tok.Value name := tok.Value
if p.next().Type != TokenEqual { if p.peek().Type != TokenEqual {
return nil, fmt.Errorf("%d:%d: expected =", tok.Position.Line, tok.Position.Column) p.addError(tok.Position, "expected =")
return nil, false
} }
p.next() // Consume =
// Disambiguate based on RHS
nextTok := p.peek() nextTok := p.peek()
if nextTok.Type == TokenLBrace { if nextTok.Type == TokenLBrace {
// Check if it looks like a Subnode (contains definitions) or Array (contains values)
if p.isSubnodeLookahead() { if p.isSubnodeLookahead() {
sub, err := p.parseSubnode() sub, ok := p.parseSubnode()
if err != nil { if !ok {
return nil, err return nil, false
} }
return &ObjectNode{ return &ObjectNode{
Position: tok.Position, Position: tok.Position,
Name: name, Name: name,
Subnode: sub, Subnode: sub,
}, nil }, true
} }
} }
// Default to Field val, ok := p.parseValue()
val, err := p.parseValue() if !ok {
if err != nil { return nil, false
return nil, err
} }
return &Field{ return &Field{
Position: tok.Position, Position: tok.Position,
Name: name, Name: name,
Value: val, Value: val,
}, nil }, true
case TokenObjectIdentifier: case TokenObjectIdentifier:
// node = subnode
name := tok.Value name := tok.Value
if p.next().Type != TokenEqual { if p.peek().Type != TokenEqual {
return nil, fmt.Errorf("%d:%d: expected =", tok.Position.Line, tok.Position.Column) p.addError(tok.Position, "expected =")
return nil, false
} }
sub, err := p.parseSubnode() p.next() // Consume =
if err != nil {
return nil, err sub, ok := p.parseSubnode()
if !ok {
return nil, false
} }
return &ObjectNode{ return &ObjectNode{
Position: tok.Position, Position: tok.Position,
Name: name, Name: name,
Subnode: sub, Subnode: sub,
}, nil }, true
default: default:
return nil, fmt.Errorf("%d:%d: unexpected token %v", tok.Position.Line, tok.Position.Column, tok.Value) p.addError(tok.Position, fmt.Sprintf("unexpected token %v", tok.Value))
return nil, false
} }
} }
@@ -176,10 +191,11 @@ func (p *Parser) isSubnodeLookahead() bool {
return false return false
} }
func (p *Parser) parseSubnode() (Subnode, error) { func (p *Parser) parseSubnode() (Subnode, bool) {
tok := p.next() tok := p.next()
if tok.Type != TokenLBrace { if tok.Type != TokenLBrace {
return Subnode{}, fmt.Errorf("%d:%d: expected {", tok.Position.Line, tok.Position.Column) p.addError(tok.Position, "expected {")
return Subnode{}, false
} }
sub := Subnode{Position: tok.Position} sub := Subnode{Position: tok.Position}
for { for {
@@ -190,18 +206,23 @@ func (p *Parser) parseSubnode() (Subnode, error) {
break break
} }
if t.Type == TokenEOF { if t.Type == TokenEOF {
return sub, fmt.Errorf("%d:%d: unexpected EOF, expected }", t.Position.Line, t.Position.Column) p.addError(t.Position, "unexpected EOF, expected }")
} sub.EndPosition = t.Position
def, err := p.parseDefinition() return sub, true
if err != nil {
return sub, err
} }
def, ok := p.parseDefinition()
if ok {
sub.Definitions = append(sub.Definitions, def) sub.Definitions = append(sub.Definitions, def)
} else {
if p.peek() == t {
p.next()
} }
return sub, nil }
}
return sub, true
} }
func (p *Parser) parseValue() (Value, error) { func (p *Parser) parseValue() (Value, bool) {
tok := p.next() tok := p.next()
switch tok.Type { switch tok.Type {
case TokenString: case TokenString:
@@ -209,24 +230,21 @@ func (p *Parser) parseValue() (Value, error) {
Position: tok.Position, Position: tok.Position,
Value: strings.Trim(tok.Value, "\""), Value: strings.Trim(tok.Value, "\""),
Quoted: true, Quoted: true,
}, nil }, true
case TokenNumber: case TokenNumber:
// Simplistic handling
if strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") { if strings.Contains(tok.Value, ".") || strings.Contains(tok.Value, "e") {
f, _ := strconv.ParseFloat(tok.Value, 64) f, _ := strconv.ParseFloat(tok.Value, 64)
return &FloatValue{Position: tok.Position, Value: f, Raw: tok.Value}, nil return &FloatValue{Position: tok.Position, Value: f, Raw: tok.Value}, true
} }
i, _ := strconv.ParseInt(tok.Value, 0, 64) i, _ := strconv.ParseInt(tok.Value, 0, 64)
return &IntValue{Position: tok.Position, Value: i, Raw: tok.Value}, nil return &IntValue{Position: tok.Position, Value: i, Raw: tok.Value}, true
case TokenBool: case TokenBool:
return &BoolValue{Position: tok.Position, Value: tok.Value == "true"}, return &BoolValue{Position: tok.Position, Value: tok.Value == "true"},
nil true
case TokenIdentifier: case TokenIdentifier:
// reference? return &ReferenceValue{Position: tok.Position, Value: tok.Value}, true
return &ReferenceValue{Position: tok.Position, Value: tok.Value}, nil
case TokenLBrace: case TokenLBrace:
// array
arr := &ArrayValue{Position: tok.Position} arr := &ArrayValue{Position: tok.Position}
for { for {
t := p.peek() t := p.peek()
@@ -239,14 +257,15 @@ func (p *Parser) parseValue() (Value, error) {
p.next() p.next()
continue continue
} }
val, err := p.parseValue() val, ok := p.parseValue()
if err != nil { if !ok {
return nil, err return nil, false
} }
arr.Elements = append(arr.Elements, val) arr.Elements = append(arr.Elements, val)
} }
return arr, nil return arr, true
default: default:
return nil, fmt.Errorf("%d:%d: unexpected value token %v", tok.Position.Line, tok.Position.Column, tok.Value) p.addError(tok.Position, fmt.Sprintf("unexpected value token %v", tok.Value))
return nil, false
} }
} }

View File

@@ -7,7 +7,27 @@ package schema
States!: {...} // type: node States!: {...} // type: node
... ...
} }
Message: {
...
}
StateMachineEvent: {
NextState!: string
NextStateError!: string
Timeout: uint32
[_= !~"^(Class|NextState|Timeout|NextStateError|[#_$].+)$"]: Message
...
}
_State: {
Class: "ReferenceContainer"
ENTER?: {
Class: "ReferenceContainer"
...
}
[_ = !~"^(Class|ENTER)$"]: StateMachineEvent
...
}
StateMachine: { StateMachine: {
[_ = !~"^(Class|[$].*)$"]: _State
... ...
} }
RealTimeState: { RealTimeState: {

View File

@@ -8,9 +8,9 @@ import (
"cuelang.org/go/cue" "cuelang.org/go/cue"
"cuelang.org/go/cue/errors" "cuelang.org/go/cue/errors"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/schema" "github.com/marte-community/marte-dev-tools/internal/schema"
) )
type DiagnosticLevel int type DiagnosticLevel int

View File

@@ -29,7 +29,12 @@ The LSP server should provide the following capabilities:
- **Go to Definition**: Jump to the definition of a reference, supporting navigation across any file in the current project. - **Go to Definition**: Jump to the definition of a reference, supporting navigation across any file in the current project.
- **Go to References**: Find usages of a node or field, supporting navigation across any file in the current project. - **Go to References**: Find usages of a node or field, supporting navigation across any file in the current project.
- **Code Completion**: Autocomplete fields, values, and references. - **Code Completion**: Autocomplete fields, values, and references.
- **Code Snippets**: Provide snippets for common patterns. - **Context-Aware**: Suggestions depend on the cursor position (e.g., inside an object, assigning a value).
- **Schema-Driven**: Field suggestions are derived from the CUE schema for the current object's Class, indicating mandatory vs. optional fields.
- **Reference Suggestions**:
- `DataSource` fields suggest available DataSource objects.
- `Functions` (in Threads) suggest available GAM objects.
- **Code Snippets**: Provide snippets for common patterns (e.g., `+Object = { ... }`).
- **Formatting**: Format the document using the same rules and engine as the `fmt` command. - **Formatting**: Format the document using the same rules and engine as the `fmt` command.
## Build System & File Structure ## Build System & File Structure
@@ -47,9 +52,9 @@ The LSP server should provide the following capabilities:
- **Namespace Consistency**: The build tool must verify that all input files belong to the same project namespace (the first segment of the `#package` URI). If multiple project namespaces are detected, the build must fail with an error. - **Namespace Consistency**: The build tool must verify that all input files belong to the same project namespace (the first segment of the `#package` URI). If multiple project namespaces are detected, the build must fail with an error.
- **Target**: The build output is written to a single target file (e.g., provided via CLI or API). - **Target**: The build output is written to a single target file (e.g., provided via CLI or API).
- **Multi-File Definitions**: Nodes and objects can be defined across multiple files. The build tool, validator, and LSP must merge these definitions (including all fields and sub-nodes) from the entire project to create a unified view before processing or validating. - **Multi-File Definitions**: Nodes and objects can be defined across multiple files. The build tool, validator, and LSP must merge these definitions (including all fields and sub-nodes) from the entire project to create a unified view before processing or validating.
- **Global References**: References to nodes, signals, or objects can point to definitions located in any file within the project. - **Global References**: References to nodes, signals, or objects can point to definitions located in any file within the project. Support for dot-separated paths (e.g., `Node.SubNode`) is required.
- **Merging Order**: For objects defined across multiple files, the **first file** to be considered is the one containing the `Class` field definition. - **Merging Order**: For objects defined across multiple files, definitions are merged. The build tool must preserve the relative order of fields and sub-nodes as they appear in the source files, interleaving them correctly in the final output.
- **Field Order**: Within a single file, the relative order of defined fields must be maintained. - **Field Order**: Within a single file (and across merged files), the relative order of defined fields must be maintained in the output.
- The LSP indexes only files belonging to the same project/namespace scope. - The LSP indexes only files belonging to the same project/namespace scope.
- **Output**: The output format is the same as the input configuration but without the `#package` macro. - **Output**: The output format is the same as the input configuration but without the `#package` macro.
@@ -160,13 +165,13 @@ The tool must build an index of the configuration to support LSP features and va
- **Field Order**: Verification that specific fields appear in a prescribed order when required by the class definition. - **Field Order**: Verification that specific fields appear in a prescribed order when required by the class definition.
- **Conditional Fields**: Validation of fields whose presence or value depends on the values of other fields within the same node or context. - **Conditional Fields**: Validation of fields whose presence or value depends on the values of other fields within the same node or context.
- **Schema Definition**: - **Schema Definition**:
- Class validation rules must be defined in a separate schema file. - Class validation rules must be defined in a separate schema file using the **CUE** language.
- **Project-Specific Classes**: Developers can define their own project-specific classes and corresponding validation rules, expanding the validation capabilities for their specific needs. - **Project-Specific Classes**: Developers can define their own project-specific classes and corresponding validation rules, expanding the validation capabilities for their specific needs.
- **Schema Loading**: - **Schema Loading**:
- **Default Schema**: The tool should look for a default schema file `marte_schema.json` in standard system locations: - **Default Schema**: The tool should look for a default schema file `marte_schema.cue` in standard system locations:
- `/usr/share/mdt/marte_schema.json` - `/usr/share/mdt/marte_schema.cue`
- `$HOME/.local/share/mdt/marte_schema.json` - `$HOME/.local/share/mdt/marte_schema.cue`
- **Project Schema**: If a file named `.marte_schema.json` exists in the project root, it must be loaded. - **Project Schema**: If a file named `.marte_schema.cue` exists in the project root, it must be loaded.
- **Merging**: The final schema is a merge of the built-in schema, the system default schema (if found), and the project-specific schema. Rules in later sources (Project > System > Built-in) append to or override earlier ones. - **Merging**: The final schema is a merge of the built-in schema, the system default schema (if found), and the project-specific schema. Rules in later sources (Project > System > Built-in) append to or override earlier ones.
- **Duplicate Fields**: - **Duplicate Fields**:
- **Constraint**: A field must not be defined more than once within the same object/node scope, even if those definitions are spread across different files. - **Constraint**: A field must not be defined more than once within the same object/node scope, even if those definitions are spread across different files.

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/builder" "github.com/marte-community/marte-dev-tools/internal/builder"
) )
func TestMultiFileBuildMergeAndOrder(t *testing.T) { func TestMultiFileBuildMergeAndOrder(t *testing.T) {

View File

@@ -7,11 +7,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/builder" "github.com/marte-community/marte-dev-tools/internal/builder"
"github.com/marte-dev/marte-dev-tools/internal/formatter" "github.com/marte-community/marte-dev-tools/internal/formatter"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestCheckCommand(t *testing.T) { func TestCheckCommand(t *testing.T) {

320
test/lsp_completion_test.go Normal file
View File

@@ -0,0 +1,320 @@
package integration
import (
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-community/marte-dev-tools/internal/schema"
)
func TestHandleCompletion(t *testing.T) {
setup := func() {
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema()
}
uri := "file://test.marte"
path := "test.marte"
t.Run("Suggest Classes", func(t *testing.T) {
setup()
content := "+Obj = { Class = "
lsp.Documents[uri] = content
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 0, Character: len(content)},
}
list := lsp.HandleCompletion(params)
if list == nil || len(list.Items) == 0 {
t.Fatal("Expected class suggestions, got none")
}
found := false
for _, item := range list.Items {
if item.Label == "RealTimeApplication" {
found = true
break
}
}
if !found {
t.Error("Expected RealTimeApplication in class suggestions")
}
})
t.Run("Suggest Fields", func(t *testing.T) {
setup()
content := `
+MyApp = {
Class = RealTimeApplication
}
`
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, _ := p.Parse()
lsp.Tree.AddFile(path, cfg)
// Position at line 3 (empty line inside MyApp)
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 3, Character: 4},
}
list := lsp.HandleCompletion(params)
if list == nil || len(list.Items) == 0 {
t.Fatal("Expected field suggestions, got none")
}
foundData := false
for _, item := range list.Items {
if item.Label == "Data" {
foundData = true
if item.Detail != "Mandatory" {
t.Errorf("Expected Data to be Mandatory, got %s", item.Detail)
}
}
}
if !foundData {
t.Error("Expected 'Data' in field suggestions for RealTimeApplication")
}
})
t.Run("Suggest References (DataSource)", func(t *testing.T) {
setup()
content := `
$App = {
$Data = {
+InDS = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
}
}
+MyGAM = {
Class = IOGAM
+InputSignals = {
S1 = { DataSource = }
}
}
`
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, _ := p.Parse()
lsp.Tree.AddFile(path, cfg)
lsp.Tree.ResolveReferences()
// Position at end of "DataSource = "
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 14, Character: 28},
}
list := lsp.HandleCompletion(params)
if list == nil || len(list.Items) == 0 {
t.Fatal("Expected DataSource suggestions, got none")
}
foundDS := false
for _, item := range list.Items {
if item.Label == "InDS" {
foundDS = true
break
}
}
if !foundDS {
t.Error("Expected 'InDS' in suggestions for DataSource field")
}
})
t.Run("Filter Existing Fields", func(t *testing.T) {
setup()
content := `
+MyThread = {
Class = RealTimeThread
Functions = { }
}
`
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, _ := p.Parse()
lsp.Tree.AddFile(path, cfg)
// Position at line 4
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 4},
}
list := lsp.HandleCompletion(params)
for _, item := range list.Items {
if item.Label == "Functions" || item.Label == "Class" {
t.Errorf("Did not expect already defined field %s in suggestions", item.Label)
}
}
})
t.Run("Scope-aware suggestions", func(t *testing.T) {
setup()
// Define a project DataSource in one file
cfg1, _ := parser.NewParser("#package MYPROJ.Data\n+ProjectDS = { Class = FileReader +Signals = { S1 = { Type = int32 } } }").Parse()
lsp.Tree.AddFile("project_ds.marte", cfg1)
// Define an isolated file
contentIso := "+MyGAM = { Class = IOGAM +InputSignals = { S1 = { DataSource = } } }"
lsp.Documents["file://iso.marte"] = contentIso
cfg2, _ := parser.NewParser(contentIso).Parse()
lsp.Tree.AddFile("iso.marte", cfg2)
lsp.Tree.ResolveReferences()
// Completion in isolated file
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: "file://iso.marte"},
Position: lsp.Position{Line: 0, Character: strings.Index(contentIso, "DataSource = ") + len("DataSource = ") + 1},
}
list := lsp.HandleCompletion(params)
foundProjectDS := false
if list != nil {
for _, item := range list.Items {
if item.Label == "ProjectDS" {
foundProjectDS = true
break
}
}
}
if foundProjectDS {
t.Error("Did not expect ProjectDS in isolated file suggestions")
}
// Completion in a project file
lineContent := "+MyGAM = { Class = IOGAM +InputSignals = { S1 = { DataSource = Dummy } } }"
contentPrj := "#package MYPROJ.App\n" + lineContent
lsp.Documents["file://prj.marte"] = contentPrj
pPrj := parser.NewParser(contentPrj)
cfg3, err := pPrj.Parse()
if err != nil {
t.Logf("Parser error in contentPrj: %v", err)
}
lsp.Tree.AddFile("prj.marte", cfg3)
lsp.Tree.ResolveReferences()
paramsPrj := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: "file://prj.marte"},
Position: lsp.Position{Line: 1, Character: strings.Index(lineContent, "Dummy")},
}
listPrj := lsp.HandleCompletion(paramsPrj)
foundProjectDS = false
if listPrj != nil {
for _, item := range listPrj.Items {
if item.Label == "ProjectDS" {
foundProjectDS = true
break
}
}
}
if !foundProjectDS {
t.Error("Expected ProjectDS in project file suggestions")
}
})
t.Run("Suggest Signal Types", func(t *testing.T) {
setup()
content := `
+DS = {
Class = FileReader
Signals = {
S1 = { Type = }
}
}
`
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, _ := p.Parse()
lsp.Tree.AddFile(path, cfg)
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: strings.Index(content, "Type = ") + len("Type = ") + 1},
}
list := lsp.HandleCompletion(params)
if list == nil {
t.Fatal("Expected signal type suggestions")
}
foundUint32 := false
for _, item := range list.Items {
if item.Label == "uint32" {
foundUint32 = true
break
}
}
if !foundUint32 {
t.Error("Expected uint32 in suggestions")
}
})
t.Run("Suggest CUE Enums", func(t *testing.T) {
setup()
// Inject custom schema with enum
custom := []byte(`
package schema
#Classes: {
TestEnumClass: {
Mode: "Auto" | "Manual"
}
}
`)
val := lsp.GlobalSchema.Context.CompileBytes(custom)
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
content := `
+Obj = {
Class = TestEnumClass
Mode =
}
`
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, _ := p.Parse()
lsp.Tree.AddFile(path, cfg)
params := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 3, Character: strings.Index(content, "Mode = ") + len("Mode = ") + 1},
}
list := lsp.HandleCompletion(params)
if list == nil {
t.Fatal("Expected enum suggestions")
}
foundAuto := false
for _, item := range list.Items {
if item.Label == "\"Auto\"" { // CUE string value includes quotes
foundAuto = true
break
}
}
if !foundAuto {
// Check if it returned without quotes?
// v.String() returns quoted for string.
t.Error("Expected \"Auto\" in suggestions")
for _, item := range list.Items {
t.Logf("Suggestion: %s", item.Label)
}
}
})
}

View File

@@ -3,8 +3,8 @@ package integration
import ( import (
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
func TestLSPHoverDoc(t *testing.T) { func TestLSPHoverDoc(t *testing.T) {

View File

@@ -3,8 +3,8 @@ package integration
import ( import (
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
func TestGetNodeContaining(t *testing.T) { func TestGetNodeContaining(t *testing.T) {

View File

@@ -1,4 +1,4 @@
package lsp package integration
import ( import (
"encoding/json" "encoding/json"
@@ -7,8 +7,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/lsp"
"github.com/marte-community/marte-dev-tools/internal/parser"
) )
func TestInitProjectScan(t *testing.T) { func TestInitProjectScan(t *testing.T) {
@@ -24,50 +25,38 @@ func TestInitProjectScan(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// File 2: Reference // File 2: Reference
// +Source = { Class = C Link = Target }
// Link = Target starts at index ...
// #package Test.Common (21 chars including newline)
// +Source = { Class = C Link = Target }
// 012345678901234567890123456789012345
// Previous offset was 29.
// Now add 21?
// #package Test.Common\n
// +Source = ...
// So add 21 to Character? Or Line 1?
// It's on Line 1 (0-based 1).
if err := os.WriteFile(filepath.Join(tmpDir, "ref.marte"), []byte("#package Test.Common\n+Source = { Class = C Link = Target }"), 0644); err != nil { if err := os.WriteFile(filepath.Join(tmpDir, "ref.marte"), []byte("#package Test.Common\n+Source = { Class = C Link = Target }"), 0644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// 2. Initialize // 2. Initialize
tree = index.NewProjectTree() // Reset global tree lsp.Tree = index.NewProjectTree() // Reset global tree
initParams := InitializeParams{RootPath: tmpDir} initParams := lsp.InitializeParams{RootPath: tmpDir}
paramsBytes, _ := json.Marshal(initParams) paramsBytes, _ := json.Marshal(initParams)
msg := &JsonRpcMessage{ msg := &lsp.JsonRpcMessage{
Method: "initialize", Method: "initialize",
Params: paramsBytes, Params: paramsBytes,
ID: 1, ID: 1,
} }
handleMessage(msg) lsp.HandleMessage(msg)
// Query the reference in ref.marte at "Target" // Query the reference in ref.marte at "Target"
// Target starts at index 29 (0-based) on Line 1 defParams := lsp.DefinitionParams{
defParams := DefinitionParams{ TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + filepath.Join(tmpDir, "ref.marte")},
TextDocument: TextDocumentIdentifier{URI: "file://" + filepath.Join(tmpDir, "ref.marte")}, Position: lsp.Position{Line: 1, Character: 29},
Position: Position{Line: 1, Character: 29},
} }
res := handleDefinition(defParams) res := lsp.HandleDefinition(defParams)
if res == nil { if res == nil {
t.Fatal("Definition not found via LSP after initialization") t.Fatal("Definition not found via LSP after initialization")
} }
locs, ok := res.([]Location) locs, ok := res.([]lsp.Location)
if !ok { if !ok {
t.Fatalf("Expected []Location, got %T", res) t.Fatalf("Expected []lsp.Location, got %T", res)
} }
if len(locs) == 0 { if len(locs) == 0 {
@@ -83,7 +72,7 @@ func TestInitProjectScan(t *testing.T) {
func TestHandleDefinition(t *testing.T) { func TestHandleDefinition(t *testing.T) {
// Reset tree for test // Reset tree for test
tree = index.NewProjectTree() lsp.Tree = index.NewProjectTree()
content := ` content := `
+MyObject = { +MyObject = {
@@ -100,28 +89,28 @@ func TestHandleDefinition(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Parse failed: %v", err) t.Fatalf("Parse failed: %v", err)
} }
tree.AddFile(path, config) lsp.Tree.AddFile(path, config)
tree.ResolveReferences() lsp.Tree.ResolveReferences()
t.Logf("Refs: %d", len(tree.References)) t.Logf("Refs: %d", len(lsp.Tree.References))
for _, r := range tree.References { for _, r := range lsp.Tree.References {
t.Logf(" %s at %d:%d", r.Name, r.Position.Line, r.Position.Column) t.Logf(" %s at %d:%d", r.Name, r.Position.Line, r.Position.Column)
} }
// Test Go to Definition on MyObject reference // Test Go to Definition on MyObject reference
params := DefinitionParams{ params := lsp.DefinitionParams{
TextDocument: TextDocumentIdentifier{URI: "file://" + path}, TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + path},
Position: Position{Line: 6, Character: 15}, // "MyObject" in RefField = MyObject Position: lsp.Position{Line: 6, Character: 15}, // "MyObject" in RefField = MyObject
} }
result := handleDefinition(params) result := lsp.HandleDefinition(params)
if result == nil { if result == nil {
t.Fatal("handleDefinition returned nil") t.Fatal("HandleDefinition returned nil")
} }
locations, ok := result.([]Location) locations, ok := result.([]lsp.Location)
if !ok { if !ok {
t.Fatalf("Expected []Location, got %T", result) t.Fatalf("Expected []lsp.Location, got %T", result)
} }
if len(locations) != 1 { if len(locations) != 1 {
@@ -135,7 +124,7 @@ func TestHandleDefinition(t *testing.T) {
func TestHandleReferences(t *testing.T) { func TestHandleReferences(t *testing.T) {
// Reset tree for test // Reset tree for test
tree = index.NewProjectTree() lsp.Tree = index.NewProjectTree()
content := ` content := `
+MyObject = { +MyObject = {
@@ -155,17 +144,17 @@ func TestHandleReferences(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Parse failed: %v", err) t.Fatalf("Parse failed: %v", err)
} }
tree.AddFile(path, config) lsp.Tree.AddFile(path, config)
tree.ResolveReferences() lsp.Tree.ResolveReferences()
// Test Find References for MyObject (triggered from its definition) // Test Find References for MyObject (triggered from its definition)
params := ReferenceParams{ params := lsp.ReferenceParams{
TextDocument: TextDocumentIdentifier{URI: "file://" + path}, TextDocument: lsp.TextDocumentIdentifier{URI: "file://" + path},
Position: Position{Line: 1, Character: 1}, // "+MyObject" Position: lsp.Position{Line: 1, Character: 1}, // "+MyObject"
Context: ReferenceContext{IncludeDeclaration: true}, Context: lsp.ReferenceContext{IncludeDeclaration: true},
} }
locations := handleReferences(params) locations := lsp.HandleReferences(params)
if len(locations) != 3 { // 1 declaration + 2 references if len(locations) != 3 { // 1 declaration + 2 references
t.Fatalf("Expected 3 locations, got %d", len(locations)) t.Fatalf("Expected 3 locations, got %d", len(locations))
} }
@@ -181,15 +170,15 @@ Field=1
` `
uri := "file:///test.marte" uri := "file:///test.marte"
// Open (populate documents map) // Open (populate Documents map)
documents[uri] = content lsp.Documents[uri] = content
// Format // Format
params := DocumentFormattingParams{ params := lsp.DocumentFormattingParams{
TextDocument: TextDocumentIdentifier{URI: uri}, TextDocument: lsp.TextDocumentIdentifier{URI: uri},
} }
edits := handleFormatting(params) edits := lsp.HandleFormatting(params)
if len(edits) != 1 { if len(edits) != 1 {
t.Fatalf("Expected 1 edit, got %d", len(edits)) t.Fatalf("Expected 1 edit, got %d", len(edits))

View File

@@ -3,9 +3,9 @@ package integration
import ( import (
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestLSPSignalReferences(t *testing.T) { func TestLSPSignalReferences(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"io/ioutil" "io/ioutil"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
// Helper to load and parse a file // Helper to load and parse a file

View File

@@ -1,9 +1,9 @@
package parser_test package integration
import ( import (
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
func TestParserStrictness(t *testing.T) { func TestParserStrictness(t *testing.T) {

View File

@@ -1,7 +1,9 @@
package parser package integration
import ( import (
"testing" "testing"
"github.com/marte-community/marte-dev-tools/internal/parser"
) )
func TestParseBasic(t *testing.T) { func TestParseBasic(t *testing.T) {
@@ -22,7 +24,7 @@ $Node2 = {
Array = {1 2 3} Array = {1 2 3}
} }
` `
p := NewParser(input) p := parser.NewParser(input)
config, err := p.Parse() config, err := p.Parse()
if err != nil { if err != nil {
t.Fatalf("Parse error: %v", err) t.Fatalf("Parse error: %v", err)

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestMDSWriterValidation(t *testing.T) { func TestMDSWriterValidation(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestPIDGAMValidation(t *testing.T) { func TestPIDGAMValidation(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestRealTimeApplicationValidation(t *testing.T) { func TestRealTimeApplicationValidation(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestSDNSubscriberValidation(t *testing.T) { func TestSDNSubscriberValidation(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestFunctionsArrayValidation(t *testing.T) { func TestFunctionsArrayValidation(t *testing.T) {

View File

@@ -0,0 +1,85 @@
package integration
import (
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-community/marte-dev-tools/internal/validator"
)
func TestGAMSignalDirectionality(t *testing.T) {
content := `
$App = {
$Data = {
+InDS = { Class = FileReader Filename="f" +Signals = { S1 = { Type = uint32 } } }
+OutDS = { Class = FileWriter Filename="f" +Signals = { S1 = { Type = uint32 } } }
+InOutDS = { Class = FileDataSource Filename="f" +Signals = { S1 = { Type = uint32 } } }
}
+ValidGAM = {
Class = IOGAM
InputSignals = {
S1 = { DataSource = InDS }
S2 = { DataSource = InOutDS Alias = S1 }
}
OutputSignals = {
S3 = { DataSource = OutDS Alias = S1 }
S4 = { DataSource = InOutDS Alias = S1 }
}
}
+InvalidGAM = {
Class = IOGAM
InputSignals = {
BadIn = { DataSource = OutDS Alias = S1 }
}
OutputSignals = {
BadOut = { DataSource = InDS Alias = S1 }
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("dir.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
// Check ValidGAM has NO directionality errors
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "is Output-only but referenced in InputSignals") ||
strings.Contains(d.Message, "is Input-only but referenced in OutputSignals") {
if strings.Contains(d.Message, "ValidGAM") {
t.Errorf("Unexpected direction error for ValidGAM: %s", d.Message)
}
}
}
// Check InvalidGAM HAS errors
foundBadIn := false
foundBadOut := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "InvalidGAM") {
if strings.Contains(d.Message, "is Output-only but referenced in InputSignals") {
foundBadIn = true
}
if strings.Contains(d.Message, "is Input-only but referenced in OutputSignals") {
foundBadOut = true
}
}
}
if !foundBadIn {
t.Error("Expected error for OutDS in InputSignals of InvalidGAM")
}
if !foundBadOut {
t.Error("Expected error for InDS in OutputSignals of InvalidGAM")
}
}

View File

@@ -3,9 +3,9 @@ package integration
import ( import (
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestGAMSignalLinking(t *testing.T) { func TestGAMSignalLinking(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestGAMSignalValidation(t *testing.T) { func TestGAMSignalValidation(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestGlobalPragmaDebug(t *testing.T) { func TestGlobalPragmaDebug(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestGlobalPragma(t *testing.T) { func TestGlobalPragma(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestGlobalPragmaUpdate(t *testing.T) { func TestGlobalPragmaUpdate(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestIgnorePragma(t *testing.T) { func TestIgnorePragma(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestImplicitSignal(t *testing.T) { func TestImplicitSignal(t *testing.T) {

View File

@@ -5,9 +5,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func parseAndAddToIndex(t *testing.T, idx *index.ProjectTree, filePath string) { func parseAndAddToIndex(t *testing.T, idx *index.ProjectTree, filePath string) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestPragmaSuppression(t *testing.T) { func TestPragmaSuppression(t *testing.T) {

View File

@@ -6,9 +6,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestProjectSpecificSchema(t *testing.T) { func TestProjectSpecificSchema(t *testing.T) {

View File

@@ -4,44 +4,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestSchemaValidationMandatory(t *testing.T) {
// StateMachine requires "States"
content := `
+MySM = {
Class = StateMachine
// Missing States
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("test.marte", config)
v := validator.NewValidator(idx, ".")
v.ValidateProject()
found := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "States: field is required") {
found = true
break
}
}
if !found {
t.Error("Expected error for missing mandatory field 'States', but found none")
}
}
func TestSchemaValidationType(t *testing.T) { func TestSchemaValidationType(t *testing.T) {
// OrderedClass: First (int), Second (string) // OrderedClass: First (int), Second (string)
content := ` content := `

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestSignalProperties(t *testing.T) { func TestSignalProperties(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestSignalValidation(t *testing.T) { func TestSignalValidation(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestSignalsContentValidation(t *testing.T) { func TestSignalsContentValidation(t *testing.T) {

View File

@@ -3,9 +3,9 @@ package integration
import ( import (
"testing" "testing"
"github.com/marte-dev/marte-dev-tools/internal/index" "github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator" "github.com/marte-community/marte-dev-tools/internal/validator"
) )
func TestUnusedGAM(t *testing.T) { func TestUnusedGAM(t *testing.T) {