Compare commits
17 Commits
5853365707
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffcecf19e | ||
|
|
761cf83b8e | ||
|
|
7caf3a5da5 | ||
|
|
94ee7e4880 | ||
|
|
ce9b68200e | ||
|
|
e3c84fcf60 | ||
|
|
4a515fd6c3 | ||
|
|
14cba1b530 | ||
|
|
462c832651 | ||
|
|
77fe3e9cac | ||
|
|
0ee44c0a27 | ||
|
|
d450d358b4 | ||
|
|
2cdcfe2812 | ||
|
|
ef7729475a | ||
|
|
99bd5bffdd | ||
|
|
4379960835 | ||
|
|
2aeec1e5f6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
build
|
build
|
||||||
*.log
|
*.log
|
||||||
mdt
|
mdt
|
||||||
|
*.out
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
24
Makefile
Normal 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
96
README.md
Normal 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
|
||||||
@@ -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
2
go.mod
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, ¶ms); err == nil {
|
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||||
handleDidOpen(params)
|
HandleDidOpen(params)
|
||||||
}
|
}
|
||||||
case "textDocument/didChange":
|
case "textDocument/didChange":
|
||||||
var params DidChangeTextDocumentParams
|
var params DidChangeTextDocumentParams
|
||||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||||
handleDidChange(params)
|
HandleDidChange(params)
|
||||||
}
|
}
|
||||||
case "textDocument/hover":
|
case "textDocument/hover":
|
||||||
var params HoverParams
|
var params HoverParams
|
||||||
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
if err := json.Unmarshal(msg.Params, ¶ms); 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, ¶ms); err == nil {
|
if err := json.Unmarshal(msg.Params, ¶ms); 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, ¶ms); err == nil {
|
if err := json.Unmarshal(msg.Params, ¶ms); err == nil {
|
||||||
respond(msg.ID, handleReferences(params))
|
respond(msg.ID, HandleReferences(params))
|
||||||
|
}
|
||||||
|
case "textDocument/completion":
|
||||||
|
var params CompletionParams
|
||||||
|
if err := json.Unmarshal(msg.Params, ¶ms); 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, ¶ms); err == nil {
|
if err := json.Unmarshal(msg.Params, ¶ms); 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != nil {
|
||||||
|
Tree.AddFile(path, config)
|
||||||
|
Tree.ResolveReferences()
|
||||||
|
runValidation(params.TextDocument.URI)
|
||||||
}
|
}
|
||||||
tree.AddFile(path, config)
|
|
||||||
tree.ResolveReferences()
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != nil {
|
||||||
|
Tree.AddFile(path, config)
|
||||||
|
Tree.ResolveReferences()
|
||||||
|
runValidation(params.TextDocument.URI)
|
||||||
}
|
}
|
||||||
tree.AddFile(path, config)
|
|
||||||
tree.ResolveReferences()
|
|
||||||
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 := ""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
} else {
|
||||||
|
// Synchronization: skip token if not consumed to make progress
|
||||||
|
if p.peek() == tok {
|
||||||
|
p.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
config.Definitions = append(config.Definitions, def)
|
|
||||||
}
|
}
|
||||||
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
|
||||||
|
return sub, true
|
||||||
}
|
}
|
||||||
def, err := p.parseDefinition()
|
def, ok := p.parseDefinition()
|
||||||
if err != nil {
|
if ok {
|
||||||
return sub, err
|
sub.Definitions = append(sub.Definitions, def)
|
||||||
|
} else {
|
||||||
|
if p.peek() == t {
|
||||||
|
p.next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sub.Definitions = append(sub.Definitions, def)
|
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
320
test/lsp_completion_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -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)
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
85
test/validator_gam_direction_test.go
Normal file
85
test/validator_gam_direction_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 := `
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user