Compare commits
10 Commits
970b5697bd
...
1ea518a58a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ea518a58a | ||
|
|
0654062d08 | ||
|
|
a88f833f49 | ||
|
|
b2e963fc04 | ||
|
|
8fe319de2d | ||
|
|
93d48bd3ed | ||
|
|
164dad896c | ||
|
|
f111bf1aaa | ||
|
|
4a624aa929 | ||
|
|
5b0834137b |
27
examples/pragma_test.marte
Normal file
27
examples/pragma_test.marte
Normal 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
6416
examples/test_app.marte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,12 +270,14 @@ 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) {
|
||||
if len(params.ContentChanges) == 0 {
|
||||
@@ -284,12 +288,14 @@ 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 {
|
||||
uri := params.TextDocument.URI
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
35
internal/parser/parser_strictness_test.go
Normal file
35
internal/parser/parser_strictness_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
73
test/lsp_hover_context_test.go
Normal file
73
test/lsp_hover_context_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
74
test/validator_functions_array_test.go
Normal file
74
test/validator_functions_array_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
81
test/validator_gam_signals_linking_test.go
Normal file
81
test/validator_gam_signals_linking_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
108
test/validator_gam_signals_test.go
Normal file
108
test/validator_gam_signals_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
65
test/validator_global_pragma_debug_test.go
Normal file
65
test/validator_global_pragma_debug_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
67
test/validator_global_pragma_test.go
Normal file
67
test/validator_global_pragma_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
75
test/validator_global_pragma_update_test.go
Normal file
75
test/validator_global_pragma_update_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
59
test/validator_ignore_pragma_test.go
Normal file
59
test/validator_ignore_pragma_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
107
test/validator_implicit_signal_test.go
Normal file
107
test/validator_implicit_signal_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
69
test/validator_pragma_test.go
Normal file
69
test/validator_pragma_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
108
test/validator_signal_properties_test.go
Normal file
108
test/validator_signal_properties_test.go
Normal 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)")
|
||||
}
|
||||
}
|
||||
73
test/validator_signal_test.go
Normal file
73
test/validator_signal_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
59
test/validator_signals_content_test.go
Normal file
59
test/validator_signals_content_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user