Compare commits

..

10 Commits

Author SHA1 Message Date
Martino Ferrari
1ea518a58a minor improvment in the hover doc 2026-01-22 13:38:47 +01:00
Martino Ferrari
0654062d08 Almost done 2026-01-22 03:55:00 +01:00
Martino Ferrari
a88f833f49 Improving parsing and specs 2026-01-22 03:15:42 +01:00
Martino Ferrari
b2e963fc04 Implementing pragmas 2026-01-22 02:51:36 +01:00
Martino Ferrari
8fe319de2d Pragma and signal validation added 2026-01-22 02:29:54 +01:00
Martino Ferrari
93d48bd3ed mostly good 2026-01-22 02:19:14 +01:00
Martino Ferrari
164dad896c better indexing 2026-01-22 01:53:50 +01:00
Martino Ferrari
f111bf1aaa better indexing 2026-01-22 01:53:45 +01:00
Martino Ferrari
4a624aa929 better indexing 2026-01-22 01:26:24 +01:00
Martino Ferrari
5b0834137b not bad 2026-01-22 01:26:17 +01:00
27 changed files with 8469 additions and 161 deletions

View File

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

6416
examples/test_app.marte Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@ type ProjectTree struct {
Root *ProjectNode
References []Reference
IsolatedFiles map[string]*ProjectNode
GlobalPragmas map[string][]string
}
func (pt *ProjectTree) ScanDirectory(rootPath string) error {
@@ -50,6 +51,8 @@ type ProjectNode struct {
Children map[string]*ProjectNode
Parent *ProjectNode
Metadata map[string]string // Store extra info like Class, Type, Size
Target *ProjectNode // Points to referenced node (for Direct References/Links)
Pragmas []string
}
type Fragment struct {
@@ -57,6 +60,7 @@ type Fragment struct {
Definitions []parser.Definition
IsObject bool
ObjectPos parser.Position
EndPos parser.Position
Doc string // Documentation for this fragment (if object)
}
@@ -67,6 +71,7 @@ func NewProjectTree() *ProjectTree {
Metadata: make(map[string]string),
},
IsolatedFiles: make(map[string]*ProjectNode),
GlobalPragmas: make(map[string][]string),
}
}
@@ -87,6 +92,7 @@ func (pt *ProjectTree) RemoveFile(file string) {
pt.References = newRefs
delete(pt.IsolatedFiles, file)
delete(pt.GlobalPragmas, file)
pt.removeFileFromNode(pt.Root, file)
}
@@ -154,6 +160,15 @@ func (pt *ProjectTree) extractFieldMetadata(node *ProjectNode, f *parser.Field)
func (pt *ProjectTree) AddFile(file string, config *parser.Configuration) {
pt.RemoveFile(file)
// Collect global pragmas
for _, p := range config.Pragmas {
txt := strings.TrimSpace(strings.TrimPrefix(p.Text, "//!"))
normalized := strings.ReplaceAll(txt, " ", "")
if strings.HasPrefix(normalized, "allow(") || strings.HasPrefix(normalized, "ignore(") {
pt.GlobalPragmas[file] = append(pt.GlobalPragmas[file], txt)
}
}
if config.Package == nil {
node := &ProjectNode{
Children: make(map[string]*ProjectNode),
@@ -200,6 +215,7 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
for _, def := range config.Definitions {
doc := pt.findDoc(config.Comments, def.Pos())
pragmas := pt.findPragmas(config.Pragmas, def.Pos())
switch d := def.(type) {
case *parser.Field:
@@ -228,7 +244,11 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
child.Doc += doc
}
pt.addObjectFragment(child, file, d, doc, config.Comments)
if len(pragmas) > 0 {
child.Pragmas = append(child.Pragmas, pragmas...)
}
pt.addObjectFragment(child, file, d, doc, config.Comments, config.Pragmas)
}
}
@@ -237,16 +257,18 @@ func (pt *ProjectTree) populateNode(node *ProjectNode, file string, config *pars
}
}
func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment) {
func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *parser.ObjectNode, doc string, comments []parser.Comment, pragmas []parser.Pragma) {
frag := &Fragment{
File: file,
IsObject: true,
ObjectPos: obj.Position,
EndPos: obj.Subnode.EndPosition,
Doc: doc,
}
for _, def := range obj.Subnode.Definitions {
subDoc := pt.findDoc(comments, def.Pos())
subPragmas := pt.findPragmas(pragmas, def.Pos())
switch d := def.(type) {
case *parser.Field:
@@ -276,7 +298,11 @@ func (pt *ProjectTree) addObjectFragment(node *ProjectNode, file string, obj *pa
child.Doc += subDoc
}
pt.addObjectFragment(child, file, d, subDoc, comments)
if len(subPragmas) > 0 {
child.Pragmas = append(child.Pragmas, subPragmas...)
}
pt.addObjectFragment(child, file, d, subDoc, comments, pragmas)
}
}
@@ -321,6 +347,30 @@ func (pt *ProjectTree) findDoc(comments []parser.Comment, pos parser.Position) s
return docBuilder.String()
}
func (pt *ProjectTree) findPragmas(pragmas []parser.Pragma, pos parser.Position) []string {
var found []string
targetLine := pos.Line - 1
for i := len(pragmas) - 1; i >= 0; i-- {
p := pragmas[i]
if p.Position.Line > pos.Line {
continue
}
if p.Position.Line == pos.Line {
continue
}
if p.Position.Line == targetLine {
txt := strings.TrimSpace(strings.TrimPrefix(p.Text, "//!"))
found = append(found, txt)
targetLine--
} else if p.Position.Line < targetLine {
break
}
}
return found
}
func (pt *ProjectTree) indexValue(file string, val parser.Value) {
switch v := val.(type) {
case *parser.ReferenceValue:
@@ -384,6 +434,22 @@ func (pt *ProjectTree) Query(file string, line, col int) *QueryResult {
return pt.queryNode(pt.Root, file, line, col)
}
func (pt *ProjectTree) Walk(visitor func(*ProjectNode)) {
if pt.Root != nil {
pt.walkRecursive(pt.Root, visitor)
}
for _, node := range pt.IsolatedFiles {
pt.walkRecursive(node, visitor)
}
}
func (pt *ProjectTree) walkRecursive(node *ProjectNode, visitor func(*ProjectNode)) {
visitor(node)
for _, child := range node.Children {
pt.walkRecursive(child, visitor)
}
}
func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int) *QueryResult {
for _, frag := range node.Fragments {
if frag.File == file {
@@ -410,3 +476,44 @@ func (pt *ProjectTree) queryNode(node *ProjectNode, file string, line, col int)
}
return nil
}
func (pt *ProjectTree) GetNodeContaining(file string, pos parser.Position) *ProjectNode {
if isoNode, ok := pt.IsolatedFiles[file]; ok {
if found := pt.findNodeContaining(isoNode, file, pos); found != nil {
return found
}
return isoNode
}
if pt.Root != nil {
if found := pt.findNodeContaining(pt.Root, file, pos); found != nil {
return found
}
for _, frag := range pt.Root.Fragments {
if frag.File == file && !frag.IsObject {
return pt.Root
}
}
}
return nil
}
func (pt *ProjectTree) findNodeContaining(node *ProjectNode, file string, pos parser.Position) *ProjectNode {
for _, child := range node.Children {
if res := pt.findNodeContaining(child, file, pos); res != nil {
return res
}
}
for _, frag := range node.Fragments {
if frag.File == file && frag.IsObject {
start := frag.ObjectPos
end := frag.EndPos
if (pos.Line > start.Line || (pos.Line == start.Line && pos.Column >= start.Column)) &&
(pos.Line < end.Line || (pos.Line == end.Line && pos.Column <= end.Column)) {
return node
}
}
}
return nil
}

View File

@@ -196,7 +196,9 @@ func handleMessage(msg *JsonRpcMessage) {
if root != "" {
projectRoot = root
logger.Printf("Scanning workspace: %s\n", root)
tree.ScanDirectory(root)
if err := tree.ScanDirectory(root); err != nil {
logger.Printf("ScanDirectory failed: %v\n", err)
}
tree.ResolveReferences()
}
}
@@ -268,11 +270,13 @@ func handleDidOpen(params DidOpenTextDocumentParams) {
documents[params.TextDocument.URI] = params.TextDocument.Text
p := parser.NewParser(params.TextDocument.Text)
config, err := p.Parse()
if err == nil {
if err != nil {
publishParserError(params.TextDocument.URI, err)
return
}
tree.AddFile(path, config)
tree.ResolveReferences()
runValidation(params.TextDocument.URI)
}
}
func handleDidChange(params DidChangeTextDocumentParams) {
@@ -284,11 +288,13 @@ func handleDidChange(params DidChangeTextDocumentParams) {
path := uriToPath(params.TextDocument.URI)
p := parser.NewParser(text)
config, err := p.Parse()
if err == nil {
if err != nil {
publishParserError(params.TextDocument.URI, err)
return
}
tree.AddFile(path, config)
tree.ResolveReferences()
runValidation(params.TextDocument.URI)
}
}
func handleFormatting(params DocumentFormattingParams) []TextEdit {
@@ -378,6 +384,44 @@ func runValidation(uri string) {
}
}
func publishParserError(uri string, err error) {
var line, col int
var msg string
// Try parsing "line:col: message"
n, _ := fmt.Sscanf(err.Error(), "%d:%d: ", &line, &col)
if n == 2 {
parts := strings.SplitN(err.Error(), ": ", 2)
if len(parts) == 2 {
msg = parts[1]
}
} else {
// Fallback
line = 1
col = 1
msg = err.Error()
}
diag := LSPDiagnostic{
Range: Range{
Start: Position{Line: line - 1, Character: col - 1},
End: Position{Line: line - 1, Character: col},
},
Severity: 1, // Error
Message: msg,
Source: "mdt-parser",
}
notification := JsonRpcMessage{
Jsonrpc: "2.0",
Method: "textDocument/publishDiagnostics",
Params: mustMarshal(PublishDiagnosticsParams{
URI: uri,
Diagnostics: []LSPDiagnostic{diag},
}),
}
send(notification)
}
func collectFiles(node *index.ProjectNode, files map[string]bool) {
for _, frag := range node.Fragments {
files[frag.File] = true
@@ -406,7 +450,11 @@ func handleHover(params HoverParams) *Hover {
var content string
if res.Node != nil {
if res.Node.Target != nil {
content = fmt.Sprintf("**Link**: `%s` -> `%s`\n\n%s", res.Node.RealName, res.Node.Target.RealName, formatNodeInfo(res.Node.Target))
} else {
content = formatNodeInfo(res.Node)
}
} else if res.Field != nil {
content = fmt.Sprintf("**Field**: `%s`", res.Field.Name)
} else if res.Reference != nil {
@@ -454,8 +502,12 @@ func handleDefinition(params DefinitionParams) any {
if res.Reference != nil && res.Reference.Target != nil {
targetNode = res.Reference.Target
} else if res.Node != nil {
if res.Node.Target != nil {
targetNode = res.Node.Target
} else {
targetNode = res.Node
}
}
if targetNode != nil {
var locations []Location
@@ -497,23 +549,30 @@ func handleReferences(params ReferenceParams) []Location {
return nil
}
// Resolve canonical target (follow link if present)
canonical := targetNode
if targetNode.Target != nil {
canonical = targetNode.Target
}
var locations []Location
if params.Context.IncludeDeclaration {
for _, frag := range targetNode.Fragments {
for _, frag := range canonical.Fragments {
if frag.IsObject {
locations = append(locations, Location{
URI: "file://" + frag.File,
Range: 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)},
End: Position{Line: frag.ObjectPos.Line - 1, Character: frag.ObjectPos.Column - 1 + len(canonical.RealName)},
},
})
}
}
}
// 1. References from index (Aliases)
for _, ref := range tree.References {
if ref.Target == targetNode {
if ref.Target == canonical {
locations = append(locations, Location{
URI: "file://" + ref.File,
Range: Range{
@@ -524,17 +583,33 @@ func handleReferences(params ReferenceParams) []Location {
}
}
// 2. References from Node Targets (Direct References)
tree.Walk(func(node *index.ProjectNode) {
if node.Target == canonical {
for _, frag := range node.Fragments {
if frag.IsObject {
locations = append(locations, Location{
URI: "file://" + frag.File,
Range: 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(node.RealName)},
},
})
}
}
}
})
return locations
}
func formatNodeInfo(node *index.ProjectNode) string {
class := node.Metadata["Class"]
if class == "" {
class = "Unknown"
info := ""
if class := node.Metadata["Class"]; class != "" {
info = fmt.Sprintf("`%s:%s`\n\n", class, node.RealName[1:])
} else {
info = fmt.Sprintf("`%s`\n\n", node.RealName)
}
info := fmt.Sprintf("**Object**: `%s`\n\n**Class**: `%s`", node.RealName, class)
// Check if it's a Signal (has Type or DataSource)
typ := node.Metadata["Type"]
ds := node.Metadata["DataSource"]
@@ -550,7 +625,7 @@ func formatNodeInfo(node *index.ProjectNode) string {
// Size
dims := node.Metadata["NumberOfDimensions"]
elems := node.Metadata["NumberOfElements"]
elems := node.Metadata["NumberOfElements"]
if dims != "" || elems != "" {
sigInfo += fmt.Sprintf("**Size**: `[%s]`, `%s` dims ", elems, dims)
}
@@ -560,6 +635,57 @@ elems := node.Metadata["NumberOfElements"]
if node.Doc != "" {
info += fmt.Sprintf("\n\n%s", node.Doc)
}
// Find references
var refs []string
for _, ref := range tree.References {
if ref.Target == node {
container := tree.GetNodeContaining(ref.File, ref.Position)
if container != nil {
threadName := ""
stateName := ""
curr := container
for curr != nil {
if cls, ok := curr.Metadata["Class"]; ok {
if cls == "RealTimeThread" {
threadName = curr.RealName
}
if cls == "RealTimeState" {
stateName = curr.RealName
}
}
curr = curr.Parent
}
if threadName != "" || stateName != "" {
refStr := ""
if stateName != "" {
refStr += fmt.Sprintf("State: `%s`", stateName)
}
if threadName != "" {
if refStr != "" {
refStr += ", "
}
refStr += fmt.Sprintf("Thread: `%s`", threadName)
}
refs = append(refs, refStr)
}
}
}
}
if len(refs) > 0 {
uniqueRefs := make(map[string]bool)
info += "\n\n**Referenced in**:\n"
for _, r := range refs {
if !uniqueRefs[r] {
uniqueRefs[r] = true
info += fmt.Sprintf("- %s\n", r)
}
}
}
return info
}

View File

@@ -22,6 +22,7 @@ const (
TokenPragma
TokenComment
TokenDocstring
TokenComma
)
type Token struct {
@@ -121,6 +122,8 @@ func (l *Lexer) NextToken() Token {
return l.emit(TokenLBrace)
case '}':
return l.emit(TokenRBrace)
case ',':
return l.emit(TokenComma)
case '"':
return l.lexString()
case '/':
@@ -148,7 +151,7 @@ func (l *Lexer) NextToken() Token {
func (l *Lexer) lexIdentifier() Token {
for {
r := l.next()
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' || r == ':' {
continue
}
l.backup()
@@ -186,7 +189,7 @@ func (l *Lexer) lexString() Token {
func (l *Lexer) lexNumber() Token {
for {
r := l.next()
if unicode.IsDigit(r) || r == '.' || r == 'x' || r == 'b' || r == 'e' || r == '-' {
if unicode.IsDigit(r) || unicode.IsLetter(r) || r == '.' || r == '-' || r == '+' {
continue
}
l.backup()
@@ -206,6 +209,20 @@ func (l *Lexer) lexComment() Token {
}
return l.lexUntilNewline(TokenComment)
}
if r == '*' {
for {
r := l.next()
if r == -1 {
return l.emit(TokenError)
}
if r == '*' {
if l.peek() == '/' {
l.next() // consume /
return l.emit(TokenComment)
}
}
}
}
l.backup()
return l.emit(TokenError)
}

View File

@@ -235,6 +235,10 @@ func (p *Parser) parseValue() (Value, error) {
arr.EndPosition = endTok.Position
break
}
if t.Type == TokenComma {
p.next()
continue
}
val, err := p.parseValue()
if err != nil {
return nil, err

View File

@@ -0,0 +1,35 @@
package parser_test
import (
"testing"
"github.com/marte-dev/marte-dev-tools/internal/parser"
)
func TestParserStrictness(t *testing.T) {
// Case 1: content not a definition (missing =)
invalidDef := `
A = {
Field = 10
XXX
}
`
p := parser.NewParser(invalidDef)
_, err := p.Parse()
if err == nil {
t.Error("Expected error for invalid definition XXX, got nil")
}
// Case 2: Missing closing bracket
missingBrace := `
A = {
SUBNODE = {
FIELD = 10
}
`
p2 := parser.NewParser(missingBrace)
_, err2 := p2.Parse()
if err2 == nil {
t.Error("Expected error for missing closing bracket, got nil")
}
}

View File

@@ -9,7 +9,17 @@
},
"StateMachine": {
"fields": [
{"name": "States", "type": "node", "mandatory": true}
{"name": "States", "type": "node", "mandatory": false}
]
},
"RealTimeState": {
"fields": [
{"name": "Threads", "type": "node", "mandatory": true}
]
},
"RealTimeThread": {
"fields": [
{"name": "Functions", "type": "array", "mandatory": true}
]
},
"GAMScheduler": {
@@ -18,7 +28,8 @@
]
},
"TimingDataSource": {
"fields": []
"fields": [],
"direction": "IN"
},
"IOGAM": {
"fields": [
@@ -43,66 +54,79 @@
"fields": [
{"name": "Filename", "type": "string", "mandatory": true},
{"name": "Format", "type": "string", "mandatory": false}
]
],
"direction": "INOUT"
},
"LoggerDataSource": {
"fields": []
"fields": [],
"direction": "OUT"
},
"DANStream": {
"fields": [
{"name": "Timeout", "type": "int", "mandatory": false}
]
],
"direction": "OUT"
},
"EPICSCAInput": {
"fields": []
"fields": [],
"direction": "IN"
},
"EPICSCAOutput": {
"fields": []
"fields": [],
"direction": "OUT"
},
"EPICSPVAInput": {
"fields": []
"fields": [],
"direction": "IN"
},
"EPICSPVAOutput": {
"fields": []
"fields": [],
"direction": "OUT"
},
"SDNSubscriber": {
"fields": [
{"name": "Address", "type": "string", "mandatory": true},
{"name": "Port", "type": "int", "mandatory": true},
{"name": "Interface", "type": "string", "mandatory": false}
]
],
"direction": "IN"
},
"SDNPublisher": {
"fields": [
{"name": "Address", "type": "string", "mandatory": true},
{"name": "Port", "type": "int", "mandatory": true},
{"name": "Interface", "type": "string", "mandatory": false}
]
],
"direction": "OUT"
},
"UDPReceiver": {
"fields": [
{"name": "Port", "type": "int", "mandatory": true},
{"name": "Address", "type": "string", "mandatory": false}
]
],
"direction": "IN"
},
"UDPSender": {
"fields": [
{"name": "Destination", "type": "string", "mandatory": true}
]
],
"direction": "OUT"
},
"FileReader": {
"fields": [
{"name": "Filename", "type": "string", "mandatory": true},
{"name": "Format", "type": "string", "mandatory": false},
{"name": "Interpolate", "type": "string", "mandatory": false}
]
],
"direction": "IN"
},
"FileWriter": {
"fields": [
{"name": "Filename", "type": "string", "mandatory": true},
{"name": "Format", "type": "string", "mandatory": false},
{"name": "StoreOnTrigger", "type": "int", "mandatory": false}
]
],
"direction": "OUT"
},
"OrderedClass": {
"ordered": true,
@@ -148,7 +172,7 @@
"TimeCorrectionGAM": { "fields": [] },
"TriggeredIOGAM": { "fields": [] },
"WaveformGAM": { "fields": [] },
"DAN": { "fields": [] },
"DAN": { "fields": [], "direction": "OUT" },
"LinuxTimer": {
"fields": [
{"name": "ExecutionMode", "type": "string", "mandatory": false},
@@ -158,16 +182,18 @@
{"name": "CPUMask", "type": "int", "mandatory": false},
{"name": "TimeProvider", "type": "node", "mandatory": false},
{"name": "Signals", "type": "node", "mandatory": true}
]
],
"direction": "IN"
},
"LinkDataSource": { "fields": [] },
"LinkDataSource": { "fields": [], "direction": "INOUT" },
"MDSReader": {
"fields": [
{"name": "TreeName", "type": "string", "mandatory": true},
{"name": "ShotNumber", "type": "int", "mandatory": true},
{"name": "Frequency", "type": "float", "mandatory": true},
{"name": "Signals", "type": "node", "mandatory": true}
]
],
"direction": "IN"
},
"MDSWriter": {
"fields": [
@@ -183,27 +209,29 @@
{"name": "NumberOfPostTriggers", "type": "int", "mandatory": false},
{"name": "Signals", "type": "node", "mandatory": true},
{"name": "Messages", "type": "node", "mandatory": false}
]
],
"direction": "OUT"
},
"NI1588TimeStamp": { "fields": [] },
"NI6259ADC": { "fields": [] },
"NI6259DAC": { "fields": [] },
"NI6259DIO": { "fields": [] },
"NI6368ADC": { "fields": [] },
"NI6368DAC": { "fields": [] },
"NI6368DIO": { "fields": [] },
"NI9157CircularFifoReader": { "fields": [] },
"NI9157MxiDataSource": { "fields": [] },
"OPCUADSInput": { "fields": [] },
"OPCUADSOutput": { "fields": [] },
"NI1588TimeStamp": { "fields": [], "direction": "IN" },
"NI6259ADC": { "fields": [], "direction": "IN" },
"NI6259DAC": { "fields": [], "direction": "OUT" },
"NI6259DIO": { "fields": [], "direction": "INOUT" },
"NI6368ADC": { "fields": [], "direction": "IN" },
"NI6368DAC": { "fields": [], "direction": "OUT" },
"NI6368DIO": { "fields": [], "direction": "INOUT" },
"NI9157CircularFifoReader": { "fields": [], "direction": "IN" },
"NI9157MxiDataSource": { "fields": [], "direction": "INOUT" },
"OPCUADSInput": { "fields": [], "direction": "IN" },
"OPCUADSOutput": { "fields": [], "direction": "OUT" },
"RealTimeThreadAsyncBridge": { "fields": [] },
"RealTimeThreadSynchronisation": { "fields": [] },
"UARTDataSource": { "fields": [] },
"UARTDataSource": { "fields": [], "direction": "INOUT" },
"BaseLib2Wrapper": { "fields": [] },
"EPICSCAClient": { "fields": [] },
"EPICSPVA": { "fields": [] },
"MemoryGate": { "fields": [] },
"OPCUA": { "fields": [] },
"SysLogger": { "fields": [] }
"SysLogger": { "fields": [] },
"GAMDataSource": { "fields": [], "direction": "INOUT" }
}
}

View File

@@ -18,6 +18,7 @@ type Schema struct {
type ClassDefinition struct {
Fields []FieldDefinition `json:"fields"`
Ordered bool `json:"ordered"`
Direction string `json:"direction"`
}
type FieldDefinition struct {
@@ -96,6 +97,9 @@ func (s *Schema) Merge(other *Schema) {
if classDef.Ordered {
existingClass.Ordered = true
}
if classDef.Direction != "" {
existingClass.Direction = classDef.Direction
}
s.Classes[className] = existingClass
} else {
s.Classes[className] = classDef

View File

@@ -2,6 +2,8 @@ package validator
import (
"fmt"
"strings"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/schema"
@@ -38,6 +40,9 @@ func (v *Validator) ValidateProject() {
if v.Tree == nil {
return
}
// Ensure references are resolved (if not already done by builder/lsp)
v.Tree.ResolveReferences()
if v.Tree.Root != nil {
v.validateNode(v.Tree.Root)
}
@@ -47,17 +52,48 @@ func (v *Validator) ValidateProject() {
}
func (v *Validator) validateNode(node *index.ProjectNode) {
// Collect fields and their definitions
fields := make(map[string][]*parser.Field)
fieldOrder := []string{} // Keep track of order of appearance (approximate across fragments)
// Check for invalid content in Signals container of DataSource
if node.RealName == "Signals" && node.Parent != nil && isDataSource(node.Parent) {
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if _, exists := fields[f.Name]; !exists {
fieldOrder = append(fieldOrder, f.Name)
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Invalid content in Signals container: Field '%s' is not allowed. Only Signal objects are allowed.", f.Name),
Position: f.Position,
File: frag.File,
})
}
}
}
}
// Collect fields and their definitions
fields := v.getFields(node)
fieldOrder := []string{}
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if _, exists := fields[f.Name]; exists { // already collected
// Maintain order logic if needed, but getFields collects all.
// For strict order check we might need this loop.
// Let's assume getFields is enough for validation logic,
// but for "duplicate check" and "class validation" we iterate fields map.
// We need to construct fieldOrder.
// Just reuse loop for fieldOrder
}
}
}
}
// Re-construct fieldOrder for order validation
seen := make(map[string]bool)
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
if !seen[f.Name] {
fieldOrder = append(fieldOrder, f.Name)
seen[f.Name] = true
}
fields[f.Name] = append(fields[f.Name], f)
}
}
}
@@ -65,7 +101,6 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
// 1. Check for duplicate fields
for name, defs := range fields {
if len(defs) > 1 {
// Report error on the second definition
firstFile := v.getFileForField(defs[0], node)
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
@@ -80,13 +115,7 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
className := ""
if node.RealName != "" && (node.RealName[0] == '+' || node.RealName[0] == '$') {
if classFields, ok := fields["Class"]; ok && len(classFields) > 0 {
// Extract class name from value
switch val := classFields[0].Value.(type) {
case *parser.StringValue:
className = val.Value
case *parser.ReferenceValue:
className = val.Value
}
className = v.getFieldValue(classFields[0])
}
hasType := false
@@ -104,6 +133,10 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
File: file,
})
}
if className == "RealTimeThread" {
v.checkFunctionsArray(node, fields)
}
}
// 3. Schema Validation
@@ -113,6 +146,16 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
}
}
// 4. Signal Validation (for DataSource signals)
if isSignal(node) {
v.validateSignal(node, fields)
}
// 5. GAM Validation (Signal references)
if isGAM(node) {
v.validateGAM(node)
}
// Recursively validate children
for _, child := range node.Children {
v.validateNode(child)
@@ -120,14 +163,13 @@ func (v *Validator) validateNode(node *index.ProjectNode) {
}
func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.ClassDefinition, fields map[string][]*parser.Field, fieldOrder []string) {
// Check Mandatory Fields
// ... (same as before)
for _, fieldDef := range classDef.Fields {
if fieldDef.Mandatory {
found := false
if _, ok := fields[fieldDef.Name]; ok {
found = true
} else if fieldDef.Type == "node" {
// Check children for nodes
if _, ok := node.Children[fieldDef.Name]; ok {
found = true
}
@@ -144,10 +186,9 @@ func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.Class
}
}
// Check Field Types
for _, fieldDef := range classDef.Fields {
if fList, ok := fields[fieldDef.Name]; ok {
f := fList[0] // Check the first definition (duplicates handled elsewhere)
f := fList[0]
if !v.checkType(f.Value, fieldDef.Type) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
@@ -159,21 +200,14 @@ func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.Class
}
}
// Check Field Order
if classDef.Ordered {
// Verify that fields present in the node appear in the order defined in the schema
// Only consider fields that are actually in the schema's field list
schemaIdx := 0
for _, nodeFieldName := range fieldOrder {
// Find this field in schema
foundInSchema := false
for i, fd := range classDef.Fields {
if fd.Name == nodeFieldName {
foundInSchema = true
// Check if this field appears AFTER the current expected position
if i < schemaIdx {
// This field appears out of order (it should have appeared earlier, or previous fields were missing but this one came too late? No, simple relative order)
// Actually, simple check: `i` must be >= `lastSeenSchemaIdx`.
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Field '%s' is out of order", nodeFieldName),
@@ -187,13 +221,340 @@ func (v *Validator) validateClass(node *index.ProjectNode, classDef schema.Class
}
}
if !foundInSchema {
// Ignore extra fields for order check? Spec doesn't say strict closed schema.
}
}
}
}
func (v *Validator) validateSignal(node *index.ProjectNode, fields map[string][]*parser.Field) {
// ... (same as before)
if typeFields, ok := fields["Type"]; !ok || len(typeFields) == 0 {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Signal '%s' is missing mandatory field 'Type'", node.RealName),
Position: v.getNodePosition(node),
File: v.getNodeFile(node),
})
} else {
typeVal := typeFields[0].Value
var typeStr string
switch t := typeVal.(type) {
case *parser.StringValue:
typeStr = t.Value
case *parser.ReferenceValue:
typeStr = t.Value
default:
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Field 'Type' in Signal '%s' must be a type name", node.RealName),
Position: typeFields[0].Position,
File: v.getFileForField(typeFields[0], node),
})
return
}
if !isValidType(typeStr) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeStr, node.RealName),
Position: typeFields[0].Position,
File: v.getFileForField(typeFields[0], node),
})
}
}
}
func (v *Validator) validateGAM(node *index.ProjectNode) {
if inputs, ok := node.Children["InputSignals"]; ok {
v.validateGAMSignals(node, inputs, "Input")
}
if outputs, ok := node.Children["OutputSignals"]; ok {
v.validateGAMSignals(node, outputs, "Output")
}
}
func (v *Validator) validateGAMSignals(gamNode, signalsContainer *index.ProjectNode, direction string) {
for _, signal := range signalsContainer.Children {
v.validateGAMSignal(gamNode, signal, direction)
}
}
func (v *Validator) validateGAMSignal(gamNode, signalNode *index.ProjectNode, direction string) {
fields := v.getFields(signalNode)
var dsName string
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
dsName = v.getFieldValue(dsFields[0])
}
if dsName == "" {
return // Ignore implicit signals or missing datasource (handled elsewhere if mandatory)
}
dsNode := v.resolveReference(dsName, v.getNodeFile(signalNode), isDataSource)
if dsNode == nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Unknown DataSource '%s' referenced in signal '%s'", dsName, signalNode.RealName),
Position: v.getNodePosition(signalNode),
File: v.getNodeFile(signalNode),
})
return
}
// Link DataSource reference
if dsFields, ok := fields["DataSource"]; ok && len(dsFields) > 0 {
if val, ok := dsFields[0].Value.(*parser.ReferenceValue); ok {
v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, dsNode)
}
}
// Check Direction
dsClass := v.getNodeClass(dsNode)
if dsClass != "" {
if classDef, ok := v.Schema.Classes[dsClass]; ok {
dsDir := classDef.Direction
if dsDir != "" {
if direction == "Input" && dsDir == "OUT" {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("DataSource '%s' (Class %s) is Output-only but referenced in InputSignals of GAM '%s'", dsName, dsClass, gamNode.RealName),
Position: v.getNodePosition(signalNode),
File: v.getNodeFile(signalNode),
})
}
if direction == "Output" && dsDir == "IN" {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("DataSource '%s' (Class %s) is Input-only but referenced in OutputSignals of GAM '%s'", dsName, dsClass, gamNode.RealName),
Position: v.getNodePosition(signalNode),
File: v.getNodeFile(signalNode),
})
}
}
}
}
// Check Signal Existence
targetSignalName := index.NormalizeName(signalNode.RealName)
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
targetSignalName = v.getFieldValue(aliasFields[0]) // Alias is usually the name in DataSource
}
var targetNode *index.ProjectNode
if signalsContainer, ok := dsNode.Children["Signals"]; ok {
targetNorm := index.NormalizeName(targetSignalName)
if child, ok := signalsContainer.Children[targetNorm]; ok {
targetNode = child
} else {
// Fallback check
for _, child := range signalsContainer.Children {
if index.NormalizeName(child.RealName) == targetNorm {
targetNode = child
break
}
}
}
}
if targetNode == nil {
suppressed := v.isGloballyAllowed("implicit", v.getNodeFile(signalNode))
if !suppressed {
for _, p := range signalNode.Pragmas {
if strings.HasPrefix(p, "implicit:") || strings.HasPrefix(p, "ignore(implicit)") {
suppressed = true
break
}
}
}
if !suppressed {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning,
Message: fmt.Sprintf("Implicitly Defined Signal: '%s' is defined in GAM '%s' but not in DataSource '%s'", targetSignalName, gamNode.RealName, dsName),
Position: v.getNodePosition(signalNode),
File: v.getNodeFile(signalNode),
})
}
if typeFields, ok := fields["Type"]; !ok || len(typeFields) == 0 {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Implicit signal '%s' must define Type", targetSignalName),
Position: v.getNodePosition(signalNode),
File: v.getNodeFile(signalNode),
})
} else {
// Check Type validity even for implicit
typeVal := v.getFieldValue(typeFields[0])
if !isValidType(typeVal) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeVal, signalNode.RealName),
Position: typeFields[0].Position,
File: v.getNodeFile(signalNode),
})
}
}
} else {
signalNode.Target = targetNode
// Link Alias reference
if aliasFields, ok := fields["Alias"]; ok && len(aliasFields) > 0 {
if val, ok := aliasFields[0].Value.(*parser.ReferenceValue); ok {
v.updateReferenceTarget(v.getNodeFile(signalNode), val.Position, targetNode)
}
}
// Property checks
v.checkSignalProperty(signalNode, targetNode, "Type")
v.checkSignalProperty(signalNode, targetNode, "NumberOfElements")
v.checkSignalProperty(signalNode, targetNode, "NumberOfDimensions")
// Check Type validity if present
if typeFields, ok := fields["Type"]; ok && len(typeFields) > 0 {
typeVal := v.getFieldValue(typeFields[0])
if !isValidType(typeVal) {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Invalid Type '%s' for Signal '%s'", typeVal, signalNode.RealName),
Position: typeFields[0].Position,
File: v.getNodeFile(signalNode),
})
}
}
}
}
func (v *Validator) checkSignalProperty(gamSig, dsSig *index.ProjectNode, prop string) {
gamVal := gamSig.Metadata[prop]
dsVal := dsSig.Metadata[prop]
if gamVal == "" {
return
}
if dsVal != "" && gamVal != dsVal {
if prop == "Type" {
if v.checkCastPragma(gamSig, dsVal, gamVal) {
return
}
}
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Signal '%s' property '%s' mismatch: defined '%s', referenced '%s'", gamSig.RealName, prop, dsVal, gamVal),
Position: v.getNodePosition(gamSig),
File: v.getNodeFile(gamSig),
})
}
}
func (v *Validator) checkCastPragma(node *index.ProjectNode, defType, curType string) bool {
for _, p := range node.Pragmas {
if strings.HasPrefix(p, "cast(") {
content := strings.TrimPrefix(p, "cast(")
if idx := strings.Index(content, ")"); idx != -1 {
content = content[:idx]
parts := strings.Split(content, ",")
if len(parts) == 2 {
d := strings.TrimSpace(parts[0])
c := strings.TrimSpace(parts[1])
if d == defType && c == curType {
return true
}
}
}
}
}
return false
}
func (v *Validator) updateReferenceTarget(file string, pos parser.Position, target *index.ProjectNode) {
for i := range v.Tree.References {
ref := &v.Tree.References[i]
if ref.File == file && ref.Position == pos {
ref.Target = target
return
}
}
}
// Helpers
func (v *Validator) getFields(node *index.ProjectNode) map[string][]*parser.Field {
fields := make(map[string][]*parser.Field)
for _, frag := range node.Fragments {
for _, def := range frag.Definitions {
if f, ok := def.(*parser.Field); ok {
fields[f.Name] = append(fields[f.Name], f)
}
}
}
return fields
}
func (v *Validator) getFieldValue(f *parser.Field) string {
switch val := f.Value.(type) {
case *parser.StringValue:
return val.Value
case *parser.ReferenceValue:
return val.Value
case *parser.IntValue:
return val.Raw
case *parser.FloatValue:
return val.Raw
}
return ""
}
func (v *Validator) resolveReference(name string, file string, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
if isoNode, ok := v.Tree.IsolatedFiles[file]; ok {
if found := v.findNodeRecursive(isoNode, name, predicate); found != nil {
return found
}
return nil
}
if v.Tree.Root == nil {
return nil
}
return v.findNodeRecursive(v.Tree.Root, name, predicate)
}
func (v *Validator) findNodeRecursive(root *index.ProjectNode, name string, predicate func(*index.ProjectNode) bool) *index.ProjectNode {
// Simple recursive search matching name
if root.RealName == name || root.Name == index.NormalizeName(name) {
if predicate == nil || predicate(root) {
return root
}
}
// Recursive
for _, child := range root.Children {
if found := v.findNodeRecursive(child, name, predicate); found != nil {
return found
}
}
return nil
}
func (v *Validator) getNodeClass(node *index.ProjectNode) string {
if cls, ok := node.Metadata["Class"]; ok {
return cls
}
return ""
}
func isValidType(t string) bool {
switch t {
case "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64",
"float32", "float64", "string", "bool", "char8":
return true
}
return false
}
func (v *Validator) checkType(val parser.Value, expectedType string) bool {
// ... (same as before)
switch expectedType {
case "int":
_, ok := val.(*parser.IntValue)
@@ -202,8 +563,9 @@ func (v *Validator) checkType(val parser.Value, expectedType string) bool {
_, ok := val.(*parser.FloatValue)
return ok
case "string":
_, ok := val.(*parser.StringValue)
return ok
_, okStr := val.(*parser.StringValue)
_, okRef := val.(*parser.ReferenceValue)
return okStr || okRef
case "bool":
_, ok := val.(*parser.BoolValue)
return ok
@@ -214,15 +576,7 @@ func (v *Validator) checkType(val parser.Value, expectedType string) bool {
_, ok := val.(*parser.ReferenceValue)
return ok
case "node":
// This is tricky. A field cannot really be a "node" type in the parser sense (Node = { ... } is an ObjectNode, not a Field).
// But if the schema says "FieldX" is type "node", maybe it means it expects a reference to a node?
// Or maybe it means it expects a Subnode?
// In MARTe, `Field = { ... }` is parsed as ArrayValue usually.
// If `Field = SubNode`, it's `ObjectNode`.
// Schema likely refers to `+SubNode = { ... }`.
// But `validateClass` iterates `fields`.
// If schema defines a "field" of type "node", it might mean it expects a child node with that name.
return true // skip for now
return true
case "any":
return true
}
@@ -248,6 +602,13 @@ func (v *Validator) CheckUnused() {
}
}
if v.Tree.Root != nil {
v.collectTargetUsage(v.Tree.Root, referencedNodes)
}
for _, node := range v.Tree.IsolatedFiles {
v.collectTargetUsage(node, referencedNodes)
}
if v.Tree.Root != nil {
v.checkUnusedRecursive(v.Tree.Root, referencedNodes)
}
@@ -256,10 +617,29 @@ func (v *Validator) CheckUnused() {
}
}
func (v *Validator) collectTargetUsage(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) {
if node.Target != nil {
referenced[node.Target] = true
}
for _, child := range node.Children {
v.collectTargetUsage(child, referenced)
}
}
func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map[*index.ProjectNode]bool) {
// Heuristic for GAM
if isGAM(node) {
if !referenced[node] {
suppress := v.isGloballyAllowed("unused", v.getNodeFile(node))
if !suppress {
for _, p := range node.Pragmas {
if strings.HasPrefix(p, "unused:") || strings.HasPrefix(p, "ignore(unused)") {
suppress = true
break
}
}
}
if !suppress {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning,
Message: fmt.Sprintf("Unused GAM: %s is defined but not referenced in any thread or scheduler", node.RealName),
@@ -268,11 +648,24 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map
})
}
}
}
// Heuristic for DataSource and its signals
if isDataSource(node) {
for _, signal := range node.Children {
if signalsNode, ok := node.Children["Signals"]; ok {
for _, signal := range signalsNode.Children {
if !referenced[signal] {
if v.isGloballyAllowed("unused", v.getNodeFile(signal)) {
continue
}
suppress := false
for _, p := range signal.Pragmas {
if strings.HasPrefix(p, "unused:") || strings.HasPrefix(p, "ignore(unused)") {
suppress = true
break
}
}
if !suppress {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelWarning,
Message: fmt.Sprintf("Unused Signal: %s is defined in DataSource %s but never referenced", signal.RealName, node.RealName),
@@ -282,6 +675,8 @@ func (v *Validator) checkUnusedRecursive(node *index.ProjectNode, referenced map
}
}
}
}
}
for _, child := range node.Children {
v.checkUnusedRecursive(child, referenced)
@@ -304,6 +699,15 @@ func isDataSource(node *index.ProjectNode) bool {
return false
}
func isSignal(node *index.ProjectNode) bool {
if node.Parent != nil && node.Parent.Name == "Signals" {
if isDataSource(node.Parent.Parent) {
return true
}
}
return false
}
func (v *Validator) getNodePosition(node *index.ProjectNode) parser.Position {
if len(node.Fragments) > 0 {
return node.Fragments[0].ObjectPos
@@ -317,3 +721,63 @@ func (v *Validator) getNodeFile(node *index.ProjectNode) string {
}
return ""
}
func (v *Validator) checkFunctionsArray(node *index.ProjectNode, fields map[string][]*parser.Field) {
if funcs, ok := fields["Functions"]; ok && len(funcs) > 0 {
f := funcs[0]
if arr, ok := f.Value.(*parser.ArrayValue); ok {
for _, elem := range arr.Elements {
if ref, ok := elem.(*parser.ReferenceValue); ok {
target := v.resolveReference(ref.Value, v.getNodeFile(node), isGAM)
if target == nil {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: fmt.Sprintf("Function '%s' not found or is not a valid GAM", ref.Value),
Position: ref.Position,
File: v.getNodeFile(node),
})
}
} else {
v.Diagnostics = append(v.Diagnostics, Diagnostic{
Level: LevelError,
Message: "Functions array must contain references",
Position: f.Position,
File: v.getNodeFile(node),
})
}
}
}
}
}
func (v *Validator) isGloballyAllowed(warningType string, contextFile string) bool {
prefix1 := fmt.Sprintf("allow(%s)", warningType)
prefix2 := fmt.Sprintf("ignore(%s)", warningType)
// If context file is isolated, only check its own pragmas
if _, isIsolated := v.Tree.IsolatedFiles[contextFile]; isIsolated {
if pragmas, ok := v.Tree.GlobalPragmas[contextFile]; ok {
for _, p := range pragmas {
normalized := strings.ReplaceAll(p, " ", "")
if strings.HasPrefix(normalized, prefix1) || strings.HasPrefix(normalized, prefix2) {
return true
}
}
}
return false
}
// If project file, check all non-isolated files
for file, pragmas := range v.Tree.GlobalPragmas {
if _, isIsolated := v.Tree.IsolatedFiles[file]; isIsolated {
continue
}
for _, p := range pragmas {
normalized := strings.ReplaceAll(p, " ", "")
if strings.HasPrefix(normalized, prefix1) || strings.HasPrefix(normalized, prefix2) {
return true
}
}
}
return false
}

BIN
mdt

Binary file not shown.

View File

@@ -84,8 +84,13 @@ The LSP server should provide the following capabilities:
- **Nodes (`+` / `$`)**: The prefixes `+` and `$` indicate that the node represents an object.
- **Constraint**: These nodes _must_ contain a field named `Class` within their subnode definition (across all files where the node is defined).
- **Signals**: Signals are considered nodes but **not** objects. They do not require a `Class` field.
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored.
- **Pragmas (`//!`)**: Used to suppress specific diagnostics. The developer can use these to explain why a rule is being ignored. Supported pragmas:
- `//!unused: REASON` or `//!ignore(unused): REASON` - Suppress "Unused GAM" or "Unused Signal" warnings.
- `//!implicit: REASON` or `//!ignore(implicit): REASON` - Suppress "Implicitly Defined Signal" warnings.
- `//!allow(WARNING_TYPE): REASON` or `//!ignore(WARNING_TYPE): REASON` - Global suppression for a specific warning type across the whole project (supported: `unused`, `implicit`).
- `//!cast(DEF_TYPE, CUR_TYPE): REASON` - Suppress "Type Inconsistency" errors if types match.
- **Structure**: A configuration is composed by one or more definitions.
- **Strictness**: Any content that is not a valid comment (or pragma/docstring) or a valid definition (Field, Node, or Object) is **not allowed** and must generate a parsing error.
### Core MARTe Classes
@@ -105,29 +110,33 @@ MARTe configurations typically involve several main categories of objects:
- **Requirements**:
- All signal definitions **must** include a `Type` field with a valid value.
- **Size Information**: Signals can optionally include `NumberOfDimensions` and `NumberOfElements` fields. If not explicitly defined, these default to `1`.
- **Property Matching**: Signal references in GAMs must match the properties (`Type`, `NumberOfElements`, `NumberOfDimensions`) of the defined signal in the `DataSource`.
- **Extensibility**: Signal definitions can include additional fields as required by the specific application context.
- **Signal Reference Syntax**:
- Signals are referenced or defined in `InputSignals` or `OutputSignals` sub-nodes using one of the following formats:
1. **Direct Reference**:
1. **Direct Reference (Option 1)**:
```
SIGNAL_NAME = {
DataSource = SIGNAL_DATASOURCE
DataSource = DATASOURCE_NAME
// Other fields if necessary
}
```
2. **Aliased Reference**:
In this case, the GAM signal name is the same as the DataSource signal name.
2. **Aliased Reference (Option 2)**:
```
NAME = {
GAM_SIGNAL_NAME = {
Alias = SIGNAL_NAME
DataSource = SIGNAL_DATASOURCE
DataSource = DATASOURCE_NAME
// ...
}
```
In this case, `Alias` points to the DataSource signal name.
- **Implicit Definition Constraint**: If a signal is implicitly defined within a GAM, the `Type` field **must** be present in the reference block to define the signal's properties.
- **Directionality**: DataSources and their signals are directional:
- `Input`: Only providing data.
- `Output`: Only receiving data.
- `Inout`: Bidirectional data flow.
- `Input` (IN): Only providing data. Signals can only be used in `InputSignals`.
- `Output` (OUT): Only receiving data. Signals can only be used in `OutputSignals`.
- `Inout` (INOUT): Bidirectional data flow. Signals can be used in both `InputSignals` and `OutputSignals`.
- **Validation**: The tool must validate that signal usage in GAMs respects the direction of the referenced DataSource.
### Object Indexing & References
@@ -183,18 +192,20 @@ The `fmt` command must format the code according to the following rules:
The LSP and `check` command should report the following:
- **Warnings**:
- **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler.
- **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`.
- **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`.
- **Unused GAM**: A GAM is defined but not referenced in any thread or scheduler. (Suppress with `//!unused`)
- **Unused Signal**: A signal is explicitly defined in a `DataSource` but never referenced in any `GAM`. (Suppress with `//!unused`)
- **Implicitly Defined Signal**: A signal is defined only within a `GAM` and not in its parent `DataSource`. (Suppress with `//!implicit`)
- **Errors**:
- **Type Inconsistency**: A signal is referenced with a type different from its definition.
- **Type Inconsistency**: A signal is referenced with a type different from its definition. (Suppress with `//!cast`)
- **Size Inconsistency**: A signal is referenced with a size (dimensions/elements) different from its definition.
- **Invalid Signal Content**: The `Signals` container of a `DataSource` contains invalid elements (e.g., fields instead of nodes).
- **Duplicate Field Definition**: A field is defined multiple times within the same node scope (including across multiple files).
- **Validation Errors**:
- Missing mandatory fields.
- Field type mismatches.
- Grammar errors (e.g., missing closing brackets).
- **Invalid Function Reference**: Elements in the `Functions` array of a `State.Thread` must be valid references to defined GAM nodes.
## Logging

View File

@@ -0,0 +1,73 @@
package integration
import (
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
)
func TestGetNodeContaining(t *testing.T) {
content := `
+App = {
Class = RealTimeApplication
+State1 = {
Class = RealTimeState
+Thread1 = {
Class = RealTimeThread
Functions = { GAM1 }
}
}
}
+GAM1 = { Class = IOGAM }
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
file := "hover_context.marte"
idx.AddFile(file, config)
idx.ResolveReferences()
// Find reference to GAM1
var gamRef *index.Reference
for i := range idx.References {
ref := &idx.References[i]
if ref.Name == "GAM1" {
gamRef = ref
break
}
}
if gamRef == nil {
t.Fatal("Reference to GAM1 not found")
}
// Check containing node
container := idx.GetNodeContaining(file, gamRef.Position)
if container == nil {
t.Fatal("Container not found")
}
if container.RealName != "+Thread1" {
t.Errorf("Expected container +Thread1, got %s", container.RealName)
}
// Check traversal up to State
curr := container
foundState := false
for curr != nil {
if curr.RealName == "+State1" {
foundState = true
break
}
curr = curr.Parent
}
if !foundState {
t.Error("State parent not found")
}
}

View File

@@ -5,16 +5,30 @@ import (
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestLSPSignalMetadata(t *testing.T) {
func TestLSPSignalReferences(t *testing.T) {
content := `
+MySignal = {
Class = Signal
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
MySig = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
MySig = {
DataSource = MyDS
Type = uint32
NumberOfElements = 10
NumberOfDimensions = 1
DataSource = DDB1
}
}
}
`
p := parser.NewParser(content)
@@ -24,26 +38,50 @@ func TestLSPSignalMetadata(t *testing.T) {
}
idx := index.NewProjectTree()
file := "signal.marte"
idx.AddFile(file, config)
idx.AddFile("signal_refs.marte", config)
idx.ResolveReferences()
res := idx.Query(file, 2, 2) // Query +MySignal
if res == nil || res.Node == nil {
t.Fatal("Query failed for signal definition")
v := validator.NewValidator(idx, ".")
v.ValidateProject()
// Find definition of MySig in MyDS
root := idx.IsolatedFiles["signal_refs.marte"]
if root == nil {
t.Fatal("Root node not found")
}
meta := res.Node.Metadata
if meta["Class"] != "Signal" {
t.Errorf("Expected Class Signal, got %s", meta["Class"])
}
if meta["Type"] != "uint32" {
t.Errorf("Expected Type uint32, got %s", meta["Type"])
}
if meta["NumberOfElements"] != "10" {
t.Errorf("Expected 10 elements, got %s", meta["NumberOfElements"])
// Traverse to MySig
dataNode := root.Children["Data"]
if dataNode == nil { t.Fatal("Data node not found") }
myDS := dataNode.Children["MyDS"]
if myDS == nil { t.Fatal("MyDS node not found") }
signals := myDS.Children["Signals"]
if signals == nil { t.Fatal("Signals node not found") }
mySigDef := signals.Children["MySig"]
if mySigDef == nil {
t.Fatal("Definition of MySig not found in tree")
}
// Since handleHover logic is in internal/lsp which we can't easily test directly without
// exposing formatNodeInfo, we rely on the fact that Metadata is populated correctly.
// If Metadata is correct, server.go logic (verified by code review) should display it.
// Now simulate "Find References" on mySigDef
foundRefs := 0
idx.Walk(func(node *index.ProjectNode) {
if node.Target == mySigDef {
foundRefs++
// Check if node is the GAM signal
if node.RealName != "MySig" { // In GAM it is MySig
t.Errorf("Unexpected reference node name: %s", node.RealName)
}
// Check parent is InputSignals -> MyGAM
if node.Parent == nil || node.Parent.Parent == nil || node.Parent.Parent.RealName != "+MyGAM" {
t.Errorf("Reference node not in MyGAM")
}
}
})
if foundRefs != 1 {
t.Errorf("Expected 1 reference (Direct), found %d", foundRefs)
}
}

View File

@@ -140,3 +140,16 @@ func TestLSPHover(t *testing.T) {
t.Errorf("Expected +MyObject, got %s", res.Node.RealName)
}
}
func TestParserError(t *testing.T) {
invalidContent := `
A = {
Field =
}
`
p := parser.NewParser(invalidContent)
_, err := p.Parse()
if err == nil {
t.Fatal("Expected parser error, got nil")
}
}

View File

@@ -0,0 +1,74 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestFunctionsArrayValidation(t *testing.T) {
content := `
+App = {
Class = RealTimeApplication
+State = {
Class = RealTimeState
+Thread = {
Class = RealTimeThread
Functions = {
ValidGAM,
InvalidGAM, // Not a GAM (DataSource)
MissingGAM, // Not found
"String", // Not reference
}
}
}
}
+ValidGAM = { Class = IOGAM InputSignals = {} }
+InvalidGAM = { Class = FileReader }
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("funcs.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
foundInvalid := false
foundMissing := false
foundNotRef := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "not found or is not a valid GAM") {
// This covers both InvalidGAM and MissingGAM cases
if strings.Contains(d.Message, "InvalidGAM") {
foundInvalid = true
}
if strings.Contains(d.Message, "MissingGAM") {
foundMissing = true
}
}
if strings.Contains(d.Message, "must contain references") {
foundNotRef = true
}
}
if !foundInvalid {
t.Error("Expected error for InvalidGAM")
}
if !foundMissing {
t.Error("Expected error for MissingGAM")
}
if !foundNotRef {
t.Error("Expected error for non-reference element")
}
}

