Compare commits

...

24 Commits

Author SHA1 Message Date
Martino Ferrari
71c86f1dcb removed examples 2026-01-28 13:44:15 +01:00
Martino Ferrari
ab22a939d7 improved init 2026-01-28 13:44:05 +01:00
Martino Ferrari
01bcd66594 Improving CLI tool and improving documentation 2026-01-28 13:32:32 +01:00
Martino Ferrari
31996ae710 minor improvement on the cue schema validator 2026-01-28 01:18:26 +01:00
Martino Ferrari
776b1fddc3 removed project node from output 2026-01-28 01:18:09 +01:00
Martino Ferrari
597fd3eddf improved sdnpublisher schema 2026-01-28 00:07:10 +01:00
Martino Ferrari
6781d50ee4 Minor changes 2026-01-27 15:39:25 +01:00
Martino Ferrari
1d7dc665d6 More tests on AST 2026-01-27 15:31:01 +01:00
Martino Ferrari
4ea406a17b more tests 2026-01-27 15:27:34 +01:00
Martino Ferrari
fed39467fd improved doc and tests 2026-01-27 15:19:49 +01:00
Martino Ferrari
15afdc91f4 Improved performances and hover 2026-01-27 15:14:47 +01:00
Martino Ferrari
213fc81cfb Improving LSP 2026-01-27 14:42:46 +01:00
Martino Ferrari
71a3c40108 Better LSP error handling 2026-01-27 08:58:38 +01:00
Martino Ferrari
aedc715ef3 Better code 2026-01-27 00:04:36 +01:00
Martino Ferrari
73cfc43f4b Updated readme. 2026-01-26 23:27:01 +01:00
Martino Ferrari
599beb6f4f updated license 2026-01-26 14:25:47 +01:00
Martino Ferrari
30a105df63 updated readme 2026-01-26 14:24:36 +01:00
Martino Ferrari
04196d8a1f Implement better completion 2026-01-25 15:21:38 +01:00
Martino Ferrari
02274f1bbf Implemented suggestion / autocompletion for signal in GAM 2026-01-25 00:28:50 +01:00
Martino Ferrari
12ed4cfbd2 reverse symbol renaming for signals 2026-01-25 00:18:40 +01:00
Martino Ferrari
bbeb344d19 Improved indexing, hover documentation and implemente renaming 2026-01-25 00:13:07 +01:00
Martino Ferrari
eeb4f5da2e added gam referencing 2026-01-24 23:47:59 +01:00
Martino Ferrari
8e13020d50 better signal hover message 2026-01-24 21:37:08 +01:00
Martino Ferrari
c9cc67f663 Minimal changes 2026-01-24 15:33:23 +01:00
45 changed files with 3180 additions and 6615 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 MARTe Community Copyright (c) 2026 Martino G. Ferrari <manda.mgf@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,9 +1,9 @@
BINARY_NAME=mdt BINARY_NAME=mdt
BUILD_DIR=build BUILD_DIR=build
.PHONY: all build test coverage clean install .PHONY: all build test coverage clean install vet fmt
all: test build all: vet test build
build: build:
mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)
@@ -16,6 +16,12 @@ coverage:
go test -cover -coverprofile=coverage.out ./test/... -coverpkg=./internal/... go test -cover -coverprofile=coverage.out ./test/... -coverpkg=./internal/...
go tool cover -func=coverage.out go tool cover -func=coverage.out
vet:
go vet ./...
fmt:
go fmt ./...
clean: clean:
rm -rf $(BUILD_DIR) rm -rf $(BUILD_DIR)
rm -f coverage.out rm -f coverage.out

View File