View File

@@ -0,0 +1,81 @@
package integration
import (
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestGAMSignalLinking(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test.txt"
Signals = {
MySig = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
MySig = {
DataSource = MyDS
Type = uint32
}
AliasedSig = {
Alias = MySig
DataSource = MyDS
Type = uint32
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("gam_signals_linking.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
if len(v.Diagnostics) > 0 {
for _, d := range v.Diagnostics {
t.Logf("Diagnostic: %s", d.Message)
}
t.Fatalf("Validation failed with %d issues", len(v.Diagnostics))
}
foundMyDSRef := 0
foundAliasRef := 0
for _, ref := range idx.References {
if ref.Name == "MyDS" {
if ref.Target != nil && ref.Target.RealName == "+MyDS" {
foundMyDSRef++
}
}
if ref.Name == "MySig" {
if ref.Target != nil && ref.Target.RealName == "MySig" {
foundAliasRef++
}
}
}
if foundMyDSRef < 2 {
t.Errorf("Expected at least 2 resolved MyDS references, found %d", foundMyDSRef)
}
if foundAliasRef < 1 {
t.Errorf("Expected at least 1 resolved Alias MySig reference, found %d", foundAliasRef)
}
}

View File

@@ -0,0 +1,108 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestGAMSignalValidation(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+InDS = {
Class = FileReader
Signals = {
SigIn = { Type = uint32 }
}
}
+OutDS = {
Class = FileWriter
Signals = {
SigOut = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
SigIn = {
DataSource = InDS
Type = uint32
}
// Error: OutDS is OUT only
BadInput = {
DataSource = OutDS
Alias = SigOut
Type = uint32
}
// Error: MissingSig not in InDS
Missing = {
DataSource = InDS
Alias = MissingSig
Type = uint32
}
}
OutputSignals = {
SigOut = {
DataSource = OutDS
Type = uint32
}
// Error: InDS is IN only
BadOutput = {
DataSource = InDS
Alias = SigIn
Type = uint32
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("gam_signals.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
foundBadInput := false
foundMissing := false
foundBadOutput := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "DataSource 'OutDS' (Class FileWriter) is Output-only but referenced in InputSignals") {
foundBadInput = true
}
if strings.Contains(d.Message, "Signal 'MissingSig' not found in DataSource 'InDS'") {
foundMissing = true
}
if strings.Contains(d.Message, "DataSource 'InDS' (Class FileReader) is Input-only but referenced in OutputSignals") {
foundBadOutput = true
}
}
if !foundBadInput || !foundMissing || !foundBadOutput {
for _, d := range v.Diagnostics {
t.Logf("Diagnostic: %s", d.Message)
}
}
if !foundBadInput {
t.Error("Expected error for OutDS in InputSignals")
}
if !foundMissing {
t.Error("Expected error for missing signal reference")
}
if !foundBadOutput {
t.Error("Expected error for InDS in OutputSignals")
}
}

View File

@@ -0,0 +1,65 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestGlobalPragmaDebug(t *testing.T) {
content := `//! allow(implicit): Debugging
//! allow(unused): Debugging
+Data={Class=ReferenceContainer}
+GAM={Class=IOGAM InputSignals={Impl={DataSource=Data Type=uint32}}}
+UnusedGAM={Class=IOGAM}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Check if pragma parsed
if len(config.Pragmas) == 0 {
t.Fatal("Pragma not parsed")
}
t.Logf("Parsed Pragma 0: %s", config.Pragmas[0].Text)
idx := index.NewProjectTree()
idx.AddFile("debug.marte", config)
idx.ResolveReferences()
// Check if added to GlobalPragmas
pragmas, ok := idx.GlobalPragmas["debug.marte"]
if !ok || len(pragmas) == 0 {
t.Fatal("GlobalPragmas not populated")
}
t.Logf("Global Pragma stored: %s", pragmas[0])
v := validator.NewValidator(idx, ".")
v.ValidateProject()
v.CheckUnused() // Must call this for unused check!
foundImplicitWarning := false
foundUnusedWarning := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Implicitly Defined Signal") {
foundImplicitWarning = true
t.Logf("Found warning: %s", d.Message)
}
if strings.Contains(d.Message, "Unused GAM") {
foundUnusedWarning = true
t.Logf("Found warning: %s", d.Message)
}
}
if foundImplicitWarning {
t.Error("Expected implicit warning to be suppressed")
}
if foundUnusedWarning {
t.Error("Expected unused warning to be suppressed")
}
}

View File

@@ -0,0 +1,67 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestGlobalPragma(t *testing.T) {
content := `
//!allow(unused): Suppress all unused
//!allow(implicit): Suppress all implicit
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
UnusedSig = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
ImplicitSig = { DataSource = MyDS Type = uint32 }
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("global_pragma.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
v.CheckUnused()
foundUnusedWarning := false
foundImplicitWarning := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Unused Signal") {
foundUnusedWarning = true
}
if strings.Contains(d.Message, "Implicitly Defined Signal") {
foundImplicitWarning = true
}
}
if foundUnusedWarning {
t.Error("Expected warning for UnusedSig to be suppressed globally")
}
if foundImplicitWarning {
t.Error("Expected warning for ImplicitSig to be suppressed globally")
}
}

View File

@@ -0,0 +1,75 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestGlobalPragmaUpdate(t *testing.T) {
// Scenario: Project scope. File A has pragma. File B has warning.
fileA := "fileA.marte"
contentA_WithPragma := `
#package my.project
//!allow(unused): Suppress
`
contentA_NoPragma := `
#package my.project
// No pragma
`
fileB := "fileB.marte"
contentB := `
#package my.project
+Data={Class=ReferenceContainer +DS={Class=FileReader Filename="t" Signals={Unused={Type=uint32}}}}
`
idx := index.NewProjectTree()
// Helper to validate
check := func() bool {
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
v.CheckUnused()
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Unused Signal") {
return true // Found warning
}
}
return false
}
// 1. Add A (with pragma) and B
pA := parser.NewParser(contentA_WithPragma)
cA, _ := pA.Parse()
idx.AddFile(fileA, cA)
pB := parser.NewParser(contentB)
cB, _ := pB.Parse()
idx.AddFile(fileB, cB)
if check() {
t.Error("Step 1: Expected warning to be suppressed")
}
// 2. Update A (remove pragma)
pA2 := parser.NewParser(contentA_NoPragma)
cA2, _ := pA2.Parse()
idx.AddFile(fileA, cA2)
if !check() {
t.Error("Step 2: Expected warning to appear")
}
// 3. Update A (add pragma back)
idx.AddFile(fileA, cA) // Re-use config A
if check() {
t.Error("Step 3: Expected warning to be suppressed again")
}
}

View File

@@ -0,0 +1,59 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestIgnorePragma(t *testing.T) {
content := `
//!ignore(unused): Suppress global unused
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
Unused1 = { Type = uint32 }
//!ignore(unused): Suppress local unused
Unused2 = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
//!ignore(implicit): Suppress local implicit
ImplicitSig = { DataSource = MyDS Type = uint32 }
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("ignore.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
v.CheckUnused()
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Unused Signal") {
t.Errorf("Unexpected warning: %s", d.Message)
}
if strings.Contains(d.Message, "Implicitly Defined Signal") {
t.Errorf("Unexpected warning: %s", d.Message)
}
}
}

View File

@@ -0,0 +1,107 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestImplicitSignal(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
ExplicitSig = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
ExplicitSig = {
DataSource = MyDS
Type = uint32
}
ImplicitSig = {
DataSource = MyDS
Type = uint32
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("implicit_signal.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
foundWarning := false
foundError := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Implicitly Defined Signal") {
if strings.Contains(d.Message, "ImplicitSig") {
foundWarning = true
}
}
if strings.Contains(d.Message, "Signal 'ExplicitSig' not found") {
foundError = true
}
}
if !foundWarning || foundError {
for _, d := range v.Diagnostics {
t.Logf("Diagnostic: %s", d.Message)
}
}
if !foundWarning {
t.Error("Expected warning for ImplicitSig")
}
if foundError {
t.Error("Unexpected error for ExplicitSig")
}
// Test missing Type for implicit
contentMissingType := `
+Data = { Class = ReferenceContainer +DS={Class=FileReader Filename="" Signals={}} }
+GAM = { Class = IOGAM InputSignals = { Impl = { DataSource = DS } } }
`
p2 := parser.NewParser(contentMissingType)
config2, err2 := p2.Parse()
if err2 != nil {
t.Fatalf("Parse2 failed: %v", err2)
}
idx2 := index.NewProjectTree()
idx2.AddFile("missing_type.marte", config2)
idx2.ResolveReferences()
v2 := validator.NewValidator(idx2, ".")
v2.ValidateProject()
foundTypeErr := false
for _, d := range v2.Diagnostics {
if strings.Contains(d.Message, "Implicit signal 'Impl' must define Type") {
foundTypeErr = true
}
}
if !foundTypeErr {
for _, d := range v2.Diagnostics {
t.Logf("Diagnostic2: %s", d.Message)
}
t.Error("Expected error for missing Type in implicit signal")
}
}

View File

@@ -0,0 +1,69 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestPragmaSuppression(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
//!unused: Ignore this
UnusedSig = { Type = uint32 }
UsedSig = { Type = uint32 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
UsedSig = { DataSource = MyDS Type = uint32 }
//!implicit: Ignore this implicit
ImplicitSig = { DataSource = MyDS Type = uint32 }
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("pragma.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
v.CheckUnused()
foundUnusedWarning := false
foundImplicitWarning := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Unused Signal") && strings.Contains(d.Message, "UnusedSig") {
foundUnusedWarning = true
}
if strings.Contains(d.Message, "Implicitly Defined Signal") && strings.Contains(d.Message, "ImplicitSig") {
foundImplicitWarning = true
}
}
if foundUnusedWarning {
t.Error("Expected warning for UnusedSig to be suppressed")
}
if foundImplicitWarning {
t.Error("Expected warning for ImplicitSig to be suppressed")
}
}

View File

@@ -0,0 +1,108 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestSignalProperties(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+MyDS = {
Class = FileReader
Filename = "test"
Signals = {
Correct = { Type = uint32 NumberOfElements = 10 }
}
}
}
+MyGAM = {
Class = IOGAM
InputSignals = {
// Correct reference
Correct = { DataSource = MyDS Type = uint32 NumberOfElements = 10 }
// Mismatch Type
BadType = {
Alias = Correct
DataSource = MyDS
Type = float32 // Error
}
// Mismatch Elements
BadElements = {
Alias = Correct
DataSource = MyDS
Type = uint32
NumberOfElements = 20 // Error
}
// Valid Cast
//!cast(uint32, float32): Cast reason
CastSig = {
Alias = Correct
DataSource = MyDS
Type = float32 // OK
}
// Invalid Cast (Wrong definition type in pragma)
//!cast(int32, float32): Wrong def type
BadCast = {
Alias = Correct
DataSource = MyDS
Type = float32 // Error because pragma mismatch
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("signal_props.marte", config)
idx.ResolveReferences()
v := validator.NewValidator(idx, ".")
v.ValidateProject()
foundBadType := false
foundBadElements := false
foundBadCast := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "property 'Type' mismatch") {
if strings.Contains(d.Message, "'BadType'") {
foundBadType = true
}
if strings.Contains(d.Message, "'BadCast'") {
foundBadCast = true
}
if strings.Contains(d.Message, "'CastSig'") {
t.Error("Unexpected error for CastSig (should be suppressed by pragma)")
}
}
if strings.Contains(d.Message, "property 'NumberOfElements' mismatch") {
foundBadElements = true
}
}
if !foundBadType {
t.Error("Expected error for BadType")
}
if !foundBadElements {
t.Error("Expected error for BadElements")
}
if !foundBadCast {
t.Error("Expected error for BadCast (pragma mismatch)")
}
}

View File

@@ -0,0 +1,73 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestSignalValidation(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+ValidDS = {
Class = DataSource
Signals = {
ValidSig = {
Type = uint32
}
}
}
+MissingTypeDS = {
Class = DataSource
Signals = {
InvalidSig = {
// Missing Type
Dummy = 1
}
}
}
+InvalidTypeDS = {
Class = DataSource
Signals = {
InvalidSig = {
Type = invalid_type
}
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("signal_test.marte", config)
v := validator.NewValidator(idx, ".")
v.ValidateProject()
foundMissing := false
foundInvalid := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "missing mandatory field 'Type'") {
foundMissing = true
}
if strings.Contains(d.Message, "Invalid Type 'invalid_type'") {
foundInvalid = true
}
}
if !foundMissing {
t.Error("Expected error for missing Type field in Signal")
}
if !foundInvalid {
t.Error("Expected error for invalid Type value in Signal")
}
}

View File

@@ -0,0 +1,59 @@
package integration
import (
"strings"
"testing"
"github.com/marte-dev/marte-dev-tools/internal/index"
"github.com/marte-dev/marte-dev-tools/internal/parser"
"github.com/marte-dev/marte-dev-tools/internal/validator"
)
func TestSignalsContentValidation(t *testing.T) {
content := `
+Data = {
Class = ReferenceContainer
+BadDS = {
Class = DataSource
Signals = {
BadField = 1
BadArray = { 1 2 }
// Valid signal
ValidSig = {
Type = uint32
}
}
}
}
`
p := parser.NewParser(content)
config, err := p.Parse()
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
idx := index.NewProjectTree()
idx.AddFile("signals_content.marte", config)
v := validator.NewValidator(idx, ".")
v.ValidateProject()
foundBadField := false
foundBadArray := false
for _, d := range v.Diagnostics {
if strings.Contains(d.Message, "Field 'BadField' is not allowed") {
foundBadField = true
}
if strings.Contains(d.Message, "Field 'BadArray' is not allowed") {
foundBadArray = true
}
}
if !foundBadField {
t.Error("Expected error for BadField in Signals")
}
if !foundBadArray {
t.Error("Expected error for BadArray in Signals")
}
}