@@ -4,11 +4,29 @@
## Features ## Features
- **Portability**: A single statically compiled executable compatible with any Linux 3.2+ machine (as well as possible to compile and run on Windows and Mac OS X)
- **LSP Server**: Real-time syntax checking, validation, autocomplete, hover documentation, and navigation (Go to Definition/References). - **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. - **Builder**: Merges multiple configuration files into a single, ordered output file.
- **Formatter**: Standardizes configuration file formatting. - **Formatter**: Standardizes configuration file formatting.
- **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness. - **Validator**: Advanced semantic validation using [CUE](https://cuelang.org/) schemas, ensuring type safety and structural correctness.
### MARTe extended configuration language
Few additional features have been added to the standard MARTe configuration language:
- Multi file configuration support
- Multi file definition merging
- File level namespace / node
- Doc-strings support
- Pragmas for warning suppression / documentation
## Documentation
- [Step-by-Step Tutorial](docs/TUTORIAL.md)
- [Editor Integration Guide](docs/EDITOR_INTEGRATION.md)
- [Configuration Guide](docs/CONFIGURATION_GUIDE.md)
- [Examples Readme](/examples/README.md)
## Installation ## Installation
### From Source ### From Source
@@ -23,13 +41,17 @@ go install github.com/marte-community/marte-dev-tools/cmd/mdt@latest
### CLI Commands ### CLI Commands
- **Init**: Initialize a MARTe project.
```bash
mdt init project_name
```
- **Check**: Run validation on a file or project. - **Check**: Run validation on a file or project.
```bash ```bash
mdt check path/to/project mdt check path/to/project
``` ```
- **Build**: Merge project files into a single output. - **Build**: Merge project files into a single output.
```bash ```bash
mdt build -o output.marte main.marte mdt build [-o output.marte] main.marte ...
``` ```
- **Format**: Format configuration files. - **Format**: Format configuration files.
```bash ```bash
@@ -47,6 +69,7 @@ go install github.com/marte-community/marte-dev-tools/cmd/mdt@latest
## MARTe Configuration ## MARTe Configuration
The tools support the MARTe configuration format with extended features: The tools support the MARTe configuration format with extended features:
- **Objects**: `+Node = { Class = ... }` - **Objects**: `+Node = { Class = ... }`
- **Signals**: `Signal = { Type = ... }` - **Signals**: `Signal = { Type = ... }`
- **Namespaces**: `#package PROJECT.NODE` for organizing multi-file projects. - **Namespaces**: `#package PROJECT.NODE` for organizing multi-file projects.
@@ -59,11 +82,16 @@ Validation is fully schema-driven using CUE.
- **Custom Schema**: Add a `.marte_schema.cue` file to your project root to extend or override definitions. - **Custom Schema**: Add a `.marte_schema.cue` file to your project root to extend or override definitions.
**Example `.marte_schema.cue`:** **Example `.marte_schema.cue`:**
```cue ```cue
package schema package schema
#Classes: { #Classes: {
MyCustomGAM: { MyCustomGAM: {
#meta: {
direction: "INOUT"
multithreaded: true
}
Param1: int Param1: int
Param2?: string Param2?: string
... ...
@@ -83,14 +111,17 @@ Use comments starting with `//!` to control validation behavior:
## Development ## Development
### Building ### Building
```bash ```bash
go build ./cmd/mdt go build ./cmd/mdt
``` ```
### Running Tests ### Running Tests
```bash ```bash
go test ./... go test ./...
``` ```
## License ## License
MIT MIT

View File

@@ -16,7 +16,11 @@ import (
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
logger.Println("Usage: mdt <command> [arguments]") logger.Println("Usage: mdt <command> [arguments]")
logger.Println("Commands: lsp, build, check, fmt") logger.Println("Commands: lsp, build, check, fmt, init")
logger.Println(" build [-o output_file] <input_files...>")
logger.Println(" check <input_files...>")
logger.Println(" fmt <input_files...>")
logger.Println(" init <project_name>")
os.Exit(1) os.Exit(1)
} }
@@ -30,6 +34,8 @@ func main() {
runCheck(os.Args[2:]) runCheck(os.Args[2:])
case "fmt": case "fmt":
runFmt(os.Args[2:]) runFmt(os.Args[2:])
case "init":
runInit(os.Args[2:])
default: default:
logger.Printf("Unknown command: %s\n", command) logger.Printf("Unknown command: %s\n", command)
os.Exit(1) os.Exit(1)
@@ -42,12 +48,45 @@ func runLSP() {
func runBuild(args []string) { func runBuild(args []string) {
if len(args) < 1 { if len(args) < 1 {
logger.Println("Usage: mdt build <input_files...>") logger.Println("Usage: mdt build [-o output_file] <input_files...>")
os.Exit(1) os.Exit(1)
} }
b := builder.NewBuilder(args) var outputFilePath string
err := b.Build(os.Stdout) var inputFiles []string
for i := 0; i < len(args); i++ {
if args[i] == "-o" {
if i+1 < len(args) {
outputFilePath = args[i+1]
i++
} else {
logger.Println("Error: -o requires a file path")
os.Exit(1)
}
} else {
inputFiles = append(inputFiles, args[i])
}
}
if len(inputFiles) < 1 {
logger.Println("Usage: mdt build [-o output_file] <input_files...>")
os.Exit(1)
}
output := os.Stdout
if outputFilePath != "" {
f, err := os.Create(outputFilePath)
if err != nil {
logger.Printf("Error creating output file %s: %v\n", outputFilePath, err)
os.Exit(1)
}
defer f.Close()
output = f
}
b := builder.NewBuilder(inputFiles)
err := b.Build(output)
if err != nil { if err != nil {
logger.Printf("Build failed: %v\n", err) logger.Printf("Build failed: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -61,7 +100,6 @@ func runCheck(args []string) {
} }
tree := index.NewProjectTree() tree := index.NewProjectTree()
// configs := make(map[string]*parser.Configuration) // We don't strictly need this map if we just build the tree
for _, file := range args { for _, file := range args {
content, err := os.ReadFile(file) content, err := os.ReadFile(file)
@@ -80,14 +118,9 @@ func runCheck(args []string) {
tree.AddFile(file, config) tree.AddFile(file, config)
} }
// idx.ResolveReferences() // Not implemented in new tree yet, but Validator uses Tree directly
v := validator.NewValidator(tree, ".") v := validator.NewValidator(tree, ".")
v.ValidateProject() v.ValidateProject()
// Legacy loop removed as ValidateProject covers it via recursion
v.CheckUnused()
for _, diag := range v.Diagnostics { for _, diag := range v.Diagnostics {
level := "ERROR" level := "ERROR"
if diag.Level == validator.LevelWarning { if diag.Level == validator.LevelWarning {
@@ -134,3 +167,32 @@ func runFmt(args []string) {
logger.Printf("Formatted %s\n", file) logger.Printf("Formatted %s\n", file)
} }
} }
func runInit(args []string) {
if len(args) < 1 {
logger.Println("Usage: mdt init <project_name>")
os.Exit(1)
}
projectName := args[0]
if err := os.MkdirAll("src", 0755); err != nil {
logger.Fatalf("Error creating project directories: %v", err)
}
files := map[string]string{
"Makefile": "MDT=mdt\n\nall: check build\n\ncheck:\n\t$(MDT) check src/*.marte\n\nbuild:\n\t$(MDT) build -o app.marte src/*.marte\n\nfmt:\n\t$(MDT) fmt src/*.marte\n",
".marte_schema.cue": "package schema\n\n#Classes: {\n // Add your project-specific classes here\n}\n",
"src/app.marte": "#package " + projectName + "\n\n+App = {\n Class = RealTimeApplication\n +Data = {\n Class = ReferenceContainer\n }\n +Functions = {\n Class = ReferenceContainer\n }\n +States = {\n Class = ReferenceContainer\n }\n +Scheduler = {\n Class = GAMScheduler\n TimingDataSource = TimingDataSource\n }\n}\n",
"src/data.marte": "#package " + projectName + ".App.Data\n\n// Define your DataSources here\nDefaultDataSource = DDB\n//# Default DB\n+DDB = {\n Class=GAMDataSource\n}\n//# Timing Data Source to track threads timings\n+TimingDataSource = {\n Class = TimingDataSource\n}",
"src/functions.marte": "#package " + projectName + ".App.Functions\n\n// Define your GAMs here\n",
}
for path, content := range files {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
logger.Fatalf("Error creating file %s: %v", path, err)
}
logger.Printf("Created %s\n", path)
}
logger.Printf("Project '%s' initialized successfully.\n", projectName)
}

106
docs/CODE_DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,106 @@
# mdt Internal Code Documentation
This document provides a detailed overview of the `mdt` codebase architecture and internal components.
## Architecture Overview
`mdt` is built as a modular system where core functionalities are separated into internal packages. The data flow typically follows this pattern:
1. **Parsing**: Source code is parsed into an Abstract Syntax Tree (AST).
2. **Indexing**: ASTs from multiple files are aggregated into a unified `ProjectTree`.
3. **Processing**: The `ProjectTree` is used by the Validator, Builder, and LSP server to perform their respective tasks.
## Package Structure
```
cmd/
mdt/ # Application entry point (CLI)
internal/
builder/ # Logic for merging and building configurations
formatter/ # Code formatting engine
index/ # Symbol table and project structure management
logger/ # Centralized logging
lsp/ # Language Server Protocol implementation
parser/ # Lexer, Parser, and AST definitions
schema/ # CUE schema loading and integration
validator/ # Semantic analysis and validation logic
```
## Core Packages
### 1. `internal/parser`
Responsible for converting MARTe configuration text into structured data.
* **Lexer (`lexer.go`)**: Tokenizes the input stream. Handles MARTe specific syntax like `#package`, `//!` pragmas, and `//#` docstrings. Supports standard identifiers and `#`-prefixed identifiers.
* **Parser (`parser.go`)**: Recursive descent parser. Converts tokens into a `Configuration` object containing definitions, comments, and pragmas.
* **AST (`ast.go`)**: Defines the node types (`ObjectNode`, `Field`, `Value`, etc.). All nodes implement the `Node` interface providing position information.
### 2. `internal/index`
The brain of the system. It maintains a holistic view of the project.
* **ProjectTree**: The central data structure. It holds the root of the configuration hierarchy (`Root`), references, and isolated files.
* **ProjectNode**: Represents a logical node in the configuration. Since a node can be defined across multiple files (fragments), `ProjectNode` aggregates these fragments.
* **NodeMap**: A hash map index (`map[string][]*ProjectNode`) for $O(1)$ symbol lookups, optimizing `FindNode` operations.
* **Reference Resolution**: The `ResolveReferences` method links `Reference` objects to their target `ProjectNode` using the `NodeMap`.
### 3. `internal/validator`
Ensures configuration correctness.
* **Validator**: Iterates over the `ProjectTree` to check rules.
* **Checks**:
* **Structure**: Duplicate fields, invalid content.
* **Schema**: Unifies nodes with CUE schemas (loaded via `internal/schema`) to validate types and mandatory fields.
* **Signals**: Verifies that signals referenced in GAMs exist in DataSources and match types.
* **Threading**: Checks `checkDataSourceThreading` to ensure non-multithreaded DataSources are not shared across threads in the same state.
* **Unused**: Detects unused GAMs and Signals (suppressible via pragmas).
### 4. `internal/lsp`
Implements the Language Server Protocol.
* **Server (`server.go`)**: Handles JSON-RPC messages over stdio.
* **Incremental Sync**: Supports `textDocumentSync: 2`. `HandleDidChange` applies patches to the in-memory document buffers using `offsetAt` logic.
* **Features**:
* `HandleCompletion`: Context-aware suggestions (Schema fields, Signal references, Class names).
* `HandleHover`: Shows documentation, signal types, and usage analysis (e.g., "Used in GAMs: Controller (Input)").
* `HandleDefinition` / `HandleReferences`: specific lookup using the `index`.
### 5. `internal/builder`
Merges multiple MARTe files into a single output.
* **Logic**: It parses all input files, builds a temporary `ProjectTree`, and then reconstructs the source code.
* **Merging**: It interleaves fields and subnodes from different file fragments to produce a coherent single-file configuration, respecting the `#package` hierarchy.
### 6. `internal/schema`
Manages CUE schemas.
* **Loading**: Loads the embedded default schema (`marte.cue`) and merges it with any user-provided `.marte_schema.cue`.
* **Metadata**: Handles the `#meta` field in schemas to extract properties like `direction` and `multithreaded` support for the validator.
## Key Data Flows
### Reference Resolution
1. **Scan**: Files are parsed and added to the `ProjectTree`.
2. **Index**: `RebuildIndex` populates `NodeMap`.
3. **Resolve**: `ResolveReferences` iterates all recorded references (values) and calls `FindNode`.
4. **Link**: If found, `ref.Target` is set to the `ProjectNode`.
### Validation Lifecycle
1. `mdt check` or LSP `didChange` triggers validation.
2. A new `Validator` is created with the current `Tree`.
3. `ValidateProject` is called.
4. It walks the tree, runs checks, and populates `Diagnostics`.
5. Diagnostics are printed (CLI) or published via `textDocument/publishDiagnostics` (LSP).
### Threading Check Logic
1. Finds the `RealTimeApplication` node.
2. Iterates through `States` and `Threads`.
3. For each Thread, resolves the `Functions` (GAMs).
4. For each GAM, resolves connected `DataSources` via Input/Output signals.
5. Maps `DataSource -> Thread` within the context of a State.
6. If a DataSource is seen in >1 Thread, it checks the `#meta.multithreaded` property. If false (default), an error is raised.

162
docs/CONFIGURATION_GUIDE.md Normal file
View File

@@ -0,0 +1,162 @@
# MARTe Configuration Guide
This guide explains the syntax, features, and best practices for writing MARTe configurations using `mdt`.
## 1. Syntax Overview
MARTe configurations use a hierarchical object-oriented syntax.
### Objects (Nodes)
Objects are defined using `+` (public/instantiated) or `$` (template/class-like) prefixes. Every object **must** have a `Class` field.
```marte
+MyObject = {
Class = MyClass
Field1 = 100
Field2 = "Hello"
}
```
### Fields and Values
- **Fields**: Alphanumeric identifiers (e.g., `Timeout`, `CycleTime`).
- **Values**:
- Integers: `10`, `-5`, `0xFA`
- Floats: `3.14`, `1e-3`
- Strings: `"Text"`
- Booleans: `true`, `false`
- References: `MyObject`, `MyObject.SubNode`
- Arrays: `{ 1 2 3 }` or `{ "A" "B" }`
### Comments and Documentation
- Line comments: `// This is a comment`
- Docstrings: `//# This documents the following node`. These appear in hover tooltips.
```marte
//# This is the main application
+App = { ... }
```
## 2. Signals and Data Flow
Signals define how data moves between DataSources (drivers) and GAMs (algorithms).
### Defining Signals
Signals are typically defined in a `DataSource`. They must have a `Type`.
```marte
+MyDataSource = {
Class = GAMDataSource
Signals = {
Signal1 = { Type = uint32 }
Signal2 = { Type = float32 }
}
}
```
### Using Signals in GAMs
GAMs declare inputs and outputs. You can refer to signals directly or alias them.
```marte
+MyGAM = {
Class = IOGAM
InputSignals = {
Signal1 = {
DataSource = MyDataSource
Type = uint32 // Must match DataSource definition
}
MyAlias = {
Alias = Signal2
DataSource = MyDataSource
Type = float32
}
}
}
```
### Threading Rules
**Validation Rule**: A DataSource that is **not** marked as multithreaded (default) cannot be used by GAMs running in different threads within the same State.
To allow sharing, the DataSource class in the schema must have `#meta: multithreaded: true`.
## 3. Schemas and Validation
`mdt` validates your configuration against CUE schemas.
### Built-in Schema
Common classes (`RealTimeApplication`, `StateMachine`, `IOGAM`, etc.) are built-in.
### Custom Schemas
You can extend the schema by creating a `.marte_schema.cue` file in your project root.
**Example: Adding a custom GAM**
```cue
package schema
#Classes: {
MyCustomGAM: {
// Metadata for Validator/LSP
#meta: {
direction: "INOUT" // "IN", "OUT", "INOUT"
multithreaded: false
}
// Fields
Gain: float
Offset?: float // Optional
InputSignals: {...}
OutputSignals: {...}
}
}
```
## 4. Multi-file Projects
You can split your configuration into multiple files.
### Namespaces
Use `#package` to define where the file's content fits in the hierarchy.
**file1.marte**
```marte
#package MyApp.Controller
+MyController = { ... }
```
This places `MyController` under `MyApp.Controller`.
### Building
The `build` command merges all files.
```bash
mdt build -o final.marte src/*.marte
```
## 5. Pragmas (Suppressing Warnings)
If validation is too strict, you can suppress warnings using pragmas (`//!`).
- **Suppress Unused Warning**:
```marte
+MyGAM = {
Class = IOGAM
//! ignore(unused): This GAM is triggered externally
}
```
- **Suppress Implicit Signal Warning**:
```marte
InputSignals = {
//! ignore(implicit)
ImplicitSig = { Type = uint32 }
}
```
- **Type Casting**:
```marte
Sig1 = {
//! cast(uint32, int32): Intentional mismatch
DataSource = DS
Type = int32
}
```

158
docs/EDITOR_INTEGRATION.md Normal file
View File

@@ -0,0 +1,158 @@
# Editor Integration Guide
`mdt` includes a Language Server Protocol (LSP) implementation that provides features like:
- Syntax highlighting and error reporting
- Auto-completion
- Go to Definition / References
- Hover documentation
- Symbol renaming
The LSP server is started via the command:
```bash
mdt lsp
```
It communicates via **stdio**.
## VS Code
You can use a generic LSP extension like [Generic LSP Client](https://marketplace.visualstudio.com/items?itemName=summne.vscode-generic-lsp-client) or configure a custom task.
**Using "Run on Save" or similar extensions is an option, but for true LSP support:**
1. Install the **"glspc"** (Generic LSP Client) extension or similar.
2. Configure it in your `settings.json`:
```json
"glspc.languageServer configurations": [
{
"languageId": "marte",
"command": "mdt",
"args": ["lsp"],
"rootUri": "${workspaceFolder}"
}
]
```
3. Associate `.marte` files with the language ID:
```json
"files.associations": {
"*.marte": "marte"
}
```
## Neovim (Native LSP)
Add the following to your `init.lua` or `init.vim` (using `nvim-lspconfig`):
```lua
local lspconfig = require'lspconfig'
local configs = require'lspconfig.configs'
if not configs.marte then
configs.marte = {
default_config = {
cmd = {'mdt', 'lsp'},
filetypes = {'marte'},
root_dir = lspconfig.util.root_pattern('.git', 'go.mod', '.marte_schema.cue'),
settings = {},
},
}
end
lspconfig.marte.setup{}
-- Add filetype detection
vim.cmd([[
autocmd BufNewFile,BufRead *.marte setfiletype marte
]])
```
## Helix
Add this to your `languages.toml` (usually in `~/.config/helix/languages.toml`):
```toml
[[language]]
name = "marte"
scope = "source.marte"
injection-regex = "marte"
file-types = ["marte"]
roots = [".git", ".marte_schema.cue"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-servers = [ "mdt-lsp" ]
[language-server.mdt-lsp]
command = "mdt"
args = ["lsp"]
```
## Vim
### Using `vim-lsp`
```vim
if executable('mdt')
au User lsp_setup call lsp#register_server({
\ 'name': 'mdt-lsp',
\ 'cmd': {server_info->['mdt', 'lsp']},
\ 'whitelist': ['marte'],
\ })
endif
au BufRead,BufNewFile *.marte set filetype=marte
```
### Using `ALE`
```vim
call ale#linter#define('marte', {
\ 'name': 'mdt',
\ 'lsp': 'stdio',
\ 'executable': 'mdt',
\ 'command': '%e lsp',
\ 'project_root': function('ale#handlers#python#FindProjectRoot'),
\})
```
## Zed
Add to your `settings.json`:
```json
"lsp": {
"marte": {
"binary": {
"path": "mdt",
"arguments": ["lsp"]
}
}
}
```
## Kakoune (kak-lsp)
In your `kak-lsp.toml`:
```toml
[language.marte]
filetypes = ["marte"]
roots = [".git", ".marte_schema.cue"]
command = "mdt"
args = ["lsp"]
```
## Eclipse
1. Install **LSP4E** plugin.
2. Go to **Preferences > Language Servers**.
3. Add a new Language Server:
- **Content Type**: Text / Custom (Associate `*.marte` with a content type).
- **Launch configuration**: Program.
- **Command**: `mdt`
- **Arguments**: `lsp`
- **Input/Output**: Standard Input/Output.

173
docs/TUTORIAL.md Normal file
View File

@@ -0,0 +1,173 @@
# Creating a MARTe Application with mdt
This tutorial will guide you through creating, building, and validating a complete MARTe application using the `mdt` toolset.
## Prerequisites
- `mdt` installed and available in your PATH.
- `make` (optional but recommended).
## Step 1: Initialize the Project
Start by creating a new project named `MyControlApp`.
```bash
mdt init MyControlApp
cd MyControlApp
```
This command creates a standard project structure:
- `Makefile`: For building and checking the project.
- `.marte_schema.cue`: For defining custom schemas (if needed).
- `src/app.marte`: The main application definition.
- `src/components.marte`: A placeholder for defining components (DataSources).
## Step 2: Define Components
Open `src/components.marte`. This file uses the `#package App.Data` namespace, meaning all definitions here will be children of `App.Data`.
Let's define a **Timer** (input source) and a **Logger** (output destination).
```marte
#package MyContollApp.App.Data
+DDB = {
Class = GAMDataSource
}
+TimingDataSource = {
Class = TimingDataSource
}
+Timer = {
Class = LinuxTimer
Signals = {
Counter = {
Type = uint32
}
Time = {
Type = uint32
}
}
}
+Logger = {
Class = LoggerDataSource
Signals = {
LogValue = {
Type = float32
}
}
}
```
## Step 3: Implement Logic (GAM)
Open `src/app.marte`. This file defines the `App` node.
We will add a GAM that takes the time from the Timer, converts it, and logs it.
Add the GAM definition inside the `+Main` object (or as a separate object if you prefer modularity). Let's modify `src/app.marte`:
```marte
#package MyContollApp
+App = {
Class = RealTimeApplication
+Functions = {
Class = RefenceContainer
// Define the GAM
+Converter = {
Class = IOGAM
InputSignals = {
TimeIn = {
DataSource = Timer
Type = uint32
Frequency = 100 //Hz
Alias = Time // Refers to 'Time' signal in Timer
}
}
OutputSignals = {
LogOut = {
DataSource = Logger
Type = float32
Alias = LogValue
}
}
}
}
+States = {
Class = ReferenceContainer
+Run = {
Class = RealTimeState
+MainThread = {
Class = RealTimeThread
Functions = { Converter } // Run our GAM
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = TimingDataSource
}
}
```
## Step 4: Validate
Run the validation check to ensure everything is correct (types match, references are valid).
```bash
mdt check src/*.marte
```
Or using Make:
```bash
make check
```
If you made a mistake (e.g., mismatched types), `mdt` will report an error.
## Step 5: Build
Merge all files into a single configuration file.
```bash
mdt build -o final_app.marte src/*.marte
```
Or using Make:
```bash
make build
```
This produces `app.marte` (or `final_app.marte`), which contains the flattened, merged configuration ready for the MARTe framework.
## Step 6: Advanced - Custom Schema
Suppose you want to enforce that your DataSources support multithreading. You can modify `.marte_schema.cue`.
```cue
package schema
#Classes: {
// Enforce that LinuxTimer must be multithreaded (example)
LinuxTimer: {
#meta: {
multithreaded: true
}
...
}
}
```
Now, if you use `LinuxTimer` in multiple threads, `mdt check` will allow it (because of `#meta.multithreaded: true`). By default, it would disallow it.
## Conclusion
You have successfully initialized, implemented, validated, and built a MARTe application using `mdt`.

44
examples/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Examples
This directory contains example projects demonstrating different features and usage patterns of `mdt`.
## Directory Structure
```
examples/
simple/ # A basic, single-file application
complex/ # A multi-file project with custom schema
README.md # This file
```
## Running Examples
Prerequisite: `mdt` must be built (or installed). The Makefiles in the examples assume `mdt` is available at `../../build/mdt`.
### Simple Project
Demonstrates a minimal setup:
- Single `main.marte` file.
- Basic Thread and GAM definition.
**Run:**
```bash
cd simple
make check
make build
```
### Complex Project
Demonstrates advanced features:
- **Multi-file Structure**: `src/app.marte` (Logic) and `src/components.marte` (Data).
- **Namespaces**: Use of `#package` to organize nodes.
- **Custom Schema**: `.marte_schema.cue` defines a custom class (`CustomController`) with specific metadata (`#meta.multithreaded`).
- **Validation**: Enforces strict typing and custom rules.
**Run:**
```bash
cd complex
make check
make build
```

View File

@@ -0,0 +1,12 @@
package schema
#Classes: {
CustomController: {
#meta: {
multithreaded: false
}
Gain: float
InputSignals: {...}
OutputSignals: {...}
}
}

12
examples/complex/Makefile Normal file
View File

@@ -0,0 +1,12 @@
MDT=../../build/mdt
all: check build
check:
$(MDT) check src/*.marte
build:
$(MDT) build -o app_full.marte src/*.marte
fmt:
$(MDT) fmt src/*.marte

View File

@@ -0,0 +1,42 @@
#package complex_ex
+App = {
Class = RealTimeApplication
+States = {
Class = ReferenceContainer
+Run = {
Class = RealTimeState
+ControlThread = {
Class = RealTimeThread
Functions = { Controller }
}
}
}
+Functions = {
Class = ReferenceContainer
+Controller = {
Class = CustomController // Defined in .marte_schema.cue
Gain = 10.5
InputSignals = {
Ref = {
DataSource = App.Data.References
Type = float32
}
}
OutputSignals = {
Actuation = {
DataSource = App.Data.Actuators
Type = float32
}
}
}
}
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB1
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = TimingDS
}
}

View File

@@ -0,0 +1,24 @@
#package complex_ex.App.Data
+References = {
Class = GAMDataSource
Signals = {
Ref = {
Type = float32
}
}
}
+Actuators = {
Class = GAMDataSource
Signals = {
Actuation = {
Type = float32
}
}
}
+TimingDS = {
Class = TimingDataSource
}
+DDB1 = {
Class = GAMDataSource
}

View File

@@ -1,27 +0,0 @@
//!allow(unused): Ignore unused GAMs in this file
//!allow(implicit): Ignore implicit signals in this file
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
// Implicit signal (not in MyDS)
ImplicitSig = {
DataSource = MyDS
Type = uint32
}
}
}
// Unused GAM
+UnusedGAM = {
Class = IOGAM
}

12
examples/simple/Makefile Normal file
View File

@@ -0,0 +1,12 @@
MDT=../../build/mdt
all: check build
check:
$(MDT) check main.marte
build:
$(MDT) build -o output.marte main.marte
fmt:
$(MDT) fmt main.marte

View File

@@ -0,0 +1,60 @@
//# Main Application
+App = {
Class = RealTimeApplication
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB1
+Timer = {
Class = LinuxTimer
Signals = {
Counter = {
Type = uint32
}
//! unused: Time variable is not used
Time = {
Type = uint32
}
}
}
+Logger = {
Class = LoggerDataSource
}
+DDB1 = {
Class = GAMDataSource
}
}
+States = {
Class = ReferenceContainer
+Idle = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
CPUs = 0x1
Functions = { MyGAM }
}
}
}
+Functions = {
Class = ReferenceContainer
+MyGAM = {
Class = IOGAM
InputSignals = {
Counter = {
DataSource = Timer
Type = uint32
Frequency = 100 //Hz
}
}
OutputSignals = {
CounterCopy = {
DataSource = Logger
Type = uint32
}
}
}
}
+Scheduler = {
Class = GAMScheduler
TimingDataSource = Timer
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -56,27 +56,44 @@ func (b *Builder) Build(f *os.File) error {
tree.AddFile(file, config) tree.AddFile(file, config)
} }
// Determine root node to print
rootNode := tree.Root
if expectedProject != "" {
if child, ok := tree.Root.Children[expectedProject]; ok {
rootNode = child
}
}
// Write entire root content (definitions and children) to the single output file // Write entire root content (definitions and children) to the single output file
b.writeNodeContent(f, tree.Root, 0) b.writeNodeBody(f, rootNode, 0)
return nil return nil
} }
func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent int) { func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent int) {
// 1. Sort Fragments: Class first
sort.SliceStable(node.Fragments, func(i, j int) bool {
return hasClass(node.Fragments[i]) && !hasClass(node.Fragments[j])
})
indentStr := strings.Repeat(" ", indent) indentStr := strings.Repeat(" ", indent)
// If this node has a RealName (e.g. +App), we print it as an object definition // If this node has a RealName (e.g. +App), we print it as an object definition
if node.RealName != "" { if node.RealName != "" {
fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName) fmt.Fprintf(f, "%s%s = {\n", indentStr, node.RealName)
indent++ indent++
indentStr = strings.Repeat(" ", indent)
} }
b.writeNodeBody(f, node, indent)
if node.RealName != "" {
indent--
indentStr = strings.Repeat(" ", indent)
fmt.Fprintf(f, "%s}\n", indentStr)
}
}
func (b *Builder) writeNodeBody(f *os.File, node *index.ProjectNode, indent int) {
// 1. Sort Fragments: Class first
sort.SliceStable(node.Fragments, func(i, j int) bool {
return hasClass(node.Fragments[i]) && !hasClass(node.Fragments[j])
})
writtenChildren := make(map[string]bool) writtenChildren := make(map[string]bool)
// 2. Write definitions from fragments // 2. Write definitions from fragments
@@ -110,12 +127,6 @@ func (b *Builder) writeNodeContent(f *os.File, node *index.ProjectNode, indent i
child := node.Children[k] child := node.Children[k]
b.writeNodeContent(f, child, indent) b.writeNodeContent(f, child, indent)
} }
if node.RealName != "" {
indent--
indentStr = strings.Repeat(" ", indent)
fmt.Fprintf(f, "%s}\n", indentStr)
}
} }
func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int) { func (b *Builder) writeDefinition(f *os.File, def parser.Definition, indent int) {

View File

@@ -5,7 +5,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/marte-community/marte-dev-tools/internal/logger"
"github.com/marte-community/marte-dev-tools/internal/parser" "github.com/marte-community/marte-dev-tools/internal/parser"
) )
@@ -14,6 +13,7 @@ type ProjectTree struct {
References []Reference References []Reference
IsolatedFiles map[string]*ProjectNode IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string GlobalPragmas map[string][]string
NodeMap map[string][]*ProjectNode
} }
func (pt *ProjectTree) ScanDirectory(rootPath string) error { func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -120,8 +120,11 @@ func (pt *ProjectTree) removeFileFromNode(node *ProjectNode, file string) {
node.Metadata = make(map[string]string) node.Metadata = make(map[string]string)
pt.rebuildMetadata(node) pt.rebuildMetadata(node)
for _, child := range node.Children { for name, child := range node.Children {
pt.removeFileFromNode(child, file) pt.removeFileFromNode(child, file)
if len(child.Fragments) == 0 && len(child.Children) == 0 {
delete(node.Children, name)
}
} }
} }
@@ -181,13 +184,8 @@ func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
node := pt.Root node := pt.Root
parts := strings.Split(config.Package.URI, ".") parts := strings.Split(config.Package.URI, ".")
// Skip first part as per spec (Project Name is namespace only)
startIdx := 0
if len(parts) > 0 {
startIdx = 1
}
for i := startIdx; i < len(parts); i++ { for i := 0; i < len(parts); i++ {
part := strings.TrimSpace(parts[i]) part := strings.TrimSpace(parts[i])
if part == "" { if part == "" {
continue continue
@@ -388,7 +386,19 @@ func (pt *ProjectTree) indexValue(file string, val parser.Value) {
} }
} }
func (pt *ProjectTree) RebuildIndex() {
pt.NodeMap = make(map[string][]*ProjectNode)
visitor := func(n *ProjectNode) {
pt.NodeMap[n.Name] = append(pt.NodeMap[n.Name], n)
if n.RealName != n.Name {
pt.NodeMap[n.RealName] = append(pt.NodeMap[n.RealName], n)
}
}
pt.Walk(visitor)
}
func (pt *ProjectTree) ResolveReferences() { func (pt *ProjectTree) ResolveReferences() {
pt.RebuildIndex()
for i := range pt.References { for i := range pt.References {
ref := &pt.References[i] ref := &pt.References[i]
if isoNode, ok := pt.IsolatedFiles[ref.File]; ok { if isoNode, ok := pt.IsolatedFiles[ref.File]; ok {
@@ -400,14 +410,21 @@ func (pt *ProjectTree) ResolveReferences() {
} }
func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode { func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*ProjectNode) bool) *ProjectNode {
if pt.NodeMap == nil {
pt.RebuildIndex()
}
if strings.Contains(name, ".") { if strings.Contains(name, ".") {
parts := strings.Split(name, ".") parts := strings.Split(name, ".")
rootName := parts[0] rootName := parts[0]
var candidates []*ProjectNode candidates := pt.NodeMap[rootName]
pt.findAllNodes(root, rootName, &candidates)
for _, cand := range candidates { for _, cand := range candidates {
if !pt.isDescendant(cand, root) {
continue
}
curr := cand curr := cand
valid := true valid := true
for i := 1; i < len(parts); i++ { for i := 1; i < len(parts); i++ {
@@ -429,26 +446,33 @@ func (pt *ProjectTree) FindNode(root *ProjectNode, name string, predicate func(*
return nil return nil
} }
if root.RealName == name || root.Name == name { candidates := pt.NodeMap[name]
if predicate == nil || predicate(root) { for _, cand := range candidates {
return root if !pt.isDescendant(cand, root) {
continue
} }
} if predicate == nil || predicate(cand) {
for _, child := range root.Children { return cand
if res := pt.FindNode(child, name, predicate); res != nil {
return res
} }
} }
return nil return nil
} }
func (pt *ProjectTree) findAllNodes(root *ProjectNode, name string, results *[]*ProjectNode) { func (pt *ProjectTree) isDescendant(node, root *ProjectNode) bool {
if root.RealName == name || root.Name == name { if node == root {
*results = append(*results, root) return true
} }
for _, child := range root.Children { if root == nil {
pt.findAllNodes(child, name, results) return true
} }
curr := node
for curr != nil {
if curr == root {
return true
}
curr = curr.Parent
}
return false
} }
type QueryResult struct { type QueryResult struct {
@@ -458,9 +482,7 @@ type QueryResult struct {
} }
func (pt *ProjectTree) Query(file string, line, col int) *QueryResult { func (pt *ProjectTree) Query(file string, line, col int) *QueryResult {
logger.Printf("File: %s:%d:%d", file, line, col)
for i := range pt.References { for i := range pt.References {
logger.Printf("%s", pt.Root.Name)
ref := &pt.References[i] ref := &pt.References[i]
if ref.File == file { if ref.File == file {
if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) { if line == ref.Position.Line && col >= ref.Position.Column && col < ref.Position.Column+len(ref.Name) {

View File

@@ -20,12 +20,10 @@ import (
"cuelang.org/go/cue" "cuelang.org/go/cue"
) )
type CompletionParams struct { type CompletionParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"` TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"` Position Position `json:"position"`
Context CompletionContext `json:"context,omitempty"` Context CompletionContext `json:"context"`
} }
type CompletionContext struct { type CompletionContext struct {
@@ -51,6 +49,7 @@ var Tree = index.NewProjectTree()
var Documents = make(map[string]string) var Documents = make(map[string]string)
var ProjectRoot string var ProjectRoot string
var GlobalSchema *schema.Schema var GlobalSchema *schema.Schema
var Output io.Writer = os.Stdout
type JsonRpcMessage struct { type JsonRpcMessage struct {
Jsonrpc string `json:"jsonrpc"` Jsonrpc string `json:"jsonrpc"`
@@ -93,6 +92,8 @@ type VersionedTextDocumentIdentifier struct {
} }
type TextDocumentContentChangeEvent struct { type TextDocumentContentChangeEvent struct {
Range *Range `json:"range,omitempty"`
RangeLength int `json:"rangeLength,omitempty"`
Text string `json:"text"` Text string `json:"text"`
} }
@@ -161,6 +162,16 @@ type DocumentFormattingParams struct {
Options FormattingOptions `json:"options"` Options FormattingOptions `json:"options"`
} }
type RenameParams struct {
TextDocument TextDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"`
NewName string `json:"newName"`
}
type WorkspaceEdit struct {
Changes map[string][]TextEdit `json:"changes"`
}
type FormattingOptions struct { type FormattingOptions struct {
TabSize int `json:"tabSize"` TabSize int `json:"tabSize"`
InsertSpaces bool `json:"insertSpaces"` InsertSpaces bool `json:"insertSpaces"`
@@ -171,7 +182,6 @@ type TextEdit struct {
NewText string `json:"newText"` NewText string `json:"newText"`
} }
func RunServer() { func RunServer() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
@@ -215,6 +225,12 @@ func readMessage(reader *bufio.Reader) (*JsonRpcMessage, error) {
} }
func HandleMessage(msg *JsonRpcMessage) { func HandleMessage(msg *JsonRpcMessage) {
defer func() {
if r := recover(); r != nil {
logger.Printf("Panic in HandleMessage: %v", r)
}
}()
switch msg.Method { switch msg.Method {
case "initialize": case "initialize":
var params InitializeParams var params InitializeParams
@@ -239,11 +255,12 @@ func HandleMessage(msg *JsonRpcMessage) {
respond(msg.ID, map[string]any{ respond(msg.ID, map[string]any{
"capabilities": map[string]any{ "capabilities": map[string]any{
"textDocumentSync": 1, // Full sync "textDocumentSync": 2, // Incremental sync
"hoverProvider": true, "hoverProvider": true,
"definitionProvider": true, "definitionProvider": true,
"referencesProvider": true, "referencesProvider": true,
"documentFormattingProvider": true, "documentFormattingProvider": true,
"renameProvider": true,
"completionProvider": map[string]any{ "completionProvider": map[string]any{
"triggerCharacters": []string{"=", " "}, "triggerCharacters": []string{"=", " "},
}, },
@@ -300,6 +317,11 @@ func HandleMessage(msg *JsonRpcMessage) {
if err := json.Unmarshal(msg.Params, &params); err == nil { if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, HandleFormatting(params)) respond(msg.ID, HandleFormatting(params))
} }
case "textDocument/rename":
var params RenameParams
if err := json.Unmarshal(msg.Params, &params); err == nil {
respond(msg.ID, HandleRename(params))
}
} }
} }
@@ -327,28 +349,76 @@ func HandleDidOpen(params DidOpenTextDocumentParams) {
} }
func HandleDidChange(params DidChangeTextDocumentParams) { func HandleDidChange(params DidChangeTextDocumentParams) {
if len(params.ContentChanges) == 0 { uri := params.TextDocument.URI
return text, ok := Documents[uri]
if !ok {
// If not found, rely on full sync being first or error
} }
text := params.ContentChanges[0].Text
Documents[params.TextDocument.URI] = text for _, change := range params.ContentChanges {
path := uriToPath(params.TextDocument.URI) if change.Range == nil {
text = change.Text
} else {
text = applyContentChange(text, change)
}
}
Documents[uri] = text
path := uriToPath(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(uri, err)
} else { } else {
publishParserError(params.TextDocument.URI, nil) publishParserError(uri, nil)
} }
if config != nil { if config != nil {
Tree.AddFile(path, config) Tree.AddFile(path, config)
Tree.ResolveReferences() Tree.ResolveReferences()
runValidation(params.TextDocument.URI) runValidation(uri)
} }
} }
func applyContentChange(text string, change TextDocumentContentChangeEvent) string {
startOffset := offsetAt(text, change.Range.Start)
endOffset := offsetAt(text, change.Range.End)
if startOffset == -1 || endOffset == -1 {
return text
}
return text[:startOffset] + change.Text + text[endOffset:]
}
func offsetAt(text string, pos Position) int {
line := 0
col := 0
for i, r := range text {
if line == pos.Line && col == pos.Character {
return i
}
if line > pos.Line {
break
}
if r == '\n' {
line++
col = 0
} else {
if r >= 0x10000 {
col += 2
} else {
col++
}
}
}
if line == pos.Line && col == pos.Character {
return len(text)
}
return -1
}
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]
@@ -382,10 +452,9 @@ func HandleFormatting(params DocumentFormattingParams) []TextEdit {
} }
} }
func runValidation(uri string) { func runValidation(_ string) {
v := validator.NewValidator(Tree, ProjectRoot) v := validator.NewValidator(Tree, ProjectRoot)
v.ValidateProject() v.ValidateProject()
v.CheckUnused()
// Group diagnostics by file // Group diagnostics by file
fileDiags := make(map[string][]LSPDiagnostic) fileDiags := make(map[string][]LSPDiagnostic)
@@ -567,10 +636,7 @@ func HandleCompletion(params CompletionParams) *CompletionList {
} }
lineStr := lines[params.Position.Line] lineStr := lines[params.Position.Line]
col := params.Position.Character col := min(params.Position.Character, len(lineStr))
if col > len(lineStr) {
col = len(lineStr)
}
prefix := lineStr[:col] prefix := lineStr[:col]
@@ -601,12 +667,88 @@ func HandleCompletion(params CompletionParams) *CompletionList {
// Case 2: Typing a key inside an object // Case 2: Typing a key inside an object
container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1}) container := Tree.GetNodeContaining(path, parser.Position{Line: params.Position.Line + 1, Column: col + 1})
if container != nil { if container != nil {
if container.Parent != nil && isGAM(container.Parent) {
if container.Name == "InputSignals" {
return suggestGAMSignals(container, "Input")
}
if container.Name == "OutputSignals" {
return suggestGAMSignals(container, "Output")
}
}
return suggestFields(container) return suggestFields(container)
} }
return nil return nil
} }
func suggestGAMSignals(_ *index.ProjectNode, direction string) *CompletionList {
var items []CompletionItem
processNode := func(node *index.ProjectNode) {
if !isDataSource(node) {
return
}
cls := node.Metadata["Class"]
if cls == "" {
return
}
dir := "NIL"
if GlobalSchema != nil {
classPath := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", cls))
val := GlobalSchema.Value.LookupPath(classPath)
if val.Err() == nil {
var s string
if err := val.Decode(&s); err == nil {
dir = s
}
}
}
compatible := false
switch direction {
case "Input":
compatible = dir == "IN" || dir == "INOUT"
case "Output":
compatible = dir == "OUT" || dir == "INOUT"
default:
compatible = false
}
if !compatible {
return
}
signalsContainer := node.Children["Signals"]
if signalsContainer == nil {
return
}
for _, sig := range signalsContainer.Children {
dsName := node.Name
sigName := sig.Name
label := fmt.Sprintf("%s:%s", dsName, sigName)
insertText := fmt.Sprintf("%s = {\n DataSource = %s \n}", sigName, dsName)
items = append(items, CompletionItem{
Label: label,
Kind: 6, // Variable
Detail: "Signal from " + dsName,
InsertText: insertText,
InsertTextFormat: 2, // Snippet
})
}
}
Tree.Walk(processNode)
if len(items) > 0 {
return &CompletionList{Items: items}
}
return nil
}
func suggestClasses() *CompletionList { func suggestClasses() *CompletionList {
if GlobalSchema == nil { if GlobalSchema == nil {
return nil return nil
@@ -811,14 +953,11 @@ func suggestObjects(root *index.ProjectNode, filter string) *CompletionList {
var walk func(*index.ProjectNode) var walk func(*index.ProjectNode)
walk = func(node *index.ProjectNode) { walk = func(node *index.ProjectNode) {
match := false match := false
if filter == "GAM" { switch filter {
if isGAM(node) { case "GAM":
match = true match = isGAM(node)
} case "DataSource":
} else if filter == "DataSource" { match = isDataSource(node)
if isDataSource(node) {
match = true
}
} }
if match { if match {
@@ -981,6 +1120,14 @@ func formatNodeInfo(node *index.ProjectNode) string {
typ := node.Metadata["Type"] typ := node.Metadata["Type"]
ds := node.Metadata["DataSource"] ds := node.Metadata["DataSource"]
if ds == "" {
if node.Parent != nil && node.Parent.Name == "Signals" {
if node.Parent.Parent != nil {
ds = node.Parent.Parent.Name
}
}
}
if typ != "" || ds != "" { if typ != "" || ds != "" {
sigInfo := "\n" sigInfo := "\n"
if typ != "" { if typ != "" {
@@ -1053,19 +1200,214 @@ func formatNodeInfo(node *index.ProjectNode) string {
} }
} }
// Find GAM usages
var gams []string
// 1. Check References (explicit text references)
for _, ref := range Tree.References {
if ref.Target == node {
container := Tree.GetNodeContaining(ref.File, ref.Position)
if container != nil {
curr := container
for curr != nil {
if isGAM(curr) {
suffix := ""
p := container
for p != nil && p != curr {
if p.Name == "InputSignals" {
suffix = " (Input)"
break
}
if p.Name == "OutputSignals" {
suffix = " (Output)"
break
}
p = p.Parent
}
gams = append(gams, curr.RealName+suffix)
break
}
curr = curr.Parent
}
}
}
}
// 2. Check Direct Usages (Nodes targeting this node)
Tree.Walk(func(n *index.ProjectNode) {
if n.Target == node {
if n.Parent != nil && (n.Parent.Name == "InputSignals" || n.Parent.Name == "OutputSignals") {
if n.Parent.Parent != nil && isGAM(n.Parent.Parent) {
suffix := " (Input)"
if n.Parent.Name == "OutputSignals" {
suffix = " (Output)"
}
gams = append(gams, n.Parent.Parent.RealName+suffix)
}
}
}
})
if len(gams) > 0 {
uniqueGams := make(map[string]bool)
info += "\n\n**Used in GAMs**:\n"
for _, g := range gams {
if !uniqueGams[g] {
uniqueGams[g] = true
info += fmt.Sprintf("- %s\n", g)
}
}
}
return info return info
} }
func HandleRename(params RenameParams) *WorkspaceEdit {
path := uriToPath(params.TextDocument.URI)
line := params.Position.Line + 1
col := params.Position.Character + 1
res := Tree.Query(path, line, col)
if res == nil {
return nil
}
var targetNode *index.ProjectNode
var targetField *parser.Field
if res.Node != nil {
if res.Node.Target != nil {
targetNode = res.Node.Target
} else {
targetNode = res.Node
}
} else if res.Field != nil {
targetField = res.Field
} else if res.Reference != nil {
if res.Reference.Target != nil {
targetNode = res.Reference.Target
} else {
return nil
}
}
changes := make(map[string][]TextEdit)
addEdit := func(file string, rng Range, newText string) {
uri := "file://" + file
changes[uri] = append(changes[uri], TextEdit{Range: rng, NewText: newText})
}
if targetNode != nil {
// 1. Rename Definitions
prefix := ""
if len(targetNode.RealName) > 0 {
first := targetNode.RealName[0]
if first == '+' || first == '$' {
prefix = string(first)
}
}
normNewName := strings.TrimLeft(params.NewName, "+$")
finalDefName := prefix + normNewName
for _, frag := range targetNode.Fragments {
if frag.IsObject {
rng := Range{
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(targetNode.RealName)},
}
addEdit(frag.File, rng, finalDefName)
}
}
// 2. Rename References
for _, ref := range Tree.References {
if ref.Target == targetNode {
// Handle qualified names (e.g. Pkg.Node)
if strings.Contains(ref.Name, ".") {
if strings.HasSuffix(ref.Name, "."+targetNode.Name) {
prefixLen := len(ref.Name) - len(targetNode.Name)
rng := Range{
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + prefixLen},
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
}
addEdit(ref.File, rng, normNewName)
} else if ref.Name == targetNode.Name {
rng := Range{
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
}
addEdit(ref.File, rng, normNewName)
}
} else {
rng := Range{
Start: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1},
End: Position{Line: ref.Position.Line - 1, Character: ref.Position.Column - 1 + len(ref.Name)},
}
addEdit(ref.File, rng, normNewName)
}
}
}
// 3. Rename Implicit Node References (Signals in GAMs relying on name match)
Tree.Walk(func(n *index.ProjectNode) {
if n.Target == targetNode {
hasAlias := false
for _, frag := range n.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok && f.Name == "Alias" {
hasAlias = true
}
}
}
if !hasAlias {
for _, frag := range n.Fragments {
if frag.IsObject {
rng := Range{
Start: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1},
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(n.RealName)},
}
addEdit(frag.File, rng, normNewName)
}
}
}
}
})
return &WorkspaceEdit{Changes: changes}
} else if targetField != nil {
container := Tree.GetNodeContaining(path, targetField.Position)
if container != nil {
for _, frag := range container.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if f.Name == targetField.Name {
rng := Range{
Start: Position{Line: f.Position.Line - 1, Character: f.Position.Column - 1},
End: Position{Line: f.Position.Line - 1, Character: f.Position.Column - 1 + len(f.Name)},
}
addEdit(frag.File, rng, params.NewName)
}
}
}
}
}
return &WorkspaceEdit{Changes: changes}
}
return nil
}
func respond(id any, result any) { func respond(id any, result any) {
msg := JsonRpcMessage{ msg := map[string]any{
Jsonrpc: "2.0", "jsonrpc": "2.0",
ID: id, "id": id,
Result: result, "result": result,
} }
send(msg) send(msg)
} }
func send(msg any) { func send(msg any) {
body, _ := json.Marshal(msg) body, _ := json.Marshal(msg)
fmt.Printf("Content-Length: %d\r\n\r\n%s", len(body), body) fmt.Fprintf(Output, "Content-Length: %d\r\n\r\n%s", len(body), body)
} }

View File

@@ -45,6 +45,8 @@ type Subnode struct {
Definitions []Definition Definitions []Definition
} }
func (s *Subnode) Pos() Position { return s.Position }
type Value interface { type Value interface {
Node Node
isValue() isValue()
@@ -115,7 +117,11 @@ type Comment struct {
Doc bool // true if starts with //# Doc bool // true if starts with //#
} }
func (c *Comment) Pos() Position { return c.Position }
type Pragma struct { type Pragma struct {
Position Position Position Position
Text string Text string
} }
func (p *Pragma) Pos() Position { return p.Position }

View File

@@ -129,7 +129,7 @@ func (l *Lexer) NextToken() Token {
case '/': case '/':
return l.lexComment() return l.lexComment()
case '#': case '#':
return l.lexPackage() return l.lexHashIdentifier()
case '+': case '+':
fallthrough fallthrough
case '$': case '$':
@@ -243,18 +243,19 @@ func (l *Lexer) lexUntilNewline(t TokenType) Token {
} }
} }
func (l *Lexer) lexPackage() Token { func (l *Lexer) lexHashIdentifier() Token {
// We are at '#', l.start is just before it // We are at '#', l.start is just before it
for { for {
r := l.next() r := l.next()
if unicode.IsLetter(r) { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' || r == '#' {
continue continue
} }
l.backup() l.backup()
break break
} }
if l.input[l.start:l.pos] == "#package" { val := l.input[l.start:l.pos]
if val == "#package" {
return l.lexUntilNewline(TokenPackage) return l.lexUntilNewline(TokenPackage)
} }
return l.emit(TokenError) return l.emit(TokenIdentifier)
} }

View File

@@ -2,9 +2,32 @@ package schema
#Classes: { #Classes: {
RealTimeApplication: { RealTimeApplication: {
Functions: {...} // type: node Functions!: {
Data!: {...} // type: node Class: "ReferenceContainer"
States!: {...} // type: node [_= !~"^Class$"]: {
#meta: type: "gam"
...
}
} // type: node
Data!: {
Class: "ReferenceContainer"
DefaultDataSource: string
[_= !~"^(Class|DefaultDataSource)$"]: {
#meta: type: "datasource"
...
}
}
States!: {
Class: "ReferenceContainer"
[_= !~"^Class$"]: {
Class: "RealTimeState"
...
}
} // type: node
Scheduler!: {
...
#meta: type: "scheduler"
}
... ...
} }
Message: { Message: {
@@ -13,7 +36,7 @@ package schema
StateMachineEvent: { StateMachineEvent: {
NextState!: string NextState!: string
NextStateError!: string NextStateError!: string
Timeout: uint32 Timeout?: uint32
[_= !~"^(Class|NextState|Timeout|NextStateError|[#_$].+)$"]: Message [_= !~"^(Class|NextState|Timeout|NextStateError|[#_$].+)$"]: Message
... ...
} }
@@ -23,7 +46,7 @@ package schema
Class: "ReferenceContainer" Class: "ReferenceContainer"
... ...
} }
[_ = !~"^(Class|ENTER)$"]: StateMachineEvent [_ = !~"^(Class|ENTER|EXIT)$"]: StateMachineEvent
... ...
} }
StateMachine: { StateMachine: {
@@ -40,15 +63,19 @@ package schema
} }
GAMScheduler: { GAMScheduler: {
TimingDataSource: string // type: reference TimingDataSource: string // type: reference
#meta: type: "scheduler"
... ...
} }
TimingDataSource: { TimingDataSource: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
IOGAM: { IOGAM: {
InputSignals?: {...} // type: node InputSignals?: {...} // type: node
OutputSignals?: {...} // type: node OutputSignals?: {...} // type: node
#meta: type: "gam"
... ...
} }
ReferenceContainer: { ReferenceContainer: {
@@ -56,81 +83,114 @@ package schema
} }
ConstantGAM: { ConstantGAM: {
... ...
#meta: type: "gam"
} }
PIDGAM: { PIDGAM: {
Kp: float | int // type: float (allow int as it promotes) Kp: float | int // type: float (allow int as it promotes)
Ki: float | int Ki: float | int
Kd: float | int Kd: float | int
#meta: type: "gam"
... ...
} }
FileDataSource: { FileDataSource: {
Filename: string Filename: string
Format?: string Format?: string
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
LoggerDataSource: { LoggerDataSource: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
DANStream: { DANStream: {
Timeout?: int Timeout?: int
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
EPICSCAInput: { EPICSCAInput: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
EPICSCAOutput: { EPICSCAOutput: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
EPICSPVAInput: { EPICSPVAInput: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
EPICSPVAOutput: { EPICSPVAOutput: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
SDNSubscriber: { SDNSubscriber: {
Address: string ExecutionMode?: *"IndependentThread" | "RealTimeThread"
Port: int Topic!: string
Interface?: string Address?: string
direction: "IN" Interface!: string
CPUs?: uint32
InternalTimeout?: uint32
Timeout?: uint32
IgnoreTimeoutError?: 0 | 1
#meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
SDNPublisher: { SDNPublisher: {
Address: string Address: string
Port: int Port: int
Interface?: string Interface?: string
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
UDPReceiver: { UDPReceiver: {
Port: int Port: int
Address?: string Address?: string
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
UDPSender: { UDPSender: {
Destination: string Destination: string
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
FileReader: { FileReader: {
Filename: string Filename: string
Format?: string Format?: string
Interpolate?: string Interpolate?: string
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
FileWriter: { FileWriter: {
Filename: string Filename: string
Format?: string Format?: string
StoreOnTrigger?: int StoreOnTrigger?: int
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
OrderedClass: { OrderedClass: {
@@ -138,15 +198,25 @@ package schema
Second: string Second: string
... ...
} }
BaseLib2GAM: {...} BaseLib2GAM: {
ConversionGAM: {...} #meta: type: "gam"
DoubleHandshakeGAM: {...} ...
}
ConversionGAM: {
#meta: type: "gam"
...
}
DoubleHandshakeGAM: {
#meta: type: "gam"
...
}
FilterGAM: { FilterGAM: {
Num: [...] Num: [...]
Den: [...] Den: [...]
ResetInEachState?: _ ResetInEachState?: _
InputSignals?: {...} InputSignals?: {...}
OutputSignals?: {...} OutputSignals?: {...}
#meta: type: "gam"
... ...
} }
HistogramGAM: { HistogramGAM: {
@@ -154,26 +224,60 @@ package schema
StateChangeResetName?: string StateChangeResetName?: string
InputSignals?: {...} InputSignals?: {...}
OutputSignals?: {...} OutputSignals?: {...}
#meta: type: "gam"
...
}
Interleaved2FlatGAM: {
#meta: type: "gam"
...
}
FlattenedStructIOGAM: {
#meta: type: "gam"
... ...
} }
Interleaved2FlatGAM: {...}
FlattenedStructIOGAM: {...}
MathExpressionGAM: { MathExpressionGAM: {
Expression: string Expression: string
InputSignals?: {...} InputSignals?: {...}
OutputSignals?: {...} OutputSignals?: {...}
#meta: type: "gam"
...
}
MessageGAM: {
#meta: type: "gam"
...
}
MuxGAM: {
#meta: type: "gam"
...
}
SimulinkWrapperGAM: {
#meta: type: "gam"
...
}
SSMGAM: {
#meta: type: "gam"
...
}
StatisticsGAM: {
#meta: type: "gam"
...
}
TimeCorrectionGAM: {
#meta: type: "gam"
...
}
TriggeredIOGAM: {
#meta: type: "gam"
...
}
WaveformGAM: {
#meta: type: "gam"
... ...
} }
MessageGAM: {...}
MuxGAM: {...}
SimulinkWrapperGAM: {...}
SSMGAM: {...}
StatisticsGAM: {...}
TimeCorrectionGAM: {...}
TriggeredIOGAM: {...}
WaveformGAM: {...}
DAN: { DAN: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
... ...
} }
LinuxTimer: { LinuxTimer: {
@@ -184,11 +288,15 @@ package schema
CPUMask?: int CPUMask?: int
TimeProvider?: {...} TimeProvider?: {...}
Signals: {...} Signals: {...}
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
LinkDataSource: { LinkDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
MDSReader: { MDSReader: {
@@ -196,7 +304,9 @@ package schema
ShotNumber: int ShotNumber: int
Frequency: float | int Frequency: float | int
Signals: {...} Signals: {...}
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
MDSWriter: { MDSWriter: {
@@ -212,57 +322,88 @@ package schema
NumberOfPostTriggers?: int NumberOfPostTriggers?: int
Signals: {...} Signals: {...}
Messages?: {...} Messages?: {...}
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
NI1588TimeStamp: { NI1588TimeStamp: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI6259ADC: { NI6259ADC: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI6259DAC: { NI6259DAC: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
NI6259DIO: { NI6259DIO: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
NI6368ADC: { NI6368ADC: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI6368DAC: { NI6368DAC: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
... ...
} }
NI6368DIO: { NI6368DIO: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
NI9157CircularFifoReader: { NI9157CircularFifoReader: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
NI9157MxiDataSource: { NI9157MxiDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
OPCUADSInput: { OPCUADSInput: {
direction: "IN" #meta: multithreaded: bool | *false
#meta: direction: "IN"
#meta: type: "datasource"
... ...
} }
OPCUADSOutput: { OPCUADSOutput: {
direction: "OUT" #meta: multithreaded: bool | *false
#meta: direction: "OUT"
#meta: type: "datasource"
...
}
RealTimeThreadAsyncBridge: {
#meta: direction: "INOUT"
#meta: multithreaded: bool | true
#meta: type: "datasource"
... ...
} }
RealTimeThreadAsyncBridge: {...}
RealTimeThreadSynchronisation: {...} RealTimeThreadSynchronisation: {...}
UARTDataSource: { UARTDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
BaseLib2Wrapper: {...} BaseLib2Wrapper: {...}
@@ -272,16 +413,25 @@ package schema
OPCUA: {...} OPCUA: {...}
SysLogger: {...} SysLogger: {...}
GAMDataSource: { GAMDataSource: {
direction: "INOUT" #meta: multithreaded: bool | *false
#meta: direction: "INOUT"
#meta: type: "datasource"
... ...
} }
} }
#Meta: {
direction?: "IN" | "OUT" | "INOUT"
multithreaded?: bool
...
}
// Definition for any Object. // Definition for any Object.
// It must have a Class field. // It must have a Class field.
// Based on Class, it validates against #Classes. // Based on Class, it validates against #Classes.
#Object: { #Object: {
Class: string Class: string
"#meta"?: #Meta
// Allow any other field by default (extensibility), // Allow any other field by default (extensibility),
// unless #Classes definition is closed. // unless #Classes definition is closed.
// We allow open structs now. // We allow open structs now.

View File

@@ -53,6 +53,8 @@ func (v *Validator) ValidateProject() {
for _, node := range v.Tree.IsolatedFiles { for _, node := range v.Tree.IsolatedFiles {
v.validateNode(node) v.validateNode(node)
} }
v.CheckUnused()
v.CheckDataSourceThreading()
} }
func (v *Validator) validateNode(node *index.ProjectNode) { func (v *Validator) validateNode(node *index.ProjectNode) {
@@ -313,8 +315,8 @@ func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, di
dsClass := v.getNodeClass(dsNode) dsClass := v.getNodeClass(dsNode)
if dsClass != "" { if dsClass != "" {
// Lookup class definition in Schema // Lookup class definition in Schema
// path: #Classes.ClassName.direction // path: #Classes.ClassName.#meta.direction
path := cue.ParsePath(fmt.Sprintf("#Classes.%s.direction", dsClass)) path := cue.ParsePath(fmt.Sprintf("#Classes.%s.#meta.direction", dsClass))
val := v.Schema.Value.LookupPath(path) val := v.Schema.Value.LookupPath(path)
if val.Err() == nil { if val.Err() == nil {
@@ -509,6 +511,8 @@ func (v *Validator) getFieldValue(f *parser.Field) string {
return val.Raw return val.Raw
case *parser.FloatValue: case *parser.FloatValue:
return val.Raw return val.Raw
case *parser.BoolValue:
return strconv.FormatBool(val.Value)
} }
return "" return ""
} }
@@ -542,11 +546,6 @@ func isValidType(t string) bool {
return false return false
} }
func (v *Validator) checkType(val parser.Value, expectedType string) bool {
// Legacy function, replaced by CUE.
return true
}
func (v *Validator) getFileForField(f *parser.Field, node *index.ProjectNode) string { func (v *Validator) getFileForField(f *parser.Field, node *index.ProjectNode) string {
for _, frag := range node.Fragments { for _, frag := range node.Fragments {
for _, def := range frag.Definitions { for _, def := range frag.Definitions {
@@ -746,3 +745,142 @@ func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bo
} }
return false return false
} }
func (v *Validator) CheckDataSourceThreading() {
if v.Tree.Root == nil {
return
}
// 1. Find RealTimeApplication
var appNode *index.ProjectNode
findApp := func(n *index.ProjectNode) {
if cls, ok := n.Metadata["Class"]; ok && cls == "RealTimeApplication" {
appNode = n
}
}
v.Tree.Walk(findApp)
if appNode == nil {
return
}
// 2. Find States
var statesNode *index.ProjectNode
if s, ok := appNode.Children["States"]; ok {
statesNode = s
} else {
for _, child := range appNode.Children {
if cls, ok := child.Metadata["Class"]; ok && cls == "StateMachine" {
statesNode = child
break
}
}
}
if statesNode == nil {
return
}
// 3. Iterate States
for _, state := range statesNode.Children {
dsUsage := make(map[*index.ProjectNode]string) // DS Node -> Thread Name
var threads []*index.ProjectNode
// Search for threads in the state (either direct children or inside "Threads" container)
for _, child := range state.Children {
if child.RealName == "Threads" {
for _, t := range child.Children {
if cls, ok := t.Metadata["Class"]; ok && cls == "RealTimeThread" {
threads = append(threads, t)
}
}
} else {
if cls, ok := child.Metadata["Class"]; ok && cls == "RealTimeThread" {
threads = append(threads, child)
}
}
}
for _, thread := range threads {
gams := v.getThreadGAMs(thread)
for _, gam := range gams {
dss := v.getGAMDataSources(gam)
for _, ds := range dss {
if existingThread, ok := dsUsage[ds]; ok {
if existingThread != thread.RealName {
if !v.isMultithreaded(ds) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("DataSource '%s' is not multithreaded but used in multiple threads (%s, %s) in state '%s'", ds.RealName, existingThread, thread.RealName, state.RealName),
Position: v.getNodePosition(gam),
File: v.getNodeFile(gam),
})
}
}
} else {
dsUsage[ds] = thread.RealName
}
}
}
}
}
}
func (v *Validator) getThreadGAMs(thread *index.ProjectNode) []*index.ProjectNode {
var gams []*index.ProjectNode
fields := v.getFields(thread)
if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 {
f := funcs[0]
if arr, ok := f.Value.(*parser.ArrayValue); ok {
for _, elem := range arr.Elements {
if ref, ok := elem.(*parser.ReferenceValue); ok {
target := v.resolveReference(ref.Value, v.getNodeFile(thread), isGAM)
if target != nil {
gams = append(gams, target)
}
}
}
}
}
return gams
}
func (v *Validator) getGAMDataSources(gam *index.ProjectNode) []*index.ProjectNode {
dsMap := make(map[*index.ProjectNode]bool)
processSignals := func(container *index.ProjectNode) {
if container == nil {
return
}
for _, sig := range container.Children {
fields := v.getFields(sig)
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName := v.getFieldValue(dsFields[0])
dsNode := v.resolveReference(dsName, v.getNodeFile(sig), isDataSource)
if dsNode != nil {
dsMap[dsNode] = true
}
}
}
}
processSignals(gam.Children["InputSignals"])
processSignals(gam.Children["OutputSignals"])
var dss []*index.ProjectNode
for ds := range dsMap {
dss = append(dss, ds)
}
return dss
}
func (v *Validator) isMultithreaded(ds *index.ProjectNode) bool {
if meta, ok := ds.Children["#meta"]; ok {
fields := v.getFields(meta)
if mt, ok := fields["multithreaded"]; ok && len(mt) > 0 {
val := v.getFieldValue(mt[0])
return val == "true"
}
}
return false
}

View File

@@ -21,11 +21,12 @@ The executable should support the following subcommands:
The LSP server should provide the following capabilities: The LSP server should provide the following capabilities:
- **Diagnostics**: Report syntax errors and validation issues. - **Diagnostics**: Report syntax errors and validation issues.
- **Incremental Sync**: Supports `textDocumentSync` kind 2 (Incremental) for better performance with large files.
- **Hover Documentation**: - **Hover Documentation**:
- **Objects**: Display `CLASS::Name` and any associated docstrings. - **Objects**: Display `CLASS::Name` and any associated docstrings.
- **Signals**: Display `DataSource.Name TYPE (SIZE) [IN/OUT/INOUT]` along with docstrings. - **Signals**: Display `DataSource.Name TYPE (SIZE) [IN/OUT/INOUT]` along with docstrings.
- **GAMs**: Show the list of States where the GAM is referenced. - **GAMs**: Show the list of States where the GAM is referenced.
- **Referenced Signals**: Show the list of GAMs where the signal is referenced. - **Referenced Signals**: Show the list of GAMs where the signal is referenced (indicating Input/Output direction).
- **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.
@@ -34,6 +35,13 @@ The LSP server should provide the following capabilities:
- **Reference Suggestions**: - **Reference Suggestions**:
- `DataSource` fields suggest available DataSource objects. - `DataSource` fields suggest available DataSource objects.
- `Functions` (in Threads) suggest available GAM objects. - `Functions` (in Threads) suggest available GAM objects.
- **Signal Completion**: Inside `InputSignals` or `OutputSignals` of a GAM:
- Suggests available signals from valid DataSources (filtering by direction: `IN`/`INOUT` for Inputs, `OUT`/`INOUT` for Outputs).
- Format: `SIGNAL_NAME:DATASOURCE_NAME`.
- Auto-inserts: `SIGNAL_NAME = { DataSource = DATASOURCE_NAME }`.
- **Rename Symbol**: Rename an object, field, or reference across the entire project scope.
- Supports renaming of Definitions (`+Name` or `Name`), preserving any modifiers (`+`/`$`).
- Updates all references to the renamed symbol, including qualified references (e.g., `Pkg.Name`).
- **Code Snippets**: Provide snippets for common patterns (e.g., `+Object = { ... }`). - **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.
@@ -50,7 +58,7 @@ The LSP server should provide the following capabilities:
- **Build Process**: - **Build Process**:
- The build tool merges all files sharing the same base namespace into a **single output configuration**. - The build tool merges all files sharing the same base namespace into a **single output configuration**.
- **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 standard output (`stdout`) by default. It can be written to a target file if the `-o` (or `--output`) argument is provided via CLI.
- **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. Support for dot-separated paths (e.g., `Node.SubNode`) is required. - **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, 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. - **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.
@@ -166,6 +174,7 @@ The tool must build an index of the configuration to support LSP features and va
- **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 using the **CUE** language. - Class validation rules must be defined in a separate schema file using the **CUE** language.
- **Metadata**: Class properties like direction (`#direction`) and multithreading support (`#multithreaded`) are stored within a `#meta` field in the class definition (e.g., `#meta: { direction: "IN", multithreaded: true }`).
- **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.cue` in standard system locations: - **Default Schema**: The tool should look for a default schema file `marte_schema.cue` in standard system locations:
@@ -211,6 +220,7 @@ The LSP and `check` command should report the following:
- Field type mismatches. - Field type mismatches.
- Grammar errors (e.g., missing closing brackets). - Grammar errors (e.g., missing closing brackets).
- **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes. - **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes.
- **Threading Violation**: A DataSource that is not marked as multithreaded (via `#meta.multithreaded`) is used by GAMs running in different threads within the same State.
## Logging ## Logging

109
test/ast_test.go Normal file
View File

@@ -0,0 +1,109 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
func TestASTCoverage(t *testing.T) {
pos := parser.Position{Line: 1, Column: 1}
var n parser.Node
var d parser.Definition
var v parser.Value
// Field
f := &parser.Field{Position: pos}
n = f
d = f
if n.Pos() != pos {
t.Error("Field.Pos failed")
}
_ = d
// ObjectNode
o := &parser.ObjectNode{Position: pos}
n = o
d = o
if n.Pos() != pos {
t.Error("ObjectNode.Pos failed")
}
// StringValue
sv := &parser.StringValue{Position: pos}
n = sv
v = sv
if n.Pos() != pos {
t.Error("StringValue.Pos failed")
}
_ = v
// IntValue
iv := &parser.IntValue{Position: pos}
n = iv
v = iv
if n.Pos() != pos {
t.Error("IntValue.Pos failed")
}
// FloatValue
fv := &parser.FloatValue{Position: pos}
n = fv
v = fv
if n.Pos() != pos {
t.Error("FloatValue.Pos failed")
}
// BoolValue
bv := &parser.BoolValue{Position: pos}
n = bv
v = bv
if n.Pos() != pos {
t.Error("BoolValue.Pos failed")
}
// ReferenceValue
rv := &parser.ReferenceValue{Position: pos}
n = rv
v = rv
if n.Pos() != pos {
t.Error("ReferenceValue.Pos failed")
}
// ArrayValue
av := &parser.ArrayValue{Position: pos}
n = av
v = av
if n.Pos() != pos {
t.Error("ArrayValue.Pos failed")
}
// Package
pkg := &parser.Package{Position: pos}
n = pkg
if n.Pos() != pos {
t.Error("Package.Pos failed")
}
// Subnode
sn := &parser.Subnode{Position: pos}
n = sn
if n.Pos() != pos {
t.Error("Subnode.Pos failed")
}
// Comment
cmt := &parser.Comment{Position: pos}
n = cmt
if n.Pos() != pos {
t.Error("Comment.Pos failed")
}
// Pragma
prg := &parser.Pragma{Position: pos}
n = prg
if n.Pos() != pos {
t.Error("Pragma.Pos failed")
}
}

View File

@@ -0,0 +1,58 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
func TestIndexCleanup(t *testing.T) {
idx := index.NewProjectTree()
file := "cleanup.marte"
content := `
#package Pkg
+Node = { Class = Type }
`
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
idx.AddFile(file, cfg)
// Check node exists
// Root -> Pkg -> Node
pkgNode := idx.Root.Children["Pkg"]
if pkgNode == nil {
t.Fatal("Pkg node should exist")
}
if pkgNode.Children["Node"] == nil {
t.Fatal("Node should exist")
}
// Update file: remove +Node
content2 := `
#package Pkg
// Removed node
`
p2 := parser.NewParser(content2)
cfg2, _ := p2.Parse()
idx.AddFile(file, cfg2)
// Check Node is gone
pkgNode = idx.Root.Children["Pkg"]
if pkgNode == nil {
// Pkg should exist because of #package Pkg
t.Fatal("Pkg node should exist after update")
}
if pkgNode.Children["Node"] != nil {
t.Error("Node should be gone")
}
// Test removing file completely
idx.RemoveFile(file)
if len(idx.Root.Children) != 0 {
t.Errorf("Root should be empty after removing file, got %d children", len(idx.Root.Children))
}
}

66
test/index_test.go Normal file
View File

@@ -0,0 +1,66 @@
package integration
import (
"testing"
"github.com/marte-community/marte-dev-tools/internal/index"
)
func TestNodeMap(t *testing.T) {
pt := index.NewProjectTree()
root := pt.Root
// Create structure: +A -> +B -> +C
nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root}
root.Children["A"] = nodeA
nodeB := &index.ProjectNode{Name: "B", RealName: "+B", Children: make(map[string]*index.ProjectNode), Parent: nodeA}
nodeA.Children["B"] = nodeB
nodeC := &index.ProjectNode{Name: "C", RealName: "+C", Children: make(map[string]*index.ProjectNode), Parent: nodeB}
nodeB.Children["C"] = nodeC
// Rebuild Index
pt.RebuildIndex()
// Find by Name
found := pt.FindNode(root, "C", nil)
if found != nodeC {
t.Errorf("FindNode(C) failed. Got %v, want %v", found, nodeC)
}
// Find by RealName
found = pt.FindNode(root, "+C", nil)
if found != nodeC {
t.Errorf("FindNode(+C) failed. Got %v, want %v", found, nodeC)
}
// Find by Path
found = pt.FindNode(root, "A.B.C", nil)
if found != nodeC {
t.Errorf("FindNode(A.B.C) failed. Got %v, want %v", found, nodeC)
}
// Find by Path with RealName
found = pt.FindNode(root, "+A.+B.+C", nil)
if found != nodeC {
t.Errorf("FindNode(+A.+B.+C) failed. Got %v, want %v", found, nodeC)
}
}
func TestResolveReferencesWithMap(t *testing.T) {
pt := index.NewProjectTree()
root := pt.Root
nodeA := &index.ProjectNode{Name: "A", RealName: "+A", Children: make(map[string]*index.ProjectNode), Parent: root}
root.Children["A"] = nodeA
ref := index.Reference{Name: "A", File: "test.marte"}
pt.References = append(pt.References, ref)
pt.ResolveReferences()
if pt.References[0].Target != nodeA {
t.Error("ResolveReferences failed to resolve A")
}
}

59
test/logger_test.go Normal file
View File

@@ -0,0 +1,59 @@
package integration
import (
"os"
"os/exec"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/logger"
)
func TestLoggerPrint(t *testing.T) {
if os.Getenv("TEST_LOGGER_PRINT") == "1" {
logger.Printf("Test Printf %d", 123)
logger.Println("Test Println")
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestLoggerPrint")
cmd.Env = append(os.Environ(), "TEST_LOGGER_PRINT=1")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("process failed: %v", err)
}
output := string(out)
if !strings.Contains(output, "Test Printf 123") {
t.Error("Printf output missing")
}
if !strings.Contains(output, "Test Println") {
t.Error("Println output missing")
}
}
func TestLoggerFatal(t *testing.T) {
if os.Getenv("TEST_LOGGER_FATAL") == "1" {
logger.Fatal("Test Fatal")
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestLoggerFatal")
cmd.Env = append(os.Environ(), "TEST_LOGGER_FATAL=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return // Success (exit code non-zero)
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}
func TestLoggerFatalf(t *testing.T) {
if os.Getenv("TEST_LOGGER_FATALF") == "1" {
logger.Fatalf("Test Fatalf %d", 456)
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestLoggerFatalf")
cmd.Env = append(os.Environ(), "TEST_LOGGER_FATALF=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return // Success
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}

View File

@@ -0,0 +1,90 @@
package integration
import (
"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 TestSuggestSignalsRobustness(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema()
// Inject schema with INOUT
custom := []byte(`
package schema
#Classes: {
InOutReader: { #meta: direction: "INOUT" }
}
`)
val := lsp.GlobalSchema.Context.CompileBytes(custom)
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
content := `
+DS = {
Class = InOutReader
+Signals = {
Sig = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
}
+OutputSignals = {
}
}
`
uri := "file://robust.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("robust.marte", cfg)
// Check Input (Line 10)
paramsIn := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 10, Character: 8},
}
listIn := lsp.HandleCompletion(paramsIn)
found := false
if listIn != nil {
for _, item := range listIn.Items {
if item.Label == "DS:Sig" {
found = true
}
}
}
if !found {
t.Error("INOUT signal not found in InputSignals")
}
// Check Output (Line 13)
paramsOut := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 13, Character: 8},
}
listOut := lsp.HandleCompletion(paramsOut)
found = false
if listOut != nil {
for _, item := range listOut.Items {
if item.Label == "DS:Sig" {
found = true
}
}
}
if !found {
t.Error("INOUT signal not found in OutputSignals")
}
}

View File

@@ -0,0 +1,128 @@
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 TestSuggestSignalsInGAM(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema()
// Inject schema for directionality
custom := []byte(`
package schema
#Classes: {
FileReader: { direction: "IN" }
FileWriter: { direction: "OUT" }
}
`)
val := lsp.GlobalSchema.Context.CompileBytes(custom)
lsp.GlobalSchema.Value = lsp.GlobalSchema.Value.Unify(val)
content := `
+InDS = {
Class = FileReader
+Signals = {
InSig = { Type = uint32 }
}
}
+OutDS = {
Class = FileWriter
+Signals = {
OutSig = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
}
+OutputSignals = {
}
}
`
uri := "file://signals.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("signals.marte", cfg)
// 1. Suggest in InputSignals
// Line 16 (empty line inside InputSignals)
paramsIn := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 16, Character: 8},
}
listIn := lsp.HandleCompletion(paramsIn)
if listIn == nil {
t.Fatal("Expected suggestions in InputSignals")
}
foundIn := false
foundOut := false
for _, item := range listIn.Items {
if item.Label == "InDS:InSig" {
foundIn = true
// Normalize spaces for check
insert := strings.ReplaceAll(item.InsertText, " ", "")
expected := "InSig={DataSource=InDS}"
if !strings.Contains(insert, expected) && !strings.Contains(item.InsertText, "InSig = {") {
// Snippet might differ slightly, but should contain essentials
t.Errorf("InsertText mismatch: %s", item.InsertText)
}
}
if item.Label == "OutDS:OutSig" {
foundOut = true
}
}
if !foundIn {
t.Error("Did not find InDS:InSig")
}
if foundOut {
t.Error("Should not find OutDS:OutSig in InputSignals")
}
// 2. Suggest in OutputSignals
// Line 19
paramsOut := lsp.CompletionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 19, Character: 8},
}
listOut := lsp.HandleCompletion(paramsOut)
if listOut == nil {
t.Fatal("Expected suggestions in OutputSignals")
}
foundIn = false
foundOut = false
for _, item := range listOut.Items {
if item.Label == "InDS:InSig" {
foundIn = true
}
if item.Label == "OutDS:OutSig" {
foundOut = true
}
}
if foundIn {
t.Error("Should not find InDS:InSig in OutputSignals")
}
if !foundOut {
t.Error("Did not find OutDS:OutSig in OutputSignals")
}
}

110
test/lsp_coverage_test.go Normal file
View File

@@ -0,0 +1,110 @@
package integration
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"strings"
"testing"
"github.com/marte-community/marte-dev-tools/internal/lsp"
)
func TestLSPIncrementalSync(t *testing.T) {
lsp.Documents = make(map[string]string)
var buf bytes.Buffer
lsp.Output = &buf
content := "Line1\nLine2\nLine3"
uri := "file://inc.marte"
lsp.Documents[uri] = content
// Replace "Line2" (Line 1, 0-5) with "Modified"
change := lsp.TextDocumentContentChangeEvent{
Range: &lsp.Range{
Start: lsp.Position{Line: 1, Character: 0},
End: lsp.Position{Line: 1, Character: 5},
},
Text: "Modified",
}
params := lsp.DidChangeTextDocumentParams{
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: 2},
ContentChanges: []lsp.TextDocumentContentChangeEvent{change},
}
lsp.HandleDidChange(params)
expected := "Line1\nModified\nLine3"
if lsp.Documents[uri] != expected {
t.Errorf("Incremental update failed. Got:\n%q\nWant:\n%q", lsp.Documents[uri], expected)
}
// Insert at end
change2 := lsp.TextDocumentContentChangeEvent{
Range: &lsp.Range{
Start: lsp.Position{Line: 2, Character: 5},
End: lsp.Position{Line: 2, Character: 5},
},
Text: "\nLine4",
}
params2 := lsp.DidChangeTextDocumentParams{
TextDocument: lsp.VersionedTextDocumentIdentifier{URI: uri, Version: 3},
ContentChanges: []lsp.TextDocumentContentChangeEvent{change2},
}
lsp.HandleDidChange(params2)
expected2 := "Line1\nModified\nLine3\nLine4"
if lsp.Documents[uri] != expected2 {
t.Errorf("Incremental insert failed. Got:\n%q\nWant:\n%q", lsp.Documents[uri], expected2)
}
}
func TestLSPLifecycle(t *testing.T) {
var buf bytes.Buffer
lsp.Output = &buf
// Shutdown
msgShutdown := &lsp.JsonRpcMessage{
Method: "shutdown",
ID: 1,
}
lsp.HandleMessage(msgShutdown)
if !strings.Contains(buf.String(), `"result":null`) {
t.Error("Shutdown response incorrect")
}
// Exit
if os.Getenv("TEST_LSP_EXIT") == "1" {
msgExit := &lsp.JsonRpcMessage{Method: "exit"}
lsp.HandleMessage(msgExit)
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestLSPLifecycle")
cmd.Env = append(os.Environ(), "TEST_LSP_EXIT=1")
err := cmd.Run()
if err != nil {
t.Errorf("Exit failed: %v", err)
}
}
func TestLSPMalformedParams(t *testing.T) {
var buf bytes.Buffer
lsp.Output = &buf
// Malformed Hover
msg := &lsp.JsonRpcMessage{
Method: "textDocument/hover",
ID: 2,
Params: json.RawMessage(`{invalid`),
}
lsp.HandleMessage(msg)
output := buf.String()
// Should respond with nil result
if !strings.Contains(output, `"result":null`) {
t.Errorf("Expected nil result for malformed params, got: %s", output)
}
}

74
test/lsp_crash_test.go Normal file
View File

@@ -0,0 +1,74 @@
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"
)
func TestLSPCrashOnUndefinedReference(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+App = {
Class = RealTimeApplication
+State = {
Class = RealTimeState
+Thread = {
Class = RealTimeThread
Functions = { UndefinedGAM }
}
}
}
`
uri := "file://crash.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("crash.marte", cfg)
lsp.Tree.ResolveReferences()
// Line 7: " Functions = { UndefinedGAM }"
// 12 spaces + "Functions" (9) + " = { " (5) = 26 chars prefix.
// UndefinedGAM starts at 26.
params := lsp.DefinitionParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 7, Character: 27},
}
// This should NOT panic
defer func() {
if r := recover(); r != nil {
t.Errorf("Recovered from panic: %v", r)
}
}()
res := lsp.HandleDefinition(params)
if res != nil {
t.Error("Expected nil for undefined reference definition")
}
// 2. Hover
hParams := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 7, Character: 27},
}
hover := lsp.HandleHover(hParams)
if hover == nil {
t.Error("Expected hover for unresolved reference")
} else {
content := hover.Contents.(lsp.MarkupContent).Value
if !strings.Contains(content, "Unresolved") {
t.Errorf("Expected 'Unresolved' in hover, got: %s", content)
}
}
}

View File

@@ -0,0 +1,81 @@
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"
)
func TestHoverDataSourceName(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS1 = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM1 = {
Class = IOGAM
+InputSignals = {
S1 = {
DataSource = DS1
Alias = Sig1
}
}
}
`
uri := "file://test_ds.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatalf("Parse error: %v", err)
}
lsp.Tree.AddFile("test_ds.marte", cfg)
lsp.Tree.ResolveReferences()
// Test 1: Explicit Signal (Sig1)
// Position: "Sig1" at line 5 (0-based 4)
// Line 4: " Sig1 = { Type = uint32 }"
// Col: 8
params1 := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 9},
}
hover1 := lsp.HandleHover(params1)
if hover1 == nil {
t.Fatal("Expected hover for Sig1")
}
content1 := hover1.Contents.(lsp.MarkupContent).Value
// Expectation: explicit signal shows owner datasource
if !strings.Contains(content1, "**DataSource**: `+DS1`") && !strings.Contains(content1, "**DataSource**: `DS1`") {
t.Errorf("Expected DataSource: +DS1 in hover for Sig1, got: %s", content1)
}
// Test 2: Implicit Signal (S1)
// Position: "S1" at line 11 (0-based 10)
params2 := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 10, Character: 9},
}
hover2 := lsp.HandleHover(params2)
if hover2 == nil {
t.Fatal("Expected hover for S1")
}
content2 := hover2.Contents.(lsp.MarkupContent).Value
// Expectation: implicit signal shows referenced datasource
if !strings.Contains(content2, "**DataSource**: `DS1`") {
t.Errorf("Expected DataSource: DS1 in hover for S1, got: %s", content2)
}
}

View File

@@ -0,0 +1,75 @@
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"
)
func TestHoverGAMUsage(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS1 = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM1 = {
Class = IOGAM
+InputSignals = {
S1 = {
DataSource = DS1
Alias = Sig1
}
}
}
+GAM2 = {
Class = IOGAM
+OutputSignals = {
S2 = {
DataSource = DS1
Alias = Sig1
}
}
}
`
uri := "file://test_gam_usage.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("test_gam_usage.marte", cfg)
lsp.Tree.ResolveReferences()
// Query hover for Sig1 (Line 5)
// Line 4: Sig1... (0-based)
params := lsp.HoverParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 9},
}
hover := lsp.HandleHover(params)
if hover == nil {
t.Fatal("Expected hover")
}
contentHover := hover.Contents.(lsp.MarkupContent).Value
if !strings.Contains(contentHover, "**Used in GAMs**") {
t.Errorf("Expected 'Used in GAMs' section, got:\n%s", contentHover)
}
if !strings.Contains(contentHover, "- +GAM1") {
t.Error("Expected +GAM1 in usage list")
}
if !strings.Contains(contentHover, "- +GAM2") {
t.Error("Expected +GAM2 in usage list")
}
}

View File

@@ -0,0 +1,89 @@
package integration
import (
"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/validator"
)
func TestRenameImplicitToDefinition(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
// Implicit usage
Sig1 = { DataSource = DS }
}
}
`
uri := "file://rename_imp.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("rename_imp.marte", cfg)
lsp.Tree.ResolveReferences()
// Run validator to link targets
v := validator.NewValidator(lsp.Tree, ".")
v.ValidateProject()
// Rename Implicit Sig1 (Line 11, 0-based 11)
// Line 11: " Sig1 = { DataSource = DS }"
params := lsp.RenameParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 11, Character: 9},
NewName: "NewSig",
}
edit := lsp.HandleRename(params)
if edit == nil {
t.Fatal("Expected edits")
}
edits := edit.Changes[uri]
// Expect:
// 1. Rename Implicit Sig1 (Line 9) -> NewSig
// 2. Rename Definition Sig1 (Line 4) -> NewSig
if len(edits) != 2 {
t.Errorf("Expected 2 edits, got %d", len(edits))
for _, e := range edits {
t.Logf("Edit at line %d", e.Range.Start.Line)
}
}
foundDef := false
foundImp := false
for _, e := range edits {
if e.Range.Start.Line == 4 {
foundDef = true
}
if e.Range.Start.Line == 11 {
foundImp = true
}
}
if !foundDef {
t.Error("Definition not renamed")
}
if !foundImp {
t.Error("Implicit usage not renamed")
}
}

View File

@@ -0,0 +1,110 @@
package integration
import (
"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/validator"
)
func TestRenameSignalInGAM(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
+DS = {
Class = FileReader
+Signals = {
Sig1 = { Type = uint32 }
}
}
+GAM = {
Class = IOGAM
+InputSignals = {
// Implicit match
Sig1 = { DataSource = DS }
// Explicit Alias
S2 = { DataSource = DS Alias = Sig1 }
}
}
`
uri := "file://rename_sig.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("rename_sig.marte", cfg)
lsp.Tree.ResolveReferences()
// Run validator to populate Targets
v := validator.NewValidator(lsp.Tree, ".")
v.ValidateProject()
// Rename DS.Sig1 to NewSig
// Sig1 is at Line 5.
// Line 0: empty
// Line 1: +DS
// Line 2: Class
// Line 3: +Signals
// Line 4: Sig1
params := lsp.RenameParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 4, Character: 9}, // Sig1
NewName: "NewSig",
}
edit := lsp.HandleRename(params)
if edit == nil {
t.Fatal("Expected edits")
}
edits := edit.Changes[uri]
// Expect:
// 1. Definition of Sig1 in DS (Line 5) -> NewSig
// 2. Definition of Sig1 in GAM (Line 10) -> NewSig (Implicit match)
// 3. Alias reference in S2 (Line 12) -> NewSig
// Line 10: Sig1 = ... (0-based 9)
// Line 12: S2 = ... Alias = Sig1 (0-based 11)
expectedCount := 3
if len(edits) != expectedCount {
t.Errorf("Expected %d edits, got %d", expectedCount, len(edits))
for _, e := range edits {
t.Logf("Edit: %s at %d", e.NewText, e.Range.Start.Line)
}
}
// Check Implicit Signal Rename
foundImplicit := false
for _, e := range edits {
if e.Range.Start.Line == 11 {
if e.NewText == "NewSig" {
foundImplicit = true
}
}
}
if !foundImplicit {
t.Error("Did not find implicit signal rename")
}
// Check Alias Rename
foundAlias := false
for _, e := range edits {
if e.Range.Start.Line == 13 {
// Alias = Sig1. Range should cover Sig1.
if e.NewText == "NewSig" {
foundAlias = true
}
}
}
if !foundAlias {
t.Error("Did not find alias reference rename")
}
}

92
test/lsp_rename_test.go Normal file
View File

@@ -0,0 +1,92 @@
package integration
import (
"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"
)
func TestHandleRename(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
content := `
#package Some
+MyNode = {
Class = Type
}
+Consumer = {
Link = MyNode
PkgLink = Some.MyNode
}
`
uri := "file://rename.marte"
lsp.Documents[uri] = content
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
lsp.Tree.AddFile("rename.marte", cfg)
lsp.Tree.ResolveReferences()
// Rename +MyNode to NewNode
// +MyNode is at Line 2 (after #package)
// Line 0: empty
// Line 1: #package
// Line 2: +MyNode
params := lsp.RenameParams{
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
Position: lsp.Position{Line: 2, Character: 4}, // +MyNode
NewName: "NewNode",
}
edit := lsp.HandleRename(params)
if edit == nil {
t.Fatal("Expected edits")
}
edits := edit.Changes[uri]
if len(edits) != 3 {
t.Errorf("Expected 3 edits (Def, Link, PkgLink), got %d", len(edits))
}
// Verify Definition change (+MyNode -> +NewNode)
foundDef := false
for _, e := range edits {
if e.NewText == "+NewNode" {
foundDef = true
if e.Range.Start.Line != 2 {
t.Errorf("Definition edit line wrong: %d", e.Range.Start.Line)
}
}
}
if !foundDef {
t.Error("Did not find definition edit +NewNode")
}
// Verify Link change (MyNode -> NewNode)
foundLink := false
for _, e := range edits {
if e.NewText == "NewNode" && e.Range.Start.Line == 6 { // Link = MyNode
foundLink = true
}
}
if !foundLink {
t.Error("Did not find Link edit")
}
// Verify PkgLink change (Some.MyNode -> Some.NewNode)
foundPkg := false
for _, e := range edits {
if e.NewText == "NewNode" && e.Range.Start.Line == 7 { // PkgLink = Some.MyNode
foundPkg = true
}
}
if !foundPkg {
t.Error("Did not find PkgLink edit")
}
}

View File

@@ -0,0 +1,77 @@
package integration
import (
"bytes"
"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/schema"
)
func TestLSPValidationThreading(t *testing.T) {
// Setup
lsp.Tree = index.NewProjectTree()
lsp.Documents = make(map[string]string)
lsp.ProjectRoot = "."
lsp.GlobalSchema = schema.NewSchema() // Empty schema but not nil
// Capture Output
var buf bytes.Buffer
lsp.Output = &buf
content := `
+Data = {
Class = ReferenceContainer
+SharedDS = {
Class = GAMDataSource
#meta = {
direction = "INOUT"
multithreaded = false
}
Signals = {
Sig1 = { Type = uint32 }
}
}
}
+GAM1 = { Class = IOGAM InputSignals = { Sig1 = { DataSource = SharedDS Type = uint32 } } }
+GAM2 = { Class = IOGAM OutputSignals = { Sig1 = { DataSource = SharedDS Type = uint32 } } }
+App = {
Class = RealTimeApplication
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Thread1 = { Class = RealTimeThread Functions = { GAM1 } }
+Thread2 = { Class = RealTimeThread Functions = { GAM2 } }
}
}
}
`
uri := "file://threading.marte"
// Call HandleDidOpen directly
params := lsp.DidOpenTextDocumentParams{
TextDocument: lsp.TextDocumentItem{
URI: uri,
Text: content,
},
}
lsp.HandleDidOpen(params)
// Check output
output := buf.String()
// We look for publishDiagnostics notification
if !strings.Contains(output, "textDocument/publishDiagnostics") {
t.Fatal("Did not receive publishDiagnostics")
}
// We look for the specific error message
expectedError := "DataSource '+SharedDS' is not multithreaded but used in multiple threads"
if !strings.Contains(output, expectedError) {
t.Errorf("Expected error '%s' not found in LSP output. Output:\n%s", expectedError, output)
}
}

View File

@@ -0,0 +1,124 @@
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 TestDataSourceThreadingValidation(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+SharedDS = {
Class = GAMDataSource
#meta = {
direction = "INOUT"
multithreaded = false
}
Signals = {
Sig1 = { Type = uint32 }
}
}
+MultiDS = {
Class = GAMDataSource
#meta = {
direction = "INOUT"
multithreaded = true
}
Signals = {
Sig1 = { Type = uint32 }
}
}
}
+GAM1 = {
Class = IOGAM
InputSignals = {
Sig1 = { DataSource = SharedDS Type = uint32 }
}
}
+GAM2 = {
Class = IOGAM
OutputSignals = {
Sig1 = { DataSource = SharedDS Type = uint32 }
}
}
+GAM3 = {
Class = IOGAM
InputSignals = {
Sig1 = { DataSource = MultiDS Type = uint32 }
}
}
+GAM4 = {
Class = IOGAM
OutputSignals = {
Sig1 = { DataSource = MultiDS Type = uint32 }
}
}
+App = {
Class = RealTimeApplication
+States = {
Class = ReferenceContainer
+State1 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM1 }
}
+Thread2 = {
Class = RealTimeThread
Functions = { GAM2 }
}
}
+State2 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM3 }
}
+Thread2 = {
Class = RealTimeThread
Functions = { GAM4 }
}
}
}
}
`
pt := index.NewProjectTree()
p := parser.NewParser(content)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
pt.AddFile("main.marte", cfg)
// Since we don't load schema here (empty path), it won't validate classes via CUE,
// but CheckDataSourceThreading relies on parsing logic, not CUE schema unification.
// So it should work.
v := validator.NewValidator(pt, "")
v.ValidateProject()
foundError := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "not multithreaded but used in multiple threads") {
if strings.Contains(d.Message, "SharedDS") {
foundError = true
}
if strings.Contains(d.Message, "MultiDS") {
t.Error("Unexpected threading error for MultiDS")
}
}
}
if !foundError {
t.Error("Expected threading error for SharedDS")
// Debug
for _, d := range v.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
}
}

View File

@@ -15,7 +15,7 @@ func TestSDNSubscriberValidation(t *testing.T) {
+MySDN = { +MySDN = {
Class = SDNSubscriber Class = SDNSubscriber
Address = "239.0.0.1" Address = "239.0.0.1"
// Missing Port // Missing Interface
} }
` `
p := parser.NewParser(content) p := parser.NewParser(content)
@@ -32,7 +32,7 @@ func TestSDNSubscriberValidation(t *testing.T) {
found := false found := false
for _, d := range v.Diagnostics { for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Port: incomplete value") { if strings.Contains(d.Message, "Interface: field is required but not present") {
found = true found = true
break break
} }

View File

@@ -23,6 +23,7 @@ func TestGAMSignalLinking(t *testing.T) {
+MyGAM = { +MyGAM = {
Class = IOGAM Class = IOGAM
//! ignore(unused)
InputSignals = { InputSignals = {
MySig = { MySig = {
DataSource = MyDS DataSource = MyDS

View File

@@ -107,7 +107,11 @@ func TestHierarchicalPackageMerge(t *testing.T) {
} }
// We can also inspect the tree to verify FieldX is there (optional, but good for confidence) // We can also inspect the tree to verify FieldX is there (optional, but good for confidence)
baseNode := idx.Root.Children["Base"] projNode := idx.Root.Children["Proj"]
if projNode == nil {
t.Fatal("Proj node not found")
}
baseNode := projNode.Children["Base"]
if baseNode == nil { if baseNode == nil {
t.Fatal("Base node not found") t.Fatal("Base node not found")
} }

View File

@@ -0,0 +1,79 @@
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 TestSchemaMetaValidation(t *testing.T) {
// 1. Valid Usage
validContent := `
+App = {
Class = RealTimeApplication
Functions = { Class = ReferenceContainer }
Data = { Class = ReferenceContainer DefaultDataSource = "DS" }
States = { Class = ReferenceContainer }
Scheduler = { Class = GAMScheduler TimingDataSource = "DS" }
#meta = {
multithreaded = true
}
}
`
pt := index.NewProjectTree()
p := parser.NewParser(validContent)
cfg, err := p.Parse()
if err != nil {
t.Fatal(err)
}
pt.AddFile("valid.marte", cfg)
v := validator.NewValidator(pt, "")
v.ValidateProject()
if len(v.Diagnostics) > 0 {
for _, d := range v.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
t.Errorf("Expected no errors for valid #meta")
}
// 2. Invalid Usage (Wrong Type)
invalidContent := `
+App = {
Class = RealTimeApplication
Functions = { Class = ReferenceContainer }
Data = { Class = ReferenceContainer DefaultDataSource = "DS" }
States = { Class = ReferenceContainer }
Scheduler = { Class = GAMScheduler TimingDataSource = "DS" }
#meta = {
multithreaded = "yes" // Should be bool
}
}
`
pt2 := index.NewProjectTree()
p2 := parser.NewParser(invalidContent)
cfg2, _ := p2.Parse()
pt2.AddFile("invalid.marte", cfg2)
v2 := validator.NewValidator(pt2, "")
v2.ValidateProject()
foundError := false
for _, d := range v2.Diagnostics {
// CUE validation error message
if strings.Contains(d.Message, "mismatched types") || strings.Contains(d.Message, "conflicting values") {
foundError = true
}
}
if !foundError {
t.Error("Expected error for invalid #meta type, got nothing")
for _, d := range v2.Diagnostics {
t.Logf("Diag: %s", d.Message)
}
}
}